feat(web): AcoustID card on Integrations — key, threshold, coverage by source (M401 #3922)
release / go (push) Successful in 2m25s
release / web (push) Failing after 26s
release / govulncheck (push) Successful in 21s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 3m34s
release / android (push) Canceled after 1m3s
release / Build signed APK (releases and dev) (push) Canceled after 1m3s

The card takes the slot of the unimplemented "MusicBrainz overrides"
placeholder. Rows:
- the on switch
- a write-only key field (the stored key is never sent back), with a
  link to register an application
- the minimum score (0.5 to 1)

Below them, recording-id coverage reads as a column: from tags, looked
up, none, and of the none how many are waiting, no match, ambiguous or
failed. There is a "Look up now" button and a folded list of the tracks
the lookup could not settle.

Off, keyless and stopped-short passes are each a visible state with the
reason (rule 164).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 23:29:34 -04:00
co-authored by Claude Opus 5.5
parent 3c575b137c
commit 0a7f788390
5 changed files with 600 additions and 17 deletions
+60
View File
@@ -398,6 +398,66 @@ export async function updateLoudnessSettings(s: LoudnessSettings): Promise<Loudn
return api.put<LoudnessSettings>('/api/admin/library/loudness-settings', s);
}
// AcoustID recording-id lookup (M401) ---------------------------------------
export type AcoustIDSettings = {
enabled: boolean;
min_score: number;
/** The key itself is never sent back, only whether one is stored. */
api_key_set: boolean;
};
export type AcoustIDOverview = {
settings: AcoustIDSettings;
/** Switched on and holding a key: the worker will call AcoustID. */
ready: boolean;
status: { running: boolean; last_pass_at: string | null; problem: string };
/** from_tag + from_acoustid + tracks with no MBID = total. */
coverage: {
total: number;
from_tag: number;
from_acoustid: number;
ambiguous: number;
no_match: number;
failed: number;
pending: number;
};
};
export type AcoustIDUnsettled = {
track_id: string;
title: string;
artist: string;
album: string;
file_path: string;
state: 'ambiguous' | 'failed';
best_score: number | null;
candidates: number;
detail: string | null;
looked_up_at: string;
};
export async function getAcoustID(): Promise<AcoustIDOverview> {
return api.get<AcoustIDOverview>('/api/admin/library/acoustid');
}
/** api_key left out keeps the stored key; an empty string clears it. */
export async function updateAcoustIDSettings(s: {
enabled: boolean;
min_score: number;
api_key?: string;
}): Promise<AcoustIDSettings> {
return api.put<AcoustIDSettings>('/api/admin/library/acoustid-settings', s);
}
export async function runAcoustIDLookup(): Promise<void> {
await api.post<null>('/api/admin/library/acoustid/run');
}
export async function listUnsettledAcoustID(): Promise<AcoustIDUnsettled[]> {
return api.get<AcoustIDUnsettled[]>('/api/admin/library/acoustid/unsettled');
}
// Cover-art providers ------------------------------------------------------
export type CoverProviderCapability = 'album_cover' | 'artist_thumb' | 'artist_fanart';