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 }