Rewrites the internal/db/queries comments to rule 32 and regenerates
dbq, whose doc comments and embedded SQL carry them. The bugs behind the
tiered mixes, the md5 order seed, the score bands and the round-robin
arms are restated as the constraint each one enforces; counts from one
library and stale cross-references are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration 0091 drops lidarr_config and renames the quarantine tables, types,
indexes and the history column off the Lidarr name. The Integrations Lidarr
card, the Reports "Delete via Lidarr" action, the merge and remove-track
unmonitor options, the Lidarr audit actions and error codes go, on the
server, web and Android. The README describes acquisition as it is now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Milestone #160 Opt 2b (mood half of the era+mood option). A fourth taste
facet alongside artists + genre tags + eras: signed weights over canonical
mood buckets (melancholic / energetic / chill / …) derived from a track's
enriched folksonomy tags (#1490).
- internal/mood: shared vocabulary — Of(tags) maps folksonomy tags to
canonical mood buckets (synonyms collapse). Imported by both the taste
builder and the scorer so a track's mood is derived identically.
- Migration 0047: taste_profile_moods table + taste_tuning.mood_scale
(DEFAULT 0.5).
- Build side (internal/taste): Config.MoodScale ([0,1] damper, mirrors
EraScale); accumulate folds each play/like's mood buckets at
base*MoodScale; persist atomic-replaces the mood rows.
- Scorer (internal/recommendation): TasteProfile gains a mood term
(own tanh scale + additive 0.12 share, so it never weakens the existing
signal when a track has no mood tags). Match now takes the candidate's
mood buckets; loaded per candidate (ListTrackTagsForTracks → mood.Of) in
the primary similarity loader only — the near-whole-library fallback
pool passes nil (mood → 0) to avoid a full-library tag scan.
- Tuning lab: mood_scale threaded through recsettings + admin API + web
card ("Mood weight" row) + Go/web tests.
Coverage is partial (grows with tag enrichment; richer once Last.fm is
keyed), so mood is a supplement — neutral for tracks with no mood tags.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Milestone #160 Opt 2 (era half). A third taste facet alongside artists
+ genre tags: signed weights over decade buckets ("1990s") derived from
albums.release_date, rebuilt daily and scored into the taste match.
- Migration 0045: taste_profile_eras table (mirrors taste_profile_tags)
+ taste_tuning.era_scale column (DEFAULT 0.5).
- Build side (internal/taste): Config.EraScale ([0,1] damper, mirrors
EnrichedTagScale), accumulate folds each play/like's decade at
base*EraScale, persist atomic-replaces the era rows.
- Scorer (internal/recommendation): TasteProfile gains an era term (own
tanh scale + additive 0.15 share so it never weakens the existing
artist/tag signal when a track is undated); candidate queries return
album release_date; decadeOf mirrors the builder helper.
- Tuning lab: era_scale threaded through recsettings + admin API + web
card (auto-renders the new row) + Go/web tests.
Mood facet deferred to #1534 (partial enrichment coverage + needs
candidate-side enriched-tag loading).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The taste recompute's tag facet now unions the cached track_tags
(MusicBrainz/Last.fm folksonomy tags) alongside raw ID3 genre, so a coarse
"Rock" gains "post-punk / shoegaze / melancholic".
- taste_profile.sql: ListPlayEngagementInputsForUser +
ListLikedTrackTasteInputsForUser now return track_id to key the
enriched-tag lookup.
- accumulate(): for each play, fold its track's enriched tags weighted by
engagement × tag.weight × EnrichedTagScale; for each liked track, by the
tag-like bonus × tag.weight × scale. A track with no cached tags
contributes genre only (graceful).
- New Config.EnrichedTagScale (default 0.5) — enriched tags augment the
ID3 signal without swamping it; 0 = genre-only. Flows through
recsettings.TasteConfig() (starts from DefaultConfig). Promoting it into
the admin tuning lab is a small follow-up.
Unit-tested the pure foldEnrichedTags helper (overlap accumulation +
scale=0 disable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build a persistent, decaying model of each user's taste, recomputed daily,
that later phases consume across every recommendation surface. Phase 1 only
BUILDS the object — no behaviour change to what's surfaced yet.
Core mechanic — graded engagement (replaces binary was_skipped for learning;
was_skipped stays for History): a play's completion ratio maps to a signal in
[-1,+1] via two linear ramps (instant-skip → -1, ~0.30 neutral, ≥0.90 → +1).
Time-decayed (half-life ~75d) so recent behaviour dominates and the profile
tracks drift.
Per operator constraints:
- No explicit dislike button — negatives come only from passive behaviour
(early skips). Nothing recorded to regret or opt out of.
- Negatives are track-scoped; artist/tag weight is the decayed SUM of their
tracks' engagement, so one skip nets out against many good plays (a
DB test asserts a liked artist stays positive despite an early-skipped
track). A floor clamp bounds how negative any single entity can get.
- migration 0035: taste_profile_artists / taste_profile_tags (signed weight,
indexed by (user, weight DESC)).
- internal/taste: engagement.go (pure curve + decay) + profile.go
(accumulate plays + like bonuses, floor damping, size caps, atomic-replace).
- scheduler: rebuildUserDaily recomputes the profile before the playlist
build (so phase 2 can read it), best-effort — a taste failure never blocks
playlist building. Wired into the daily job + startup catch-up only (not
manual/lazy rebuilds).
- tests: pure (engagement curve, decay, ranking, floor, genre split) +
DB-backed (positive/negative weights, aggregation-protects-artist, like
bonus, atomic replace). All green vs real Postgres.
Config knobs live in taste.DefaultConfig() for now; wiring them into the
server RecommendationConfig is a later follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>