Commit Graph
1997 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 ca9917d8cd chore(web): clear kit 3's alias and Vite's config-loader deprecations (#5021)
release / govulncheck (push) Successful in 23s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 4m49s
release / android (push) Successful in 5m32s
release / Build signed APK (releases and dev) (push) Successful in 5m39s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 28s
release / Verify release artifacts (tag releases only) (push) Skipped
CI run 8789 warned on both:

- kit 3 deprecates `config.alias`. The $test-utils alias becomes a
  `#test-utils/*` subpath import, matching #lib, and its 43 import sites
  move with it.
- Vite's coming native config loader needs the extension on
  vitest.config.ts's import of vite.config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:03:38 -04:00
bvandeusenandClaude Opus 5.5 b1b10b0c77 fix(web): run Tailwind 4 through @tailwindcss/vite, not PostCSS (#5021)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 56s
release / go (push) Successful in 2m22s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m43s
release / Build signed APK (releases and dev) (push) Successful in 5m54s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m14s
release / Verify release artifacts (tag releases only) (push) Skipped
The image build on fecd030b failed in `vite build`:

  [postcss] ENOENT: no such file or directory, open '/web/tailwindcss'

With a PostCSS config present, Vite's own @import inliner resolves
`@import 'tailwindcss'` before @tailwindcss/postcss sees it, and reads it
as a relative file. svelte-check and Vitest never build CSS, so only the
image job caught it. The Vite plugin is Tailwind's documented setup for
Vite projects and handles the import itself, so postcss.config.cjs and
the direct postcss and @tailwindcss/postcss dependencies go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:55:48 -04:00
bvandeusenandClaude Opus 5.5 94a9c8cbe3 chore(deps): SvelteKit 3 with adapter-static 4 and TypeScript 6, full-tree npm audit (#5021)
Merges Renovate's kit 3 (PR #149) and adapter-static 4 (PR #148) bumps,
plus the migration they need. The mechanical part is `sv migrate
sveltekit-3`, run one task at a time and reviewed:

- svelte.config.js is gone. Its options move into sveltekit() in
  vite.config.ts, exported as kitOptions so vitest.config.ts runs the
  same kit setup, including the $test-utils alias the tests import.
- $lib becomes #lib through package.json "imports". There is no
  src/lib/index, so only the "#lib/*" entry is kept.
- tsconfig extends $app/tsconfig.
- Peer floors raised to kit 3's requirements: svelte ^5.57.1, vite
  ^8.0.12, svelte-check ^4.7.5.

By hand, from the codemod's list of non-automated tasks:

- goto's replaceState option is now replace; keepFocus becomes
  reset: false. For the search typeahead, reset: false also stops the
  scroll-to-top, which is wanted while typing.
- The test setup mocks drop pushState/replaceState and $app/paths
  base/assets, which kit 3 removed, and mock refreshAll in place of
  invalidateAll.
- The other flagged files only read page.url or goto internal routes,
  so they needed no change.

TypeScript goes to ^6, not the ^7 Renovate offers: kit 3 declares
typescript ^6 as a peer and svelte-check 4.7 accepts ^5 || ^6. Move to
7 once both accept it.

With Tailwind 4 and kit 3 in, `npm audit` on the whole tree reports 0,
so the web lane now audits every dependency rather than only what
ships to browsers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:50:39 -04:00
bvandeusen beba5b2082 Merge remote-tracking branch 'origin/renovate/sveltejs-kit-3.x' into dev 2026-10-08 10:49:44 -04:00
bvandeusenandClaude Opus 5.5 fecd030b68 chore(deps): Tailwind 4 (#5021)
release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m53s
release / integration (push) Successful in 5m37s
release / android (push) Successful in 6m54s
release / Build signed APK (releases and dev) (push) Successful in 7m27s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Failing after 34s
release / Verify release artifacts (tag releases only) (push) Skipped
Renovate's tailwindcss bump (PR #150) plus the migration it needs:

- Theme moves from tailwind.config.js into app.css as `@theme inline`,
  mapping the same FabledSword tokens. tailwind.config.js is gone.
- PostCSS runs @tailwindcss/postcss; autoprefixer is dropped, since
  Tailwind 4 prefixes through Lightning CSS.
- Class renames from @tailwindcss/upgrade 4.3.3, reviewed: outline-none
  -> outline-hidden, focus-visible:outline -> outline-solid, shadow ->
  shadow-sm, shadow-sm -> shadow-xs, flex-shrink-0 -> shrink-0. Bare
  `rounded` stays: v4 keeps it at 0.25rem, as before.
- Three v3 preflight defaults kept in a base layer so nothing changes on
  screen: gray-200 default border colour, gray-400 placeholder text and
  the pointer cursor on buttons.
- The unused class-based dark variant is not carried over; no template
  uses `dark:`.

Clears the five high and two moderate npm audit findings that came in
through Tailwind 3 (braces, chokidar, micromatch, fast-glob,
postcss-selector-parser).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:46:07 -04:00
Renovate Bot 0202c86338 chore(deps): update dependency tailwindcss to v4
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
2026-10-08 14:45:03 +00:00
Renovate Bot 0500f6aac5 chore(deps): update dependency @sveltejs/kit to v3
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
2026-10-08 14:45:01 +00:00
Renovate Bot c83933b61a chore(deps): update dependency @sveltejs/adapter-static to v4
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
2026-10-08 14:45:00 +00:00
bvandeusenandClaude Opus 5.5 308045d056 chore(deps): vite 8, vite-plugin-svelte 7 and vitest 5 together (#5021)
release / govulncheck (push) Successful in 33s
release / web (push) Successful in 1m41s
release / go (push) Successful in 1m50s
release / integration (push) Successful in 4m57s
release / android (push) Successful in 6m25s
release / Build signed APK (releases and dev) (push) Successful in 6m37s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m13s
release / Verify release artifacts (tag releases only) (push) Skipped
Merges Renovate's three branches (PRs #145, #146, #147), which fail
alone: vite-plugin-svelte 7 requires vite 8, and vitest 5 is the vitest
for vite 8. Renovate changed only package.json, so npm ci failed on each.

The lockfile is regenerated for just these packages: the stale entries
for vite, vitest, @vitest/* and vite-plugin-svelte (with its old
inspector) were dropped and re-resolved, leaving everything else locked.
SvelteKit stays on 2.70.3, which accepts vite 8 and plugin 7. Vite 8
builds with Rolldown, so esbuild moves to 0.28 and rollup leaves the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:24:51 -04:00
bvandeusen 5d5af359b6 Merge remote-tracking branch 'origin/renovate/vite-8.x' into dev 2026-10-08 10:23:28 -04:00
Renovate Bot 67f1975f53 chore(deps): update dependency vitest to v5
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
2026-10-08 14:19:20 +00:00
Renovate Bot 5dfbe1361e chore(deps): update dependency vite to v8
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
2026-10-08 14:19:18 +00:00
Renovate Bot 39c33e4306 chore(deps): update dependency @sveltejs/vite-plugin-svelte to v7
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
2026-10-08 14:19:16 +00:00
bvandeusenandClaude Opus 5.5 9be5bbae3c fix(discover): cap the taste-matched arm per album and artist before its LIMIT (#5356)
release / web (push) Successful in 1m37s
release / govulncheck (push) Successful in 53s
release / go (push) Successful in 2m7s
release / integration (push) Successful in 5m2s
release / android (push) Successful in 6m34s
release / Build signed APK (releases and dev) (push) Successful in 6m7s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m36s
release / Verify release artifacts (tag releases only) (push) Skipped
Summed tag weight rewards a track for carrying many of the user's tags, so
on the deploy two artists whose every track carries the whole lo-fi profile
took all 120 rows of the taste-unheard query. capByAlbumAndArtist ran after
the LIMIT and left 6, and the arm with the lowest skip rate (12% against
~23%) handed its slots to dormant and random.

The query now ranks within album, then within artist over what the album
cap kept, before the LIMIT: the same walk the Go cap makes, so the bucket
fills from as many artists as match. The caps come from the Go constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:24:20 -04:00
bvandeusenandClaude Opus 5.5 9940bc375d feat(android): notifications when the app is closed, a specialUse delivery service (#5347)
release / govulncheck (push) Successful in 37s
release / web (push) Successful in 1m8s
release / go (push) Successful in 1m31s
release / integration (push) Successful in 5m6s
release / android (push) Successful in 6m17s
release / Build signed APK (releases and dev) (push) Successful in 6m13s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m13s
release / Verify release artifacts (tag releases only) (push) Skipped
Roundtable's shape: no FCM. A specialUse foreground service keeps the
process alive; EventsStream stays connected; DeliveryLauncher runs the
catch-up on every notification.created nudge and every reconnect, and is
the single owner of the service's lifetime (signed in AND the device's
"Notifications when the app is closed", on by default).

- NotificationSync pulls the newest page and announces unread notices past
  a high-water mark (auth_session.notifiedUpTo, read and written through
  the DAO), honouring the per-kind phone pref. A first look sets the mark
  without announcing; more than three collapse to one line; nothing is
  posted while the app is on screen.
- Two channels: "Your requests" and "Library health"; the ongoing notice
  sits on a MIN "Background connection" channel.
- EventsStream: a connected flow, 2s→5min backoff with ±25% jitter, and an
  immediate reconnect when a network comes up (a hint, not VALIDATED) or
  the app comes to the foreground.
- BootReceiver restarts delivery after a reboot or a self-update.
- A tap opens what the notice links to (routeForLink), a pile the inbox.
- POST_NOTIFICATIONS is asked for on Android 13+ once delivery is wanted,
  and again when the toggle is turned on.
- Room v12: auth_session.backgroundDelivery and notifiedUpTo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:59:11 -04:00
bvandeusenandClaude Opus 5.5 63709a433d feat(notifications): grouped email digest, new music as a daily summary (#5346)
release / govulncheck (push) Successful in 21s
release / web (push) Successful in 1m19s
release / go (push) Successful in 1m39s
release / integration (push) Successful in 5m27s
release / android (push) Successful in 5m47s
release / Build signed APK (releases and dev) (push) Successful in 5m34s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 26s
release / Verify release artifacts (tag releases only) (push) Skipped
Nothing is emailed per event. New music (request_completed) goes out at
most once a day, at the summary hour in each user's own timezone, grouped
by artist. Everything else is batched: one email a window after the first
un-emailed item, holding whatever accumulated.

- Migration 0074: notification_email_settings (summary hour, batch window,
  admin-configurable) and user_notification_email_state (batch start, last
  sent, failures and retry_after per user and group). Existing rows are
  stamped emailed so the upgrade sends no backlog.
- The Notifier stamps emailed_at at write time when the recipient's email
  channel is off, so turning email on later doesn't send old items.
- Read rows are never selected. A row is stamped only after the mailer
  accepts, in one transaction with the state, against the read's clock, so
  a coalesced row updated mid-send stays pending.
- A failed send backs off 5m doubling to 6h; SMTP not configured just waits.
- Links come from the public address; without one the email has none.
- The mailer now RFC 2047-encodes subjects and strips line breaks from them.
- Admin → Integrations gains a Notification emails card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:48:22 -04:00
bvandeusenandClaude Opus 5.5 62f76290fb fix(android): split notification kinds out of the replayer's dispatch (detekt)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m16s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 4m39s
release / android (push) Successful in 4m59s
release / Build signed APK (releases and dev) (push) Successful in 5m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 14s
release / Verify release artifacts (tag releases only) (push) Skipped
dispatch reached cyclomatic complexity 16 with the three M489 kinds; they
now share one entry that hands off to dispatchNotification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:38:43 -04:00
bvandeusenandClaude Opus 5.5 c416157a1b feat(android): notifications bell, inbox screen and settings, offline-first (#5343, #5345)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m11s
release / go (push) Successful in 1m29s
release / android (push) Failing after 1m41s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 2m49s
release / Build signed APK (releases and dev) (push) Canceled after 2m50s
- The top bar carries a bell with a badge. The badge is inverse, not
  error red, and counts to 9, then shows 9+. It opens the Notifications
  screen: one line per notice, how long ago, "Mark all read", pull to
  refresh. A tap marks the notice read and opens its screen (albums,
  artists, requests, admin requests and quarantine; other admin pages
  land on Admin).
- The newest 50 notices live in Room (cached_notifications, v11 with
  MIGRATION_10_11), so the badge and the list work offline. A refresh
  keeps a read made here that the server hasn't seen yet.
- Reads, read-all and setting toggles go through the MutationQueue
  (rule 100): NOTIFICATION_READ, NOTIFICATIONS_READ_ALL and
  NOTIFICATION_SETTING_SET.
  - Read-all sends the newest notice shown, rounded up a millisecond
    (Room keeps ms, the server µs), so a late replay leaves newer notices
    unread.
  - Settings collapse per kind and channel.
- The `notification.created` live event, a return to the foreground and
  reconnecting all refresh the inbox.
- Settings → Notifications: a row per kind with Inbox, Phone and Email.
  Admin kinds sit under "Library health". Phone and email ride on the
  inbox. One line says why email is off. If the system blocks
  notifications, a row opens Minstrel's notification settings; it is
  re-checked on resume.
- Signing out clears the cached inbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:35:56 -04:00
bvandeusenandClaude Opus 5.5 f11dba2336 feat(api): read-all takes an optional up_to cutoff (M489)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m40s
release / integration (push) Successful in 4m40s
release / android (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m12s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
POST /api/me/notifications/read-all accepts {"up_to": RFC3339}. Android
queues "mark all read" for replay when offline, and a replay landing later
must not mark notices that arrived in between, which the user never saw.
A coalesced notice updated since then has a newer created_at, so it stays
unread. An empty body still marks everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:27:29 -04:00
bvandeusenandClaude Opus 5.5 87f8ed6147 feat(web): notification settings per kind and channel (#5345, web half)
- A Notifications section on Settings has a row per kind and a toggle
  each for Inbox, Phone and Email. Labels are short, menu-style.
- Admin kinds sit under "Library health", for admins only.
- Toggles are optimistic and send only the kind and channel touched. A
  failed save reverts unless something newer has happened (snippet
  #5106's generation counter).
- With the inbox off, phone and email are disabled: they ride on it.
- When email isn't usable, one line says why. With no address it links
  to the profile. With no SMTP an admin gets a link to Integrations and a
  listener is simply told. Saving the profile refreshes the settings so
  the line clears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:25:31 -04:00
bvandeusenandClaude Opus 5.5 956058d4a0 feat(web): notifications bell, unread badge and inbox panel in the header (#5342)
- The bell sits between search and the user menu. Its badge is
  parchment on obsidian, not the accent, which the house style keeps
  off general chrome. It counts up to 9, then shows 9+.
- The panel lists the server-rendered title, body and relative time,
  newest first, with unread rows marked. Clicking a row marks it read
  and opens its link. "Mark all read" appears while anything is unread,
  and an empty inbox says "Nothing waiting for you."
- createNotificationsQuery and createUnreadCountQuery poll every 60s
  while the tab is visible. The `notification.created` live event
  invalidates ['notifications'] so the badge and list refresh promptly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:23:32 -04:00
bvandeusenandClaude Opus 5.5 1e9408c835 feat(notifications): library health reaches admins, coalesced (#5341)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m43s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m16s
release / Build signed APK (releases and dev) (push) Successful in 5m27s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
- A failed scan run sends scan_failed. Each failure adds to the count and
  the notice shows the latest error. A scan cut short by shutdown says
  nothing.
- Marking tracks missing sends tracks_missing with a running count.
- A duplicate sweep that proposes a group it had not proposed before
  sends duplicates_found, counting everything awaiting review. A sweep
  that only re-finds known groups stays quiet, so a read notice isn't
  repeated every sweep (CountDuplicateGroupsDetectedSince).
- A playback-error report sends playback_errors, counting the unresolved
  errors (CountUnresolvedPlaybackErrors).

The library package gets its notifier as a package-level SetNotifier
beside SetEventBus, for the same reason the bus is package-level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:20:33 -04:00
bvandeusenandClaude Opus 5.5 069baeb14d feat(notifications): requests and flags reach the people who act on them (#5340)
- A new request still pending after any auto-approval notifies the
  admins (request_pending), but not the requester if they are an admin.
  A request that dedups into one already in flight is not announced again.
- Approving or rejecting a request notifies the requester, and a
  rejection carries the admin's notes as the reason. An admin deciding
  their own request gets nothing.
- The reconciler notifies the requester when their request arrives
  (request_completed), linking the matched album or artist.
- A request the re-acquisition sweeper files and cannot approve itself
  notifies the admins.
- A quarantine flag notifies every admin except the flagger, naming the
  track, the flagger and the reason.

lidarrrequests.Service.CreateTracked reports whether a request was
inserted or deduped; Create wraps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:17:11 -04:00
bvandeusenandClaude Opus 5.5 94ef8c1887 feat(api): notifications inbox and per-user settings endpoints (#5339)
release / go (push) Successful in 1m35s
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m18s
release / integration (push) Successful in 4m35s
release / android (push) Successful in 4m58s
release / Build signed APK (releases and dev) (push) Successful in 5m14s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 14s
release / Verify release artifacts (tag releases only) (push) Skipped
M489 step 2.

- GET /api/me/notifications?limit&before: newest first, keyset-paged on
  (created_at, id) with an opaque cursor, plus the unread count.
- GET /api/me/notifications/unread-count: the badge's cheap call.
- POST /api/me/notifications/{id}/read and /read-all. Mark-read is
  idempotent; another user's id is a 404, the same as a malformed one.
- GET/PUT /api/me/notification-settings: every kind the caller can receive
  (admin kinds only for admins) with inbox/phone/email. PUT is partial, so an
  offline replay sends only what was touched, and a batch with any invalid
  change applies nothing. The response says whether email can be delivered
  at all: no address on file, or SMTP not configured. A failed SMTP config
  read is a 500, not "not configured".

notifications.Render turns kind + payload into title, body and link on the
server, so the web inbox, the Android inbox, the phone's shade and the email
digest all say the same thing. mailer.Configured lifts Send's readiness
check out so settings can report it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:10:05 -04:00
bvandeusenandClaude Opus 5.5 8bf333e748 feat(notifications): the inbox store, one writer, coalescing and retention (#5338)
release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m34s
release / go (push) Successful in 1m51s
release / integration (push) Successful in 4m59s
release / android (push) Successful in 5m24s
release / Build signed APK (releases and dev) (push) Successful in 5m32s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m20s
release / Verify release artifacts (tag releases only) (push) Skipped
M489 step 1. The event bus is fire-and-forget, so a client that isn't
connected never hears that a request completed or that tracks went missing.
user_notifications is the durable record; the bus only nudges.

- Migration 0073: user_notifications (kind CHECK-gated, payload jsonb,
  read_at, coalesce_key, emailed_at) and user_notification_prefs (per user,
  per kind: inbox, phone, email). A missing pref row means the kind's
  defaults, so nothing is seeded.
- internal/notifications.Notifier is the only writer. It resolves recipients
  (admin kinds reach admins only, and never the excepted user), honours the
  inbox pref (phone and email ride on it), writes, and publishes a
  contentless notification.created nudge per recipient.
- Burst-prone admin kinds coalesce into one unread row: tracks_missing and
  scan_failed add up their counts, duplicates_found and playback_errors take
  the latest total. Once read, the next event is a new row.
- Retention: read rows go after 90 days, anything after a year, on the
  library_changes compactor's daily shape.

Tests: unit (channel rules, kind table) and integration (recipients, nudge,
coalescing both ways, prefs, owner-scoped idempotent mark-read, every kind
against both schema CHECKs, retention cut-offs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:06:39 -04:00
bvandeusenandClaude Opus 5.5 5dffe51b95 feat(web): duplicates report flags identical audio under different titles (#3885)
release / govulncheck (push) Successful in 15s
release / Build + push container image (push) Successful in 26s
release / Verify release artifacts (tag releases only) (push) Skipped
release / web (push) Successful in 1m10s
release / go (push) Successful in 1m28s
release / integration (push) Successful in 4m21s
release / android (push) Successful in 4m43s
release / Build signed APK (releases and dev) (push) Successful in 4m57s
release / Attach APK to the Release (tag releases only) (push) Skipped
An exact-tier group whose copies carry different titles means at least one
file's tags are wrong, and the recording the other title names may be missing
from the library. WWW (2020) was this: "WWW" was a second copy of the
instrumental, the vocal was absent, and nothing said so. The report now names
the titles and says what it implies, so the absence surfaces at the moment of
choosing which copy to keep.

Titles compare case- and whitespace-insensitively. Acoustic-tier groups are
left alone: across encodings a "Remastered" suffix is routine, not a mislabel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:33:37 -04:00
bvandeusenandClaude Opus 5.5 43c369f082 test(android): queue drag math is a pure function, held to web's cases (#2436)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m11s
release / go (push) Successful in 1m29s
release / android (push) Successful in 4m46s
release / Build signed APK (releases and dev) (push) Successful in 4m58s
release / integration (push) Successful in 15m34s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 14s
release / Verify release artifacts (tag releases only) (push) Skipped
The drag-offset-to-row arithmetic lived inline in queueReorderDrag's
onDragEnd lambda, which no JVM test can reach. Web's copy, offsetToDelta,
has been extracted and tested since the start, and the Android comment
says it mirrors web, but only one side could be held to that.

- QueueDragMath.kt adds queueDragDelta (web's offsetToDelta) and
  queueDragTarget (delta plus the clamp to the queue). Kotlin's roundToInt
  breaks ties toward positive infinity, the same as JS Math.round, so the
  web cases carry over exactly, including half a row up staying put.
- queueDragTarget returns the start index for an empty queue instead of
  letting coerceIn(0, -1) throw.
- QueueDragMathTest mirrors queue-row-math.test.ts one case at a time,
  plus clamping past either end, a sub-half-row drag, an unmeasured row,
  and the empty queue.

No behaviour change for a non-empty queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:18:43 -04:00
bvandeusenandClaude Opus 5.5 b46c080d19 fix(web): all accent text and icons use accent-fg (#5318)
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m11s
release / go (push) Successful in 1m26s
release / integration (push) Successful in 4m18s
release / android (push) Successful in 4m44s
release / Build signed APK (releases and dev) (push) Successful in 4m53s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 26s
release / Verify release artifacts (tag releases only) (push) Skipped
The raw accent fails AA as text on every dark surface, not only on its
own tint: 3.04:1 on the page, 2.70 on iron, 2.21 on slate, against 4.5.
accent-fg (the house formula, 45% toward parchment) measures 5.62 at
worst across both modes. The operator chose the readable colour over the
signature teal for text, on 2026-10-08.

- 36 sites swap. They are 35 Tailwind uses: links, "Now playing", the
  ingest progress line, active shuffle/repeat, the liked heart, the app
  download icon and its hover. The last is the alphabet rail's pending
  spinner in CSS. Icons follow the text: as graphics they need only
  3:1, and the raw accent misses even that on iron.
- check-tint-contrast adds accent to TEXT_NEVER_RAW, so a new raw
  text-accent or color: var(--fs-accent) fails the web lane. Run against
  the files before the swap, it finds all 36. Borders, rings and fills
  keep the raw accent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:17:19 -04:00
bvandeusenandClaude Opus 5.5 37d3a5bcd3 fix(library): read every genre of FLAC, Ogg, Opus and MP4 files (#2500)
release / govulncheck (push) Successful in 50s
release / web (push) Successful in 2m3s
release / go (push) Successful in 2m18s
release / integration (push) Successful in 5m42s
release / android (push) Successful in 6m30s
release / Build signed APK (releases and dev) (push) Successful in 6m0s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m20s
release / Verify release artifacts (tag releases only) (push) Skipped
Vorbis comments repeat a field to give it several values (GENRE=Boom Bap,
GENRE=Downtempo, ...). dhowden/tag keeps comments in a map keyed by field
name, so each repeat overwrote the previous one and only the last genre
was stored. MP4 has the same gap: several data atoms in one ©gen atom, or
repeated ©gen atoms, collapse to one value.

On the operator's library 3,658 of 4,092 FLACs declare more than one
genre. Kupla's Life Forms carries eight and was stored as "Instrumental
Hip Hop" alone, so browse and the taste profile never saw the other seven.

- vorbisgenre.go reads the comment block directly: FLAC's metadata block
  (including FLACs behind an ID3v2 tag), and the comment packet of Ogg
  Vorbis and Opus, reassembled across pages when cover art makes it span
  several.
- mp4genre.go walks moov > udta > meta > ilst and returns every ©gen text
  value. It handles ISO and QuickTime meta layouts and a moov placed after
  mdat. A file with only the numeric gnre atom still falls back to
  dhowden, which resolves it.
- extractGenres routes VORBIS and MP4 through them, as #2499 did for
  ID3v2.
- tagReadVersion 3 -> 4, so the next scan re-reads the tags of files
  already indexed. Unchanged files keep their duration and fingerprint,
  so the pass costs tag reads only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:40:56 -04:00
bvandeusenandClaude Opus 5.5 efa3bf54ce fix(playlists): a missing track never seeds For You or Songs-like (#2701)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m11s
release / go (push) Successful in 1m29s
release / integration (push) Successful in 4m27s
release / android (push) Successful in 5m2s
release / Build signed APK (releases and dev) (push) Successful in 5m17s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m15s
release / Verify release artifacts (tag releases only) (push) Skipped
PickTopPlayedTracksForUser's liked tier read general_likes without
joining tracks. With no plays to seed from, For You could pick a liked
track whose file is gone. The play tiers were already safe: their
play_events join filters missing_since.

The same gap was in PickTopPlayedTrackForArtistByUser's fallback, which
seeds Songs-like from the artist's newest album when there are no recent
plays. It could pick a missing track, and since #5296 a missing track is
never fetched for similarity, so that seed has no edges either. The
caller already skips an empty seed, so an artist whose tracks are all
missing gets no Songs-like mix instead of one aimed at nothing.

Integration tests cover both cases: a liked-but-missing track is not a
seed, and the Songs-like fallback moves to the next album once the
newest one's track goes missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:10:17 -04:00
bvandeusenandClaude Opus 5.5 743b6f5eac fix(web): error text uses error-fg on every surface, not only on tints (#3150)
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m6s
release / go (push) Successful in 1m29s
release / integration (push) Successful in 4m30s
release / android (push) Successful in 5m18s
release / Build signed APK (releases and dev) (push) Successful in 5m32s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
Raw error red fails AA as text even with no tint behind it: in dark mode
it measures 3.63:1 on obsidian, 3.23 on iron and 2.64 on slate, against
4.5. error-fg (the house formula, 50% toward parchment) measures 5.30 at
worst across both modes.

- All 19 text-error uses become text-error-fg: the "Couldn't load"
  messages on the admin pages, the integrations form errors, the flag
  popover, and the error toast's text. The toast keeps its error border,
  since a border is a graphic with a 3:1 floor.
- check-tint-contrast flags raw error text anywhere (text-error,
  class:text-error, color: var(--fs-error)) and leaves borders and
  outlines alone. Run against the files before the swap, it finds all 19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:00:28 -04:00
bvandeusenandClaude Opus 5.5 fdee77eaec fix(web): text on a tint of its own hue uses the house -fg tokens (#3150)
release / govulncheck (push) Successful in 38s
release / integration (push) Successful in 5m1s
release / Build + push container image (push) Successful in 1m17s
release / Verify release artifacts (tag releases only) (push) Skipped
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m48s
release / android (push) Successful in 5m41s
release / Build signed APK (releases and dev) (push) Successful in 5m11s
release / Attach APK to the Release (tag releases only) (push) Skipped
A hue painted as text on a color-mix tint of itself sits close to the
surface under it. On Minstrel's surfaces the raw accent on its 15% tint
measures 1.97:1 at worst (dark mode, hover surface), against AA's 4.5.

- tokens.json gains colors.fg: the five FabledSword -fg formulas (accent
  45%, success 45%, warning, error and info 50%), each mixed toward
  parchment so one declaration serves both modes. Success is Minstrel's
  moss. tokens-to-css emits them in :root.
- Tailwind exposes them as text-accent-fg, text-warning-fg, text-error-fg
  and text-info-fg.
- 23 sites swapped: 14 Tailwind class strings (PlayerBar and the admin
  count pills) and 9 CSS rules (StatusPill's four tones and five accent
  chips). Worst case after: accent-fg 5.03, error-fg 4.75, warning-fg
  4.92, success-fg 4.85.
- scripts/check-tint-contrast.js finds the pair in either spelling. Its
  test scans src in the web Vitest lane and fails on any new site, with
  fixture cases showing it can fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:51:52 -04:00
bvandeusenandClaude Opus 5.5 c17f4273c6 test(web): stub SvelteKit app modules suite-wide so no test loads the client runtime (#3943)
release / govulncheck (push) Successful in 32s
release / web (push) Successful in 1m25s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 4m55s
release / android (push) Successful in 6m15s
release / Build signed APK (releases and dev) (push) Successful in 6m42s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m4s
The home page reaches the real $app/navigation through AlbumCard and
AlbumMenu. That loads SvelteKit's client runtime, whose $app/paths reads
__SVELTEKIT_PAYLOAD__ at module load. The global is only there when the
kit plugin's define reaches the module, and under vitest that is not
reliable: page.test.ts failed to load on CI run 6576 and passed on its
re-run. #374 was the same class of failure.

vitest.setup.ts now mocks $app/navigation, $app/state and $app/paths for
every test. Per-file mocks still win. A small guard test fails if the
suite-wide mocks are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:26:21 -04:00
bvandeusenandClaude Opus 5.5 e5dac9ddf0 fix(similarity): keep ListenBrainz's whole answer and resolve it locally (#5296)
release / govulncheck (push) Successful in 45s
release / web (push) Successful in 1m27s
release / go (push) Successful in 1m51s
release / integration (push) Successful in 5m36s
release / android (push) Successful in 7m47s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build signed APK (releases and dev) (push) Successful in 8m29s
release / Build + push container image (push) Successful in 1m47s
release / Verify release artifacts (tag releases only) (push) Skipped
The worker kept only the similar recordings already in the library, at most
20 of ListenBrainz's 50, and judged freshness by the edges it had written.
Two failures followed, both measured on the operator's library (#3879):

- A seed whose answer matched nothing wrote nothing, so it was never fresh.
  With the queue ordered by id, 25 such seeds held its head and were
  re-asked every hour; 17 of 2,466 played seeds had any edges.
- A recording that reached the library after its seed was fetched (a
  Lidarr import, an MBID from the AcoustID lookup) was never linked until
  a refetch, which for the stuck seeds never came.

Now every answer is cached whole in listenbrainz_similar_recordings and
every answer, an empty one or a permanent 4xx included, is recorded in
track_similarity_fetches. The queue reads the fetch record: never-fetched
first, then the oldest, refreshed after 30 days. The listenbrainz edges are
derived in SQL from the cache, one present track per recording and no cap,
for the seed just fetched and for every seed once per tick, so new arrivals
link within the hour without asking ListenBrainz again.

Artists get the same queue fix via artist_similarity_fetches; their answer
was already kept in artist_similarity and artist_similarity_unmatched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:56:22 -04:00
bvandeusenandClaude Opus 5.5 aa390c711a fix(recommendation): artist-level arms take one track per related artist in turn (#5297)
The similar_artists and coplay_artists arms ordered by artist score, so the
closest related artist's catalogue filled the whole LIMIT. On the operator's
library the 30-row similar_artists arm held exactly one artist for all 17
seeds measured (#3879), though each seed had 7-33 similar artists in the
library. Both arms now rank tracks within each artist and take every
artist's first track, best artist first, before anyone's second.

Both also skip missing tracks: the outer select already dropped them, but
only after they had taken places in the arm's LIMIT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:56:21 -04:00
bvandeusenandClaude Opus 5.5 865a3176c9 feat(library): fold a missing track into its on-disk replacement (M485 #5286 #5287)
release / web (push) Successful in 2m21s
release / go (push) Successful in 2m34s
release / govulncheck (push) Successful in 40s
release / integration (push) Successful in 6m17s
release / android (push) Successful in 6m36s
release / Build signed APK (releases and dev) (push) Successful in 6m6s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m19s
release / Verify release artifacts (tag releases only) (push) Skipped
A track marked missing whose replacement is already on disk under another
row — on the operator's library 355 of 451 missing tracks, nearly all Lidarr
mp3 -> flac upgrades — is folded into the replacement: likes, plays, playlist
entries and tags move across and the missing row goes. Move adoption could
not catch these: the replacements were re-encodes (no shared audio hash) with
no recording MBID at import.

Pairs (ListMissingTrackPairs): the same recording MBID within the album
group, or the same album row and title ignoring case. Each side must have
exactly one candidate; conflicting MBIDs refuse a pair. No duration or track
position gate: on the 142 pairs known to be one recording, 18% differed by
over 2s and the poorly tagged set is where numbering is broken (spike #5274).

Each pair folds in its own transaction after locking both rows and checking
the pair still holds. Runs automatically (operator, 2026-10-07) after a full
scan, after a watcher batch that added or updated tracks, and after an
AcoustID pass that matched any track. The first scan after deploy repairs the
existing rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:23:32 -04:00
bvandeusenandClaude Opus 5.5 2df28345b4 refactor(library): the per-copy fold is a helper the duplicate merge calls (M485 #5285)
MergeDuplicateGroup's loop body — repoint and copy every FK from a removed
copy onto the survivor, inherit its MBID, delete its row, tidy an emptied
album — moves into foldTrackInto, so the missing-pair pass can fold a stale
missing row into its replacement with the same mechanics. The group lock,
file removal and group bookkeeping stay in the merge. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:23:32 -04:00
bvandeusenandClaude Opus 5.5 0766349397 feat(brand): the browser tab icon is the full logo (#5267)
release / web (push) Successful in 1m15s
release / govulncheck (push) Successful in 47s
release / go (push) Successful in 2m19s
release / integration (push) Successful in 5m34s
release / Build signed APK (releases and dev) (push) Successful in 6m23s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / android (push) Successful in 6m10s
release / Build + push container image (push) Successful in 1m30s
release / Verify release artifacts (tag releases only) (push) Skipped
The tab icon was a hand-drawn reduced hat, made because the traced art loses
detail at 16px. A redrawing reads as a different logo. The tab icon now uses
the same traced mark as the header: a high-resolution screen draws a tab icon
from 32px, where it holds, and at 16px it keeps the logo's shape.

The drawn reduced mark had no other consumer, so it leaves the generator,
and mark-small.svg (referenced nowhere) is removed. Regenerating changed only
favicon.svg and favicon.png; every other brand asset is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:19:10 -04:00
bvandeusenandClaude Opus 5.5 fcbad3ad40 fix(deps): raise golang.org/x/text to v0.41.0 for GO-2026-6629
release / govulncheck (push) Successful in 12s
release / web (push) Successful in 1m18s
release / go (push) Successful in 1m35s
release / integration (push) Successful in 4m34s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (releases and dev) (push) Canceled after 6m5s
release / android (push) Canceled after 6m8s
govulncheck began failing on a new advisory: a panic parsing crafted input in
x/text/secure/precis, reachable from db.Open via pgxpool. x/text is indirect
(through pgx), so the Dependency Dashboard has no update queued for it.
x/text v0.41.0 requires x/sync v0.22.0, which go mod tidy raised with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:13:09 -04:00
bvandeusenandClaude Opus 5.5 4e3ce4065c fix(lidarr): complete a request only when the album actually came back (#5263)
release / govulncheck (push) Failing after 33s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m44s
release / integration (push) Successful in 4m37s
release / android (push) Successful in 5m29s
release / Build signed APK (releases and dev) (push) Successful in 5m34s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
Re-acquisition targets albums with ANY track missing, and completion only
asked for a track on disk, so the tracks that never left completed every
re-acquisition request the moment Lidarr accepted the add (52 on the deploy,
each ~150ms after its add).

An album or track request now completes when an album named by its release
or group has a track on disk AND either a track arrived after the request
(a new album, or Lidarr fetching another release into its own row) or no
track that was missing at the request is still missing. added_at is the
arrival clock; updated_at moves on every tag re-read.

Migration 0071 reopens completed album/track requests whose matched album
fails that test, as approved with the match cleared; the Lidarr add stays
confirmed, so nothing is re-sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:55:32 -04:00
bvandeusenandClaude Opus 5.5 e509d7d5a9 feat(lidarr): ask Lidarr for release groups; repair stored release-id requests (M483 #5244)
release / web (push) Successful in 1m41s
release / go (push) Successful in 2m33s
release / govulncheck (push) Successful in 22s
release / integration (push) Successful in 6m0s
release / android (push) Successful in 6m7s
release / Build signed APK (releases and dev) (push) Successful in 6m8s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m43s
release / Verify release artifacts (tag releases only) (push) Skipped
Lidarr's metadata is keyed by MusicBrainz release group, but re-acquisition
requested albums by their release id, so every add came back "not found".

- Sweeper requests an album by its tag-supplied release group, else the one
  MusicBrainz names (cached onto the album). An album MusicBrainz cannot name
  is skipped and counted, with no attempt spent.
- Reconciler: an add refused as not found re-reads the request's album id as
  a release (library first, then MusicBrainz), rewrites the request to the
  group and adds again. This repairs the requests already stored.
- Completion matches an album by release id or release group, and only once a
  track of it is on disk, so a re-acquisition request no longer completes
  against the row of the album it is trying to bring back.

Closes #5241.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:38:45 -04:00
bvandeusenandClaude Opus 5.5 2b4e274b12 feat(tags): resolve a MusicBrainz release to its release group (M483 #5243)
ReleaseGroupForRelease asks /ws/2/release/<id>?inc=release-groups through
the registered MusicBrainz provider, so it shares that provider's client
and 1 req/s limiter with tag enrichment and respects its on/off switch.
ErrNotFound when switched off or MusicBrainz has no such release (an id
that is already a release group included); ErrTransient to retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:34:04 -04:00
bvandeusenandClaude Opus 5.5 5c19a916ba feat(library): store each album's MusicBrainz release-group id (M483 #5242)
release / govulncheck (push) Successful in 22s
release / web (push) Successful in 1m14s
release / go (push) Successful in 1m33s
release / integration (push) Successful in 4m36s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 5m24s
release / Build signed APK (releases and dev) (push) Canceled after 5m29s
albums.mbid is the release id (Picard's musicbrainz_albumid, one edition).
Lidarr names albums by release group, so re-acquisition and request
completion need that id too (#5241).

Migration 0070 adds albums.release_group_mbid (nullable, non-unique index:
several releases share a group). The scanner reads musicbrainz_releasegroupid
through extractReleaseGroupMBID, writes it on insert and heals it onto
existing rows when NULL. tagReadVersion goes to 3 so the next scan fills
it for the library already indexed, bound by tag reads (no ffprobe, no
decode).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:33:26 -04:00
bvandeusenandClaude Opus 5.5 bf6364b709 fix(lidarr): send the artist add's monitor choice in addOptions (#5239)
release / govulncheck (push) Successful in 29s
release / web (push) Successful in 1m58s
release / go (push) Successful in 2m14s
release / integration (push) Successful in 5m17s
release / android (push) Successful in 6m21s
release / Build signed APK (releases and dev) (push) Successful in 6m38s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m1s
release / Verify release artifacts (tag releases only) (push) Skipped
ArtistResource has no top-level `monitor`. Lidarr reads the choice from
AddOptions (AddArtistOptions, a MonitoringOptions), so the "all"/"future"
we sent there was dropped on deserialisation. AddOptions.Monitor stayed
Unknown, and AlbumMonitoredService.SetAlbumMonitoredStatus returns early
on Unknown. The request's monitoring was never applied.

Send monitor and monitored inside addOptions with searchForMissingAlbums,
the shape Lidarr's getNewArtist.js posts, and set monitorNewItems "all"
explicitly: both choices mean new releases are watched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:58:55 -04:00
bvandeusenandClaude Opus 5.5 670b30c954 fix(lidarr): add an album as the looked-up resource with its artist nested (#5234)
release / web (push) Successful in 1m42s
release / go (push) Successful in 2m7s
release / govulncheck (push) Successful in 37s
release / integration (push) Successful in 5m31s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 5m14s
release / Build signed APK (releases and dev) (push) Canceled after 4m42s
Lidarr's POST /api/v1/album validates `artist` as a nested resource
(AlbumController: RuleFor(s => s.Artist).NotNull()), so the flat payload
we sent was refused with "'Artist' must not be empty" every time. The
album add has never worked against a real Lidarr; approved album and
track requests sat in the reconciler retrying every 5 minutes.

AddAlbum now does what Lidarr's own add-album UI does (getNewAlbum /
getNewArtist): look the album up by MBID (album/lookup?term=lidarr:<mbid>),
then POST that resource back with monitored + searchForNewAlbum. When
Lidarr doesn't have the artist yet, the nested artist gets the request's
quality/metadata profile and root folder, monitors this album only
(monitor "none" + albumsToMonitor, which AlbumMonitoredService prefers)
and no future releases. An artist Lidarr already has is left as it is.
An MBID Lidarr's metadata doesn't know is ErrNotFound with no POST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:55:41 -04:00
bvandeusenandClaude Opus 5.5 e8eee55325 fix(web): AcoustID card's loading line names itself (M401 #3922)
release / integration (push) Successful in 6m4s
release / android (push) Successful in 8m1s
release / Build signed APK (releases and dev) (push) Successful in 8m22s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m31s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 28s
release / go (push) Successful in 1m41s
release / web (push) Successful in 1m21s
Its bare "Loading…" made the Integrations page's cover-providers test find
two matches for /loading…/i (Vitest, run 8487). "Loading AcoustID
settings…" also says which card is loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:47:14 -04:00
bvandeusenandClaude Opus 5.5 03e07e7c66 fix(web): send the empty body api.post requires for AcoustID run-now (M401 #3922)
release / govulncheck (push) Successful in 25s
release / web (push) Failing after 1m18s
release / go (push) Successful in 1m43s
release / Build + push container image (push) Canceled after 0s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 4m51s
release / android (push) Canceled after 4m51s
release / Build signed APK (releases and dev) (push) Canceled after 4m6s
svelte-check on 0a7f7883: api.post takes a body. Every other body-less POST
in the client passes {}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:42:25 -04:00
bvandeusenandClaude Opus 5.5 0a7f788390 feat(web): AcoustID card on Integrations — key, threshold, coverage by source (M401 #3922)
release / go (push) Successful in 2m25s
release / web (push) Failing after 26s
release / govulncheck (push) Successful in 21s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 3m34s
release / android (push) Canceled after 1m3s
release / Build signed APK (releases and dev) (push) Canceled after 1m3s
The card takes the slot of the unimplemented "MusicBrainz overrides"
placeholder. Rows:
- the on switch
- a write-only key field (the stored key is never sent back), with a
  link to register an application
- the minimum score (0.5 to 1)

Below them, recording-id coverage reads as a column: from tags, looked
up, none, and of the none how many are waiting, no match, ambiguous or
failed. There is a "Look up now" button and a folded list of the tracks
the lookup could not settle.

Off, keyless and stopped-short passes are each a visible state with the
reason (rule 164).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:29:34 -04:00
bvandeusenandClaude Opus 5.5 3c575b137c feat(library): AcoustID lookup worker fills the MBIDs tags leave empty (M401 #3920 #3921)
release / web (push) Successful in 1m44s
release / go (push) Successful in 2m1s
release / govulncheck (push) Successful in 17s
release / integration (push) Successful in 5m22s
release / android (push) Successful in 5m48s
release / Build signed APK (releases and dev) (push) Successful in 5m53s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m6s
release / Verify release artifacts (tag releases only) (push) Skipped
Migration 0069 adds tracks.mbid_source (tag | acoustid), a lookup state per
track (matched | ambiguous | no_match | failed) and the acoustid_settings
row (off, no key, min score 0.85).

The file's tag outranks a lookup (D4). UpsertTrack keeps a looked-up id
through a re-read that finds no tag id and replaces it as soon as one
appears. SetTrackMbidFromAcoustID refuses to write over a tag id.

The worker fingerprints each untagged track with fpcalc's compressed
print, looks it up and writes an id only when D5 settles it: one
recording at or above the threshold, or one left after matching title and
length. Ambiguous and no-match results write nothing. A key AcoustID
refuses, or the service being unreachable, stops the pass and is reported
in the worker's status. It never counts as a verdict on a track.

A changed file drops its lookup in the scan. The re-lookup takes back an
id that no longer matches.

Admin API: GET /api/admin/library/acoustid (settings, status, coverage by
source), PUT …/acoustid-settings (write-only key), POST …/acoustid/run,
GET …/acoustid/unsettled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:26:03 -04:00
bvandeusenandClaude Opus 5.5 f13da62797 feat(library): AcoustID lookup client and the compressed fpcalc print (M401 #3919)
internal/acoustid posts AcoustID's v2 lookup as a gzip form with
meta=recordings, one request per 400ms (their limit is 3/s) and a 20s
deadline. It returns every linked recording with its best score; choosing
among them is the worker's job (D5). The server's error codes map to an
invalid key (stop and say so), a rejected fingerprint (a verdict on the
track) or unavailable (try again later). A cancelled caller stays a
cancellation.

fpcalcLookupArgs and parseFpcalcCompressed read fpcalc's default output,
the compressed string the lookup takes (D1), always over the first 120s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:16:52 -04:00