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

22 KiB

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

{
  "username": "admin",
  "password": "password"
}

Response:

{
  "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: <key from .env>

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:

{
  "username": "newuser",
  "password": "securepassword"
}

Response:

{
  "id": 2,
  "username": "newuser",
  "isAdmin": false
}

First registered user becomes admin automatically.

Login

POST /api/auth/login

Authenticate and receive JWT token.

Request:

{
  "username": "admin",
  "password": "password"
}

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 1,
    "username": "admin",
    "isAdmin": true
  }
}

Get Current User

GET /api/auth/me

Retrieve authenticated user details.

Response:

{
  "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:

{
  "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:

{
  "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:

{
  "name": "New Artist",
  "sortName": "Artist, New",
  "areaIds": [1, 2]
}

Response:

{
  "id": 151,
  "name": "New Artist",
  "slug": "new-artist",
  "sortName": "Artist, New"
}

Update Artist

PATCH /api/artists/:id

Update artist details. Admin only.

Request:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "entryId": 1,
  "newIndex": 5
}

Search Endpoints

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:

{
  "artists": [
    {
      "id": 1,
      "name": "The Beatles",
      "slug": "the-beatles",
      "illustration": { ... }
    }
  ],
  "albums": [ ... ],
  "songs": [ ... ],
  "videos": [ ... ]
}

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:

{
  "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:

{
  "description": "Artist biography...",
  "rating": 85,
  "sources": [
    {
      "provider": "wikipedia",
      "url": "https://..."
    }
  ]
}

Scrobbler Endpoints

List Scrobblers

GET /api/scrobblers

Retrieve user's connected scrobblers.

Response:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "status": "healthy",
  "activeTasks": 0,
  "lastScan": "2024-01-20T15:30:00Z"
}

List Tasks

GET /scanner/tasks

Retrieve active scan tasks.

Response:

{
  "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:

{
  "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:

{
  "removedFiles": 15,
  "removedTracks": 10
}

Refresh Metadata

POST /scanner/refresh

Re-query providers for existing tracks.

Response:

{
  "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:

{
  "status": "healthy",
  "queueLength": 5,
  "activeProviders": ["musicbrainz", "genius", "wikipedia", "lrclib"]
}

Error Responses

All endpoints return consistent error format:

{
  "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:

{
  "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:

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.