- gRPC service with MusicBrainz provider - PostgreSQL schema with migrations - Service layer with database-first caching - Repository pattern for data access - YAML configuration support - Research documentation for 17 music metadata projects
25 KiB
Bedrock-API API Reference
Protocol Buffer Definition
File: proto/bedrock_service.proto
Lines: 622
Package: bedrock
Go Package: github.com/feralbureau/bedrock-api/proto
Service Definition
service BedrockService {
// Search operations
rpc SearchTracks(SearchRequest) returns (SearchTracksResponse);
rpc SearchAlbums(SearchRequest) returns (SearchAlbumsResponse);
rpc SearchArtists(SearchRequest) returns (SearchArtistsResponse);
rpc SearchPlaylists(SearchRequest) returns (SearchPlaylistsResponse);
// Retrieval operations
rpc GetTrack(GetRequest) returns (Track);
rpc GetAlbum(GetRequest) returns (Album);
rpc GetArtist(GetRequest) returns (Artist);
rpc GetPlaylist(GetRequest) returns (Playlist);
// Streaming
rpc GetStreamURL(GetRequest) returns (StreamURLResponse);
// Recommendations
rpc GetSimilarTracks(SimilarTracksRequest) returns (SearchTracksResponse);
// Lyrics
rpc GetLyrics(LyricsRequest) returns (LyricsResponse);
rpc GetSyncedLyrics(LyricsRequest) returns (SyncedLyricsResponse);
// Statistics
rpc GetTopTracks(TopRequest) returns (SearchTracksResponse);
rpc GetTopAlbums(TopRequest) returns (SearchAlbumsResponse);
rpc GetTopArtists(TopRequest) returns (SearchArtistsResponse);
// Import
rpc ImportPlaylist(ImportPlaylistRequest) returns (Playlist);
// Service status
rpc GetServiceStatus(Empty) returns (ServiceStatusResponse);
// Authentication
rpc Register(AuthRequest) returns (AuthResponse);
rpc Login(AuthRequest) returns (AuthResponse);
rpc RefreshToken(RefreshTokenRequest) returns (AuthResponse);
}
Total Methods: 23
Method Categories
| Category | Methods | Authentication Required |
|---|---|---|
| Search | 4 | Yes |
| Retrieval | 4 | Yes |
| Streaming | 1 | Yes |
| Recommendations | 1 | Yes |
| Lyrics | 2 | Yes |
| Statistics | 3 | Yes |
| Import | 1 | Yes |
| Service Status | 1 | No |
| Authentication | 3 | No (except RefreshToken) |
Search Operations
SearchTracks
Request:
message SearchRequest {
string query = 1;
int32 limit = 2; // Default: 20, Max: 50
Platform platform = 3; // Optional: filter by platform
}
Response:
message SearchTracksResponse {
repeated Track tracks = 1;
ResponseStatus status = 2;
repeated ProviderError errors = 3;
}
Behavior:
- Queries all enabled providers in parallel
- Aggregates results from all platforms
- Returns partial results if some providers fail
- Results are not deduplicated (same track from multiple platforms appears multiple times)
Example:
resp, err := client.SearchTracks(ctx, &pb.SearchRequest{
Query: "Bohemian Rhapsody",
Limit: 10,
})
// resp.Tracks contains results from Spotify, SoundCloud, Deezer, YouTube Music
// Each track has platform-namespaced ID (e.g., "spotify:track:abc123")
SearchAlbums
Request: Same as SearchTracks
Response:
message SearchAlbumsResponse {
repeated Album albums = 1;
ResponseStatus status = 2;
repeated ProviderError errors = 3;
}
Behavior: Same parallel fan-out pattern as SearchTracks
SearchArtists
Request: Same as SearchTracks
Response:
message SearchArtistsResponse {
repeated Artist artists = 1;
ResponseStatus status = 2;
repeated ProviderError errors = 3;
}
Behavior: Same parallel fan-out pattern as SearchTracks
SearchPlaylists
Request: Same as SearchTracks
Response:
message SearchPlaylistsResponse {
repeated Playlist playlists = 1;
ResponseStatus status = 2;
repeated ProviderError errors = 3;
}
Behavior: Same parallel fan-out pattern as SearchTracks
Retrieval Operations
GetTrack
Request:
message GetRequest {
string id = 1; // Namespaced ID (e.g., "spotify:track:abc123")
}
Response:
message Track {
string id = 1;
string title = 2;
string artist = 3;
string artist_id = 4;
string album = 5;
string album_id = 6;
int32 duration = 7; // Seconds
string cover_url = 8;
int32 year = 9;
string genre = 10;
int64 play_count = 11;
bool explicit = 12;
string isrc = 13;
Platform platform = 14;
}
Behavior:
- Parses namespaced ID to determine platform
- Routes request to specific provider
- Returns single track or error
Example:
track, err := client.GetTrack(ctx, &pb.GetRequest{
Id: "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp",
})
GetAlbum
Request: Same as GetTrack
Response:
message Album {
string id = 1;
string title = 2;
string artist = 3;
string artist_id = 4;
int32 year = 5;
string cover_url = 6;
int32 track_count = 7;
repeated Track tracks = 8;
string genre = 9;
string label = 10;
Platform platform = 11;
}
Behavior:
- Returns album metadata
- Includes full track list in
tracksfield - Track IDs are namespaced to same platform as album
GetArtist
Request: Same as GetTrack
Response:
message Artist {
string id = 1;
string name = 2;
string image_url = 3;
repeated string genres = 4;
int64 followers = 5;
repeated Album albums = 6; // Artist discography
Platform platform = 7;
}
Behavior:
- Returns artist metadata
- Includes full discography in
albumsfield (can be large) - Deezer provider fetches albums concurrently
GetPlaylist
Request: Same as GetTrack
Response:
message Playlist {
string id = 1;
string name = 2;
string description = 3;
string owner = 4;
string cover_url = 5;
int32 track_count = 6;
repeated Track tracks = 7;
bool public = 8;
Platform platform = 9;
}
Behavior:
- Returns playlist metadata
- Includes full track list in
tracksfield - SoundCloud uses batch hydration for track details (30 IDs per request)
Streaming Operations
GetStreamURL
Request:
message GetRequest {
string id = 1; // Track ID (namespaced)
}
Response:
message StreamURLResponse {
string url = 1;
int32 bitrate = 2; // kbps
string format = 3; // mp3, opus, aac, etc.
int32 expires_at = 4; // Unix timestamp
}
Behavior:
- For SoundCloud/YouTube Music: Returns direct stream URL
- For Spotify/Deezer: Searches SoundCloud/YouTube Music for matching track, returns bridged stream URL
- Stream URLs are temporary (expire after 1-6 hours depending on provider)
Stream Resolution Algorithm:
1. Parse platform from namespaced ID
2. If platform is SoundCloud or YouTube Music:
- Call platform's GetStreamURL directly
3. If platform is Spotify or Deezer:
- Get track metadata (artist, title)
- Search SoundCloud for "{artist} - {title}"
- If found, return SoundCloud stream URL
- If not found, search YouTube Music
- If found, return YouTube Music stream URL
- If not found, return error
Example:
// Direct streaming platform
resp, err := client.GetStreamURL(ctx, &pb.GetRequest{
Id: "soundcloud:track:1234567890",
})
// resp.Url = "https://cf-media.sndcdn.com/..."
// Non-streaming platform (bridged)
resp, err := client.GetStreamURL(ctx, &pb.GetRequest{
Id: "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp",
})
// resp.Url = "https://cf-media.sndcdn.com/..." (SoundCloud match)
YouTube Music Stream Selection:
- Tries 7 different client types sequentially
- Prefers itag 251 (opus) > 140 (aac)
- Skips ciphered streams (encrypted, requires decryption)
- Falls back to SoundCloud if all YouTube clients fail
Recommendation Operations
GetSimilarTracks
Request:
message SimilarTracksRequest {
string track_id = 1; // Namespaced track ID
int32 limit = 2; // Default: 20
}
Response: Same as SearchTracksResponse
Behavior:
- Queries provider's recommendation API
- Spotify: Uses "Get Recommendations" endpoint with seed track
- YouTube Music: Uses "Get Watch Playlist" (radio)
- SoundCloud: Uses "Related Tracks" endpoint
- Deezer: No similar tracks API (returns empty)
Example:
resp, err := client.GetSimilarTracks(ctx, &pb.SimilarTracksRequest{
TrackId: "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp",
Limit: 10,
})
// Returns 10 similar tracks from Spotify's recommendation engine
Lyrics Operations
GetLyrics
Request:
message LyricsRequest {
string artist = 1;
string title = 2;
string album = 3; // Optional
int32 duration = 4; // Optional, seconds
}
Response:
message LyricsResponse {
string lyrics = 1; // Plain text
string source = 2; // "genius"
repeated Annotation annotations = 3; // Genius annotations
}
message Annotation {
string fragment = 1; // Lyric fragment
string annotation = 2; // Explanation/context
}
Behavior:
- Queries Genius API
- Returns plain text lyrics
- Includes annotations (explanations of lyric meanings)
- Requires
GENIUS_ACCESS_TOKENenvironment variable
Example:
resp, err := client.GetLyrics(ctx, &pb.LyricsRequest{
Artist: "Queen",
Title: "Bohemian Rhapsody",
})
// resp.Lyrics = "Is this the real life?\nIs this just fantasy?..."
// resp.Annotations contains explanations of lyric meanings
GetSyncedLyrics
Request: Same as GetLyrics
Response:
message SyncedLyricsResponse {
repeated LyricLine lines = 1;
string source = 2; // "lrclib"
}
message LyricLine {
int32 timestamp = 1; // Milliseconds
string text = 2;
}
Behavior:
- Queries LrcLib API
- Returns timestamped lyrics (LRC format)
- Matches by artist, title, album, duration
- 5 second timeout
- No authentication required
Example:
resp, err := client.GetSyncedLyrics(ctx, &pb.LyricsRequest{
Artist: "Queen",
Title: "Bohemian Rhapsody",
Album: "A Night at the Opera",
Duration: 354,
})
// resp.Lines = [
// {Timestamp: 0, Text: "Is this the real life?"},
// {Timestamp: 3500, Text: "Is this just fantasy?"},
// ...
// ]
Statistics Operations
GetTopTracks
Request:
message TopRequest {
Platform platform = 1; // Required
string region = 2; // ISO country code (e.g., "US", "GB")
int32 limit = 3; // Default: 20
}
Response: Same as SearchTracksResponse
Behavior:
- Queries platform's charts/top tracks API
- Spotify: Uses "Get Playlist" on regional top 50 playlists
- YouTube Music: Uses trending charts
- SoundCloud: Uses "Trending" endpoint
- Deezer: Uses "Chart" endpoint
Example:
resp, err := client.GetTopTracks(ctx, &pb.TopRequest{
Platform: pb.Platform_SPOTIFY,
Region: "US",
Limit: 10,
})
// Returns top 10 tracks in US Spotify charts
GetTopAlbums
Request: Same as GetTopTracks
Response: Same as SearchAlbumsResponse
Behavior: Similar to GetTopTracks, queries platform-specific album charts
GetTopArtists
Request: Same as GetTopTracks
Response: Same as SearchArtistsResponse
Behavior: Similar to GetTopTracks, queries platform-specific artist charts
Import Operations
ImportPlaylist
Request:
message ImportPlaylistRequest {
string url = 1; // Playlist URL from any supported platform
Platform target_platform = 2; // Platform to import to
}
Response: Same as Playlist message
Behavior:
- Parses playlist URL to determine source platform
- Fetches playlist tracks from source
- Creates new playlist on target platform
- Matches tracks across platforms (by ISRC or artist+title search)
- Returns created playlist
Example:
resp, err := client.ImportPlaylist(ctx, &pb.ImportPlaylistRequest{
Url: "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M",
TargetPlatform: pb.Platform_SOUNDCLOUD,
})
// Creates SoundCloud playlist with matched tracks from Spotify playlist
Limitations:
- Requires authentication on target platform (not implemented)
- Track matching is best-effort (some tracks may not match)
- No progress reporting for large playlists
Service Status Operations
GetServiceStatus
Request:
message Empty {}
Response:
message ServiceStatusResponse {
ServiceStatus status = 1;
repeated DependencyStatus dependencies = 2;
}
enum ServiceStatus {
HEALTHY = 0;
DEGRADED = 1;
UNHEALTHY = 2;
}
message DependencyStatus {
string name = 1; // Provider name
HealthStatus health = 2;
int32 latency = 3; // Milliseconds
}
enum HealthStatus {
HEALTHY = 0;
UNHEALTHY = 1;
UNKNOWN = 2;
}
Behavior:
- Current: Stub implementation, always returns HEALTHY
- Planned: Ping each provider, measure latency, return actual health status
Example:
resp, err := client.GetServiceStatus(ctx, &pb.Empty{})
// resp.Status = HEALTHY
// resp.Dependencies = [
// {Name: "spotify", Health: HEALTHY, Latency: 0},
// {Name: "soundcloud", Health: HEALTHY, Latency: 0},
// ...
// ]
Authentication Operations
Register
Request:
message AuthRequest {
string email = 1;
string password = 2;
}
Response:
message AuthResponse {
string access_token = 1; // JWT, 15 minute expiry
string refresh_token = 2; // JWT, 7 day expiry
User user = 3;
}
message User {
string id = 1; // UUID
string email = 2;
string role = 3; // "user" or "admin"
bool is_verified = 4;
}
Behavior:
- Validates email format
- Hashes password with bcrypt (cost 10)
- Stores user in PostgreSQL
- Generates JWT access and refresh tokens
- Returns tokens and user info
Validation:
- Email must be valid format
- Email must be unique (returns error if exists)
- No password requirements (any length, no complexity rules)
Example:
resp, err := client.Register(ctx, &pb.AuthRequest{
Email: "user@example.com",
Password: "password123",
})
// resp.AccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
// resp.RefreshToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Login
Request: Same as Register
Response: Same as Register
Behavior:
- Fetches user by email from PostgreSQL
- Verifies password with bcrypt
- Generates new JWT access and refresh tokens
- Returns tokens and user info
Security:
- No rate limiting (brute force possible)
- No account lockout after failed attempts
- No login attempt logging
Example:
resp, err := client.Login(ctx, &pb.AuthRequest{
Email: "user@example.com",
Password: "password123",
})
RefreshToken
Request:
message RefreshTokenRequest {
string refresh_token = 1;
}
Response: Same as AuthResponse
Behavior:
- Validates refresh token signature and expiration
- Extracts user ID and email from token claims
- Generates new access and refresh tokens
- Returns new tokens
Token Rotation: Both access and refresh tokens are regenerated (refresh token rotation).
Example:
resp, err := client.RefreshToken(ctx, &pb.RefreshTokenRequest{
RefreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
})
// resp.AccessToken = new 15-minute token
// resp.RefreshToken = new 7-day token
HTTP Proxy Endpoints
Stream Proxy
Endpoint: GET /stream/{service}/{id}
Parameters:
service: Platform name (spotify, soundcloud, deezer, youtube)id: Native track ID (not namespaced)
Headers:
Range: Optional, for seeking (e.g., "bytes=0-1023")
Response:
200 OK: Full stream206 Partial Content: Range response400 Bad Request: Invalid service or ID404 Not Found: Stream not found500 Internal Server Error: Upstream failure
Behavior:
- Constructs namespaced ID from service and ID
- Calls GetStreamURL gRPC method
- Proxies stream from provider
- Forwards range requests to upstream
- Streams response to client
Example:
curl http://localhost:8080/stream/soundcloud/1234567890 \
-H "Range: bytes=0-1023" \
-o audio.mp3
Cover Proxy
Endpoint: GET /cover/{service}/{id}
Parameters:
service: Platform nameid: Album or track ID
Response: Same status codes as stream proxy
Behavior:
- Fetches album/track metadata
- Extracts cover URL
- Proxies image from provider
- Supports range requests
Example:
curl http://localhost:8080/cover/spotify/3n3Ppam7vgaVa1iaRUc9Lp \
-o cover.jpg
Platform Enum
enum Platform {
SPOTIFY = 0;
YANDEX = 1;
VK = 2;
DEEZER = 3;
SOUNDCLOUD = 4;
YOUTUBE = 5;
}
Active Platforms: SPOTIFY, DEEZER, SOUNDCLOUD, YOUTUBE
Stub Platforms: YANDEX, VK
Response Status Enum
enum ResponseStatus {
OK = 0; // All providers succeeded
PARTIAL = 1; // Some providers failed, some succeeded
ERROR = 2; // All providers failed
}
Usage: All search and multi-provider operations return this status
Client Handling:
switch resp.Status {
case pb.ResponseStatus_OK:
// Use resp.Tracks/Albums/Artists
case pb.ResponseStatus_PARTIAL:
// Use resp.Tracks/Albums/Artists (partial results)
// Log resp.Errors for debugging
case pb.ResponseStatus_ERROR:
// No results available
// Check resp.Errors for failure reasons
}
Error Handling
gRPC Status Codes
| Code | Scenario |
|---|---|
OK |
Successful operation |
Unauthenticated |
Missing or invalid JWT token |
InvalidArgument |
Invalid request parameters |
NotFound |
Entity not found |
Internal |
Server error |
Provider Errors
message ProviderError {
string provider = 1; // "spotify", "soundcloud", etc.
string message = 2; // Error description
}
Included In: All search and multi-provider responses
Example:
resp, err := client.SearchTracks(ctx, &pb.SearchRequest{Query: "test"})
if err != nil {
// gRPC-level error
return err
}
if resp.Status == pb.ResponseStatus_PARTIAL {
for _, providerErr := range resp.Errors {
log.Printf("Provider %s failed: %s", providerErr.Provider, providerErr.Message)
}
}
Authentication Flow
Initial Registration
Client Server
| |
|-- Register(email, password)-->|
| |
| |-- Hash password (bcrypt)
| |-- Store in PostgreSQL
| |-- Generate access token (15min)
| |-- Generate refresh token (7 days)
| |
|<-- access_token, refresh_token|
| |
Authenticated Request
Client Server
| |
|-- SearchTracks(query) ------->|
| + metadata: |
| authorization: Bearer <token>
| |
| |-- authInterceptor validates JWT
| |-- Extract user claims
| |-- Execute search
| |
|<-- SearchTracksResponse ------|
| |
Token Refresh
Client Server
| |
|-- RefreshToken(refresh_token)|
| |
| |-- Validate refresh token
| |-- Extract user claims
| |-- Generate new access token
| |-- Generate new refresh token
| |
|<-- new access_token, refresh_token
| |
Client Implementation Examples
Go Client
package main
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
pb "github.com/feralbureau/bedrock-api/proto"
)
func main() {
conn, err := grpc.Dial("localhost:50052", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := pb.NewBedrockServiceClient(conn)
// Register
authResp, err := client.Register(context.Background(), &pb.AuthRequest{
Email: "user@example.com",
Password: "password123",
})
if err != nil {
log.Fatal(err)
}
accessToken := authResp.AccessToken
// Authenticated request
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer "+accessToken)
searchResp, err := client.SearchTracks(ctx, &pb.SearchRequest{
Query: "Bohemian Rhapsody",
Limit: 10,
})
if err != nil {
log.Fatal(err)
}
for _, track := range searchResp.Tracks {
log.Printf("%s - %s (%s)", track.Artist, track.Title, track.Platform)
}
}
Python Client
import grpc
import bedrock_pb2
import bedrock_pb2_grpc
channel = grpc.insecure_channel('localhost:50052')
client = bedrock_pb2_grpc.BedrockServiceStub(channel)
# Register
auth_resp = client.Register(bedrock_pb2.AuthRequest(
email='user@example.com',
password='password123'
))
access_token = auth_resp.access_token
# Authenticated request
metadata = [('authorization', f'Bearer {access_token}')]
search_resp = client.SearchTracks(
bedrock_pb2.SearchRequest(query='Bohemian Rhapsody', limit=10),
metadata=metadata
)
for track in search_resp.tracks:
print(f"{track.artist} - {track.title} ({track.platform})")
JavaScript Client (Node.js)
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('bedrock_service.proto');
const bedrock = grpc.loadPackageDefinition(packageDefinition).bedrock;
const client = new bedrock.BedrockService('localhost:50052', grpc.credentials.createInsecure());
// Register
client.Register({ email: 'user@example.com', password: 'password123' }, (err, authResp) => {
if (err) throw err;
const accessToken = authResp.access_token;
// Authenticated request
const metadata = new grpc.Metadata();
metadata.add('authorization', `Bearer ${accessToken}`);
client.SearchTracks(
{ query: 'Bohemian Rhapsody', limit: 10 },
metadata,
(err, searchResp) => {
if (err) throw err;
searchResp.tracks.forEach(track => {
console.log(`${track.artist} - ${track.title} (${track.platform})`);
});
}
);
});
Rate Limiting
Current: No rate limiting implemented
Risks:
- Provider API rate limits can be exceeded
- No protection against abuse
- No per-user quotas
Recommendations:
- Implement per-user rate limiting (e.g., 100 requests/minute)
- Implement per-IP rate limiting for unauthenticated endpoints
- Cache responses to reduce provider API calls
- Implement circuit breakers for failing providers
Pagination
Current: No pagination support
Limitations:
- Search results limited by
limitparameter (max 50) - No cursor or offset-based pagination
- Large result sets cannot be retrieved incrementally
Workarounds:
- Increase
limitparameter (up to 50) - Make multiple searches with different queries
Recommendations:
- Add cursor-based pagination for search results
- Add offset/limit pagination for playlists and albums
- Return total result count in responses
Versioning
Current: No API versioning
Implications:
- Breaking changes affect all clients
- No backward compatibility guarantees
- No deprecation path for old endpoints
Recommendations:
- Add version to package name (e.g.,
bedrock.v1) - Support multiple versions simultaneously
- Document breaking changes and migration paths
Performance Characteristics
Response Times (Typical)
| Operation | Latency | Notes |
|---|---|---|
| SearchTracks | 200-500ms | Parallel provider queries |
| GetTrack | 100-300ms | Single provider query |
| GetStreamURL | 200-800ms | Includes bridge resolution |
| GetLyrics | 1-3s | Genius API can be slow |
| GetSyncedLyrics | 100-500ms | LrcLib is fast |
| Register/Login | 100-200ms | bcrypt hashing overhead |
Payload Sizes (Typical)
| Operation | Response Size | Notes |
|---|---|---|
| SearchTracks (10 results) | 5-10 KB | Depends on metadata richness |
| GetAlbum (with tracks) | 20-100 KB | Depends on track count |
| GetArtist (with discography) | 50-500 KB | Can be very large |
| GetPlaylist (100 tracks) | 50-100 KB | Includes full track metadata |
| GetLyrics | 2-10 KB | Plain text |
| GetSyncedLyrics | 5-20 KB | Timestamped lines |
Security Considerations
Authentication
- JWT tokens transmitted in gRPC metadata
- No TLS by default (tokens sent in plaintext)
- No token revocation mechanism
- No refresh token rotation (fixed 7-day expiry)
Authorization
- No role-based access control (RBAC)
- All authenticated users have same permissions
- No resource ownership checks
- No admin-only endpoints
Input Validation
- No query sanitization (SQL injection risk if queries touch DB)
- No limit enforcement on request parameters
- No URL validation for ImportPlaylist
Recommendations
- Deploy behind TLS-terminating reverse proxy
- Implement token revocation list (Redis)
- Add RBAC for admin operations
- Validate and sanitize all inputs
- Add request size limits