Files
feedBack/tests/js/v3_playlist_cover.test.js
T
90fb2ee3bc feat(v3): content-dependent playlist covers + custom art (#626)
* 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>
2026-06-28 14:10:15 +02:00

29 lines
1.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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\)/);
});