← API reference

Settings

Preferences and artwork.

7 of 7 · v0.120.0
GET /api/v1/preferences #
Session

User preferences.

Session-only.

Responses

200

OK.

application/json

object

Free-form. The keys below are the ones the server reads; any other key is the web UI's own and is stored verbatim.

Takes further properties besides these, and keeps them as sent.

timezone string

An IANA region, e.g. Europe/Berlin — never a fixed abbreviation like CET. Decides "today", the heatmap and every calendar bucket. UTC when unset.

week_start integer

First day of the week, 0 = Sunday … 6 = Saturday. Monday (1) when unset.

scrobble_threshold_percent number

When a listen counts: this share of the track, 5–100. Read the clamped value from GET /api/v1/scrobble-settings.

scrobble_threshold_secs integer

…or this many seconds, whichever comes first, 10–3600.

rating_scale string

The scale rating_love_threshold is written on. Set by POST /api/v1/ratings/apply.

rating_love_threshold number

The media-server rating at or above which a track counts as loved, on rating_scale.

rating_love_starred boolean

Whether a starred (favourited) item counts as loved whatever its rating. True when unset.

theme string

light, dark or system. The web UI's.

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."
}
PUT /api/v1/preferences #
Session

Update preferences.

Session-only. A shallow merge: the keys sent replace the stored ones, omitted keys are left alone, and the merged object comes back.

timezone must be an IANA region, not a fixed abbreviation. The region carries the DST rules — Europe/Berlin is CET in winter and CEST in summer, switching on the right dates by itself, and a region with no summer time never shifts. Storing CET would freeze the offset and put half a European year an hour out, silently: listens would still store correctly and only "today", the hour-of-day heatmap and the trajectory buckets would be wrong.

Request body required

application/json

object

Free-form. The keys below are the ones the server reads; any other key is the web UI's own and is stored verbatim.

Takes further properties besides these, and keeps them as sent.

timezone string

An IANA region, e.g. Europe/Berlin — never a fixed abbreviation like CET. Decides "today", the heatmap and every calendar bucket. UTC when unset.

week_start integer

First day of the week, 0 = Sunday … 6 = Saturday. Monday (1) when unset.

scrobble_threshold_percent number

When a listen counts: this share of the track, 5–100. Read the clamped value from GET /api/v1/scrobble-settings.

scrobble_threshold_secs integer

…or this many seconds, whichever comes first, 10–3600.

rating_scale string

The scale rating_love_threshold is written on. Set by POST /api/v1/ratings/apply.

rating_love_threshold number

The media-server rating at or above which a track counts as loved, on rating_scale.

rating_love_starred boolean

Whether a starred (favourited) item counts as loved whatever its rating. True when unset.

theme string

light, dark or system. The web UI's.

Responses

200

Saved. The whole merged object.

application/json

object

Free-form. The keys below are the ones the server reads; any other key is the web UI's own and is stored verbatim.

Takes further properties besides these, and keeps them as sent.

timezone string

An IANA region, e.g. Europe/Berlin — never a fixed abbreviation like CET. Decides "today", the heatmap and every calendar bucket. UTC when unset.

week_start integer

First day of the week, 0 = Sunday … 6 = Saturday. Monday (1) when unset.

scrobble_threshold_percent number

When a listen counts: this share of the track, 5–100. Read the clamped value from GET /api/v1/scrobble-settings.

scrobble_threshold_secs integer

…or this many seconds, whichever comes first, 10–3600.

rating_scale string

The scale rating_love_threshold is written on. Set by POST /api/v1/ratings/apply.

rating_love_threshold number

The media-server rating at or above which a track counts as loved, on rating_scale.

rating_love_starred boolean

Whether a starred (favourited) item counts as loved whatever its rating. True when unset.

theme string

light, dark or system. The web UI's.

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."
}
GET /api/v1/scrobble-settings #
Token or Session

When a listen counts — follow the same rule the server does.

Half the track or four minutes, whichever comes first, is only the default: it is a per-user setting, because that convention fits a three-minute pop song far better than a forty-minute raga or a ninety-second hardcore track. A client that hardcodes it disagrees with the listener's own choice, silently, and nothing anywhere errors.

Following it is not cosmetic agreement. The same threshold decides whether a track this client announced as now-playing and never scrobbled is written off as a skip — so submitting later than this rule banks the listen and leaves a skip behind it, and submitting earlier puts a listen on a permanent public record that the listener's own setting says had not been heard.

Reported as the server computes it, never as stored: the values are clamped (fraction 0.05–1.0, seconds 10–3600), so a preferences object holding nonsense reads as the number every source here actually uses. GET /api/v1/preferences serves the raw blob and is session-only.

Accepts a plain submit token, not read: this read exists only to decide a submission, and putting it behind read would make every scrobble client carry read access to the whole listening history to learn one number it is obliged to obey.

It is its own endpoint rather than more keys on /1/validate-token — the opposite call from the introspection there, and deliberately. Those keys describe the token, are asked once at setup, and stay correct when cached. This is a setting, changed from a settings screen at any time; cached from a setup handshake it would go quietly stale, which is the failure this endpoint exists to prevent. Re-read it when a session starts.

Responses

200

OK.

application/json

object

When a listen counts, as the server applies it.

fraction number · double required

Share of the track that must have played, 0.05–1.0. This is the number the server multiplies the duration by.

percent number · double required

fraction as a percentage, which is how the setting is worded.

after_secs integer · int64 required

Seconds after which it counts regardless of the fraction.

rule string required

Always either — whichever comes first. Stated rather than implied: a client reading the two as an AND scrobbles a long track hours late and a short one never.

no_duration_after_secs integer · int64 required

What applies when the track's length is unknown — the fraction is unanswerable, so the flat number is the only rule left. Equal to after_secs; named separately so a client does not invent its own answer for that case.

source string required

user or default: whether the listener chose these numbers or inherited them, so a settings screen can say which.

default_percent number · double required

The instance default, for showing what "standard" is.

default_after_secs integer · int64 required

The instance default, for showing what "standard" is.

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."
}
500

The settings could not be read. Never answered as "they have not set one" — the default is an answer, and giving it here would have a client follow a rule the server is not applying.

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.

GET /api/v1/amai/settings #
Session

Whether the AudioMuse-AI sidecar is configured.

Admin, session-only. Returns the address — which is not a credential and is shown the way a source's URL is — and never the API token.

Responses

200

OK.

application/json

object

Where the AudioMuse-AI sidecar is, and never its token.

configured boolean required
url string | null

The address. Not a credential, and shown the way a source's URL is.

has_token boolean required
server string | null

Which of amai's media servers this instance's provider ids belong to. Absent means amai's default server.

stored boolean required

Something is stored in this instance's settings — which beats the environment.

from_env boolean required

TAPEDECK_AMAI_URL is set.

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

Admin only.

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.

PUT /api/v1/amai/settings #
Session

Point this instance at an AudioMuse-AI instance.

Admin, session-only — a token gets 401 and scopes never enter into it, the line /api/v1/sources already sits on. Server-wide rather than per-user: amai is one box the operator runs over one library, and every user here reads the same catalogue. Stored encrypted, and a stored value beats TAPEDECK_AMAI_URL / _TOKEN / _SERVER.

Each field is written only when present, so saving one does not wipe another; clearing is an explicit empty string. An address with no http:// or https:// scheme is a 400 rather than being guessed at — guessing http would silently downgrade an instance meant to be reached over TLS.

server names which of amai's own media servers this instance's provider ids belong to (its display name or internal id). Absent means amai's default server, which is the whole answer on a single-server install.

Request body required

application/json

object

Each field is written only when present; an empty string clears it.

url string | null

http://host:8000. Empty clears it and switches the sidecar off. No scheme is a 400 rather than a guess.

token string | null

Their API_TOKEN. Empty clears it.

server string | null

Which of amai's media servers our provider ids belong to — its display name or its internal id. Empty means their default server, which is the whole answer on a single-server install.

Responses

200

Saved.

application/json

object

Whether the sidecar is configured now.

configured boolean required
400

The address is unusable.

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

Admin only.

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.

POST /api/v1/amai/test #
Session

Is AudioMuse-AI there, and does the token work?

Admin, session-only. Two questions, reported separately, because they need completely different things doing about them and a screen that only says ok: false makes them the same colour. amai's own before_request barrier exempts /api/health and nothing else, so reachability is answerable without a credential and a bad token is a clean 401 — the inverse of the Discogs trap, where a bad credential is served as a normal 200.

A rejection is a 200 with ok: false, not an HTTP error: the request to Tapedeck succeeded and the answer is "no".

servers carries only server_id, name, server_type and is_default. amai's own response also holds a masked creds map; nothing here stores or logs it.

Responses

200

Result of the check.

application/json

object

Both halves of the check, reported separately.

ok boolean required
reachable boolean required
authenticated boolean required
message string required
servers array | null

amai's media servers. Present only when the token was accepted.

each item
object

One of amai's configured media servers, as /api/servers reports it.

Only these three fields are read. That response also carries a creds map — masked, but still theirs — and music_libraries. Nothing here stores or logs either: a credential belonging to another application has no business crossing into this one even in masked form.

server_id string required
name string required
server_type string
is_default boolean
400

Not configured on this server.

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

Admin only.

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.

GET /api/v1/amai/coverage #
Session

How much of this history can AudioMuse-AI answer for?

Admin, session-only. The measurement that decides whether anything is worth building on the sidecar, and it narrows in three steps.

The structural ceiling. A listen joins to amai on a media-server provider id, so one that never came off Plex, Jellyfin or Navidrome cannot join at all whatever amai has analysed. joinable_* is that, computed over the whole history because it is a plain string test.

What amai has, measured on the most-played tracks. Weighted by plays rather than sampled evenly, because a track played fifty times is worth fifty times as much to anything built on these vectors.

What the sample is a sample of — sample_share_of_listening — so the figure above is never read as a fact about the whole history. A share stated without its population is the fidelity-card failure.

One request per track against amai, so the handler owns a deadline rather than trusting N × the client timeout. deadline_hit says when it stopped early, and a failed ask is never counted as a miss: error carries the first one.

Parameters

sample query

How many of the most-played joinable tracks to ask about. Default 100, at most 500.

integer

Responses

200

The measurement.

application/json

object

How much of this history AudioMuse-AI can answer for. Every share is a percentage, and null when its population is empty.

total_plays integer · int64 required
distinct_tracks integer required
joinable_tracks integer required

Tracks that came off a media server, so could join at all.

joinable_plays integer · int64 required
joinable_share_of_listening number | null · double

The structural ceiling, over the whole history.

asked integer required

Joinable tracks actually asked about, most-played first.

analysed integer required
analysed_share_of_asked number | null · double
analysed_share_of_sampled_listening number | null · double

Weighted by plays — the figure the decision rests on.

sample_share_of_listening number | null · double

What share of all listening the sample is.

sample_requested integer required
deadline_hit boolean required

The probe stopped at its deadline before asking about the whole sample.

error string | null

The first failed ask. A failed ask is never counted as a miss.

server_asked string | null
examples_not_analysed array required

Up to twenty.

each item
object

A most-played track amai had not analysed.

artist string required
title string required
plays integer · int64 required
400

Not configured, or there are no listens to measure.

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

Admin only.

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.