# 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 ```protobuf 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**: ```protobuf message SearchRequest { string query = 1; int32 limit = 2; // Default: 20, Max: 50 Platform platform = 3; // Optional: filter by platform } ``` **Response**: ```protobuf 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**: ```go 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**: ```protobuf 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**: ```protobuf 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**: ```protobuf 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**: ```protobuf message GetRequest { string id = 1; // Namespaced ID (e.g., "spotify:track:abc123") } ``` **Response**: ```protobuf 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**: ```go track, err := client.GetTrack(ctx, &pb.GetRequest{ Id: "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp", }) ``` ### GetAlbum **Request**: Same as GetTrack **Response**: ```protobuf 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 `tracks` field - Track IDs are namespaced to same platform as album ### GetArtist **Request**: Same as GetTrack **Response**: ```protobuf 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 `albums` field (can be large) - Deezer provider fetches albums concurrently ### GetPlaylist **Request**: Same as GetTrack **Response**: ```protobuf 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 `tracks` field - SoundCloud uses batch hydration for track details (30 IDs per request) ## Streaming Operations ### GetStreamURL **Request**: ```protobuf message GetRequest { string id = 1; // Track ID (namespaced) } ``` **Response**: ```protobuf 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**: ```go // 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**: ```protobuf 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**: ```go 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**: ```protobuf message LyricsRequest { string artist = 1; string title = 2; string album = 3; // Optional int32 duration = 4; // Optional, seconds } ``` **Response**: ```protobuf 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_TOKEN` environment variable **Example**: ```go 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**: ```protobuf 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**: ```go 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**: ```protobuf 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**: ```go 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**: ```protobuf 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**: ```go 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**: ```protobuf message Empty {} ``` **Response**: ```protobuf 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**: ```go 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**: ```protobuf message AuthRequest { string email = 1; string password = 2; } ``` **Response**: ```protobuf 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**: ```go 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**: ```go resp, err := client.Login(ctx, &pb.AuthRequest{ Email: "user@example.com", Password: "password123", }) ``` ### RefreshToken **Request**: ```protobuf 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**: ```go 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 stream - `206 Partial Content`: Range response - `400 Bad Request`: Invalid service or ID - `404 Not Found`: Stream not found - `500 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**: ```bash 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 name - `id`: 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**: ```bash curl http://localhost:8080/cover/spotify/3n3Ppam7vgaVa1iaRUc9Lp \ -o cover.jpg ``` ## Platform Enum ```protobuf 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 ```protobuf 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**: ```go 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 ```protobuf message ProviderError { string provider = 1; // "spotify", "soundcloud", etc. string message = 2; // Error description } ``` **Included In**: All search and multi-provider responses **Example**: ```go 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 | | | |-- 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 ```go 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 ```python 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) ```javascript 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 `limit` parameter (max 50) - No cursor or offset-based pagination - Large result sets cannot be retrieved incrementally **Workarounds**: - Increase `limit` parameter (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