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
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>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
// Package reacquisition turns a missing file back into a Lidarr request
|
||||
// without anyone pressing anything (milestone #290).
|
||||
// Package reacquisition wants an album back when its files go missing,
|
||||
// without anyone pressing anything (milestone #290; a want rather than a
|
||||
// Lidarr request since M502 #5520).
|
||||
//
|
||||
// The unit of work is the ALBUM, not the track. Lidarr acquires releases;
|
||||
// there is no meaningful "fetch me one track", and a track-kind request needs
|
||||
// a recording MBID plenty of files lack. Grouping also does most of the
|
||||
// The unit of work is the ALBUM, not the track. Music is acquired as
|
||||
// releases; there is no meaningful "fetch me one track", and a recording
|
||||
// MBID is something plenty of files lack. Grouping also does most of the
|
||||
// safety work: the loss that produced #2523 — three reorganised albums, ~40
|
||||
// missing files — becomes three requests rather than forty.
|
||||
// missing files — becomes three wants rather than forty.
|
||||
package reacquisition
|
||||
|
||||
import (
|
||||
@@ -14,7 +15,6 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
@@ -24,13 +24,9 @@ import (
|
||||
// Settings is the operator-tunable policy, mirroring the columns and CHECK
|
||||
// ranges in migration 0056.
|
||||
type Settings struct {
|
||||
Enabled bool
|
||||
GraceHours int32
|
||||
BackoffBaseHours int32
|
||||
BackoffMaxHours int32
|
||||
MaxAttempts int32
|
||||
MaxPerPass int32
|
||||
AutoApprove bool
|
||||
Enabled bool
|
||||
GraceHours int32
|
||||
MaxPerPass int32
|
||||
}
|
||||
|
||||
// Defaults mirror migration 0056's column defaults. Duplicated here so a
|
||||
@@ -38,13 +34,9 @@ type Settings struct {
|
||||
// zero-valued one — a zero grace window would fire on every transient
|
||||
// unmount, which is the exact failure the grace period exists to prevent.
|
||||
var Defaults = Settings{
|
||||
Enabled: true,
|
||||
GraceHours: 24,
|
||||
BackoffBaseHours: 6,
|
||||
BackoffMaxHours: 168,
|
||||
MaxAttempts: 3,
|
||||
MaxPerPass: 20,
|
||||
AutoApprove: true,
|
||||
Enabled: true,
|
||||
GraceHours: 24,
|
||||
MaxPerPass: 20,
|
||||
}
|
||||
|
||||
// ErrOutOfRange is returned by Set for values migration 0056's CHECKs would
|
||||
@@ -92,13 +84,9 @@ func (s *SettingsService) Set(ctx context.Context, in Settings) (Settings, error
|
||||
return Settings{}, err
|
||||
}
|
||||
row, err := dbq.New(s.pool).UpdateReacquisitionSettings(ctx, dbq.UpdateReacquisitionSettingsParams{
|
||||
Enabled: in.Enabled,
|
||||
GraceHours: in.GraceHours,
|
||||
BackoffBaseHours: in.BackoffBaseHours,
|
||||
BackoffMaxHours: in.BackoffMaxHours,
|
||||
MaxAttempts: in.MaxAttempts,
|
||||
MaxPerPass: in.MaxPerPass,
|
||||
AutoApprove: in.AutoApprove,
|
||||
Enabled: in.Enabled,
|
||||
GraceHours: in.GraceHours,
|
||||
MaxPerPass: in.MaxPerPass,
|
||||
})
|
||||
if err != nil {
|
||||
return Settings{}, fmt.Errorf("reacquisition: save settings: %w", err)
|
||||
@@ -110,43 +98,10 @@ func (s *SettingsService) Set(ctx context.Context, in Settings) (Settings, error
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Backoff is how long to wait before the next attempt on an album that has
|
||||
// already been tried [attempts] times: base * 2^(attempts-1), clamped to the
|
||||
// configured maximum. Zero attempts means "never tried", which is always due.
|
||||
//
|
||||
// Exported and pure so the schedule is testable without a database, and so
|
||||
// the admin surface can show the same number the sweeper will act on.
|
||||
func (s Settings) Backoff(attempts int32) time.Duration {
|
||||
if attempts <= 0 {
|
||||
return 0
|
||||
}
|
||||
hours := s.BackoffBaseHours
|
||||
for i := int32(1); i < attempts; i++ {
|
||||
hours *= 2
|
||||
// Clamp inside the loop as well as after: doubling from a large base
|
||||
// enough times would overflow int32 before the comparison ran.
|
||||
if hours >= s.BackoffMaxHours {
|
||||
return time.Duration(s.BackoffMaxHours) * time.Hour
|
||||
}
|
||||
}
|
||||
if hours > s.BackoffMaxHours {
|
||||
hours = s.BackoffMaxHours
|
||||
}
|
||||
return time.Duration(hours) * time.Hour
|
||||
}
|
||||
|
||||
func validate(in Settings) error {
|
||||
switch {
|
||||
case in.GraceHours < 1 || in.GraceHours > 720:
|
||||
return fmt.Errorf("%w: grace_hours must be 1-720", ErrOutOfRange)
|
||||
case in.BackoffBaseHours < 1 || in.BackoffBaseHours > 168:
|
||||
return fmt.Errorf("%w: backoff_base_hours must be 1-168", ErrOutOfRange)
|
||||
case in.BackoffMaxHours < 1 || in.BackoffMaxHours > 720:
|
||||
return fmt.Errorf("%w: backoff_max_hours must be 1-720", ErrOutOfRange)
|
||||
case in.BackoffMaxHours < in.BackoffBaseHours:
|
||||
return fmt.Errorf("%w: backoff_max_hours must be >= backoff_base_hours", ErrOutOfRange)
|
||||
case in.MaxAttempts < 1 || in.MaxAttempts > 10:
|
||||
return fmt.Errorf("%w: max_attempts must be 1-10", ErrOutOfRange)
|
||||
case in.MaxPerPass < 1 || in.MaxPerPass > 200:
|
||||
return fmt.Errorf("%w: max_per_pass must be 1-200", ErrOutOfRange)
|
||||
}
|
||||
@@ -155,12 +110,8 @@ func validate(in Settings) error {
|
||||
|
||||
func fromRow(row dbq.ReacquisitionSetting) Settings {
|
||||
return Settings{
|
||||
Enabled: row.Enabled,
|
||||
GraceHours: row.GraceHours,
|
||||
BackoffBaseHours: row.BackoffBaseHours,
|
||||
BackoffMaxHours: row.BackoffMaxHours,
|
||||
MaxAttempts: row.MaxAttempts,
|
||||
MaxPerPass: row.MaxPerPass,
|
||||
AutoApprove: row.AutoApprove,
|
||||
Enabled: row.Enabled,
|
||||
GraceHours: row.GraceHours,
|
||||
MaxPerPass: row.MaxPerPass,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user