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>
This commit is contained in:
2026-10-10 00:46:22 -04:00
co-authored by Claude Opus 5.5
parent 867f52358c
commit 04515a977c
93 changed files with 6522 additions and 4484 deletions
+105 -77
View File
@@ -99,92 +99,92 @@ func (ns NullLidarrQuarantineReason) Value() (driver.Value, error) {
return string(ns.LidarrQuarantineReason), nil
}
type LidarrRequestKind string
type RequestKind string
const (
LidarrRequestKindArtist LidarrRequestKind = "artist"
LidarrRequestKindAlbum LidarrRequestKind = "album"
LidarrRequestKindTrack LidarrRequestKind = "track"
RequestKindArtist RequestKind = "artist"
RequestKindAlbum RequestKind = "album"
RequestKindTrack RequestKind = "track"
)
func (e *LidarrRequestKind) Scan(src interface{}) error {
func (e *RequestKind) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = LidarrRequestKind(s)
*e = RequestKind(s)
case string:
*e = LidarrRequestKind(s)
*e = RequestKind(s)
default:
return fmt.Errorf("unsupported scan type for LidarrRequestKind: %T", src)
return fmt.Errorf("unsupported scan type for RequestKind: %T", src)
}
return nil
}
type NullLidarrRequestKind struct {
LidarrRequestKind LidarrRequestKind
Valid bool // Valid is true if LidarrRequestKind is not NULL
type NullRequestKind struct {
RequestKind RequestKind
Valid bool // Valid is true if RequestKind is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullLidarrRequestKind) Scan(value interface{}) error {
func (ns *NullRequestKind) Scan(value interface{}) error {
if value == nil {
ns.LidarrRequestKind, ns.Valid = "", false
ns.RequestKind, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.LidarrRequestKind.Scan(value)
return ns.RequestKind.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullLidarrRequestKind) Value() (driver.Value, error) {
func (ns NullRequestKind) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.LidarrRequestKind), nil
return string(ns.RequestKind), nil
}
type LidarrRequestStatus string
type RequestStatus string
const (
LidarrRequestStatusPending LidarrRequestStatus = "pending"
LidarrRequestStatusApproved LidarrRequestStatus = "approved"
LidarrRequestStatusRejected LidarrRequestStatus = "rejected"
LidarrRequestStatusCompleted LidarrRequestStatus = "completed"
LidarrRequestStatusFailed LidarrRequestStatus = "failed"
RequestStatusPending RequestStatus = "pending"
RequestStatusApproved RequestStatus = "approved"
RequestStatusRejected RequestStatus = "rejected"
RequestStatusCompleted RequestStatus = "completed"
RequestStatusFailed RequestStatus = "failed"
)
func (e *LidarrRequestStatus) Scan(src interface{}) error {
func (e *RequestStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = LidarrRequestStatus(s)
*e = RequestStatus(s)
case string:
*e = LidarrRequestStatus(s)
*e = RequestStatus(s)
default:
return fmt.Errorf("unsupported scan type for LidarrRequestStatus: %T", src)
return fmt.Errorf("unsupported scan type for RequestStatus: %T", src)
}
return nil
}
type NullLidarrRequestStatus struct {
LidarrRequestStatus LidarrRequestStatus
Valid bool // Valid is true if LidarrRequestStatus is not NULL
type NullRequestStatus struct {
RequestStatus RequestStatus
Valid bool // Valid is true if RequestStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullLidarrRequestStatus) Scan(value interface{}) error {
func (ns *NullRequestStatus) Scan(value interface{}) error {
if value == nil {
ns.LidarrRequestStatus, ns.Valid = "", false
ns.RequestStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.LidarrRequestStatus.Scan(value)
return ns.RequestStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullLidarrRequestStatus) Value() (driver.Value, error) {
func (ns NullRequestStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.LidarrRequestStatus), nil
return string(ns.RequestStatus), nil
}
type AcoustidSetting struct {
@@ -384,6 +384,24 @@ type FingerprintSetting struct {
AutoResolve bool
}
type Follow struct {
ArtistMbid string
ArtistName string
ProfileID pgtype.UUID
FollowedBy pgtype.UUID
FollowedAt pgtype.Timestamptz
BaselineAt pgtype.Timestamptz
WantExisting bool
CheckedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
type FollowSeenGroup struct {
ArtistMbid string
ReleaseGroupMbid string
SeenAt pgtype.Timestamptz
}
type GeneralLike struct {
UserID pgtype.UUID
TrackID pgtype.UUID
@@ -443,31 +461,6 @@ type LidarrQuarantineAction struct {
CreatedAt pgtype.Timestamptz
}
type LidarrRequest struct {
ID pgtype.UUID
UserID pgtype.UUID
Status LidarrRequestStatus
Kind LidarrRequestKind
LidarrArtistMbid string
LidarrAlbumMbid *string
LidarrTrackMbid *string
ArtistName string
AlbumTitle *string
TrackTitle *string
QualityProfileID *int32
RootFolderPath *string
DecidedAt pgtype.Timestamptz
DecidedBy pgtype.UUID
Notes *string
CompletedAt pgtype.Timestamptz
MatchedTrackID pgtype.UUID
MatchedAlbumID pgtype.UUID
MatchedArtistID pgtype.UUID
RequestedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
LidarrAddConfirmedAt pgtype.Timestamptz
}
type ListenbrainzSimilarRecording struct {
SeedTrackID pgtype.UUID
RecordingMbid string
@@ -535,16 +528,6 @@ type MbReleaseTrack struct {
RecordingMbid string
}
type MissingReacquisition struct {
AlbumID pgtype.UUID
Attempts int32
LastAttemptAt pgtype.Timestamptz
LastRequestID pgtype.UUID
GaveUpAt pgtype.Timestamptz
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
type NetworkSetting struct {
ID bool
TrustedProxyHops int32
@@ -636,14 +619,10 @@ type PlaylistTrack struct {
}
type ReacquisitionSetting struct {
ID bool
Enabled bool
GraceHours int32
BackoffBaseHours int32
BackoffMaxHours int32
MaxAttempts int32
MaxPerPass int32
AutoApprove bool
ID bool
Enabled bool
GraceHours int32
MaxPerPass int32
}
type RecommendationTuningAudit struct {
@@ -673,6 +652,38 @@ type RegistrationSetting struct {
Mode string
}
type ReleaseTypeProfile struct {
ID pgtype.UUID
Name string
PrimaryTypes []string
SecondaryTypes []string
IsDefault bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
type Request struct {
ID pgtype.UUID
UserID pgtype.UUID
Status RequestStatus
Kind RequestKind
ArtistMbid string
AlbumMbid *string
TrackMbid *string
ArtistName string
AlbumTitle *string
TrackTitle *string
DecidedAt pgtype.Timestamptz
DecidedBy pgtype.UUID
Notes *string
CompletedAt pgtype.Timestamptz
MatchedTrackID pgtype.UUID
MatchedAlbumID pgtype.UUID
MatchedArtistID pgtype.UUID
RequestedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
type ScanRun struct {
ID pgtype.UUID
StartedAt pgtype.Timestamptz
@@ -950,6 +961,23 @@ type UserNotificationPref struct {
UpdatedAt pgtype.Timestamptz
}
type Want struct {
ID pgtype.UUID
ReleaseGroupMbid string
ArtistMbid string
ArtistName string
Title string
PrimaryType string
FirstReleaseDate string
Reason string
AlbumID pgtype.UUID
CreatedBy pgtype.UUID
WantedAt pgtype.Timestamptz
FulfilledAt pgtype.Timestamptz
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
type YouMightLikeAlbum struct {
UserID pgtype.UUID
AlbumID pgtype.UUID