feat: a held album below the quality you settle for, or holding a video rip, stays wanted for a better copy, and the import replaces the old tracks with their likes and plays carried over (M502 #5524)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m20s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 5m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 27s
release / Verify release artifacts (tag releases only) (push) Skipped

- acquisition_settings.upgrade_until (default High quality lossy, Lidarr's
  cutoff, note #5527; "" is off), an "Upgrade until" row under Searching.
- The wanted worker judges held albums hourly: tier per format from size per
  second (the scanner records no bitrate), the album's worst. Below the
  cutoff it is wanted for quality; a video rip among its tracks always.
- The schedule searches upgrades weekly, after open wants; a release no
  better than the copy held is refused ("not_better").
- The import matches each file to the track it would replace (recording,
  then disc/track) and replaces it when it is a rip or the file is a better
  tier: over the same path in one rename, or filed beside it and the old
  track folded in (library.ReplaceTrack: file first, then foldTrackInto).
  Nothing better: the release is set aside.
- Collection › Upgrades (Lidarr's Cutoff Unmet); Suspect sources says when a
  clean copy was looked for instead of Lidarr's search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 12:24:59 -04:00
co-authored by Claude Opus 5.5
parent a5ad301c94
commit c54792c51e
45 changed files with 1574 additions and 99 deletions
+88 -1
View File
@@ -59,6 +59,38 @@ func (q *Queries) ClaimGrabImport(ctx context.Context, id pgtype.UUID) (Grab, er
return i, err
}
const clearTrackSourceVerdict = `-- name: ClearTrackSourceVerdict :exec
UPDATE tracks SET source_verdict = NULL, source_verdict_at = NULL
WHERE id = $1 AND source_verdict = 'suspect'
`
// A track whose file was replaced in place is no longer the rip it was.
func (q *Queries) ClearTrackSourceVerdict(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, clearTrackSourceVerdict, id)
return err
}
const finishUpgrade = `-- name: FinishUpgrade :exec
UPDATE wants
SET album_id = COALESCE($1, album_id),
upgrade = '',
upgrade_checked_at = NULL,
updated_at = now()
WHERE id = $2
`
type FinishUpgradeParams struct {
AlbumID pgtype.UUID
ID pgtype.UUID
}
// An upgrade was imported: the want holds the album the new tracks are in,
// and is judged again by the next upgrade check.
func (q *Queries) FinishUpgrade(ctx context.Context, arg FinishUpgradeParams) error {
_, err := q.db.Exec(ctx, finishUpgrade, arg.AlbumID, arg.ID)
return err
}
const forceGrabImport = `-- name: ForceGrabImport :one
UPDATE grabs
SET import_state = '', import_forced = true, import_reason = '', import_updated_at = now()
@@ -479,6 +511,57 @@ func (q *Queries) ListReleaseBlocksPage(ctx context.Context, arg ListReleaseBloc
return items, nil
}
const listUpgradeTracks = `-- name: ListUpgradeTracks :many
SELECT id, file_path, mbid, COALESCE(disc_number, 1)::int AS disc, track_number,
file_format, file_size, duration_ms, source_verdict
FROM tracks
WHERE album_id = $1 AND missing_since IS NULL
`
type ListUpgradeTracksRow struct {
ID pgtype.UUID
FilePath string
Mbid *string
Disc int32
TrackNumber *int32
FileFormat string
FileSize int64
DurationMs int32
SourceVerdict *string
}
// The tracks an upgrade may replace (#5524): the present tracks of the album
// the want holds.
func (q *Queries) ListUpgradeTracks(ctx context.Context, albumID pgtype.UUID) ([]ListUpgradeTracksRow, error) {
rows, err := q.db.Query(ctx, listUpgradeTracks, albumID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListUpgradeTracksRow
for rows.Next() {
var i ListUpgradeTracksRow
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.Mbid,
&i.Disc,
&i.TrackNumber,
&i.FileFormat,
&i.FileSize,
&i.DurationMs,
&i.SourceVerdict,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const markWantSearched = `-- name: MarkWantSearched :exec
UPDATE wants SET searched_at = now() WHERE id = $1
`
@@ -490,7 +573,7 @@ func (q *Queries) MarkWantSearched(ctx context.Context, id pgtype.UUID) error {
}
const openWantsForGroups = `-- name: OpenWantsForGroups :many
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.primary_type, w.first_release_date, w.reason, w.album_id, w.created_by, w.wanted_at, w.fulfilled_at, w.created_at, w.updated_at, w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error FROM wants w
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.primary_type, w.first_release_date, w.reason, w.album_id, w.created_by, w.wanted_at, w.fulfilled_at, w.created_at, w.updated_at, w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error, w.upgrade, w.held_tier, w.held_quality, w.upgrade_checked_at FROM wants w
WHERE w.fulfilled_at IS NULL AND w.release_group_mbid = ANY($1::text[])
AND NOT EXISTS (SELECT 1 FROM grabs g WHERE g.want_id = w.id AND g.ended_at IS NULL)
`
@@ -526,6 +609,10 @@ func (q *Queries) OpenWantsForGroups(ctx context.Context, groups []string) ([]Wa
&i.BestRelease,
&i.BestVerdict,
&i.SearchError,
&i.Upgrade,
&i.HeldTier,
&i.HeldQuality,
&i.UpgradeCheckedAt,
); err != nil {
return nil, err
}