a1f6701bac
- 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
1084 lines
25 KiB
Markdown
1084 lines
25 KiB
Markdown
# 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 <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
|
|
|
|
```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
|