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
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ type AcquisitionSetting struct {
|
||||
LibraryListingStartedAt pgtype.Timestamptz
|
||||
LibraryListedAt pgtype.Timestamptz
|
||||
LibraryUnlisted int32
|
||||
UpgradeUntil string
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
@@ -1070,6 +1071,10 @@ type Want struct {
|
||||
BestRelease string
|
||||
BestVerdict string
|
||||
SearchError string
|
||||
Upgrade string
|
||||
HeldTier string
|
||||
HeldQuality string
|
||||
UpgradeCheckedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type WantCandidate struct {
|
||||
|
||||
@@ -217,7 +217,7 @@ ON CONFLICT (release_group_mbid) DO UPDATE
|
||||
fulfilled_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE wants.fulfilled_at IS NOT NULL
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error, upgrade, held_tier, held_quality, upgrade_checked_at
|
||||
`
|
||||
|
||||
type WantMissingAlbumParams struct {
|
||||
@@ -260,6 +260,10 @@ func (q *Queries) WantMissingAlbum(ctx context.Context, arg WantMissingAlbumPara
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
&i.Upgrade,
|
||||
&i.HeldTier,
|
||||
&i.HeldQuality,
|
||||
&i.UpgradeCheckedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func (q *Queries) EndGrab(ctx context.Context, arg EndGrabParams) error {
|
||||
|
||||
const getAcquisitionSettings = `-- name: GetAcquisitionSettings :one
|
||||
|
||||
SELECT id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at, import_root, seed_ratio, seed_days, stall_hours, library_listing_started_at, library_listed_at, library_unlisted FROM acquisition_settings WHERE id = true
|
||||
SELECT id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at, import_root, seed_ratio, seed_days, stall_hours, library_listing_started_at, library_listed_at, library_unlisted, upgrade_until FROM acquisition_settings WHERE id = true
|
||||
`
|
||||
|
||||
// Searching for wants and grabbing releases (M502 #5521).
|
||||
@@ -118,6 +118,7 @@ func (q *Queries) GetAcquisitionSettings(ctx context.Context) (AcquisitionSettin
|
||||
&i.LibraryListingStartedAt,
|
||||
&i.LibraryListedAt,
|
||||
&i.LibraryUnlisted,
|
||||
&i.UpgradeUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -201,6 +202,7 @@ func (q *Queries) GetWantCandidate(ctx context.Context, arg GetWantCandidatePara
|
||||
|
||||
const getWantForSearch = `-- name: GetWantForSearch :one
|
||||
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason, w.fulfilled_at,
|
||||
w.upgrade, w.held_tier, w.held_quality, w.album_id,
|
||||
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
|
||||
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
|
||||
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date
|
||||
@@ -217,6 +219,10 @@ type GetWantForSearchRow struct {
|
||||
Title string
|
||||
Reason string
|
||||
FulfilledAt pgtype.Timestamptz
|
||||
Upgrade string
|
||||
HeldTier string
|
||||
HeldQuality string
|
||||
AlbumID pgtype.UUID
|
||||
PrimaryType string
|
||||
SecondaryTypes []string
|
||||
FirstReleaseDate string
|
||||
@@ -233,6 +239,10 @@ func (q *Queries) GetWantForSearch(ctx context.Context, id pgtype.UUID) (GetWant
|
||||
&i.Title,
|
||||
&i.Reason,
|
||||
&i.FulfilledAt,
|
||||
&i.Upgrade,
|
||||
&i.HeldTier,
|
||||
&i.HeldQuality,
|
||||
&i.AlbumID,
|
||||
&i.PrimaryType,
|
||||
&i.SecondaryTypes,
|
||||
&i.FirstReleaseDate,
|
||||
@@ -449,16 +459,20 @@ func (q *Queries) ListWantCandidates(ctx context.Context, wantID pgtype.UUID) ([
|
||||
}
|
||||
|
||||
const listWantsDueForSearch = `-- name: ListWantsDueForSearch :many
|
||||
SELECT w.id, w.release_group_mbid, w.artist_name, w.title, w.reason,
|
||||
SELECT w.id, w.release_group_mbid, w.artist_name, w.title, w.reason, w.upgrade, w.held_tier, w.held_quality,
|
||||
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
|
||||
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
|
||||
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date
|
||||
FROM wants w
|
||||
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
|
||||
WHERE w.fulfilled_at IS NULL
|
||||
WHERE (w.fulfilled_at IS NULL OR w.upgrade <> '')
|
||||
AND NOT EXISTS (SELECT 1 FROM grabs gr WHERE gr.want_id = w.id AND gr.ended_at IS NULL)
|
||||
AND (w.searched_at IS NULL OR w.searched_at < now() - make_interval(hours => $1::int))
|
||||
ORDER BY w.searched_at IS NOT NULL,
|
||||
AND (w.searched_at IS NULL
|
||||
OR w.searched_at < now() - make_interval(hours => CASE WHEN w.fulfilled_at IS NULL
|
||||
THEN $1::int
|
||||
ELSE GREATEST($1::int, 168) END))
|
||||
ORDER BY w.fulfilled_at IS NOT NULL,
|
||||
w.searched_at IS NOT NULL,
|
||||
CASE w.reason WHEN 'request' THEN 0 WHEN 'missing' THEN 1 ELSE 2 END,
|
||||
w.searched_at NULLS FIRST, w.wanted_at, w.id
|
||||
LIMIT $2
|
||||
@@ -475,14 +489,18 @@ type ListWantsDueForSearchRow struct {
|
||||
ArtistName string
|
||||
Title string
|
||||
Reason string
|
||||
Upgrade string
|
||||
HeldTier string
|
||||
HeldQuality string
|
||||
PrimaryType string
|
||||
SecondaryTypes []string
|
||||
FirstReleaseDate string
|
||||
}
|
||||
|
||||
// Open wants with no download running that were never searched or not since
|
||||
// @hours ago: never-searched first, then requests, then missing files, then
|
||||
// followed artists' releases.
|
||||
// Wants with no download running that were never searched or not since
|
||||
// @hours ago: open wants first (never-searched, then requests, then missing
|
||||
// files, then followed artists' releases), then held albums wanted for an
|
||||
// upgrade (#5524), which are searched at most weekly.
|
||||
func (q *Queries) ListWantsDueForSearch(ctx context.Context, arg ListWantsDueForSearchParams) ([]ListWantsDueForSearchRow, error) {
|
||||
rows, err := q.db.Query(ctx, listWantsDueForSearch, arg.Hours, arg.MaxRows)
|
||||
if err != nil {
|
||||
@@ -498,6 +516,9 @@ func (q *Queries) ListWantsDueForSearch(ctx context.Context, arg ListWantsDueFor
|
||||
&i.ArtistName,
|
||||
&i.Title,
|
||||
&i.Reason,
|
||||
&i.Upgrade,
|
||||
&i.HeldTier,
|
||||
&i.HeldQuality,
|
||||
&i.PrimaryType,
|
||||
&i.SecondaryTypes,
|
||||
&i.FirstReleaseDate,
|
||||
@@ -609,9 +630,10 @@ UPDATE acquisition_settings
|
||||
seed_ratio = $5,
|
||||
seed_days = $6,
|
||||
stall_hours = $7,
|
||||
upgrade_until = $8,
|
||||
updated_at = now()
|
||||
WHERE id = true
|
||||
RETURNING id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at, import_root, seed_ratio, seed_days, stall_hours, library_listing_started_at, library_listed_at, library_unlisted
|
||||
RETURNING id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at, import_root, seed_ratio, seed_days, stall_hours, library_listing_started_at, library_listed_at, library_unlisted, upgrade_until
|
||||
`
|
||||
|
||||
type UpdateAcquisitionSettingsParams struct {
|
||||
@@ -622,6 +644,7 @@ type UpdateAcquisitionSettingsParams struct {
|
||||
SeedRatio float32
|
||||
SeedDays int32
|
||||
StallHours int32
|
||||
UpgradeUntil string
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAcquisitionSettings(ctx context.Context, arg UpdateAcquisitionSettingsParams) (AcquisitionSetting, error) {
|
||||
@@ -633,6 +656,7 @@ func (q *Queries) UpdateAcquisitionSettings(ctx context.Context, arg UpdateAcqui
|
||||
arg.SeedRatio,
|
||||
arg.SeedDays,
|
||||
arg.StallHours,
|
||||
arg.UpgradeUntil,
|
||||
)
|
||||
var i AcquisitionSetting
|
||||
err := row.Scan(
|
||||
@@ -650,6 +674,7 @@ func (q *Queries) UpdateAcquisitionSettings(ctx context.Context, arg UpdateAcqui
|
||||
&i.LibraryListingStartedAt,
|
||||
&i.LibraryListedAt,
|
||||
&i.LibraryUnlisted,
|
||||
&i.UpgradeUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -660,16 +660,17 @@ SELECT t.id,
|
||||
artists.name AS artist_name,
|
||||
t.source_verdict,
|
||||
t.source_verdict_at,
|
||||
searched.at AS searched_at
|
||||
-- The album's place on the wanted list: wanted for a clean copy, and
|
||||
-- when that was last searched for (#5524).
|
||||
COALESCE(w.upgrade, '')::text AS want_upgrade,
|
||||
w.searched_at AS searched_at
|
||||
FROM tracks t
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
-- When the resolver last asked Lidarr to search the album for a better copy
|
||||
-- (#5447).
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT max(l.created_at)::timestamptz AS at FROM audit_log l
|
||||
WHERE l.action = 'lidarr_rip_search' AND l.metadata->>'album_id' = albums.id::text
|
||||
) searched ON true
|
||||
SELECT x.upgrade, x.searched_at FROM wants x
|
||||
WHERE x.album_id = albums.id AND x.fulfilled_at IS NOT NULL LIMIT 1
|
||||
) w ON true
|
||||
WHERE t.missing_since IS NULL
|
||||
AND regexp_replace(t.file_path, '^.*/', '') ~* $1::text
|
||||
ORDER BY directory, t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.title
|
||||
@@ -696,6 +697,7 @@ type ListSuspectSourceTracksRow struct {
|
||||
ArtistName string
|
||||
SourceVerdict *string
|
||||
SourceVerdictAt pgtype.Timestamptz
|
||||
WantUpgrade string
|
||||
SearchedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
@@ -732,6 +734,7 @@ func (q *Queries) ListSuspectSourceTracks(ctx context.Context, arg ListSuspectSo
|
||||
&i.ArtistName,
|
||||
&i.SourceVerdict,
|
||||
&i.SourceVerdictAt,
|
||||
&i.WantUpgrade,
|
||||
&i.SearchedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -20,6 +20,22 @@ func (q *Queries) ClearDefaultReleaseTypeProfile(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const clearHeldUpgrades = `-- name: ClearHeldUpgrades :execrows
|
||||
UPDATE wants SET upgrade = '', held_tier = '', held_quality = '', upgrade_checked_at = now()
|
||||
WHERE fulfilled_at IS NOT NULL AND upgrade <> ''
|
||||
AND NOT EXISTS (SELECT 1 FROM tracks t
|
||||
WHERE t.album_id = wants.album_id AND t.missing_since IS NULL AND t.duration_ms > 0)
|
||||
`
|
||||
|
||||
// A held want whose album has no track left to judge is not wanted again.
|
||||
func (q *Queries) ClearHeldUpgrades(ctx context.Context) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, clearHeldUpgrades)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const countAlbumsToMatch = `-- name: CountAlbumsToMatch :one
|
||||
SELECT COUNT(*)
|
||||
FROM albums a
|
||||
@@ -52,6 +68,8 @@ const countWants = `-- name: CountWants :one
|
||||
SELECT COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason <> 'missing')::bigint AS wanted,
|
||||
COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason = 'missing')::bigint AS missing,
|
||||
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL)::bigint AS held,
|
||||
-- Held albums wanted for a better copy (#5524).
|
||||
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL AND upgrade <> '')::bigint AS upgrades,
|
||||
COUNT(DISTINCT artist_mbid)::bigint AS artists,
|
||||
-- Library albums waiting to be matched by hand (#5579).
|
||||
(SELECT COUNT(*) FROM library_album_matches m
|
||||
@@ -72,6 +90,7 @@ type CountWantsRow struct {
|
||||
Wanted int64
|
||||
Missing int64
|
||||
Held int64
|
||||
Upgrades int64
|
||||
Artists int64
|
||||
Unmatched int64
|
||||
Following int64
|
||||
@@ -87,6 +106,7 @@ func (q *Queries) CountWants(ctx context.Context) (CountWantsRow, error) {
|
||||
&i.Wanted,
|
||||
&i.Missing,
|
||||
&i.Held,
|
||||
&i.Upgrades,
|
||||
&i.Artists,
|
||||
&i.Unmatched,
|
||||
&i.Following,
|
||||
@@ -350,7 +370,7 @@ func (q *Queries) GetWantAlbum(ctx context.Context, id pgtype.UUID) (GetWantAlbu
|
||||
}
|
||||
|
||||
const getWantByReleaseGroup = `-- name: GetWantByReleaseGroup :one
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error FROM wants WHERE release_group_mbid = $1
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error, upgrade, held_tier, held_quality, upgrade_checked_at FROM wants WHERE release_group_mbid = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetWantByReleaseGroup(ctx context.Context, releaseGroupMbid string) (Want, error) {
|
||||
@@ -377,6 +397,10 @@ func (q *Queries) GetWantByReleaseGroup(ctx context.Context, releaseGroupMbid st
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
&i.Upgrade,
|
||||
&i.HeldTier,
|
||||
&i.HeldQuality,
|
||||
&i.UpgradeCheckedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -700,6 +724,69 @@ func (q *Queries) ListFollowsDue(ctx context.Context, arg ListFollowsDueParams)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listHeldWantFormats = `-- name: ListHeldWantFormats :many
|
||||
|
||||
SELECT w.id AS want_id, w.upgrade, w.held_tier, w.held_quality,
|
||||
lower(t.file_format)::text AS file_format,
|
||||
SUM(t.file_size)::bigint AS bytes,
|
||||
SUM(t.duration_ms)::bigint AS duration_ms,
|
||||
COUNT(*)::int AS tracks,
|
||||
COUNT(*) FILTER (WHERE t.source_verdict = 'suspect')::int AS rips
|
||||
FROM wants w
|
||||
JOIN tracks t ON t.album_id = w.album_id
|
||||
WHERE w.fulfilled_at IS NOT NULL
|
||||
AND t.missing_since IS NULL AND t.duration_ms > 0
|
||||
GROUP BY w.id, lower(t.file_format)
|
||||
ORDER BY w.id
|
||||
`
|
||||
|
||||
type ListHeldWantFormatsRow struct {
|
||||
WantID pgtype.UUID
|
||||
Upgrade string
|
||||
HeldTier string
|
||||
HeldQuality string
|
||||
FileFormat string
|
||||
Bytes int64
|
||||
DurationMs int64
|
||||
Tracks int32
|
||||
Rips int32
|
||||
}
|
||||
|
||||
// Upgrades (#5524). -------------------------------------------------------
|
||||
// What each held album's copy is made of, for judging whether it is worth
|
||||
// upgrading: its present tracks grouped by format, with their bytes and
|
||||
// length summed, and how many are held back as video rips. A held want whose
|
||||
// album has no track with a length yet has no rows.
|
||||
func (q *Queries) ListHeldWantFormats(ctx context.Context) ([]ListHeldWantFormatsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listHeldWantFormats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListHeldWantFormatsRow
|
||||
for rows.Next() {
|
||||
var i ListHeldWantFormatsRow
|
||||
if err := rows.Scan(
|
||||
&i.WantID,
|
||||
&i.Upgrade,
|
||||
&i.HeldTier,
|
||||
&i.HeldQuality,
|
||||
&i.FileFormat,
|
||||
&i.Bytes,
|
||||
&i.DurationMs,
|
||||
&i.Tracks,
|
||||
&i.Rips,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLibraryAlbumTracks = `-- name: ListLibraryAlbumTracks :many
|
||||
SELECT title, disc_number, track_number, duration_ms, (missing_since IS NOT NULL)::bool AS missing
|
||||
FROM tracks
|
||||
@@ -779,7 +866,7 @@ func (q *Queries) ListLibraryAlbums(ctx context.Context, arg ListLibraryAlbumsPa
|
||||
}
|
||||
|
||||
const listOpenWants = `-- name: ListOpenWants :many
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error FROM wants
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error, upgrade, held_tier, held_quality, upgrade_checked_at FROM wants
|
||||
WHERE fulfilled_at IS NULL
|
||||
ORDER BY wanted_at, id
|
||||
LIMIT $1
|
||||
@@ -815,6 +902,10 @@ func (q *Queries) ListOpenWants(ctx context.Context, limit int32) ([]Want, error
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
&i.Upgrade,
|
||||
&i.HeldTier,
|
||||
&i.HeldQuality,
|
||||
&i.UpgradeCheckedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1023,6 +1114,8 @@ func (q *Queries) ListWantArtists(ctx context.Context, arg ListWantArtistsParams
|
||||
const listWants = `-- name: ListWants :many
|
||||
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason,
|
||||
w.album_id, w.wanted_at, w.fulfilled_at,
|
||||
-- Why a held album is wanted again, and the copy it has (#5524).
|
||||
w.upgrade, w.held_quality,
|
||||
-- A request names an album before the catalogue has read it: the
|
||||
-- catalogue's type and date fill in once it has.
|
||||
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
|
||||
@@ -1049,6 +1142,7 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
|
||||
WHERE CASE $1::text
|
||||
WHEN 'all' THEN true
|
||||
WHEN 'held' THEN w.fulfilled_at IS NOT NULL
|
||||
WHEN 'upgrade' THEN w.fulfilled_at IS NOT NULL AND w.upgrade <> ''
|
||||
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
|
||||
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
|
||||
END
|
||||
@@ -1080,6 +1174,8 @@ type ListWantsRow struct {
|
||||
AlbumID pgtype.UUID
|
||||
WantedAt pgtype.Timestamptz
|
||||
FulfilledAt pgtype.Timestamptz
|
||||
Upgrade string
|
||||
HeldQuality string
|
||||
PrimaryType string
|
||||
SecondaryTypes []string
|
||||
FirstReleaseDate string
|
||||
@@ -1137,6 +1233,8 @@ func (q *Queries) ListWants(ctx context.Context, arg ListWantsParams) ([]ListWan
|
||||
&i.AlbumID,
|
||||
&i.WantedAt,
|
||||
&i.FulfilledAt,
|
||||
&i.Upgrade,
|
||||
&i.HeldQuality,
|
||||
&i.PrimaryType,
|
||||
&i.SecondaryTypes,
|
||||
&i.FirstReleaseDate,
|
||||
@@ -1330,6 +1428,38 @@ func (q *Queries) SetDefaultReleaseTypeProfile(ctx context.Context, id pgtype.UU
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const setWantUpgrades = `-- name: SetWantUpgrades :exec
|
||||
UPDATE wants w
|
||||
SET upgrade = v.upgrade,
|
||||
held_tier = v.held_tier,
|
||||
held_quality = v.held_quality,
|
||||
upgrade_checked_at = now(),
|
||||
updated_at = CASE WHEN w.upgrade <> v.upgrade THEN now() ELSE w.updated_at END
|
||||
FROM (SELECT unnest($1::uuid[]) AS id,
|
||||
unnest($2::text[]) AS upgrade,
|
||||
unnest($3::text[]) AS held_tier,
|
||||
unnest($4::text[]) AS held_quality) v
|
||||
WHERE w.id = v.id
|
||||
`
|
||||
|
||||
type SetWantUpgradesParams struct {
|
||||
Ids []pgtype.UUID
|
||||
Upgrades []string
|
||||
HeldTiers []string
|
||||
HeldQualities []string
|
||||
}
|
||||
|
||||
// Writes the upgrade check's verdicts, one row per want.
|
||||
func (q *Queries) SetWantUpgrades(ctx context.Context, arg SetWantUpgradesParams) error {
|
||||
_, err := q.db.Exec(ctx, setWantUpgrades,
|
||||
arg.Ids,
|
||||
arg.Upgrades,
|
||||
arg.HeldTiers,
|
||||
arg.HeldQualities,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const startLibraryListing = `-- name: StartLibraryListing :one
|
||||
UPDATE acquisition_settings
|
||||
SET library_listing_started_at = COALESCE(library_listing_started_at, now())
|
||||
@@ -1502,7 +1632,7 @@ INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_
|
||||
VALUES ($1, $2, $3, $4,
|
||||
$5, $6, $7, $8)
|
||||
ON CONFLICT (release_group_mbid) DO UPDATE SET updated_at = wants.updated_at
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error, upgrade, held_tier, held_quality, upgrade_checked_at
|
||||
`
|
||||
|
||||
type WantReleaseGroupParams struct {
|
||||
@@ -1551,6 +1681,10 @@ func (q *Queries) WantReleaseGroup(ctx context.Context, arg WantReleaseGroupPara
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
&i.Upgrade,
|
||||
&i.HeldTier,
|
||||
&i.HeldQuality,
|
||||
&i.UpgradeCheckedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
DROP INDEX IF EXISTS wants_upgrade_idx;
|
||||
ALTER TABLE wants
|
||||
DROP COLUMN upgrade_checked_at,
|
||||
DROP COLUMN held_quality,
|
||||
DROP COLUMN held_tier,
|
||||
DROP COLUMN upgrade;
|
||||
ALTER TABLE acquisition_settings DROP COLUMN upgrade_until;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Upgrades (M502 #5524): a held album whose copy is below the quality the
|
||||
-- operator settles for, or that holds a video rip, stays wanted for a better
|
||||
-- copy. The import replaces the old tracks with the new ones.
|
||||
--
|
||||
-- acquisition_settings.upgrade_until the tier a held album is upgraded to;
|
||||
-- '' switches quality upgrades off.
|
||||
-- Lidarr's "Any" profile upgraded up to
|
||||
-- High Quality Lossy (note #5527).
|
||||
-- wants.upgrade why a held album is wanted again:
|
||||
-- quality (below upgrade_until) or rip
|
||||
-- (a track is a video rip); '' not.
|
||||
-- wants.held_tier, held_quality the library copy's tier, estimated
|
||||
-- from format and size per second, and
|
||||
-- the same in words.
|
||||
-- wants.upgrade_checked_at when the wanted worker last judged it.
|
||||
-- want_candidates.verdict unchanged: a release no better than
|
||||
-- the copy held is "refused".
|
||||
ALTER TABLE acquisition_settings
|
||||
ADD COLUMN upgrade_until text NOT NULL DEFAULT 'lossy_high';
|
||||
|
||||
ALTER TABLE wants
|
||||
ADD COLUMN upgrade text NOT NULL DEFAULT ''
|
||||
CONSTRAINT wants_upgrade_check CHECK (upgrade IN ('', 'quality', 'rip')),
|
||||
ADD COLUMN held_tier text NOT NULL DEFAULT '',
|
||||
ADD COLUMN held_quality text NOT NULL DEFAULT '',
|
||||
ADD COLUMN upgrade_checked_at timestamptz;
|
||||
|
||||
CREATE INDEX wants_upgrade_idx ON wants (searched_at NULLS FIRST)
|
||||
WHERE fulfilled_at IS NOT NULL AND upgrade <> '';
|
||||
@@ -140,3 +140,26 @@ SELECT b.info_hash, b.name, b.reason, b.detail, b.blocked_at, b.expires_at, w.ti
|
||||
WHERE b.expires_at IS NULL OR b.expires_at > now()
|
||||
ORDER BY b.blocked_at DESC, b.info_hash
|
||||
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
|
||||
|
||||
-- name: ListUpgradeTracks :many
|
||||
-- The tracks an upgrade may replace (#5524): the present tracks of the album
|
||||
-- the want holds.
|
||||
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;
|
||||
|
||||
-- name: FinishUpgrade :exec
|
||||
-- An upgrade was imported: the want holds the album the new tracks are in,
|
||||
-- and is judged again by the next upgrade check.
|
||||
UPDATE wants
|
||||
SET album_id = COALESCE(sqlc.narg(album_id), album_id),
|
||||
upgrade = '',
|
||||
upgrade_checked_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id);
|
||||
|
||||
-- name: ClearTrackSourceVerdict :exec
|
||||
-- A track whose file was replaced in place is no longer the rip it was.
|
||||
UPDATE tracks SET source_verdict = NULL, source_verdict_at = NULL
|
||||
WHERE id = $1 AND source_verdict = 'suspect';
|
||||
|
||||
@@ -12,6 +12,7 @@ UPDATE acquisition_settings
|
||||
seed_ratio = sqlc.arg(seed_ratio),
|
||||
seed_days = sqlc.arg(seed_days),
|
||||
stall_hours = sqlc.arg(stall_hours),
|
||||
upgrade_until = sqlc.arg(upgrade_until),
|
||||
updated_at = now()
|
||||
WHERE id = true
|
||||
RETURNING *;
|
||||
@@ -20,25 +21,31 @@ RETURNING *;
|
||||
UPDATE acquisition_settings SET last_pass_at = now(), last_pass = sqlc.arg(report) WHERE id = true;
|
||||
|
||||
-- name: ListWantsDueForSearch :many
|
||||
-- Open wants with no download running that were never searched or not since
|
||||
-- @hours ago: never-searched first, then requests, then missing files, then
|
||||
-- followed artists' releases.
|
||||
SELECT w.id, w.release_group_mbid, w.artist_name, w.title, w.reason,
|
||||
-- Wants with no download running that were never searched or not since
|
||||
-- @hours ago: open wants first (never-searched, then requests, then missing
|
||||
-- files, then followed artists' releases), then held albums wanted for an
|
||||
-- upgrade (#5524), which are searched at most weekly.
|
||||
SELECT w.id, w.release_group_mbid, w.artist_name, w.title, w.reason, w.upgrade, w.held_tier, w.held_quality,
|
||||
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
|
||||
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
|
||||
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date
|
||||
FROM wants w
|
||||
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
|
||||
WHERE w.fulfilled_at IS NULL
|
||||
WHERE (w.fulfilled_at IS NULL OR w.upgrade <> '')
|
||||
AND NOT EXISTS (SELECT 1 FROM grabs gr WHERE gr.want_id = w.id AND gr.ended_at IS NULL)
|
||||
AND (w.searched_at IS NULL OR w.searched_at < now() - make_interval(hours => sqlc.arg(hours)::int))
|
||||
ORDER BY w.searched_at IS NOT NULL,
|
||||
AND (w.searched_at IS NULL
|
||||
OR w.searched_at < now() - make_interval(hours => CASE WHEN w.fulfilled_at IS NULL
|
||||
THEN sqlc.arg(hours)::int
|
||||
ELSE GREATEST(sqlc.arg(hours)::int, 168) END))
|
||||
ORDER BY w.fulfilled_at IS NOT NULL,
|
||||
w.searched_at IS NOT NULL,
|
||||
CASE w.reason WHEN 'request' THEN 0 WHEN 'missing' THEN 1 ELSE 2 END,
|
||||
w.searched_at NULLS FIRST, w.wanted_at, w.id
|
||||
LIMIT sqlc.arg(max_rows);
|
||||
|
||||
-- name: GetWantForSearch :one
|
||||
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason, w.fulfilled_at,
|
||||
w.upgrade, w.held_tier, w.held_quality, w.album_id,
|
||||
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
|
||||
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
|
||||
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date
|
||||
|
||||
@@ -288,16 +288,17 @@ SELECT t.id,
|
||||
artists.name AS artist_name,
|
||||
t.source_verdict,
|
||||
t.source_verdict_at,
|
||||
searched.at AS searched_at
|
||||
-- The album's place on the wanted list: wanted for a clean copy, and
|
||||
-- when that was last searched for (#5524).
|
||||
COALESCE(w.upgrade, '')::text AS want_upgrade,
|
||||
w.searched_at AS searched_at
|
||||
FROM tracks t
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
-- When the resolver last asked Lidarr to search the album for a better copy
|
||||
-- (#5447).
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT max(l.created_at)::timestamptz AS at FROM audit_log l
|
||||
WHERE l.action = 'lidarr_rip_search' AND l.metadata->>'album_id' = albums.id::text
|
||||
) searched ON true
|
||||
SELECT x.upgrade, x.searched_at FROM wants x
|
||||
WHERE x.album_id = albums.id AND x.fulfilled_at IS NOT NULL LIMIT 1
|
||||
) w ON true
|
||||
WHERE t.missing_since IS NULL
|
||||
AND regexp_replace(t.file_path, '^.*/', '') ~* sqlc.arg(pattern)::text
|
||||
ORDER BY directory, t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.title
|
||||
|
||||
@@ -183,6 +183,8 @@ DELETE FROM wants WHERE id = $1;
|
||||
-- artist or album title; total counts every match, past the page.
|
||||
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason,
|
||||
w.album_id, w.wanted_at, w.fulfilled_at,
|
||||
-- Why a held album is wanted again, and the copy it has (#5524).
|
||||
w.upgrade, w.held_quality,
|
||||
-- A request names an album before the catalogue has read it: the
|
||||
-- catalogue's type and date fill in once it has.
|
||||
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
|
||||
@@ -209,6 +211,7 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
|
||||
WHERE CASE sqlc.arg(state)::text
|
||||
WHEN 'all' THEN true
|
||||
WHEN 'held' THEN w.fulfilled_at IS NOT NULL
|
||||
WHEN 'upgrade' THEN w.fulfilled_at IS NOT NULL AND w.upgrade <> ''
|
||||
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
|
||||
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
|
||||
END
|
||||
@@ -270,6 +273,8 @@ SELECT title, disc_number, track_number, duration_ms, (missing_since IS NOT NULL
|
||||
SELECT COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason <> 'missing')::bigint AS wanted,
|
||||
COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason = 'missing')::bigint AS missing,
|
||||
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL)::bigint AS held,
|
||||
-- Held albums wanted for a better copy (#5524).
|
||||
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL AND upgrade <> '')::bigint AS upgrades,
|
||||
COUNT(DISTINCT artist_mbid)::bigint AS artists,
|
||||
-- Library albums waiting to be matched by hand (#5579).
|
||||
(SELECT COUNT(*) FROM library_album_matches m
|
||||
@@ -448,3 +453,44 @@ SELECT a.id, a.title, a.mbid, a.release_group_mbid, ar.name AS artist_name, ar.m
|
||||
UPDATE library_album_matches SET outcome = sqlc.arg(outcome), decided_at = now()
|
||||
WHERE album_id = sqlc.arg(album_id) AND outcome = sqlc.arg(was);
|
||||
|
||||
|
||||
-- Upgrades (#5524). -------------------------------------------------------
|
||||
|
||||
-- name: ListHeldWantFormats :many
|
||||
-- What each held album's copy is made of, for judging whether it is worth
|
||||
-- upgrading: its present tracks grouped by format, with their bytes and
|
||||
-- length summed, and how many are held back as video rips. A held want whose
|
||||
-- album has no track with a length yet has no rows.
|
||||
SELECT w.id AS want_id, w.upgrade, w.held_tier, w.held_quality,
|
||||
lower(t.file_format)::text AS file_format,
|
||||
SUM(t.file_size)::bigint AS bytes,
|
||||
SUM(t.duration_ms)::bigint AS duration_ms,
|
||||
COUNT(*)::int AS tracks,
|
||||
COUNT(*) FILTER (WHERE t.source_verdict = 'suspect')::int AS rips
|
||||
FROM wants w
|
||||
JOIN tracks t ON t.album_id = w.album_id
|
||||
WHERE w.fulfilled_at IS NOT NULL
|
||||
AND t.missing_since IS NULL AND t.duration_ms > 0
|
||||
GROUP BY w.id, lower(t.file_format)
|
||||
ORDER BY w.id;
|
||||
|
||||
-- name: SetWantUpgrades :exec
|
||||
-- Writes the upgrade check's verdicts, one row per want.
|
||||
UPDATE wants w
|
||||
SET upgrade = v.upgrade,
|
||||
held_tier = v.held_tier,
|
||||
held_quality = v.held_quality,
|
||||
upgrade_checked_at = now(),
|
||||
updated_at = CASE WHEN w.upgrade <> v.upgrade THEN now() ELSE w.updated_at END
|
||||
FROM (SELECT unnest(sqlc.arg(ids)::uuid[]) AS id,
|
||||
unnest(sqlc.arg(upgrades)::text[]) AS upgrade,
|
||||
unnest(sqlc.arg(held_tiers)::text[]) AS held_tier,
|
||||
unnest(sqlc.arg(held_qualities)::text[]) AS held_quality) v
|
||||
WHERE w.id = v.id;
|
||||
|
||||
-- name: ClearHeldUpgrades :execrows
|
||||
-- A held want whose album has no track left to judge is not wanted again.
|
||||
UPDATE wants SET upgrade = '', held_tier = '', held_quality = '', upgrade_checked_at = now()
|
||||
WHERE fulfilled_at IS NOT NULL AND upgrade <> ''
|
||||
AND NOT EXISTS (SELECT 1 FROM tracks t
|
||||
WHERE t.album_id = wants.album_id AND t.missing_since IS NULL AND t.duration_ms > 0);
|
||||
|
||||
Reference in New Issue
Block a user