mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
* feat(v3): content-dependent playlist covers + custom art upload Playlist cards were a tiny 🎵 emoji on an empty square. Now the cover reflects the playlist's contents, and you can override it with a custom image. Cover (in priority order): - custom uploaded cover, else - empty playlist -> the icon - a few songs -> the first song's album art - 4+ songs -> a 2x2 album-art mosaic Backend (server.py): - MetadataDB.list_playlists() returns each playlist's first few still-present songs' art URLs (`art_urls`) for the content cover. - GET /api/playlists and GET /api/playlists/{id} add `cover_url` when a custom cover exists. - POST/GET/DELETE /api/playlists/{id}/cover — store a small PNG thumbnail under CONFIG_DIR/playlist_covers/ (PIL-converted, mirroring song-art upload); the cover is deleted with the playlist. Cover mutators added to _MUTATING_ROUTES. Frontend (static/v3/playlists.js): playlistCoverHtml(p) renders the rules above; the playlist detail view gets "Cover" (pick an image) + "Remove cover". Tests: tests/test_playlists_api.py (art_urls + cover roundtrip / reject-non-image / delete-removes-cover — 11 pass) and tests/js/v3_playlist_cover.test.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(playlists): 400 (not 500) on non-string cover image + bust same-second cover cache Two review follow-ups on the playlist-cover endpoints: - POST /cover did `if "," in b64` before any type check, so a non-string image (e.g. {"image": 123} / null) raised TypeError -> 500. Guard with isinstance (mirrors the avatar/song-art upload) for a clean 400. +regression test covering number/null/object/list. - The cover URL busted only on int(st_mtime) (1s granularity) and GET /cover sent no cache headers, so a same-second replace/remove/re-upload could serve a stale image. Use st_mtime_ns in the cache-bust token and add the shared no-cache header (_ART_CACHE_HEADERS), matching song art. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
29 lines
1.2 KiB
JavaScript
29 lines
1.2 KiB
JavaScript
// Guard for the content-dependent playlist cover (playlists.js). A custom
|
||
// uploaded cover wins; otherwise the playlist's song art decides: icon when
|
||
// empty, a single cover for a few songs, a 2×2 mosaic at 4+. (Rendering is DOM
|
||
// glue, so this is a source-level guard on the decision branches.)
|
||
|
||
'use strict';
|
||
const { test } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
|
||
const PL = fs.readFileSync(
|
||
path.join(__dirname, '..', '..', 'static', 'v3', 'playlists.js'), 'utf8');
|
||
|
||
test('custom cover_url takes priority', () => {
|
||
assert.match(PL, /function playlistCoverHtml\(p\)/);
|
||
assert.match(PL, /if \(p\.cover_url\) return/);
|
||
});
|
||
|
||
test('empty → icon, <4 → single art, 4+ → 2×2 mosaic', () => {
|
||
assert.match(PL, /if \(!arts\.length\)[\s\S]{0,160}(🔖|🎵)/); // empty → icon
|
||
assert.match(PL, /arts\.length < 4\) return[\s\S]{0,120}arts\[0\]/); // a few → single cover
|
||
assert.match(PL, /grid-cols-2 grid-rows-2[\s\S]{0,120}slice\(0, 4\)/); // 4+ → mosaic
|
||
});
|
||
|
||
test('the card uses playlistCoverHtml (not the old static emoji box)', () => {
|
||
assert.match(PL, /playlistCoverHtml\(p\)/);
|
||
});
|