Files
metadata-agregator/docs/research/bedrock-api/analysis/API.md
T
Alexander a1f6701bac feat: initial implementation of metadata aggregator
- 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
2026-04-28 16:28:53 +02:00

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