# Meelo API Reference ## API Overview Meelo exposes REST APIs for all services. The primary API is the Server (NestJS) at port 4000, accessed via `/api/*` through Nginx. Scanner and Matcher have smaller APIs for administrative tasks. **Base URL**: `http://localhost/api` (via Nginx) or `http://localhost:4000` (direct) **Authentication**: JWT Bearer tokens or API keys **Content Type**: `application/json` **Documentation**: Swagger UI at `/api/docs` ## Authentication ### JWT Authentication Most endpoints require JWT authentication. Obtain a token via login: **POST /api/auth/login** ```json { "username": "admin", "password": "password" } ``` Response: ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": 1, "username": "admin", "isAdmin": true } } ``` Include token in subsequent requests: ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` ### API Key Authentication Scanner and Matcher use API keys for internal communication: ``` x-api-key: ``` ### Anonymous Access If `ALLOW_ANONYMOUS=1` in .env, authentication is optional. All endpoints accessible without tokens. ## Auth Endpoints ### Register User **POST /api/auth/register** Create new user account. Request: ```json { "username": "newuser", "password": "securepassword" } ``` Response: ```json { "id": 2, "username": "newuser", "isAdmin": false } ``` First registered user becomes admin automatically. ### Login **POST /api/auth/login** Authenticate and receive JWT token. Request: ```json { "username": "admin", "password": "password" } ``` Response: ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": 1, "username": "admin", "isAdmin": true } } ``` ### Get Current User **GET /api/auth/me** Retrieve authenticated user details. Response: ```json { "id": 1, "username": "admin", "isAdmin": true, "createdAt": "2024-01-15T10:30:00Z" } ``` ## Artist Endpoints ### List Artists **GET /api/artists** Retrieve paginated artist list. Query Parameters: - `skip` (number): Offset for pagination (default: 0) - `take` (number): Limit results (default: 20, max: 100) - `sortBy` (string): Sort field (name, createdAt) - `sortOrder` (string): asc or desc Response: ```json { "items": [ { "id": 1, "name": "The Beatles", "slug": "the-beatles", "sortName": "Beatles, The", "illustration": { "url": "/illustrations/artist-1.jpg", "blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH", "colors": ["#1a1a1a", "#f0f0f0"] }, "albumCount": 13, "songCount": 213 } ], "total": 150, "skip": 0, "take": 20 } ``` ### Get Artist **GET /api/artists/:id** Retrieve artist details with relationships. Query Parameters: - `include` (string[]): Related entities to include (albums, songs, videos, areas) Response: ```json { "id": 1, "name": "The Beatles", "slug": "the-beatles", "sortName": "Beatles, The", "illustration": { ... }, "externalMetadata": { "description": "The Beatles were an English rock band...", "rating": 95, "sources": [ { "provider": "wikipedia", "url": "https://en.wikipedia.org/wiki/The_Beatles" } ] }, "areas": [ { "id": 1, "name": "Liverpool", "type": "city", "iso3166": "GB-LIV" } ], "albums": [ ... ], "songs": [ ... ], "videos": [ ... ] } ``` ### Create Artist **POST /api/artists** Create new artist. Admin only. Request: ```json { "name": "New Artist", "sortName": "Artist, New", "areaIds": [1, 2] } ``` Response: ```json { "id": 151, "name": "New Artist", "slug": "new-artist", "sortName": "Artist, New" } ``` ### Update Artist **PATCH /api/artists/:id** Update artist details. Admin only. Request: ```json { "name": "Updated Name", "sortName": "Name, Updated" } ``` ### Delete Artist **DELETE /api/artists/:id** Delete artist and cascade to albums/songs. Admin only. ## Album Endpoints ### List Albums **GET /api/albums** Retrieve paginated album list. Query Parameters: - `skip`, `take`, `sortBy`, `sortOrder` (same as artists) - `type` (string): Filter by album type (studio, live, compilation, etc.) - `artistId` (number): Filter by artist - `genreId` (number): Filter by genre - `year` (number): Filter by release year Response: ```json { "items": [ { "id": 1, "name": "Abbey Road", "slug": "abbey-road", "type": "studio", "releaseDate": "1969-09-26", "illustration": { ... }, "artist": { "id": 1, "name": "The Beatles" }, "trackCount": 17, "duration": 2832 } ], "total": 500, "skip": 0, "take": 20 } ``` ### Get Album **GET /api/albums/:id** Retrieve album details with tracks and releases. Query Parameters: - `include` (string[]): Related entities (tracks, releases, genres, labels) Response: ```json { "id": 1, "name": "Abbey Road", "slug": "abbey-road", "type": "studio", "releaseDate": "1969-09-26", "illustration": { ... }, "artist": { ... }, "genres": [ { "id": 1, "name": "Rock" } ], "releases": [ { "id": 1, "name": "Abbey Road (Original)", "releaseDate": "1969-09-26", "trackCount": 17 }, { "id": 2, "name": "Abbey Road (2019 Remaster)", "releaseDate": "2019-09-27", "trackCount": 17 } ], "tracks": [ ... ], "externalMetadata": { ... } } ``` ### Album Types Enum values for `type` field: - `studio`: Studio album - `live`: Live album - `compilation`: Compilation - `soundtrack`: Soundtrack - `ep`: Extended play - `single`: Single - `remix`: Remix album - `bootleg`: Bootleg - `interview`: Interview - `other`: Other ## Song Endpoints ### List Songs **GET /api/songs** Retrieve paginated song list. Query Parameters: - `skip`, `take`, `sortBy`, `sortOrder` - `artistId` (number): Filter by artist - `albumId` (number): Filter by album - `genreId` (number): Filter by genre - `type` (string): Filter by song type Response: ```json { "items": [ { "id": 1, "name": "Come Together", "slug": "come-together", "type": "original", "artist": { "id": 1, "name": "The Beatles" }, "featuring": [], "trackCount": 3, "duration": 259 } ], "total": 2000, "skip": 0, "take": 20 } ``` ### Get Song **GET /api/songs/:id** Retrieve song details with tracks and lyrics. Query Parameters: - `include` (string[]): Related entities (tracks, lyrics, videos) Response: ```json { "id": 1, "name": "Come Together", "slug": "come-together", "type": "original", "artist": { ... }, "featuring": [], "bpm": 82, "lyrics": { "plain": "Here come old flat top...", "synced": [ { "time": 0, "text": "Here come old flat top" }, { "time": 3500, "text": "He come grooving up slowly" } ] }, "tracks": [ { "id": 1, "name": "Come Together", "type": "audio", "duration": 259, "bitrate": 320000, "release": { "id": 1, "name": "Abbey Road (Original)" } } ], "externalMetadata": { ... } } ``` ### Song Types Enum values for `type` field: - `original`: Original recording - `live`: Live performance - `acoustic`: Acoustic version - `remix`: Remix - `cover`: Cover version - `demo`: Demo recording - `instrumental`: Instrumental - `karaoke`: Karaoke version - `radio_edit`: Radio edit - `extended`: Extended version - `clean`: Clean version - `explicit`: Explicit version ## Track Endpoints ### Get Track **GET /api/tracks/:id** Retrieve track details. Response: ```json { "id": 1, "name": "Come Together", "type": "audio", "duration": 259, "bitrate": 320000, "codec": "mp3", "ripSource": "cd", "isBonus": false, "isRemastered": false, "discIndex": 1, "trackIndex": 1, "song": { ... }, "release": { ... }, "sourceFile": { "id": 1, "path": "/music/The Beatles/Abbey Road/01 Come Together.mp3", "checksum": "abc123...", "fingerprint": "AQADtE..." } } ``` ### Stream Track **GET /api/tracks/:id/stream** Stream audio file. Returns audio data with appropriate Content-Type. Query Parameters: - `transcode` (boolean): Enable transcoding (default: false) - `bitrate` (number): Target bitrate for transcoding (128, 192, 256, 320) Response: Audio stream (audio/mpeg, audio/flac, etc.) ### Rip Source Types Enum values for `ripSource` field: - `cd`: CD rip - `vinyl`: Vinyl rip - `web`: Web download - `stream`: Stream rip - `other`: Other source ## Release Endpoints ### Get Release **GET /api/releases/:id** Retrieve release details with tracks. Response: ```json { "id": 1, "name": "Abbey Road (Original)", "releaseDate": "1969-09-26", "album": { ... }, "label": { "id": 1, "name": "Apple Records" }, "tracks": [ ... ], "extensions": { "catalogNumber": "PCS 7088", "barcode": "5099969945724" } } ``` ## Library Endpoints ### List Libraries **GET /api/libraries** Retrieve all configured libraries. Response: ```json { "items": [ { "id": 1, "name": "Main Library", "path": "/music", "fileCount": 5000, "lastScan": "2024-01-20T15:30:00Z" } ] } ``` ### Create Library **POST /api/libraries** Create new library. Admin only. Request: ```json { "name": "Jazz Collection", "path": "/music/jazz" } ``` ### Update Library **PATCH /api/libraries/:id** Update library settings. Admin only. ### Delete Library **DELETE /api/libraries/:id** Delete library and all associated files. Admin only. ## File Endpoints ### List Files **GET /api/files** Retrieve files with filters. Query Parameters: - `libraryId` (number): Filter by library - `trackId` (number): Filter by track - `orphaned` (boolean): Show files without tracks Response: ```json { "items": [ { "id": 1, "path": "/music/The Beatles/Abbey Road/01 Come Together.mp3", "checksum": "abc123...", "fingerprint": "AQADtE...", "library": { ... }, "track": { ... } } ] } ``` ### Register File **POST /api/files** Register new file. Used by Scanner. Request: ```json { "path": "/music/artist/album/track.mp3", "checksum": "abc123...", "fingerprint": "AQADtE...", "libraryId": 1, "metadata": { "title": "Track Title", "artist": "Artist Name", "album": "Album Name", "duration": 259, "bitrate": 320000 } } ``` ## Genre Endpoints ### List Genres **GET /api/genres** Retrieve all genres. Response: ```json { "items": [ { "id": 1, "name": "Rock", "albumCount": 150, "songCount": 2000 } ] } ``` ### Get Genre **GET /api/genres/:id** Retrieve genre with albums and songs. ## Playlist Endpoints ### List Playlists **GET /api/playlists** Retrieve user's playlists. Query Parameters: - `public` (boolean): Filter by public/private Response: ```json { "items": [ { "id": 1, "name": "Favorites", "slug": "favorites", "isPublic": false, "allowChanges": false, "entryCount": 50, "owner": { "id": 1, "username": "admin" } } ] } ``` ### Get Playlist **GET /api/playlists/:id** Retrieve playlist with entries. Response: ```json { "id": 1, "name": "Favorites", "slug": "favorites", "isPublic": false, "allowChanges": false, "owner": { ... }, "entries": [ { "id": 1, "index": 0, "track": { ... } } ] } ``` ### Create Playlist **POST /api/playlists** Create new playlist. Request: ```json { "name": "New Playlist", "isPublic": false, "allowChanges": false } ``` ### Update Playlist **PATCH /api/playlists/:id** Update playlist details. Owner only. ### Delete Playlist **DELETE /api/playlists/:id** Delete playlist. Owner only. ### Add Track to Playlist **POST /api/playlists/:id/entries** Add track to playlist. Request: ```json { "trackId": 1, "index": 0 } ``` ### Remove Track from Playlist **DELETE /api/playlists/:id/entries/:entryId** Remove track from playlist. ### Reorder Playlist **PATCH /api/playlists/:id/entries/reorder** Reorder playlist entries. Request: ```json { "entryId": 1, "newIndex": 5 } ``` ## Search Endpoints ### Global Search **GET /api/search** Search across artists, albums, songs, videos. Query Parameters: - `q` (string): Search query (required) - `limit` (number): Max results per type (default: 10) - `types` (string[]): Filter by types (artist, album, song, video) Response: ```json { "artists": [ { "id": 1, "name": "The Beatles", "slug": "the-beatles", "illustration": { ... } } ], "albums": [ ... ], "songs": [ ... ], "videos": [ ... ] } ``` ### Reindex Search **POST /api/search/reindex** Rebuild MeiliSearch index from database. Admin only. ## External Metadata Endpoints ### Get External Metadata **GET /api/external-metadata/:entityType/:entityId** Retrieve external metadata for entity. Path Parameters: - `entityType` (string): artist, album, song - `entityId` (number): Entity ID Response: ```json { "description": "The Beatles were an English rock band...", "rating": 95, "sources": [ { "provider": "wikipedia", "url": "https://en.wikipedia.org/wiki/The_Beatles" }, { "provider": "musicbrainz", "url": "https://musicbrainz.org/artist/..." } ] } ``` ### Update External Metadata **POST /api/external-metadata/:entityType/:entityId** Push external metadata. Used by Matcher. Request: ```json { "description": "Artist biography...", "rating": 85, "sources": [ { "provider": "wikipedia", "url": "https://..." } ] } ``` ## Scrobbler Endpoints ### List Scrobblers **GET /api/scrobblers** Retrieve user's connected scrobblers. Response: ```json { "items": [ { "id": 1, "provider": "lastfm", "username": "lastfm_user", "connectedAt": "2024-01-15T10:30:00Z" } ] } ``` ### Authorize Last.fm **GET /api/scrobblers/lastfm/authorize** Redirect to Last.fm OAuth page. ### Last.fm Callback **GET /api/scrobblers/lastfm/callback** OAuth callback. Exchanges code for token. Query Parameters: - `token` (string): OAuth token from Last.fm ### Authorize ListenBrainz **POST /api/scrobblers/listenbrainz/authorize** Connect ListenBrainz account. Request: ```json { "token": "user_token_from_listenbrainz" } ``` ### Remove Scrobbler **DELETE /api/scrobblers/:id** Disconnect scrobbler. ### Scrobble Track **POST /api/scrobblers/scrobble** Scrobble track play. Called automatically by Front. Request: ```json { "trackId": 1, "timestamp": 1705320600 } ``` ## Video Endpoints ### List Videos **GET /api/videos** Retrieve music videos. Query Parameters: - `skip`, `take`, `sortBy`, `sortOrder` - `artistId` (number): Filter by artist - `songId` (number): Filter by song - `type` (string): Filter by video type Response: ```json { "items": [ { "id": 1, "name": "Come Together (Official Video)", "slug": "come-together-official-video", "type": "official", "duration": 259, "song": { ... }, "sourceFile": { ... } } ] } ``` ### Get Video **GET /api/videos/:id** Retrieve video details. ### Stream Video **GET /api/videos/:id/stream** Stream video file. Proxies to Kyoo transcoder. Query Parameters: - `resolution` (string): Target resolution (480p, 720p, 1080p) Response: HLS manifest or video stream ### Video Types Enum values for `type` field: - `official`: Official music video - `live`: Live performance - `lyric`: Lyric video - `audio`: Audio-only video - `behind_the_scenes`: Behind the scenes - `interview`: Interview - `documentary`: Documentary - `fan_made`: Fan-made video - `other`: Other ## Illustration Endpoints ### Get Illustration **GET /api/illustrations/:id** Retrieve illustration image. Response: Image file (image/jpeg, image/png) ### Upload Illustration **POST /api/illustrations** Upload illustration for entity. Admin only. Request: multipart/form-data with image file Response: ```json { "id": 1, "url": "/illustrations/1.jpg", "blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH", "colors": ["#1a1a1a", "#f0f0f0"], "aspectRatio": 1.0 } ``` ## Area Endpoints ### List Areas **GET /api/areas** Retrieve geographic areas. Query Parameters: - `type` (string): Filter by area type (country, city, region, etc.) - `parentId` (number): Filter by parent area Response: ```json { "items": [ { "id": 1, "name": "United Kingdom", "type": "country", "iso3166": "GB", "parent": null, "children": [ ... ] } ] } ``` ### Area Types Enum values for `type` field: - `country`: Country - `subdivision`: State/province - `county`: County - `municipality`: City/town - `city`: City - `district`: District - `island`: Island ## User Endpoints ### List Users **GET /api/users** Retrieve all users. Admin only. Response: ```json { "items": [ { "id": 1, "username": "admin", "isAdmin": true, "createdAt": "2024-01-15T10:30:00Z" } ] } ``` ### Update User **PATCH /api/users/:id** Update user details. Admin or self only. Request: ```json { "password": "newpassword" } ``` ### Delete User **DELETE /api/users/:id** Delete user. Admin only. ## Scanner API Scanner exposes administrative endpoints at port 8133 (via `/scanner/` through Nginx). ### Health Check **GET /scanner/** Check scanner status. Response: ```json { "status": "healthy", "activeTasks": 0, "lastScan": "2024-01-20T15:30:00Z" } ``` ### List Tasks **GET /scanner/tasks** Retrieve active scan tasks. Response: ```json { "tasks": [ { "id": "task-1", "libraryId": 1, "status": "running", "progress": 0.45, "filesProcessed": 2250, "totalFiles": 5000 } ] } ``` ### Scan Library **POST /scanner/scan/:libraryId** Trigger full library scan. Response: ```json { "taskId": "task-1", "status": "started" } ``` ### Scan All Libraries **POST /scanner/scan** Trigger scan for all libraries. ### Clean Orphans **POST /scanner/clean** Remove database entries for deleted files. Response: ```json { "removedFiles": 15, "removedTracks": 10 } ``` ### Refresh Metadata **POST /scanner/refresh** Re-query providers for existing tracks. Response: ```json { "taskId": "task-2", "status": "started" } ``` ## Matcher API Matcher exposes minimal API at port 6789 (via `/matcher/` through Nginx). ### Health Check **GET /matcher/health** Check matcher status. Response: ```json { "status": "healthy", "queueLength": 5, "activeProviders": ["musicbrainz", "genius", "wikipedia", "lrclib"] } ``` ## Error Responses All endpoints return consistent error format: ```json { "statusCode": 404, "message": "Artist not found", "error": "Not Found" } ``` Common status codes: - `400`: Bad Request (invalid input) - `401`: Unauthorized (missing/invalid token) - `403`: Forbidden (insufficient permissions) - `404`: Not Found (entity doesn't exist) - `409`: Conflict (duplicate entity) - `500`: Internal Server Error ## Rate Limiting No built-in rate limiting. Recommended to use Nginx rate limiting for public instances. ## Pagination List endpoints support pagination via `skip` and `take` parameters. Response includes total count: ```json { "items": [ ... ], "total": 500, "skip": 0, "take": 20 } ``` Calculate pages: `totalPages = Math.ceil(total / take)` ## Filtering List endpoints support filtering via query parameters. Filters are AND-combined: ``` GET /api/albums?artistId=1&type=studio&year=1969 ``` Returns studio albums by artist 1 released in 1969. ## Sorting List endpoints support sorting via `sortBy` and `sortOrder`: ``` GET /api/artists?sortBy=name&sortOrder=asc ``` Default sort is usually by creation date descending. ## Including Relations Detail endpoints support `include` parameter to fetch related entities: ``` GET /api/artists/1?include=albums&include=songs ``` Reduces round trips for complex views. ## Swagger Documentation Interactive API documentation available at `/api/docs`. Includes: - All endpoints with parameters - Request/response schemas - Try-it-out functionality - Authentication setup Useful for exploring API without reading docs. ## WebSocket Events Server emits real-time events via WebSocket at `/api/events`: ```javascript const socket = io('http://localhost/api/events', { auth: { token: 'jwt_token' } }); socket.on('track.added', (data) => { console.log('New track:', data); }); socket.on('scan.progress', (data) => { console.log('Scan progress:', data.progress); }); ``` Event types: - `track.added`: New track registered - `track.updated`: Track metadata updated - `scan.started`: Library scan started - `scan.progress`: Scan progress update - `scan.completed`: Library scan completed - `metadata.enriched`: External metadata added ## API Versioning No versioning currently. Breaking changes announced in release notes. Future versions may use `/api/v2/` prefix. ## CORS CORS enabled for all origins in development. Production should configure allowed origins in .env: ``` CORS_ORIGINS=https://meelo.example.com ``` ## Content Negotiation All endpoints return JSON. Accept header ignored. Future versions may support XML or MessagePack. ## Compression Nginx handles gzip compression for responses >1KB. No application-level compression. ## Caching No server-side caching. Clients should implement caching via TanStack Query or similar. ## Summary Meelo's API provides comprehensive access to all music metadata and playback functionality. The REST design follows standard conventions with consistent error handling, pagination, and filtering. Swagger documentation makes exploration easy. WebSocket events enable real-time UI updates. The separation of Server, Scanner, and Matcher APIs reflects the microservices architecture while maintaining a unified interface through Nginx.