GET /api/v1/playlist-lab # Session
The vector space the Playlist Lab generates from.
Session-only. Returns the candidate pool with a position for each track,
plus what every axis means.
Read granularity and note before using this. The space is ten
real dimensions, and they come from two different places:
- Six are about the music (
group: sound), from the Every Noise
genre map — x (dense/atmospheric ↔ spiky/bouncy), y (organic ↔
mechanical/electric), the colour's three channels, and how far apart a
track's own genres sit.
- Four are about the listener (
group: habit) — era, familiarity,
hour of day, recency — from their own history.
These are not per-recording audio features. Genres attach to
artists, so granularity is artist and every track by one artist
shares its six sound coordinates exactly; the habit axes are the
only thing separating them. Anything describing this as track-level
audio similarity is wrong. A client mixing the two groups without
weighting will find "similar" quietly means "played at the same hour".
Values are normalised 0–1 against the pool's own extremes, so
"newer" means newer than the rest of this collection. A track whose
genres have no place on the map is dropped, not centred — placing it
in the middle would invent a similarity it does not have — and the count
comes back as unplaced.
tracks is compact — [title, artist, album, plays, [v0…v9]] — because
on a 3,000-track pool the repeated key names would be most of the
payload. Answers 409 when the genre map has not been fetched, when
there is no history, or when nothing has been placed yet.
Parameters
min_plays query
Plays a track needs to be in the pool. Lower it to reach further into
the tail of the history.
cap query
The most tracks to return, most-played first.
Responses
application/json
object
The pool, where each track sits, and what the axes mean.
dims array required
The ten axes, in the order every track's vector lists them.
each item object
group string required
Which half of the space an axis belongs to: sound is about the music
(from the genre map), habit about the listener (from their history).
soundhabit
tracks array required
One array per track rather than an object, to keep a large pool small:
[title, artist, album, plays, vector], the vector's coordinates
rounded to three places.
loved array required
Indexes into tracks of the ones you love.
considered integer required
Tracks considered before the cut.
unplaced integer required
Tracks left out for having no genre the map places.
min_plays integer · int64 required
granularity string required
Always artist: the Sound axes are reached through each track's
artist, so every track by one artist shares them. Present so no client
can present this as per-recording analysis.
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
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."
}
409 No genre map yet, no history, or nothing placed yet — the message says which.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/playlist-targets # Session
Servers a generated list can be sent to.
Session-only, and permanently so: pushing spends the credential of a
configured source, which puts it on the same side of the line as
/api/v1/sources — the line API tokens do not cross.
The targets are the caller's enabled sources, filtered to the kinds
that can receive a playlist. protocol names the wire protocol rather
than the product, so a Navidrome source reads as Subsonic: the calls
are search3 and createPlaylist, which any OpenSubsonic server
answers. A kind missing from the list is a source that polls and cannot
be written to, not a bug.
Responses
application/json
object
targets array required
Enabled, writable sources, and TIDAL when it is connected. Empty is a
normal answer.
each item object
A server this user can push a playlist to.
source_id integer · int64 required
What source_id takes. -1 is TIDAL, which is a connection rather
than a source.
kind string required
plex, jellyfin, navidrome or tidal.
protocol string required
Plex, Jellyfin, Subsonic or TIDAL.
label string required
Falls back to the protocol name when the source has none.
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
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."
}
Create a playlist on one of your servers from a list of tracks.
Session-only — see /api/v1/playlist-targets.
Tapedeck stores names, not the server's item ids, for a generated
list, so every track is resolved by searching the target and every one of
those searches is fuzzy. A hit is checked on artist and title before it
is used; an exact title beats a (Remastered) one, and a hit for a
different artist is discarded rather than taken.
A partial push is a success, not an error. A listening history is not
the same set as any one library, so matched below requested is the
normal case and missing names every track that did not land. The push
only fails outright when nothing matched (404) or the server refused or
could not be reached (502) — the two need completely different things
from the user, so they are not collapsed.
Order is preserved; misses drop out. At most 100 tracks, which is a
wall-clock bound as much as a politeness one — the searches run in turn
against what is often a Raspberry Pi.
Request body required
application/json
object
source_id integer · int64 required
From /api/v1/playlist-targets.
name string required
Trimmed, and truncated to 120 characters.
tracks array required
each item object
One track Tapedeck wants on the far end.
Names only, because names are all the generating queries have. album is a
tiebreaker rather than a requirement: a listen's album is whichever release
the tagger claimed, which for a compilation track is not the album the
server files it under.
album string | null
A tiebreaker between pressings, never a requirement.
isrc string | null
The recording's ISRC. Filled from the history when absent, and where a
target can look one up no name matching is needed at all.
Responses
200 Created — possibly with fewer tracks than asked for.
application/json
object
What was made, and every track that did not land, with why.
playlist_id string required
The server's own id. Empty when a Subsonic server answered ok
without one.
target object required
A server this user can push a playlist to.
source_id integer · int64 required
What source_id takes. -1 is TIDAL, which is a connection rather
than a source.
kind string required
plex, jellyfin, navidrome or tidal.
protocol string required
Plex, Jellyfin, Subsonic or TIDAL.
label string required
Falls back to the protocol name when the source has none.
requested integer required
from_cache integer required
How many were answered from remembered matches without asking the far
end. Nearly everything on a re-push.
missing array required
A partial match is the normal outcome — a listening history is not the
same set as any one library.
each item object
Why a track did not land — which distinguishes a search problem from a
matching one. A mismatch carries what the server actually offered, at most
five, which is the fact that settles it at a glance.
one of option 1 object
The search itself failed — the server is unwell, not unhelpful.
reason string required
search_failed
option 2 object
The server answered, with nothing.
reason string required
no_candidates
option 3 object
It offered tracks, none by this artist.
offered array required
each item object
One track the far end offered. Carries its id, so a candidate can be
chosen rather than only read: POST it to /api/v1/playlist-matches
and every later push of that track uses it.
id string required
The far end's own id, which is what POST /api/v1/playlist-matches
takes. Opaque here; it only ever goes back to the server that made it.
label string required
What to show. The artist for an artist mismatch, the title for a title
mismatch — whichever half is the thing that disagreed.
reason string required
artist_mismatch
option 4 object
The artist agreed and no title did.
offered array required
each item object
One track the far end offered. Carries its id, so a candidate can be
chosen rather than only read: POST it to /api/v1/playlist-matches
and every later push of that track uses it.
id string required
The far end's own id, which is what POST /api/v1/playlist-matches
takes. Opaque here; it only ever goes back to the server that made it.
label string required
What to show. The artist for an artist mismatch, the title for a title
mismatch — whichever half is the thing that disagreed.
reason string required
title_mismatch
updated_existing boolean required
A playlist of this name was pushed here before and was rewritten in
place rather than made again.
400 Malformed or rejected input.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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."
}
404 No such source, or none of those tracks are on it.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
501 TIDAL was named and is not configured on this instance.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
502 The server refused the playlist or could not be reached.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/saved-playlists # Session
Save a playlist.
Session-only. Tracks are names, not file paths — Tapedeck stores no
server item ids for a generated list and resolves them against the real
library at push time.
Request body required
application/json
object
description string | null
tracks array required
In playlist order. At least one.
each item object
Names, not ids — the only shape that survives the user not owning the
track on every server they have. Resolved against a real library at push
time.
album string | null
A tiebreaker at push time, never a requirement.
note string | null
Why this track is here, in the words of whatever chose it. Never
generated by Tapedeck.
Responses
application/json
object
id integer · int64 required
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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/saved-playlists/{id}/push # Session
Send a stored playlist to a media server.
Session-only — pushing spends the credential of a configured source,
which is the line WriteAuth deliberately does not cross.
This is the review step the MCP design turns on: a list an assistant
built is saved first and reaches a media server only when the person
whose server it is says so.
A partial match is the normal outcome — a listening history is not the
same set as any one library — and every track that did not land is
named. 404 means nothing matched; 502 means the server could not be
reached. They need completely different things from the user.
Request body required
application/json
object
source_id integer · int64 required
From /api/v1/playlist-targets.
Responses
application/json
object
What was made, and every track that did not land, with why.
playlist_id string required
The server's own id. Empty when a Subsonic server answered ok
without one.
target object required
A server this user can push a playlist to.
source_id integer · int64 required
What source_id takes. -1 is TIDAL, which is a connection rather
than a source.
kind string required
plex, jellyfin, navidrome or tidal.
protocol string required
Plex, Jellyfin, Subsonic or TIDAL.
label string required
Falls back to the protocol name when the source has none.
requested integer required
from_cache integer required
How many were answered from remembered matches without asking the far
end. Nearly everything on a re-push.
missing array required
A partial match is the normal outcome — a listening history is not the
same set as any one library.
each item object
Why a track did not land — which distinguishes a search problem from a
matching one. A mismatch carries what the server actually offered, at most
five, which is the fact that settles it at a glance.
one of option 1 object
The search itself failed — the server is unwell, not unhelpful.
reason string required
search_failed
option 2 object
The server answered, with nothing.
reason string required
no_candidates
option 3 object
It offered tracks, none by this artist.
offered array required
each item object
One track the far end offered. Carries its id, so a candidate can be
chosen rather than only read: POST it to /api/v1/playlist-matches
and every later push of that track uses it.
id string required
The far end's own id, which is what POST /api/v1/playlist-matches
takes. Opaque here; it only ever goes back to the server that made it.
label string required
What to show. The artist for an artist mismatch, the title for a title
mismatch — whichever half is the thing that disagreed.
reason string required
artist_mismatch
option 4 object
The artist agreed and no title did.
offered array required
each item object
One track the far end offered. Carries its id, so a candidate can be
chosen rather than only read: POST it to /api/v1/playlist-matches
and every later push of that track uses it.
id string required
The far end's own id, which is what POST /api/v1/playlist-matches
takes. Opaque here; it only ever goes back to the server that made it.
label string required
What to show. The artist for an artist mismatch, the title for a title
mismatch — whichever half is the thing that disagreed.
reason string required
title_mismatch
updated_existing boolean required
A playlist of this name was pushed here before and was rewritten in
place rather than made again.
400 The source is off, or cannot receive a playlist.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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."
}
404 No such playlist, or none of the tracks are on that server.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
501 TIDAL was named and is not configured on this instance.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
502 The server could not be reached, or refused the playlist.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/playlist-matches # Session
Pin a track to one of the candidates a push offered.
Session-only, like the push it serves.
A push reports every miss with the candidates the far end offered.
Until this existed that list was a dead end — plausible names you could
read and not act on. Choosing one records a manual resolution, which
never expires and is never overwritten by a later automatic guess: a
person read the candidates and settled it, and a search that already got
it wrong once does not get another vote.
This is also what makes the resolution cache self-improving. Every
successful match is remembered, so a re-push costs almost no searches at
all — which matters because the far end meters them.
Request body required
application/json
object
A track the search got wrong, and which of its candidates it really is.
source_id integer · int64 required
The target this resolution is for. -1 is TIDAL.
remote_id string | null
An id from a miss's offered list. Required to record a match;
ignored by the DELETE, which forgets the mapping instead.
remote_label string | null
What the far end calls it, so the mapping can be shown back as something
a person recognises rather than an opaque id.
Responses
400 Malformed or rejected input.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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."
}