← API reference

Loves

Loved recordings, releases and artists.

8 of 8 · v0.120.0
GET /api/v1/ratings #
Session

Star ratings held from your media servers, and the rule applied to them.

Session-only. Reading is a fact about your library and pulling spends a source's credential, so a token gets 401 here and scopes never enter into it — the same line /api/v1/sources sits on.

Every rating carries the raw number the server gave and what that number was out of, because a bare 7 means nothing: Plex counts to ten (half stars, so a 7 is three and a half), Subsonic to five, and Jellyfin/Emby's legacy Rating to ten. shown is the same rating on the display scale you chose, and is presentation only — every threshold comparison is made on the underlying fraction, so a rating that reads as four stars on a coarse scale does not thereby clear a four-star bar.

threshold comes back in the units of scale, which is what you typed.

Responses

200

What is held.

application/json

object

The ratings held, on the scale you read them on, and the rule that turns them into loves.

scale string required

The display scale's id.

scale_label string required
scale_choices array required

Every threshold the scale offers.

each item
number · float
threshold number · float required

The love threshold, on this scale.

include_starred boolean required

Whether a starred item counts as loved whatever its rating.

sources array required
each item
object

What is held, per source, so a screen can say where the numbers came from without listing them.

source_id integer · int64 required
source_kind string required
label string required
rated integer · int64 required
starred integer · int64 required
scale_max number · double required
fetched_at integer | null · int64
sample array required

Up to 60 ratings, to show what the rule would do.

each item
object
kind string required

track, album or artist.

artist string required
name string required
album string | null
source_kind string required
starred boolean required
rating number | null · double

The raw number, exactly as the server gave it — "7 out of 10, from Plex".

scale_max number · double required

What rating was out of.

shown number · float required

The same rating on your display scale.

loved boolean required

Whether the current rule loves 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."
}
DELETE /api/v1/ratings #
Session

Forget the stored ratings.

Session-only. Does not un-love anything — a love already made is a statement that was made. To take those back, raise the threshold and apply, which is a separate and visible act.

Responses

200

Forgotten.

application/json

object

Ratings forgotten.

forgotten integer · int64 required
note 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."
}
POST /api/v1/ratings/pull #
Session

Read the ratings your media servers hold.

Session-only — it spends the credential of a configured source.

Walks every enabled library source and replaces what that source said last time, so a rating raised, lowered or cleared on the server is raised, lowered or cleared here: the server is the authority on its own ratings and this is a copy of them. One source failing does not stop the others, and an error is reported separately from a count of zero — a server that could not be reached is not a library with no ratings in it.

Roon is reported as having nothing rather than returning an empty list. Its extension API publishes no rating, favourite or thumb field at all, so a source that silently returned nothing would look broken.

Ratings are stored; nothing becomes a love until /api/v1/ratings/apply.

Responses

200

Done.

application/json

object

Ratings read from every enabled media server.

found integer required

Ratings stored, across every source.

sources array required

One entry per enabled source. A source that failed says why and keeps what it held before.

each item
object

What one source's pull did.

error and a zero count are different answers and both are reported: a server that could not be reached is not a library with no ratings in it, and a screen that showed them the same way would send somebody looking for ratings that are sitting right there. What one source handed over.

source_id integer · int64 required
kind string required
label string required
found integer required
error string | null
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/ratings/apply #
Session

Love what clears the threshold, and withdraw what no longer does.

Session-only.

dry_run is required and has no default: the two things this can do are "tell me what would happen" and "write to my Loved page", and a body that merely misspelled the key must not be read as an answer either way. A preview and a real run go through the same code, so a preview cannot describe something the apply then does differently — and a preview writes nothing at all, including no entities.

Two rules hold it safe. A rating on something you have never listened to is kept and not loved — loves point at entities and entities are things you actually played — and the count comes back as unmatched so it is visible rather than silently absent. And only loves this instance derived are ever withdrawn: one pressed by hand, or pulled from Last.fm or ListenBrainz, is outside the reach of a threshold however far it moves.

Nothing here is forwarded to Last.fm or ListenBrainz. These are loves being imported, not loves being pressed, and a threshold you are expected to try at several settings would otherwise be a broadcast at every setting.

Request body required

application/json

object
threshold number | null · float

The cut-off, in the units of scale. Absent means "whatever is stored".

scale string | null

Which scale threshold is expressed in. Absent means the stored one.

include_starred boolean | null
dry_run boolean required

Preview only. Required — see the note.

save boolean

Store the rule as well as applying it.

Responses

200

What happened, or what would.

application/json

object

DELETE /api/v1/ratings — forget the stored ratings.

Does not un-love anything, and that is the point rather than an omission: a love already made is a statement that was made, and this is a request to forget the ratings. Un-loving is what moving the threshold to the top and applying does, which is a separate and visible act. What a rule did, or would do.

threshold number · float required

The rule applied, on scale.

scale string required
include_starred boolean required
report object required

What applying a threshold did, or would do.

matched and unmatched are stated separately for the reason every paired count in this codebase is: a rating on something never played is real data and is kept, but it does not become a love, and a screen reporting only the loves would silently understate what the library holds.

qualifying integer required

Ratings that clear the bar.

matched integer required

…of which this many are things this user has actually listened to.

unmatched integer required

…and this many are not, so they are not loved. See the note on the rule.

loved integer required

Loves newly made by this run.

withdrawn integer required

Loves withdrawn because their rating no longer clears the bar.

already integer required

Already loved, by hand or by an earlier run. Untouched.

dry_run boolean required

Whether anything was actually written.

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."
}
POST /api/v1/loves/pull #
Session

Pull loves from Last.fm / ListenBrainz.

Session-only — it uses stored service credentials. Not one code path: ListenBrainz returns only a recording MBID so an un-enriched track cannot be matched, while Last.fm returns names and can fall back to a name match. A love whose track is not in your history is skipped, never invented — loves point at entities, and entities are things you actually played. Libre.fm has no loved-tracks call and is not asked.

Responses

200

Done.

application/json

object

Loves fetched from every connected service.

loved integer required

Newly stored, across every service.

services array required

One entry per connected service; one failing does not stop the other.

each item
object

What one service's pull did. Every field is a count of loves, not of requests, so the numbers mean the same thing across services.

service string required

listenbrainz or lastfm.

found integer required

Loves the service reported.

loved integer required

Newly stored — already-loved tracks are not counted twice.

skipped integer required

Reported by the service but not in this user's history.

error string | null
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."
}
GET /api/v1/loves #
Session or Token read

Loved entities.

Scope: read (since v0.39).

Since v0.43 each row carries what the Loved page needs to render it — plays, last played, artwork and the liner note — rather than the bare entity. Four queries regardless of how many loves there are.

Responses

200

OK.

application/json

object

The Loved page.

loves array required
each item
object

A loved entity, and what your history says about it.

entity object required
id integer · int64 required
kind string required

recording, release or artist.

name string required
artist_name string required
mbid string | null
loved_at integer · int64 required

Unix seconds.

plays integer · int64 required
last_played integer | null · int64
album string | null

Absent for an artist.

artwork_url string | null
caa_id integer | null · int64
caa_release_mbid string | null
note_id integer | null · int64
note_excerpt string | null
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"
}
POST /api/v1/loves #
Session or Token write

Love or un-love.

Scope: write (since v0.39).

Loves attach to entities, never to a listen — loving one playing of a track is a category error and is rejected.

A love of a recording is mirrored outward (Last.fm track.love, ListenBrainz recording-feedback); albums and artists stay local because neither service has the concept. ListenBrainz needs a recording MBID, so an un-enriched track is skipped rather than guessed at. The forward is spawned, not awaited — the love is already stored and the UI should not wait on Last.fm.

Request body required

application/json

object

What to love, named the ways TargetSpec allows — a track, an album or an artist; not a listen, a period or a line.

entity_id integer | null · int64
scrobble_id integer | null · int64
period_unit string | null

week, month or year, alongside period_key. Both or neither.

period_key string | null

As Reports names it — 2026, 2026-08, 2026-W32.

line integer | null

A pin: the line's number, alongside artist and name (the track's title). Blank lines are numbered.

from_scrobble integer | null · int64

With kind: track, prefer the album this listen was filed under.

kind string | null

track/recording, album/release or artist.

name string | null
artist string | null
mbid string | null
loved boolean | null

Omitted means "love it"; pass false to un-love.

Responses

200

Stored.

application/json

object

A love, set or withdrawn.

entity object | null

Something a note or a love can point at: a recording, a release, an artist.

id integer · int64 required
kind string required

recording, release or artist.

name string required
artist_name string required

Empty for kind = 'artist', where the name is the artist.

mbid string | null
created_at integer · int64 required
loved boolean 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."
}
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/loves/keys #
Session or Token read

Every loved entity key, as a flat set.

Scope: read (since v0.39). One request instead of one per history row.

Since v0.69 a love is published in up to three forms, and a row is loved if any key it can derive is in the set. Two players rarely spell one song identically — Plex files a listen under the record's artist while a scrobble client reports what the player displayed — so an exact match alone left the heart empty on the same track from another source.

  • kind|artist|name — trimmed and lowercased, an artist's artist half empty. The exact key, as before.
  • kind|artist|name folded — accents, ligatures and all punctuation removed, and the primary taken from an explicit feat. credit. Note a trailing title qualifier is not removed: "Archangel" and "Archangel (Remastered)" stay apart.
  • mbid:<kind>:<mbid> — present when the entity has a MusicBrainz id. The strongest of the three; a listen row carries a recording id.

If your derivation drifts from the server's, hearts silently stop filling — nothing errors. The folded form is pinned character for character by the_folded_key_is_exactly_what_the_client_must_reproduce.

Responses

200

OK.

application/json

object

Every loved entity as its identity key.

keys array required

kind|artist|name, lowercased — recording|aphex twin|xtal. Derive a row's key the same way and match locally.

each item
string
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"
}