Commit Graph
1 Commits
Author SHA1 Message Date
58e7407c38 v3 library: personal per-song metadata (user-difficulty / notes / tags) — P1 (#691)
* v3 library: personal per-song metadata (user-difficulty / notes / tags) — P1

The local "your relationship to the song" layer: a per-song user-difficulty
(1–5, planning-only, distinct from the authored 1–10 difficulty bands),
free-form notes, and free-form practice tags. All kept OUT of the shared
feedpak file and OUT of the `songs` table, in two new never-clobbered tables
so a rescan's `INSERT OR REPLACE INTO songs` can't wipe them. Likes stay the
existing favorites heart — no rating column.

Backend only (the Details drawer + tag/difficulty filter UI come next).

Schema (additive, idempotent):
- song_user_meta(filename PK, user_difficulty INTEGER, notes TEXT, updated_at)
- song_tags(filename, tag, created_at, PK(filename, tag)) + idx on tag

API (DB-only — distinct from POST /api/song/{f}/meta, which writes catalog
fields back into the file):
- GET  /api/song/{f}/user-meta  → {user_difficulty, notes, tags}
- PUT  /api/song/{f}/user-meta  → partial update; user_difficulty (1–5 or
  null), notes (string or null), tags (full-replace array). Tag removal is a
  full-replace array rather than a DELETE sub-route because the greedy
  DELETE /api/song/{filename:path} already owns every DELETE under /api/song
  and would shadow it.
- GET  /api/tags → tags in use with counts (for the filter UI)

Read path:
- query_page rows embed user_difficulty + tags (like `favorite`); notes stay
  out of the list payload (per-song GET — they can be long).
- Read-time filters ?user_difficulty= and ?tags= threaded through _build_where
  exactly like the mastery filter — EXISTS-style predicates, so keyset paging,
  counts, and the A–Z rail are unaffected.
- delete_song purges both personal tables inside the existing lock.

Tags are normalized (trim + lowercase + collapse whitespace) so "Rock"/"rock"
don't split. New tests cover defaults, difficulty validation (rejects out-of-
range / non-integral / bool), notes trim, tag normalize/dedupe, grid embed,
both filters, never-clobber-on-rescan, and purge (21 tests). Neighboring
library tests (filters/keyset/providers/playlists/collections/stats) stay
green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

* fix(v3): cap per-song tags at 50 to bound writes (PR #691 review)

set_song_tags capped each tag at 60 chars but not the number of tags,
so one PUT could write unbounded rows. Cap the normalized-unique tag
list to the first 50 after dedup. Adds a test asserting >50 stores 50.

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-07-02 13:25:18 +02:00