Commit Graph
16 Commits
Author SHA1 Message Date
bvandeusen 37b396a7e4 fix(scanner): read multi-value genre frames correctly — #2499
test-go / test (push) Failing after 41s
test-go / integration (push) Canceled after 4m46s
dhowden/tag's readTFrame splits ID3v2 null-separated multi-value text
frames and rejoins them with the EMPTY string, so a file tagged
"Alternative Rock" + "Rock" was stored as "Alternative RockRock". It also
leaves bare numeric ID3v1 references unresolved, which is why the
library showed genres like "4017" and "526617".

This corrupted more than the browse axis added in #367: taste_profile.sql
reads tracks.genre directly, so the welded tokens were entering the taste
profile's tag vocabulary, and recommendation.sql/discover.sql were
comparing them as single opaque tags. Genre counts were wrong everywhere.

ffprobe is not a fix — ffmpeg's read_ttag calls decode_str once with no
loop, keeping only the first value. Truncating multi-genre tags would
blunt the similarity signal genre mainly feeds. So the TCON frame is now
parsed directly (ID3v2.2/2.3/2.4, all four text encodings, per-frame and
tag-level unsynchronisation, numeric and parenthesised ID3v1 references);
everything else still comes from dhowden/tag. Values are stored
";"-delimited, which the read side already splits on, so no query changes.

Existing rows are repaired without an operator-run rebuild: migration
0054 adds tracks.tag_read_version DEFAULT 0, below the scanner's current
tagReadVersion, so the next scan re-reads tags it would otherwise skip on
mtime. Such a re-read reuses the stored duration instead of re-running
ffprobe, keeping a repair pass tag-read-bound rather than one fork+exec
per file. Bumping the constant is how a future extraction fix reaches an
existing library.

Only ID3v2 is in scope — dhowden welds nowhere else. The Vorbis/MP4
repeated-field question is #2500, unproven and deliberately not built.
2026-08-05 21:17:59 -04:00
bvandeusen cb0af5efd3 fix(dbq): commit the rest of the 0042 regen (Track model + embedders)
test-go / test (push) Successful in 30s
test-go / integration (push) Successful in 4m37s
The previous commit staged only track_tags.sql.go and left the rest of
the sqlc regen uncommitted, so HEAD referenced TrackTag / the new
tracks.tag_source columns without their definitions — a broken tree.

Adding tracks.tag_source + tag_sources_version to the tracks table
regenerated every generated file that returns/embeds the Track model
(models.go, tracks/events/history/likes/recommendation). Commit them all
together so dev HEAD compiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 18:47:34 -04:00
bvandeusenandClaude Opus 4.7 e2432caa65 fix(library): extract recording MBID → tracks.mbid; unblock LB similarity
tracks.mbid was 100% NULL: the scanner only extracted album + artist
MBIDs, never the recording MBID. The ListenBrainz similarity worker is
gated on tracks.mbid IS NOT NULL, so track_similarity could never
populate — starving For-You/radio's strongest candidate source
(lb_similar) on every library.

- mbids.go: add extractRecordingMBID (mbz.Recording / Picard
  musicbrainz_recordingid). Separate fn so extractMBIDs' signature +
  unit tests stay untouched.
- scanner.go: persist recording MBID via UpsertTrack (heals on the
  file_path conflict, so re-scans backfill for free).
- BackfillTrackMBIDs: one-shot pass mirroring BackfillMBIDs, wired as
  scan Stage 2b (idempotent via SetTrackMbidIfNull, gated by
  BackfillCap, log-only progress).
- migration 0029: tracks_mbid_unique (0002, written when the column
  was always NULL) wrongly assumes one MBID == one track. A recording
  appears on multiple releases, so rows legitimately share a recording
  MBID. Replace with a non-unique partial index. Zero-risk: column is
  100% NULL at migration time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:49:38 -04:00
bvandeusenandClaude Opus 4.7 a5e2abb8c4 feat(offline): #427 S4a — Shuffle all + offline system-playlist gate
Headline of S4. "Shuffle all" is always present (home app-bar
shuffle icon); the pool degrades with reachability:
- online  → GET /api/library/shuffle?limit=N (new): N random
  library tracks server-side, per-user quarantine filtered
  (ListRandomTracksForUser, ORDER BY random()).
- offline → ShuffleSource shuffles the whole local cache index
  (audio_cache_index ∩ cached_tracks, names from cached_artists/
  albums) — a UNION over liked AND recently-played, since the
  two-bucket split is storage-only and never filters playback.

Offline gating: refreshable (singleton) system playlists need the
live build/shuffle endpoints, so their tile play is disabled when
offlineProvider is true — Shuffle all is the offline path. User
playlists still play from cache.

playlist_card now reads offlineProvider in build → wrapped the
direct-render widget test in ProviderScope (offlineProvider is
smoke-safe from S1: tracked timers, no connectivity mount).

S4b (offline Recently-played / Liked browsable surfaces over the
cache index) next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:37:29 -04:00
bvandeusenandClaude Opus 4.7 9bf4b504b3 feat(server): GET /api/library/sync endpoint
Returns batched upserts + deletes since the supplied cursor. Empty cursor
returns full snapshot; subsequent calls pull deltas. Per-user entities
(likes, playlists) are scoped to the authed user. Composite-key entities
(likes, playlist_tracks) use stable string ids encoded by sync.EncodeLikeID
/ sync.EncodePlaylistTrackID.

Behavior:
  204 No Content - no changes since cursor
  200 OK         - JSON syncResponse {cursor, upserts, deletes}
  410 Gone       - cursor older than oldest log row; client must reset
  401 / 500      - standard envelope errors

Adds sqlc queries GetAlbumsByIDs, GetTracksByIDs, GetPlaylistsByIDs to
mirror the existing GetArtistsByIDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:36:26 -04:00
bvandeusen bffa397250 refactor(server/sql): unify *ForUser track queries via nullable user_id (A1) 2026-05-08 08:15:19 -04:00
bvandeusen 30b0edf97b feat(db): cascade-cleanup queries for M7 #372 track removal
DeleteTrack returns album_id + artist_id so the calling service can
chain the album-empty / artist-empty cascade. DeleteAlbumIfEmpty and
DeleteArtistIfEmpty are no-ops when other rows still reference the
parent — the service treats pgx.ErrNoRows as "not orphaned, skip".
2026-05-02 21:53:33 -04:00
bvandeusenandClaude Opus 4.7 f77245bc41 feat(requests): show ingest progress while requests are in flight
After Lidarr accepts a request the local reconciler imports albums and
tracks as they arrive — but until the request hit 'completed' the
operator had no way to see "is anything happening?" The /requests and
/admin/requests rows now surface a live progress line whenever the
request's matched entity has children in our library.

Backend:
- New CountAlbumsByArtist + CountTracksByArtist sqlc queries.
- requestView gains imported_album_count and imported_track_count.
- New fillProgress helper computes them from the matched entity:
  - kind=artist  → counts albums + tracks under matched_artist_id
  - kind=album   → counts tracks under matched_album_id
  - kind=track   → 1 once matched_track_id is set
  N+1 in the list endpoints; acceptable at admin scale.
- handleListRequests, handleGetRequest, and handleListAdminRequests
  populate the new fields on every response.

Frontend:
- LidarrRequest TS type extended with the two counters.
- Both the operator's /requests page and the admin /admin/requests
  page render an accent-colored line under the row meta when at
  least one counter is non-zero, e.g.:
    Artist:  "5 albums · 47 tracks ingested"
    Album:   "12 tracks ingested"
    Track:   "Track ingested"
- Updated test fixtures to include the new required fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:04:38 -04:00
bvandeusenandClaude Sonnet 4.6 b31723c5e2 feat(db): add M6a library-list + artist-tracks queries
Appends ListArtistsAlphaWithCovers, ListAlbumsAlphaWithArtist,
CountAlbums, and ListArtistTracksForUser queries; regenerates sqlc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:03:10 -04:00
bvandeusen 559fb5dd2c feat(db): add user-context track query variants honoring quarantine 2026-04-30 19:49:00 -04:00
bvandeusenandClaude Sonnet 4.6 9ceac5c639 feat(db): add lidarr_config + lidarr_requests schema (migration 0010)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:10:01 -04:00
bvandeusen cb46b3830f feat(db): add similarity lookup queries (ListActiveContextualLikesForUser, GetCurrentSessionVectorForUser) 2026-04-27 20:36:42 -04:00
bvandeusen 0c7e1b0408 feat(playevents): add Writer with start/end/skipped/synthetic paths
Owns the auto-close-prior step (caps each user at one open row),
spec §6 skip classification rule on play_ended (AND of completion <
0.5 and duration_played < 30s), and skip_events row writes. Synthetic
completed play wires the Subsonic /rest/scrobble?submission=true path
into the same store as native events.
2026-04-26 00:10:02 -04:00
bvandeusen fb49a39492 feat(db): add paged + count queries for library reads 2026-04-21 00:01:15 -04:00
bvandeusen f8249a12ac sqlc generate for CountTracksByAlbum and SearchTracks (#295) 2026-04-19 19:03:59 +00:00
bvandeusen 451971a9bd M1/#292: sqlc-generated tracks queries 2026-04-19 02:32:24 +00:00