Files
metadata-agregator/docs/research/melodee/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

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:

  1. Native REST API (/api/v1/): Modern RESTful design with JWT authentication
  2. OpenSubsonic API (/rest/): Subsonic-compatible endpoints with token/salt authentication
  3. 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 lookups
  • email: User email for display and logging
  • role: Authorization level (admin or user)
  • iat (issued at): Token creation timestamp
  • exp (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, PATCH
  • 201 Created: Successful POST
  • 204 No Content: Successful DELETE
  • 400 Bad Request: Invalid request payload
  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: Insufficient permissions
  • 404 Not Found: Resource not found
  • 409 Conflict: Resource conflict (duplicate playlist name)
  • 422 Unprocessable Entity: Validation errors
  • 429 Too Many Requests: Rate limit exceeded
  • 500 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 albums
  • newest: Recently added albums
  • highest: Highest rated albums
  • frequent: Most frequently played albums
  • recent: Recently played albums
  • alphabeticalByName: Alphabetically by album name
  • alphabeticalByArtist: Alphabetically by artist name
  • starred: User-starred albums
  • byYear: Albums by release year (requires fromYear and toYear parameters)
  • byGenre: Albums by genre (requires genre parameter)

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=300 returns 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 ID
  • time: Unix timestamp in milliseconds (optional, defaults to current time)
  • submission: true for scrobble, false for "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 error
  • 10: Required parameter missing
  • 20: Incompatible Subsonic REST protocol version
  • 30: Incompatible Subsonic REST protocol version (server too old)
  • 40: Wrong username or password
  • 41: Token authentication not supported for LDAP users
  • 50: User not authorized for operation
  • 60: Trial period over
  • 70: 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