feedBack/static/v3
ChrisBeWithYou 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
..
avatars Clean release snapshot 2026-06-16 18:47:13 +02:00
brand Clean release snapshot 2026-06-16 18:47:13 +02:00
pedals Clean release snapshot 2026-06-16 18:47:13 +02:00
audio-routing.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
badges.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
brand.js Clean release snapshot 2026-06-16 18:47:13 +02:00
card-actions-core.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
dashboard.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
feedbarcade.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
index.html fix(player): reliable Escape "Back" + resumable, optionally-confirmed song exit (#619) 2026-06-28 12:32:33 +02:00
lessons.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
live-guitar-tone-source.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
live-performance-hud.js fix(v3): reset live performance HUD on backward seek / restart (#607) 2026-06-27 00:47:43 +02:00
manifest.json Clean release snapshot 2026-06-16 18:47:13 +02:00
notifications.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
onboarding-tour.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
overlay-active.png Clean release snapshot 2026-06-16 18:47:13 +02:00
overlay-inactive.png Clean release snapshot 2026-06-16 18:47:13 +02:00
pedal-cables.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
pedal-default.svg Clean release snapshot 2026-06-16 18:47:13 +02:00
pedal-frame.svg Clean release snapshot 2026-06-16 18:47:13 +02:00
player-chrome.js fix(v3): add "Show 'Up Next'" toggle so the player pill can be turned off (#612) 2026-06-27 13:16:31 +02:00
playlists.js feat(v3): content-dependent playlist covers + custom art (#626) 2026-06-28 14:10:15 +02:00
plugins-page.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
profile.js feat(achievements): opt-in, privacy controls & data-min gate (epic PR2) (#591) 2026-06-24 17:00:30 +02:00
progress.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
progression-core.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
scoreboard-pref.js fix(highway): user-selectable scoreboard to stop duplicate HUDs 2026-06-19 20:56:38 +02:00
settings.js fix(player): reliable Escape "Back" + resumable, optionally-confirmed song exit (#619) 2026-06-28 12:32:33 +02:00
shell.js feat(v3): promote Audio Engine to a first-class sidebar entry (after Settings) (#613) 2026-06-27 18:16:36 +02:00
shop.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
songs.js feat(v3): add "Add to playlist" to a song's ⋮ More menu (#625) 2026-06-28 13:58:14 +02:00
stats-recorder.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
theme-core.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
tuner-core.js Clean release snapshot 2026-06-16 18:47:13 +02:00
v3.css fix(v3): keep Section Map's leftmost section clickable under the rail catcher (#617) 2026-06-27 21:07:44 +02:00
venue-instrument-pov.js Clean release snapshot 2026-06-16 18:47:13 +02:00
venue-mood-fx.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
venue-scene-3d.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
venue-viz.js Clean release snapshot 2026-06-16 18:47:13 +02:00