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
@@ -210,6 +210,43 @@ code { font: 0.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; bac
|
||||
.item-tags .badge { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; display: inline-block; }
|
||||
.item-year { color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; flex: none; }
|
||||
|
||||
/* ---------- preview play buttons ---------- */
|
||||
.item { position: relative; }
|
||||
.play-btn {
|
||||
position: absolute; top: 8px; right: 8px; z-index: 2;
|
||||
display: grid; place-items: center; width: 36px; height: 36px; padding: 0;
|
||||
border: 0; border-radius: 50%; cursor: pointer;
|
||||
background: rgba(10, 10, 14, 0.72); color: #fff;
|
||||
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
transition: background 0.15s, transform 0.15s;
|
||||
}
|
||||
.play-btn:hover { background: var(--accent); color: var(--accent-text); transform: scale(1.06); }
|
||||
.play-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
||||
.play-btn .icon { width: 15px; height: 15px; fill: currentColor; stroke: none; }
|
||||
.play-btn .icon:first-child { margin-left: 2px; }
|
||||
.play-btn .icon:last-child, .play-btn.is-playing .icon:first-child { display: none; }
|
||||
.play-btn.is-playing .icon:last-child { display: block; }
|
||||
.play-btn.is-playing, .play-btn.is-loading { background: var(--accent); color: var(--accent-text); }
|
||||
/* Progress ring around the button while a clip plays. */
|
||||
.play-btn::before {
|
||||
content: ""; position: absolute; inset: -3px; border-radius: 50%; pointer-events: none;
|
||||
background: conic-gradient(var(--accent) calc(var(--p, 0) * 1%), transparent 0);
|
||||
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));
|
||||
mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));
|
||||
opacity: 0; transition: opacity 0.2s;
|
||||
}
|
||||
.play-btn.is-playing::before { opacity: 1; }
|
||||
.play-btn.is-loading .icon:first-child { animation: pulse 0.9s ease-in-out infinite; }
|
||||
@keyframes pulse { 50% { opacity: 0.35; } }
|
||||
.play-btn--inline, .play-btn--mini { position: relative; top: auto; right: auto; flex: none; box-shadow: none; }
|
||||
.play-btn--inline { width: 32px; height: 32px; }
|
||||
.play-btn--inline:not(.is-playing):not(.is-loading):not(:hover) { background: var(--surface-2); color: var(--text); }
|
||||
.play-btn--mini { position: absolute; right: 3px; bottom: 3px; width: 24px; height: 24px; }
|
||||
.play-btn--mini .icon { width: 11px; height: 11px; }
|
||||
.result-art { position: relative; }
|
||||
.result-source { display: inline-block; margin-top: 3px; font-size: 10.5px; font-weight: 650; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); }
|
||||
|
||||
/* ---------- ranked items: rows ---------- */
|
||||
.items--rows { display: flex; flex-direction: column; border-top: 1px solid var(--border); }
|
||||
.items--rows .item { border-bottom: 1px solid var(--border); }
|
||||
@@ -230,6 +267,9 @@ code { font: 0.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; bac
|
||||
.items--rows .item--top3 .item-rank { color: var(--bronze); }
|
||||
.items--rows .item-body { order: 3; flex: 1; }
|
||||
.items--rows .item-title { font-size: 16px; -webkit-line-clamp: 1; }
|
||||
.items--rows .play-btn { top: 50%; right: 10px; transform: translateY(-50%); }
|
||||
.items--rows .play-btn:hover { transform: translateY(-50%) scale(1.06); }
|
||||
.items--rows .item:has(.play-btn) .item-btn { padding-right: 60px; }
|
||||
.items--rows .item-note { color: var(--muted); font-size: 13.5px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-top: 2px; }
|
||||
@media (min-width: 900px) {
|
||||
.page-home .items--rows { display: grid; grid-template-columns: 1fr 1fr; column-gap: 32px; border-top: 0; }
|
||||
|
||||
Reference in new issue
Block a user