- 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
29 KiB
Melodee: API Analysis
API Strategy Overview
Melodee implements three distinct API protocols to maximize client compatibility while maintaining a modern native API. This multi-protocol approach allows users to choose from existing client ecosystems (Subsonic, Jellyfin) or build custom integrations using the native REST API.
The three protocols are:
- Native REST API (
/api/v1/): Modern RESTful design with JWT authentication - OpenSubsonic API (
/rest/): Subsonic-compatible endpoints with token/salt authentication - Jellyfin API (
/api/jf/): Jellyfin-compatible endpoints with custom token authentication
Each protocol has distinct authentication mechanisms, rate limits, and client expectations. This analysis examines the design, implementation, and tradeoffs of each protocol.
Native REST API
Design Philosophy
The native API follows RESTful principles with resource-oriented URLs, standard HTTP methods, and JSON payloads. The design prioritizes:
- Discoverability: Scalar-generated documentation with interactive testing
- Consistency: Uniform response formats and error handling
- Extensibility: Versioned endpoints (
/api/v1/) for backward compatibility - Security: JWT-based stateless authentication
Authentication
JWT Token Flow
1. Client POSTs credentials to /api/v1/auth/login
Request: { "email": "user@example.com", "password": "secret" }
2. Server validates credentials and generates JWT
Response: { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expiresAt": "2025-04-29T10:30:00Z" }
3. Client includes token in Authorization header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
4. Server validates token signature and expiration on each request
JWT Claims
{
"sub": "42",
"email": "user@example.com",
"role": "user",
"iat": 1714301400,
"exp": 1714387800
}
Claims Breakdown:
sub(subject): User ID for database lookupsemail: User email for display and loggingrole: Authorization level (adminoruser)iat(issued at): Token creation timestampexp(expiration): Token expiry timestamp (24 hours from issue)
Token Refresh
The API does not implement refresh tokens. Clients must re-authenticate after token expiration. This simplifies implementation but creates friction for long-running sessions.
Alternative Design:
POST /api/v1/auth/refresh
Authorization: Bearer <expired-token>
Response: { "token": "new-token", "expiresAt": "..." }
Refresh tokens would require server-side storage (database or Redis) to track revocation, adding complexity.
Google OAuth Integration
1. Client redirects user to /api/v1/auth/google
2. Server redirects to Google OAuth consent screen
3. User grants permissions
4. Google redirects to /api/v1/auth/google/callback?code=...
5. Server exchanges code for access token
6. Server retrieves user profile from Google
7. Server creates or updates local user account
8. Server generates JWT and redirects to client with token
OAuth flow eliminates password management for users and delegates security to Google. The server stores minimal Google data (email, profile picture URL) and generates a local JWT for subsequent requests.
Endpoints
Library Endpoints
List Albums
GET /api/v1/albums?page=1&pageSize=20&sort=title&order=asc&genre=Rock
Response:
{
"items": [
{
"id": 42,
"title": "OK Computer",
"artistId": 10,
"artistName": "Radiohead",
"releaseDate": "1997-05-21",
"genre": "Alternative Rock",
"trackCount": 12,
"duration": 3213,
"coverArtUrl": "/api/v1/albums/42/cover",
"externalIds": {
"musicbrainz": "6a09041b-0f79-3278-88d0-0c07e1d078e0",
"spotify": "6dVIqQ8qmQ5GBnJ9shOYGE"
}
}
],
"totalCount": 150,
"page": 1,
"pageSize": 20
}
Get Album Details
GET /api/v1/albums/42
Response:
{
"id": 42,
"title": "OK Computer",
"artist": {
"id": 10,
"name": "Radiohead",
"bio": "English rock band formed in 1985...",
"imageUrl": "/api/v1/artists/10/image"
},
"releaseDate": "1997-05-21",
"genre": "Alternative Rock",
"tracks": [
{
"id": 420,
"position": 1,
"title": "Airbag",
"duration": 284,
"fileFormat": "FLAC",
"bitrate": 1411,
"sampleRate": 44100
}
],
"coverArtUrl": "/api/v1/albums/42/cover",
"externalIds": { ... }
}
Search
GET /api/v1/search?q=radiohead&type=album,artist,track
Response:
{
"albums": [ ... ],
"artists": [ ... ],
"tracks": [ ... ]
}
Search supports multiple entity types in a single request. The type parameter filters results. Omitting type returns all entity types.
Streaming Endpoints
Stream Track
GET /api/v1/stream/420?bitrate=320&format=mp3
Response: Audio stream (Content-Type: audio/mpeg)
Headers:
Content-Length: 8421376
Accept-Ranges: bytes
Content-Range: bytes 0-8421375/8421376
Transcoding Parameters:
bitrate: Target bitrate in kbps (128, 192, 256, 320)format: Target format (mp3, ogg, opus, aac)
If the source file matches the requested bitrate and format, the server streams the original file. Otherwise, FFmpeg transcodes on-the-fly.
Range Requests:
GET /api/v1/stream/420
Range: bytes=1000000-2000000
Response: Partial content (206)
Content-Range: bytes 1000000-2000000/8421376
Range requests enable seeking in audio players without downloading the entire file.
Playlist Endpoints
Create Playlist
POST /api/v1/playlists
{
"name": "Favorites",
"isPublic": false,
"isSmart": false,
"trackIds": [420, 421, 422]
}
Response:
{
"id": 5,
"name": "Favorites",
"userId": 42,
"isPublic": false,
"isSmart": false,
"trackCount": 3,
"duration": 856,
"createdAt": "2025-04-28T10:30:00Z"
}
Create Smart Playlist
POST /api/v1/playlists
{
"name": "Recently Added Jazz",
"isPublic": true,
"isSmart": true,
"smartQuery": "genre:Jazz AND added:>7d"
}
Smart playlists use MQL (Melodee Query Language) to define dynamic track lists. The server evaluates the query on each request, returning current matches.
Update Playlist
PUT /api/v1/playlists/5
{
"name": "Top Favorites",
"trackIds": [420, 421, 422, 423]
}
Add Tracks to Playlist
POST /api/v1/playlists/5/tracks
{
"trackIds": [424, 425]
}
Remove Tracks from Playlist
DELETE /api/v1/playlists/5/tracks
{
"trackIds": [421]
}
Reorder Tracks
PUT /api/v1/playlists/5/tracks/reorder
{
"trackId": 420,
"newPosition": 3
}
Scrobbling Endpoints
Submit Scrobble
POST /api/v1/scrobble
{
"trackId": 420,
"playedAt": "2025-04-28T10:30:00Z"
}
Response: 204 No Content
Scrobbles are submitted asynchronously. The server queues the scrobble for batch submission to Last.fm and immediate insertion into the internal scrobble database.
Get Scrobble History
GET /api/v1/scrobbles?page=1&pageSize=50
Response:
{
"items": [
{
"id": 1000,
"trackId": 420,
"trackTitle": "Airbag",
"artistName": "Radiohead",
"albumTitle": "OK Computer",
"playedAt": "2025-04-28T10:30:00Z"
}
],
"totalCount": 5000,
"page": 1,
"pageSize": 50
}
Statistics Endpoints
Get Listening Statistics
GET /api/v1/stats/listening?period=30d
Response:
{
"totalPlays": 450,
"totalDuration": 82800,
"uniqueTracks": 120,
"uniqueAlbums": 35,
"uniqueArtists": 28,
"topTracks": [
{
"trackId": 420,
"title": "Airbag",
"artistName": "Radiohead",
"playCount": 15
}
],
"topAlbums": [ ... ],
"topArtists": [ ... ],
"topGenres": [ ... ]
}
Get Charts
GET /api/v1/charts?type=weekly&category=tracks
Response:
{
"period": "2025-04-21 to 2025-04-28",
"items": [
{
"rank": 1,
"trackId": 420,
"title": "Airbag",
"artistName": "Radiohead",
"playCount": 15,
"change": 2
}
]
}
The change field indicates rank movement from the previous period (positive = up, negative = down, 0 = no change).
User Request Endpoints
Submit Album Request
POST /api/v1/requests
{
"type": "album",
"artist": "Radiohead",
"title": "In Rainbows",
"notes": "Missing from library"
}
Response:
{
"id": 10,
"type": "album",
"artist": "Radiohead",
"title": "In Rainbows",
"notes": "Missing from library",
"status": "pending",
"userId": 42,
"createdAt": "2025-04-28T10:30:00Z"
}
List Requests
GET /api/v1/requests?status=pending
Response:
{
"items": [
{
"id": 10,
"type": "album",
"artist": "Radiohead",
"title": "In Rainbows",
"status": "pending",
"userId": 42,
"userName": "user@example.com",
"createdAt": "2025-04-28T10:30:00Z"
}
]
}
Update Request Status (Admin only)
PUT /api/v1/requests/10
{
"status": "fulfilled",
"adminNotes": "Added to library"
}
Admin Endpoints
List Users (Admin only)
GET /api/v1/admin/users
Response:
{
"items": [
{
"id": 42,
"email": "user@example.com",
"role": "user",
"createdAt": "2025-01-01T00:00:00Z",
"lastLoginAt": "2025-04-28T10:00:00Z",
"scrobbleCount": 5000,
"playlistCount": 12
}
]
}
Create User (Admin only)
POST /api/v1/admin/users
{
"email": "newuser@example.com",
"password": "temporary",
"role": "user"
}
Update User Role (Admin only)
PUT /api/v1/admin/users/42
{
"role": "admin"
}
Trigger Library Scan (Admin only)
POST /api/v1/admin/library/scan
Response: 202 Accepted
{
"jobId": "scan-2025-04-28-103000",
"status": "queued"
}
Get Job Status (Admin only)
GET /api/v1/admin/jobs/scan-2025-04-28-103000
Response:
{
"jobId": "scan-2025-04-28-103000",
"status": "running",
"progress": 45,
"startedAt": "2025-04-28T10:30:00Z",
"message": "Processing album 450 of 1000"
}
Rate Limiting
Native API Rate Limits:
- General endpoints: 30 requests per 30 seconds per user
- Authentication endpoints: 10 requests per 60 seconds per IP address
- Streaming endpoints: No rate limit (bandwidth-limited by network)
Rate Limit Headers:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 25
X-RateLimit-Reset: 1714301430
Rate Limit Exceeded Response:
HTTP/1.1 429 Too Many Requests
Retry-After: 15
{
"error": "Rate limit exceeded",
"message": "You have exceeded the rate limit of 30 requests per 30 seconds",
"retryAfter": 15
}
Error Handling
Standard Error Response:
{
"error": "NotFound",
"message": "Album with ID 999 not found",
"details": {
"albumId": 999
},
"timestamp": "2025-04-28T10:30:00Z",
"path": "/api/v1/albums/999"
}
HTTP Status Codes:
200 OK: Successful GET, PUT, PATCH201 Created: Successful POST204 No Content: Successful DELETE400 Bad Request: Invalid request payload401 Unauthorized: Missing or invalid authentication403 Forbidden: Insufficient permissions404 Not Found: Resource not found409 Conflict: Resource conflict (duplicate playlist name)422 Unprocessable Entity: Validation errors429 Too Many Requests: Rate limit exceeded500 Internal Server Error: Server error
Validation Error Response:
{
"error": "ValidationError",
"message": "Request validation failed",
"errors": [
{
"field": "email",
"message": "Email is required"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
]
}
Pagination
Query Parameters:
page: Page number (1-indexed)pageSize: Items per page (default: 20, max: 100)
Response Format:
{
"items": [ ... ],
"totalCount": 150,
"page": 1,
"pageSize": 20,
"totalPages": 8
}
Link Headers (optional):
Link: </api/v1/albums?page=2&pageSize=20>; rel="next",
</api/v1/albums?page=8&pageSize=20>; rel="last"
Versioning
The /api/v1/ prefix enables backward-compatible API evolution. Future versions (/api/v2/) can introduce breaking changes while maintaining v1 support.
Version Negotiation (future):
GET /api/albums
Accept: application/vnd.melodee.v2+json
Response: v2 format
API Documentation
Scalar generates interactive API documentation from OpenAPI specifications. The documentation is available at /api/docs and includes:
- Endpoint reference: All endpoints with request/response schemas
- Authentication guide: JWT and OAuth flows
- Interactive testing: Try API calls directly in the browser
- Code examples: cURL, JavaScript, Python, C# examples
OpenAPI Specification (excerpt):
openapi: 3.0.0
info:
title: Melodee API
version: 1.0.0
description: Music server and metadata aggregator API
paths:
/api/v1/albums:
get:
summary: List albums
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: pageSize
in: query
schema:
type: integer
default: 20
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/AlbumListResponse'
OpenSubsonic API
Protocol Overview
OpenSubsonic is an open-source continuation of the Subsonic API, maintained by the community after Subsonic's commercialization. The protocol uses query parameters for all requests and XML or JSON responses.
Melodee implements OpenSubsonic to support existing Subsonic clients:
- DSub (Android)
- Ultrasonic (Android)
- Sublime Music (Linux desktop)
- Jamstash (Web)
- Sonixd (Desktop)
Authentication
Token and Salt Method:
1. Client generates random salt (e.g., "abc123")
2. Client computes token = MD5(password + salt)
3. Client sends username, token, and salt in query parameters
GET /rest/ping.view?u=user&t=5f4dcc3b5aa765d61d8327deb882cf99&s=abc123&v=1.16.1&c=DSub
Server Validation:
public bool ValidateToken(string username, string token, string salt)
{
var user = _userRepository.GetByUsername(username);
if (user == null)
return false;
var expectedToken = MD5Hash(user.Password + salt);
return token == expectedToken;
}
Security Considerations:
- Token is vulnerable to replay attacks without HTTPS
- MD5 is cryptographically weak but sufficient for this use case (password is already hashed)
- Salt prevents precomputed rainbow table attacks
Legacy Password Method (deprecated):
GET /rest/ping.view?u=user&p=password&v=1.16.1&c=DSub
Melodee supports this for backward compatibility but logs a warning encouraging token/salt authentication.
Endpoints
System Endpoints
Ping
GET /rest/ping.view?u=user&t=token&s=salt&v=1.16.1&c=DSub
Response (XML):
<?xml version="1.0" encoding="UTF-8"?>
<subsonic-response status="ok" version="1.16.1">
</subsonic-response>
Response (JSON):
{
"subsonic-response": {
"status": "ok",
"version": "1.16.1"
}
}
Get License
GET /rest/getLicense.view
Response:
<subsonic-response status="ok" version="1.16.1">
<license valid="true" email="opensource@melodee.dev" licenseExpires="2099-12-31T23:59:59Z" />
</subsonic-response>
OpenSubsonic clients expect a license response even though the protocol is open-source. Melodee returns a perpetual license.
Browsing Endpoints
Get Music Folders
GET /rest/getMusicFolders.view
Response:
<subsonic-response status="ok" version="1.16.1">
<musicFolders>
<musicFolder id="1" name="Music" />
</musicFolders>
</subsonic-response>
Melodee presents a single music folder representing the entire library. Multi-folder support could be added for users with separate libraries (e.g., "Music", "Podcasts").
Get Indexes
GET /rest/getIndexes.view?musicFolderId=1
Response:
<subsonic-response status="ok" version="1.16.1">
<indexes lastModified="1714301400000">
<index name="A">
<artist id="10" name="Arcade Fire" albumCount="5" />
<artist id="11" name="Arctic Monkeys" albumCount="6" />
</index>
<index name="R">
<artist id="20" name="Radiohead" albumCount="9" />
</index>
</indexes>
</subsonic-response>
Artists are grouped alphabetically for efficient browsing in clients with index navigation.
Get Artist
GET /rest/getArtist.view?id=20
Response:
<subsonic-response status="ok" version="1.16.1">
<artist id="20" name="Radiohead" albumCount="9" coverArt="ar-20">
<album id="42" name="OK Computer" artist="Radiohead" artistId="20" coverArt="al-42" songCount="12" duration="3213" created="2025-01-01T00:00:00Z" year="1997" genre="Alternative Rock" />
</artist>
</subsonic-response>
Get Album
GET /rest/getAlbum.view?id=42
Response:
<subsonic-response status="ok" version="1.16.1">
<album id="42" name="OK Computer" artist="Radiohead" artistId="20" coverArt="al-42" songCount="12" duration="3213" created="2025-01-01T00:00:00Z" year="1997" genre="Alternative Rock">
<song id="420" parent="42" title="Airbag" album="OK Computer" artist="Radiohead" track="1" year="1997" genre="Alternative Rock" coverArt="al-42" size="8421376" contentType="audio/flac" suffix="flac" duration="284" bitRate="1411" path="Radiohead/OK Computer/01 Airbag.flac" />
</album>
</subsonic-response>
Album List Endpoints
Get Album List 2
GET /rest/getAlbumList2.view?type=recent&size=20&offset=0
Response:
<subsonic-response status="ok" version="1.16.1">
<albumList2>
<album id="42" name="OK Computer" artist="Radiohead" artistId="20" coverArt="al-42" songCount="12" duration="3213" created="2025-04-28T10:00:00Z" year="1997" genre="Alternative Rock" />
</albumList2>
</subsonic-response>
Album List Types:
random: Random albumsnewest: Recently added albumshighest: Highest rated albumsfrequent: Most frequently played albumsrecent: Recently played albumsalphabeticalByName: Alphabetically by album namealphabeticalByArtist: Alphabetically by artist namestarred: User-starred albumsbyYear: Albums by release year (requiresfromYearandtoYearparameters)byGenre: Albums by genre (requiresgenreparameter)
Streaming Endpoints
Stream
GET /rest/stream.view?id=420&maxBitRate=320&format=mp3
Response: Audio stream
Download
GET /rest/download.view?id=420
Response: Original audio file
The download endpoint returns the original file without transcoding, while stream applies transcoding based on maxBitRate and format parameters.
Get Cover Art
GET /rest/getCoverArt.view?id=al-42&size=300
Response: JPEG image
Size Parameter:
- Omitted: Original size
- Numeric: Resize to NxN pixels (e.g.,
size=300returns 300x300)
Playlist Endpoints
Get Playlists
GET /rest/getPlaylists.view
Response:
<subsonic-response status="ok" version="1.16.1">
<playlists>
<playlist id="5" name="Favorites" songCount="50" duration="12000" public="false" owner="user@example.com" created="2025-01-01T00:00:00Z" changed="2025-04-28T10:00:00Z" />
</playlists>
</subsonic-response>
Get Playlist
GET /rest/getPlaylist.view?id=5
Response:
<subsonic-response status="ok" version="1.16.1">
<playlist id="5" name="Favorites" songCount="50" duration="12000" public="false" owner="user@example.com" created="2025-01-01T00:00:00Z" changed="2025-04-28T10:00:00Z">
<entry id="420" parent="42" title="Airbag" ... />
</playlist>
</subsonic-response>
Create Playlist
GET /rest/createPlaylist.view?name=New+Playlist&songId=420&songId=421
Response:
<subsonic-response status="ok" version="1.16.1">
<playlist id="6" name="New Playlist" songCount="2" ... />
</subsonic-response>
Update Playlist
GET /rest/updatePlaylist.view?playlistId=5&songIdToAdd=422&songIndexToRemove=0
Response:
<subsonic-response status="ok" version="1.16.1" />
Delete Playlist
GET /rest/deletePlaylist.view?id=5
Response:
<subsonic-response status="ok" version="1.16.1" />
Search Endpoints
Search 3
GET /rest/search3.view?query=radiohead&artistCount=10&albumCount=20&songCount=50
Response:
<subsonic-response status="ok" version="1.16.1">
<searchResult3>
<artist id="20" name="Radiohead" albumCount="9" />
<album id="42" name="OK Computer" artist="Radiohead" ... />
<song id="420" title="Airbag" artist="Radiohead" ... />
</searchResult3>
</subsonic-response>
Scrobbling Endpoints
Scrobble
GET /rest/scrobble.view?id=420&time=1714301400000&submission=true
Response:
<subsonic-response status="ok" version="1.16.1" />
Parameters:
id: Track IDtime: Unix timestamp in milliseconds (optional, defaults to current time)submission:truefor scrobble,falsefor "now playing" update
User Endpoints
Get User
GET /rest/getUser.view?username=user
Response:
<subsonic-response status="ok" version="1.16.1">
<user username="user" email="user@example.com" scrobblingEnabled="true" adminRole="false" settingsRole="true" downloadRole="true" uploadRole="false" playlistRole="true" coverArtRole="true" commentRole="true" podcastRole="true" streamRole="true" jukeboxRole="false" shareRole="true" />
</subsonic-response>
OpenSubsonic defines granular roles for different capabilities. Melodee simplifies this to two roles (admin and user) and maps them to OpenSubsonic roles.
Response Formats
XML (default):
<?xml version="1.0" encoding="UTF-8"?>
<subsonic-response status="ok" version="1.16.1">
<album id="42" name="OK Computer" ... />
</subsonic-response>
JSON (with f=json parameter):
{
"subsonic-response": {
"status": "ok",
"version": "1.16.1",
"album": {
"id": "42",
"name": "OK Computer"
}
}
}
Error Response:
<subsonic-response status="failed" version="1.16.1">
<error code="40" message="Wrong username or password" />
</subsonic-response>
Error Codes:
0: Generic error10: Required parameter missing20: Incompatible Subsonic REST protocol version30: Incompatible Subsonic REST protocol version (server too old)40: Wrong username or password41: Token authentication not supported for LDAP users50: User not authorized for operation60: Trial period over70: Requested data not found
Rate Limiting
OpenSubsonic endpoints inherit the native API rate limit (30 requests per 30 seconds). This may be too restrictive for some clients that make frequent requests for album art or metadata.
Potential Optimization:
- Increase rate limit for read-only endpoints (
getAlbum,getCoverArt) - Implement separate rate limits for streaming vs. metadata endpoints
- Use token bucket algorithm for burst tolerance
Jellyfin API
Protocol Overview
Jellyfin is a free, open-source media server (fork of Emby). Melodee implements a subset of the Jellyfin API to support Jellyfin clients:
- Jellyfin Web (Browser)
- Jellyfin Mobile (iOS, Android)
- Jellyfin Desktop (Windows, macOS, Linux)
- Jellyfin for Kodi (Kodi plugin)
Authentication
Authentication Flow:
1. Client POSTs credentials to /api/jf/Users/AuthenticateByName
Request:
{
"Username": "user@example.com",
"Pw": "password"
}
2. Server validates credentials and generates token
Response:
{
"User": {
"Id": "42",
"Name": "user@example.com",
"ServerId": "melodee-1"
},
"AccessToken": "a1b2c3d4e5f6",
"SessionInfo": {
"Id": "session-123"
}
}
3. Client includes token in X-Emby-Token header
X-Emby-Token: a1b2c3d4e5f6
Token Storage: Tokens are stored in the database with expiration (30 days). Unlike JWT, Jellyfin tokens require server-side storage for validation and revocation.
Endpoints
System Endpoints
Get System Info
GET /api/jf/System/Info
Response:
{
"ServerName": "Melodee",
"Version": "1.8.0",
"Id": "melodee-1",
"OperatingSystem": "Linux",
"HasPendingRestart": false,
"IsShuttingDown": false
}
Get Public System Info
GET /api/jf/System/Info/Public
Response:
{
"ServerName": "Melodee",
"Version": "1.8.0",
"Id": "melodee-1"
}
Library Endpoints
Get Items
GET /api/jf/Items?ParentId=0&IncludeItemTypes=MusicAlbum&Limit=20&StartIndex=0
Response:
{
"Items": [
{
"Id": "42",
"Name": "OK Computer",
"Type": "MusicAlbum",
"AlbumArtist": "Radiohead",
"ProductionYear": 1997,
"Genres": ["Alternative Rock"],
"RunTimeTicks": 32130000000,
"ImageTags": {
"Primary": "al-42"
}
}
],
"TotalRecordCount": 150,
"StartIndex": 0
}
Get Item
GET /api/jf/Items/42
Response:
{
"Id": "42",
"Name": "OK Computer",
"Type": "MusicAlbum",
"AlbumArtist": "Radiohead",
"ProductionYear": 1997,
"Genres": ["Alternative Rock"],
"RunTimeTicks": 32130000000,
"ChildCount": 12,
"ImageTags": {
"Primary": "al-42"
}
}
Ticks Conversion: Jellyfin uses "ticks" for duration (1 tick = 100 nanoseconds).
Duration in seconds = Ticks / 10,000,000
3213 seconds = 32,130,000,000 ticks
Streaming Endpoints
Get Audio Stream
GET /api/jf/Audio/420/stream?Container=mp3&AudioBitrate=320000
Response: Audio stream
Get Image
GET /api/jf/Items/42/Images/Primary?maxWidth=300&maxHeight=300
Response: JPEG image
Playback Reporting
Report Playback Start
POST /api/jf/Sessions/Playing
{
"ItemId": "420",
"PositionTicks": 0,
"IsPaused": false
}
Response: 204 No Content
Report Playback Progress
POST /api/jf/Sessions/Playing/Progress
{
"ItemId": "420",
"PositionTicks": 50000000,
"IsPaused": false
}
Response: 204 No Content
Report Playback Stopped
POST /api/jf/Sessions/Playing/Stopped
{
"ItemId": "420",
"PositionTicks": 284000000
}
Response: 204 No Content
Jellyfin clients report playback progress frequently (every 10 seconds). This explains the higher rate limit (200 requests per 60 seconds) for Jellyfin endpoints.
Rate Limiting
Jellyfin API Rate Limit: 200 requests per 60 seconds per user
The higher limit accommodates frequent playback progress updates. Without this, clients would hit rate limits during normal playback.
API Comparison
| Feature | Native REST | OpenSubsonic | Jellyfin |
|---|---|---|---|
| Authentication | JWT | Token + Salt | Custom Token |
| Request Format | JSON body | Query params | JSON body |
| Response Format | JSON | XML/JSON | JSON |
| Rate Limit | 30/30s | 30/30s | 200/60s |
| Versioning | /api/v1/ |
Version param | No versioning |
| Documentation | Scalar | Community docs | Jellyfin docs |
| Client Ecosystem | Custom | Subsonic clients | Jellyfin clients |
| Streaming | Range requests | Basic | Advanced (HLS, DASH) |
| Scrobbling | Native | Native | Via plugins |
API Design Tradeoffs
Multi-Protocol Complexity
Advantages:
- Broad client compatibility without custom client development
- Users can choose clients based on platform and preferences
- Existing client ecosystems provide mature, tested implementations
Disadvantages:
- Three authentication systems to maintain and secure
- Three sets of endpoints with overlapping functionality
- Increased testing surface area
- Potential for inconsistencies between protocols
REST vs. GraphQL
Melodee uses REST for all three protocols. GraphQL could reduce over-fetching and under-fetching:
REST:
GET /api/v1/albums/42 # Fetch album
GET /api/v1/albums/42/tracks # Fetch tracks separately
GraphQL:
query {
album(id: 42) {
title
artist { name }
tracks {
title
duration
}
}
}
GraphQL would benefit the native API but complicate OpenSubsonic and Jellyfin compatibility.
Pagination Strategies
Offset-based (current):
GET /api/v1/albums?page=5&pageSize=20
Advantages: Simple, stateless, random access Disadvantages: Performance degrades with large offsets, inconsistent results if data changes
Cursor-based (alternative):
GET /api/v1/albums?after=cursor123&limit=20
Advantages: Consistent results, better performance Disadvantages: No random access, more complex implementation
For music libraries (relatively static data), offset-based pagination is sufficient.
Caching Headers
The API should include caching headers to reduce server load:
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Mon, 28 Apr 2025 10:30:00 GMT
Clients can use If-None-Match and If-Modified-Since headers to avoid re-downloading unchanged data.
Conclusion
Melodee's multi-protocol API strategy maximizes client compatibility while maintaining a modern native API. The three protocols serve distinct purposes:
- Native REST API: Custom integrations, modern clients, full feature access
- OpenSubsonic API: Subsonic client ecosystem, broad platform support
- Jellyfin API: Jellyfin client ecosystem, advanced streaming features
The architecture demonstrates thoughtful API design with consistent error handling, comprehensive documentation, and appropriate rate limiting. The main tradeoff is increased complexity from maintaining three protocols, but this is justified by the broad client compatibility it enables.
Future improvements could include:
- GraphQL endpoint for flexible querying
- WebSocket API for real-time updates
- Cursor-based pagination for large result sets
- Enhanced caching with ETags and conditional requests
- API versioning strategy for breaking changes