Commit Graph
4 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 ead0abf0cf feat: one Missing files page under Collection: folders gone, each with its album's want, last search or download, and Search, Releases and Remove; an album held before its files went is no longer shown as held again (M504 #5626)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m46s
release / integration (push) Successful in 5m23s
release / android (push) Successful in 5m43s
release / Build signed APK (releases and dev) (push) Successful in 6m2s
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:44:33 -04:00
bvandeusenandClaude Opus 5.5 04515a977c feat: a wanted list — follow artists on release-type profiles; requests and re-acquisition become wants (M502 #5520)
release / govulncheck (push) Successful in 15s
release / web (push) Failing after 1m21s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 4m57s
release / android (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m28s
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
Minstrel keeps its own list of what it is to get, keyed by MusicBrainz
release group. A want comes from an approved request, a new release by a
followed artist, or an album whose files went missing, and is held once the
library has a full edition of it.

- Migration 0081: release_type_profiles (Standard seeded: Album, EP,
  Single, studio only), follows, follow_seen_groups and wants.
  lidarr_requests becomes requests with plain *_mbid columns; approved
  album and track requests move onto the wanted list, approved artist
  requests become follows, and the library's album artists start out
  followed. missing_reacquisitions and the retry settings go.
- internal/wanted: follows are checked against MusicBrainz discographies
  daily. The first check sets a baseline, and only releases after it are
  wanted unless the follow asked for what already exists. Profiles filter
  by primary and secondary type, so live recordings stay out unless a
  profile ticks Live.
- internal/requests (was lidarrrequests): approving puts the request on
  the wanted list, and an artist request can be approved on a chosen
  profile. A request completes when its want is held.
- The re-acquisition sweeper wants a missing album back instead of asking
  Lidarr; searching for it is the search schedule's (#5521).
- Web: an admin Wanted page (Wanted, Missing files, Held, Following), a
  follow control on artist pages, a release-type profiles card on
  Integrations, "Approve on a profile" on Requests, and want state on
  Missing files. The artist detail API returns the artist's MBID.
- Android: the request body sends artist_mbid, album_mbid and track_mbid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:46:22 -04:00
bvandeusen 414dfb23b6 feat: show what re-acquisition has done, per folder — #2527
test-web / test (push) Failing after 42s
test-go / test (push) Successful in 1m0s
test-go / integration (push) Successful in 5m3s
Completes milestone #290. The sweeper has been running and the settings
have been editable, but the list itself said nothing about either, so
the only way to tell "not tried yet" from "asked twice and nothing came
back" was to go and read the Requests queue.

Each folder now carries its album's attempt record: how many times, when
last, when next -- or that it gave up, with the reassurance that a file
coming back and going missing later starts the process over. Null when
nothing has been attempted, which is the common case for a folder that
just went missing and would be noise on every row.

next_attempt_at is computed, not stored. The schedule is a function of
the attempt count and the current settings, so persisting it would go
stale the moment an operator edited the backoff -- and the card lets
them do exactly that.

Needed a forward-looking formatter. relativeTime deliberately collapses
a future timestamp to "just now" (pinned by its own test) because that
is the right answer for a clock-skewed past event; it is the wrong one
for a scheduled future attempt, which would have rendered "next just
now". timeUntil is its companion rather than a sign-aware rewrite: the
two read differently in the same sentence -- "last tried 3d ago, next in
4h" -- and a test asserts they disagree about the future on purpose, so
nobody later "fixes" the divergence.

The state lookup is one batched query for the whole page and best-effort:
this is context on a list whose real job is showing what is missing, so
a failure leaves the groups bare rather than failing the page. The
settings service is read with a nil guard falling back to the shipped
defaults, since contexts that wire routing without services exist and a
backoff projection is not worth a nil-pointer panic (rule #48).
2026-08-17 00:19:48 -04:00
bvandeusen 4dd0a58d63 feat(api): admin surface for files the library has lost — #2527
test-go / test (push) Successful in 52s
test-go / integration (push) Successful in 5m21s
The scan has marked missing files since f6d1cf24 and every selection
path filters them out, so they cause no harm -- and are invisible. The
operator found out about the first batch only because an unrelated MBID
backfill logged "no such file or directory" forty times.

GET /api/admin/library/missing reports them, grouped by directory. The
grouping is the whole ergonomic argument: the case that produced #2523
was three reorganised albums, which a flat list renders as forty
unrelated problems and a folder list renders as three decisions.
ListMissingTracks orders by directory so the handler can fold runs
without a map, which also keeps the query's ordering instead of Go's
random map iteration.

Each row carries last_played_at, nullable, because "gone six months,
never played" and "gone yesterday, played 200 times" deserve opposite
reactions and a file path tells you neither. The correlated MAX needs
its ::timestamptz cast or sqlc infers interface{} and the Go layer
loses the type.

Read-only, deliberately. Nothing here deletes: a missing file keeps its
row, its play history and its likes because it may come back, and if it
comes back renamed the scanner adopts it (#2528). The route sits under
/library rather than /tracks so it can't be confused with the
destructive DELETE /admin/tracks/{id} beside it.
2026-08-16 11:48:45 -04:00