Files
minstrel/internal/wanted/library.go
T
bvandeusenandClaude Opus 5.5 2c677b3184
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m24s
release / go (push) Successful in 1m44s
release / integration (push) Successful in 5m19s
release / android (push) Successful in 5m31s
release / Build signed APK (releases and dev) (push) Successful in 5m35s
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
feat: the library's albums go on the wanted list as held, once; after that the list is the truth (M502 #5567)
A one-time job in the wanted worker lists every album in the library as a
held want, one per release group, with a record that it happened
(acquisition_settings.library_listed_at). Albums whose tags name only a
release are named on MusicBrainz first, 120 a pass; what nothing names is
counted and shown on the Held tab.

Re-acquisition now wants back only albums on the list. Taking a held album
off the list leaves its files, and it is never fetched back; albums put in
the library folder by hand afterwards are not added.

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

118 lines
4.0 KiB
Go

package wanted
import (
"context"
"errors"
"fmt"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
"git.fabledsword.com/bvandeusen/minstrel/internal/tags"
)
// ReasonLibrary: on the list because the library held it when the list
// began (#5567).
const ReasonLibrary = "library"
// libraryNamesPerPass bounds the MusicBrainz lookups one pass makes for
// albums whose tags named a release but not its group: about two minutes at
// MusicBrainz's one request a second, so the pass still settles its wants.
const libraryNamesPerPass = 120
// listLibrary puts the library's albums on the wanted list as held, once
// (operator, 2026-10-10: "make a record saying that the import has
// happened so later changes like unwanting hold and dont auto import from
// the file directory"). It runs a step each pass until done:
//
// - the first step stamps when the listing began, which is its cutoff:
// an album the library gains afterwards, dropped in by hand, is never
// listed;
// - every album whose release group is known goes on the list at once;
// - albums that name a release but not its group are looked up on
// MusicBrainz, a bounded number a pass, and listed as each is named;
// - once none is left to name, the listing is stamped done, with a count
// of the albums nothing names to MusicBrainz.
//
// After that it never runs again, so an album taken off the list stays off.
func (s *Service) listLibrary(ctx context.Context, res *PassResult) error {
q := dbq.New(s.pool)
state, err := q.GetLibraryListing(ctx)
if err != nil {
return fmt.Errorf("wanted: read the library listing: %w", err)
}
if state.LibraryListedAt.Valid {
return nil
}
cutoff := state.LibraryListingStartedAt
if !cutoff.Valid {
if cutoff, err = q.StartLibraryListing(ctx); err != nil {
return fmt.Errorf("wanted: start the library listing: %w", err)
}
n, err := q.ListLibraryAlbums(ctx, dbq.ListLibraryAlbumsParams{Cutoff: cutoff})
if err != nil {
return fmt.Errorf("wanted: list the library: %w", err)
}
res.Listed += int(n)
s.logger.Info("wanted: listing the library's albums as held", "listed", n)
}
for {
albums, err := q.ListLibraryAlbumsToName(ctx, dbq.ListLibraryAlbumsToNameParams{Cutoff: cutoff, MaxRows: libraryNamesPerPass})
if err != nil {
return fmt.Errorf("wanted: albums to name: %w", err)
}
if len(albums) == 0 {
break
}
for _, a := range albums {
if ctx.Err() != nil {
return ctx.Err()
}
group, err := s.releaseGroup(ctx, deref(a.Mbid))
switch {
case errors.Is(err, tags.ErrSwitchedOff):
// Nothing can be named while MusicBrainz is off: the rest are
// counted as unlisted rather than left waiting for ever.
return s.finishLibrary(ctx, q, res)
case errors.Is(err, tags.ErrNotFound):
group = "" // MusicBrainz has none: asked once
case err != nil:
// Unreachable or refusing for now: the next pass goes on.
s.logger.Info("wanted: naming the library's albums paused", "err", err)
return nil
}
if err := q.SetAlbumReleaseGroup(ctx, dbq.SetAlbumReleaseGroupParams{ID: a.ID, ReleaseGroupMbid: &group}); err != nil {
return fmt.Errorf("wanted: name album %s: %w", a.Title, err)
}
if group == "" {
continue
}
n, err := q.ListLibraryAlbums(ctx, dbq.ListLibraryAlbumsParams{Cutoff: cutoff, AlbumID: a.ID})
if err != nil {
return fmt.Errorf("wanted: list album %s: %w", a.Title, err)
}
res.Listed += int(n)
}
if len(albums) == libraryNamesPerPass {
return nil // the rest next pass
}
}
return s.finishLibrary(ctx, q, res)
}
func (s *Service) finishLibrary(ctx context.Context, q *dbq.Queries, res *PassResult) error {
done, err := q.FinishLibraryListing(ctx)
if err != nil {
return fmt.Errorf("wanted: finish the library listing: %w", err)
}
s.logger.Info("wanted: the library's albums are on the list", "listed_this_pass", res.Listed,
"unlisted", done.LibraryUnlisted)
return nil
}
func deref(p *string) string {
if p == nil {
return ""
}
return *p
}