← API reference

Maintenance

Enrichment backfills and the metadata sanitiser. Session-only.

9 of 9 · v0.120.0
GET /api/v1/lyrics/status #
Session

How far the lyric language pass has got.

Scope: session only.

Tapedeck fetches lyrics from LRCLIB for one reason: the language a transliterated title cannot give. Script detection settles any non-Latin title with certainty and gives up on Latin text, so a romanised title falls through to English/Other, which is a shrug rather than a measurement and is most of a real transliterated history. The lyric is where that title's own script went.

Lyrics are stored in the MusicBrainz cache database rather than in the listening database, so GET /api/v1/backup does not carry them — they are third-party copyrighted text, not the user's listening.

instrumental is counted separately because it is a real answer: a track with no words has no language, and folding it into "found" would report a finished lookup as an unfinished one.

Responses

200

OK.

application/json

object

How far the lyric language pass has got. The three cache counts are absent when the cache cannot be read — unknown, not zero.

available boolean required
why string | null

Why not, when not available.

asked integer | null · int64

Tracks LRCLIB has been asked about.

with_lyrics integer | null · int64
instrumental integer | null · int64

A real answer — a track with no words has no language.

unresolved integer · int64 required

Tracks still filed under English/Other that the pass could try.

GET /api/v1/mbids/status #
Session

MusicBrainz recording coverage.

Session-only.

A recording MBID is written by flush_pending alone, which only ever sees listens on their way to a sink — so an imported listen never got one, and nothing else writes the column. On a mostly-imported history that leaves most of the listening unidentified, and with it excluded from every backfill that keys off a recording: ISRC, work-language, release year.

unmatched_tracks is the reason this reports three numbers instead of two. It counts distinct tracks MusicBrainz was asked about and had nothing for — real listening simply not in their database. Without it a bar that stops short reads as a stall rather than as an answer.

Responses

200

OK.

application/json

object

Recording-level MusicBrainz coverage of your history.

total integer · int64 required

Listens.

resolved integer · int64 required

Listens carrying a recording MBID.

pending integer · int64 required
unmatched_tracks integer · int64 required

Distinct tracks MusicBrainz was asked about and had nothing for — real listening simply not in their database. Without it a progress bar that stops short reads as a stall rather than as an answer.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
POST /api/v1/mbids/backfill #
Session

Resolve missing recording MBIDs now.

Session-only, admin only. Runs in the background; poll /api/v1/mbids/status for progress.

This is the pass that unblocks the others, so it is worth running before the ISRC or language backfills on a history that came from an import.

A name search can be wrong, and a wrong MBID is worse than none — it would be written onto every play of the track and then poison the ISRC and language lookups downstream, with nothing erroring. So a result is only accepted when it agrees with the question: the artist matches, and the title matches once a trailing qualifier is stripped. Misses are negative-cached, so a track already asked about is not asked again while that answer is current.

Responses

202

Started. status is started.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status string required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
403

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
GET /api/v1/artwork/status #
Session

Artwork coverage.

Session-only. Numerator and denominator share a population — both exclude skips. They did not once, so the two were fractions of different wholes.

Responses

200

OK.

application/json

object

How far a backfill has got.

total integer · int64 required
resolved integer · int64 required
pending integer · int64 required

total - resolved.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
POST /api/v1/artwork/backfill #
Session

Run the artwork backfill now.

Session-only. Providers return URLs, never bytes — Deezer's terms discourage persisting artwork. The queue keys on artwork_url only; caa_id is written by three other paths, so keying on it silently marks rows done that have no URL.

Responses

202

Started. status is started.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status string required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
403

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
GET /api/v1/languages/status #
Session

Language classification coverage.

Session-only. The denominator counts only rows the backfill will actually process — it did not once, so the bar could never reach 100% on any history containing a skip.

Responses

200

OK.

application/json

object

How far a backfill has got.

total integer · int64 required
resolved integer · int64 required
pending integer · int64 required

total - resolved.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
POST /api/v1/languages/backfill #
Session

Run the language backfill now.

Session-only.

Responses

202

Started. status is started.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status string required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
403

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
GET /api/v1/sanitize #
Session

Preview metadata fixes.

Session-only. Preview-then-apply, never automatic — and the preview deliberately exposes how weak each guess is, because the operator has to judge it.

Three passes. Mixed releases: one listen out of twelve routinely arrives carrying a compilation's release MBID. The whole ballot is returned, most-claimed first, so "15 releases, plurality of 4" is visible as the guess it is. Featured credits: Artist feat. X is a different artist to every query in the app; only the explicit feat./ft./featuring/with forms match, because "Simon & Garfunkel" is a name, not a credit. Spelling variants: grouped on the exact string, with case_only flagged — a two-stage filter whose second stage was coarser than the first once made every case-only variant invisible.

Responses

200

The proposed changes, each individually selectable.

application/json

object

What the sanitiser would change. Nothing has been changed.

mixed_albums array required

Albums whose listens disagree about which release they are.

each item
object

An album whose listens disagree about which release they belong to.

artist string required
album string required
plays integer · int64 required
variants integer · int64 required

How many distinct release MBIDs the listens carry. Always > 1 here.

winner string | null

The MBID the most listens agree on — the proposed fix, not a verdict.

winner_plays integer · int64 required
untagged integer · int64 required

Listens of this album carrying no release MBID at all. Unifying stamps these with the chosen release too, so they're reported separately.

candidates array required

Every release the listens claim, most-claimed first, so the operator can pick a different one than the plurality.

each item
object

One release an album's listens claim to be, and how many say so.

mbid string required
plays integer · int64 required
featured_artists array required

Artist strings carrying a featured credit, and the artist they fold into.

each item
object

An artist string carrying a featured credit, and the primary artist it would fold into.

artist string required
primary string required
plays integer · int64 required
artist_variants array required

Artists with other spellings of themselves in the same history.

each item
object

An artist name with other spellings of itself in the same history.

primary string required

The most-played spelling — the one the others fold into.

primary_plays integer · int64 required
variants array required

(spelling, plays) for every other form of the same name.

each item
tuple · [string, integer · int64]
case_only boolean required

These differ only in capitalisation or spacing.

Worth separating because the consequences differ. A case-only variant costs nothing but display consistency — every read path that counts already lowercases. Anything else ("Cœur de pirate" against "Coeur de pirate") is genuinely two artists to every query, and splits the plays.

split_attributions array required

Tracks carrying two different artists on the same record.

each item
object

One track carrying two different artists on the same record.

title string required

Lowercased grouping keys.

album string required
primary string required

The record's own artist, and what everything else folds into.

primary_plays integer · int64 required

This artist's plays on this track.

primary_album_plays integer · int64 required

This artist's plays across the whole album.

primary_album_tracks integer · int64 required

How many distinct tracks of the album carry this artist. This is why they win, and it is shown so a weak case is visible as one: a director credited on twelve tracks is obvious, a winner with two is a guess.

folding array required

(artist, plays) for every other attribution on this track.

each item
tuple · [string, integer · int64]
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
POST /api/v1/sanitize #
Session

Apply selected metadata fixes.

Session-only. Explicit lists select individual edits; the booleans mean "everything in this category". A non-empty list wins over its boolean — asking for three edits and getting four hundred would be the worst reading of an ambiguous request.

The featured-credit target is derived, never taken from the request, or this would become "rename any artist to anything". A variant merge is refused unless both spellings normalise to the same key, so merging "Burial" into "Aphex Twin" cannot happen.

A spelling merge is not reversible. The feat. fold keeps the original in artist_credit; a rename keeps nothing, because both strings name the same artist.

Request body required

application/json

object

What to apply.

Two ways to say it, because both are the common case at different times: the booleans mean "everything in this category", the lists name individual edits. A non-empty list wins over its boolean — asking for specific edits and getting all of them would be the worst possible reading.

albums boolean
artists boolean
variants boolean
album_edits array
each item
object

One album to unify, and onto which release.

mbid_release is carried per edit rather than re-derived, so the operator can overrule the plurality — which is the point of reviewing at all.

artist string required
album string required
mbid_release string required
credit_edits array
each item
object
artist string required

The full credit as stored, e.g. Cœur de pirate feat. Loud.

variant_edits array
each item
object
from string required

The spelling being retired.

to string required

The spelling it becomes.

splits boolean
split_edits array
each item
object

One track whose stray attribution goes back to the record's own artist.

title string required
album string required
from string required

The attribution being folded — a singer, on a soundtrack.

to string required

The record's artist. Checked against the preview rather than trusted: taking this from the request would turn a narrow fix into "rename any track to any artist", the same reason split_featured derives its own target.

Responses

200

Applied.

application/json

object

What the sanitiser changed: how many of each fix, and how many listens each touched.

albums_fixed integer · int64 required
album_listens_changed integer · int64 required
artists_folded integer · int64 required
artist_listens_changed integer · int64 required
variants_merged integer · int64 required
variant_listens_changed integer · int64 required
splits_merged integer · int64 required
split_listens_changed integer · int64 required
400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}