mirror of
https://github.com/myronblair/kino-app
synced 2026-07-29 14:02:50 -05:00
Add iTunes and AcoustID as additional free music enrichment sources
- itunes.py: zero-auth fallback for album artwork when MusicBrainz/Cover Art Archive has no match — kicks in automatically during music enrichment, toggleable in Settings (on by default, no key required). - acoustid.py: audio fingerprint identification (Chromaprint fpcalc + the AcoustID API) for tracks with missing/unknown artist or album — nothing to text-search on, so this identifies from the actual audio content instead. Requires a free user-supplied API key from acoustid.org/api-key, off by default. New /music/identify endpoint + admin trigger button. - Dockerfile: added libchromaprint-tools for fpcalc. - Settings page: new Music Metadata & Artwork section with both toggles.
This commit is contained in:
@@ -48,6 +48,7 @@ export default function Settings() {
|
||||
sonarr_url: "", sonarr_api_key: "",
|
||||
opensubs_api_key: "", trakt_client_id: "", trakt_client_secret: "",
|
||||
auto_transcode: "off", queue_paused: false,
|
||||
music_itunes_fallback: true, music_acoustid_enabled: false, acoustid_api_key: "",
|
||||
});
|
||||
const [info, setInfo] = useState({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -58,6 +59,7 @@ export default function Settings() {
|
||||
const [pwShow, setPwShow] = useState(false);
|
||||
const [pwSaving, setPwSaving] = useState(false);
|
||||
const [enrich, setEnrich] = useState(null);
|
||||
const [identify, setIdentify] = useState(null);
|
||||
|
||||
const load = async () => {
|
||||
if (!isAdmin) return;
|
||||
@@ -68,6 +70,9 @@ export default function Settings() {
|
||||
opensubs_api_key: data.opensubs_api_key || "",
|
||||
trakt_client_id: data.trakt_client_id || "", trakt_client_secret: data.trakt_client_secret || "",
|
||||
auto_transcode: data.auto_transcode || "off", queue_paused: !!data.queue_paused,
|
||||
music_itunes_fallback: data.music_itunes_fallback !== false,
|
||||
music_acoustid_enabled: !!data.music_acoustid_enabled,
|
||||
acoustid_api_key: data.acoustid_api_key || "",
|
||||
});
|
||||
setInfo(data); setTraktStatus(ts);
|
||||
};
|
||||
@@ -131,6 +136,7 @@ export default function Settings() {
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return;
|
||||
api.get("/tmdb/enrich-all/status").then(({ data }) => setEnrich(data)).catch(() => {});
|
||||
api.get("/music/identify/status").then(({ data }) => setIdentify(data)).catch(() => {});
|
||||
}, [isAdmin]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -150,6 +156,33 @@ export default function Settings() {
|
||||
return () => clearInterval(t);
|
||||
}, [enrich?.running]);
|
||||
|
||||
const identifyAll = async () => {
|
||||
if (!window.confirm("Fingerprint every track with an unknown artist or album and identify it via AcoustID? This may take a while.")) return;
|
||||
try {
|
||||
await api.post("/music/identify");
|
||||
const { data } = await api.get("/music/identify/status");
|
||||
setIdentify(data);
|
||||
toast.success("Identification started");
|
||||
} catch (err) { toast.error(err.response?.data?.detail || "Could not start"); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!identify?.running) return;
|
||||
const t = setInterval(async () => {
|
||||
const { data } = await api.get("/music/identify/status");
|
||||
setIdentify(data);
|
||||
if (!data.running) {
|
||||
clearInterval(t);
|
||||
if (data.error) {
|
||||
toast.error(`Identification failed: ${data.error}`);
|
||||
} else {
|
||||
toast.success(`Identification done — ${data.identified} of ${data.total} tracks identified`);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
return () => clearInterval(t);
|
||||
}, [identify?.running]);
|
||||
|
||||
return (
|
||||
<FieldCtx.Provider value={{ s, setS, show, setShow }}>
|
||||
<div className="min-h-screen bg-[#050505] pt-32 pb-24" data-testid="settings-page">
|
||||
@@ -307,6 +340,65 @@ export default function Settings() {
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{/* Music Metadata & Artwork */}
|
||||
<section>
|
||||
<h2 className="font-display text-2xl font-bold text-white mb-3">Music Metadata & Artwork</h2>
|
||||
<p className="text-sm text-[#8A8A8A] mb-4">
|
||||
Album art/tags come from MusicBrainz + Cover Art Archive first (free, no key). These add free fallbacks for what that misses.
|
||||
</p>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={s.music_itunes_fallback}
|
||||
onChange={(e) => setS({ ...s, music_itunes_fallback: e.target.checked })}
|
||||
className="w-4 h-4 accent-[#D9381E]"
|
||||
data-testid="settings-itunes-fallback"
|
||||
/>
|
||||
<span className="text-sm text-white">Fall back to iTunes Search for artwork MusicBrainz doesn't have</span>
|
||||
</label>
|
||||
<p className="text-xs text-[#8A8A8A] mt-1 ml-7">No API key needed — always free, no signup.</p>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-[#222]">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm text-white">AcoustID audio fingerprint identification</span>
|
||||
<Badge on={info.acoustid_configured} />
|
||||
</div>
|
||||
<p className="text-sm text-[#8A8A8A] mb-4">
|
||||
Identifies tracks with missing/unknown artist or album by fingerprinting the actual audio file — useful for badly-tagged files with nothing to text-search on. Free key at{" "}
|
||||
<a className="text-[#D9381E]" href="https://acoustid.org/api-key" target="_blank" rel="noreferrer">acoustid.org/api-key</a>.
|
||||
</p>
|
||||
<label className="flex items-center gap-3 cursor-pointer mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={s.music_acoustid_enabled}
|
||||
onChange={(e) => setS({ ...s, music_acoustid_enabled: e.target.checked })}
|
||||
className="w-4 h-4 accent-[#D9381E]"
|
||||
data-testid="settings-acoustid-enabled"
|
||||
/>
|
||||
<span className="text-sm text-white">Enable AcoustID identification</span>
|
||||
</label>
|
||||
<Field label="API key" k="acoustid_api_key" mask />
|
||||
{info.acoustid_configured && s.music_acoustid_enabled && (
|
||||
<div className="mt-4 flex items-center gap-4 flex-wrap">
|
||||
<button type="button" onClick={identifyAll} disabled={identify?.running} className="text-xs uppercase tracking-[0.2em] border border-[#222] hover:border-[#D9381E] hover:text-[#D9381E] text-[#8A8A8A] px-4 py-2 transition-colors disabled:opacity-50" data-testid="identify-all-button">
|
||||
{identify?.running ? `Identifying… ${identify.done}/${identify.total}` : "Identify unknown tracks →"}
|
||||
</button>
|
||||
{identify && !identify.running && identify.error && (
|
||||
<span className="text-xs text-[#fca5a5]" data-testid="identify-all-error">
|
||||
Last run failed: {identify.error}
|
||||
</span>
|
||||
)}
|
||||
{identify && !identify.running && !identify.error && identify.total > 0 && (
|
||||
<span className="text-xs text-[#8A8A8A]" data-testid="identify-all-summary">
|
||||
Last run: {identify.identified} of {identify.total} identified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="submit" disabled={saving} className="bg-[#D9381E] hover:bg-[#ED4B32] disabled:opacity-60 text-white px-8 py-3 text-sm uppercase tracking-[0.2em]" data-testid="settings-save-button">
|
||||
{saving ? "Saving…" : "Save Settings"}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user