Search Deezer and MusicBrainz together; add preview clips
Search
- Albums and songs search Deezer and MusicBrainz at once and merge the
results: duplicates keep the Deezer copy, best word matches first.
Deezer's search misses albums it carries (Death Magnetic) and most game
soundtracks; MusicBrainz finds them
- MusicBrainz secondary types are shown ("Soundtrack", "Live")
- Settings: Deezer + MusicBrainz (default), Deezer only, MusicBrainz only
- Search results show which service they came from
Previews
- Songs and albums get a play button (cards, rows, detail view, editor,
and Deezer search results) for a 30-second Deezer clip; albums play
their most popular track
- Clips are looked up fresh (Deezer URLs expire), cached 10 minutes, and
streamed through Canon with Range support; hidden lists stay private
- Entries from MusicBrainz or added by hand are matched to Deezer on save
so they can preview too
- "Play previews" toggle in Settings → Profile
Tests cover merging and title/artist matching; `-tags live` runs checks
against the real services.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d92b46130
commit
b09533cc00
20 files changed
+736
-19
No files matched your search
@@ -2,6 +2,7 @@
|
||||
import { h, icon, api, readBoot, toast, confirmDialog, openDialog, dialogHead, debounce, busy, fmt } from "./common.js";
|
||||
import { sortable } from "./sortable.js";
|
||||
import { openListForm } from "./list-form.js";
|
||||
import { previewButton } from "./player.js";
|
||||
|
||||
const boot = readBoot();
|
||||
const { categories, limits, productions } = boot;
|
||||
@@ -119,6 +120,7 @@ function row(it) {
|
||||
),
|
||||
),
|
||||
h("div", { class: "rank-row-actions" },
|
||||
canPreview(it) && previewButton(`/preview/${it.id}`, `Play a preview of ${it.title}`, "play-btn play-btn--inline hide-sm"),
|
||||
h("button", { type: "button", class: "icon-btn hide-sm", "aria-label": "Move up", title: "Move up", disabled: it.rank === 1, onclick: () => moveTo(it, it.rank - 1) }, icon("chevron-up")),
|
||||
h("button", { type: "button", class: "icon-btn hide-sm", "aria-label": "Move down", title: "Move down", disabled: it.rank === list.itemCount, onclick: () => moveTo(it, it.rank + 1) }, icon("chevron-down")),
|
||||
h("button", { type: "button", class: "icon-btn", "aria-label": `Edit ${it.title}`, title: "Edit", onclick: () => openItemForm({ item: it }) }, icon("pencil")),
|
||||
@@ -127,6 +129,11 @@ function row(it) {
|
||||
);
|
||||
}
|
||||
|
||||
// Songs and albums from Deezer, or matched to Deezer when saved, can play.
|
||||
function canPreview(it) {
|
||||
return (cat.key === "song" || cat.key === "album") && (it.provider === "deezer" || !!it.extra?.deezer);
|
||||
}
|
||||
|
||||
function renderItems(flashId) {
|
||||
if (!items.length) {
|
||||
rankList.replaceChildren(h("li", { class: "rank-empty" },
|
||||
@@ -259,6 +266,7 @@ function renderResults(res) {
|
||||
const full = isFull();
|
||||
const nodes = res.map((r) => {
|
||||
const sub = [r.subtitle, r.year, r.extra?.type].filter(Boolean).join(" · ");
|
||||
const source = { deezer: "Deezer", musicbrainz: "MusicBrainz" }[r.provider];
|
||||
const action = r.existingRank
|
||||
? h("span", { class: "result-added" }, icon("check"), `#${r.existingRank}`)
|
||||
: h("button", {
|
||||
@@ -266,11 +274,14 @@ function renderResults(res) {
|
||||
title: full ? "This list is full" : `Add to #${list.itemCount + 1}`,
|
||||
onclick: (e) => addResult(r, e.currentTarget),
|
||||
}, icon("plus"), "Add");
|
||||
const art = artBox(r.imageUrl, "");
|
||||
if (r.previewRef) art.append(previewButton(`/admin/preview?ref=${encodeURIComponent(r.previewRef)}`, `Play a preview of ${r.title}`, "play-btn play-btn--mini"));
|
||||
return h("div", { class: "result", "data-key": resultKey(r) },
|
||||
artBox(r.imageUrl, ""),
|
||||
art,
|
||||
h("div", { style: { minWidth: 0 } },
|
||||
h("div", { class: "result-title" }, r.title),
|
||||
sub && h("div", { class: "result-sub" }, sub),
|
||||
source && h("span", { class: "result-source" }, source),
|
||||
(r.overview || r.hint) && h("div", { class: "result-overview" }, r.overview || r.hint),
|
||||
),
|
||||
action,
|
||||
|
||||
Reference in new issue
Block a user