release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m19s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m28s
release / android (push) Successful in 5m44s
release / Build signed APK (releases and dev) (push) Successful in 5m58s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
The one-time listing took only albums whose tags already named their release group and artist, and left 340 off the list. The rest are now matched: the release their tags name, else the artist's release groups called what the album is called, else a MusicBrainz search by artist and title. A group found by name is taken only when its tracks agree with the album's (by disc and number, then title; length and title, or recording), so a partial copy still matches and a same-titled other album does not. library_album_matches records each album the listing decided, listed or unmatched with why, so an album taken off the list is never re-listed. The albums no match was found for are named on the Held tab. Album rows with no tracks are no longer counted. The length and title checks move from the importer to release, shared by both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
334 lines
16 KiB
SQL
334 lines
16 KiB
SQL
-- The wanted list (M502 #5520): release-type profiles, follows and wants.
|
|
|
|
-- name: ListReleaseTypeProfiles :many
|
|
SELECT p.*,
|
|
(SELECT COUNT(*) FROM follows f WHERE f.profile_id = p.id)::bigint AS follow_count
|
|
FROM release_type_profiles p
|
|
ORDER BY p.is_default DESC, p.name;
|
|
|
|
-- name: GetReleaseTypeProfile :one
|
|
SELECT * FROM release_type_profiles WHERE id = $1;
|
|
|
|
-- name: GetDefaultReleaseTypeProfile :one
|
|
SELECT * FROM release_type_profiles WHERE is_default;
|
|
|
|
-- name: CreateReleaseTypeProfile :one
|
|
INSERT INTO release_type_profiles (name, primary_types, secondary_types)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *;
|
|
|
|
-- name: UpdateReleaseTypeProfile :one
|
|
UPDATE release_type_profiles
|
|
SET name = $2,
|
|
primary_types = $3,
|
|
secondary_types = $4,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: ClearDefaultReleaseTypeProfile :exec
|
|
UPDATE release_type_profiles SET is_default = false, updated_at = now() WHERE is_default;
|
|
|
|
-- name: SetDefaultReleaseTypeProfile :execrows
|
|
UPDATE release_type_profiles SET is_default = true, updated_at = now() WHERE id = $1;
|
|
|
|
-- name: DeleteReleaseTypeProfile :execrows
|
|
-- The default stays; one in use is refused by the follows foreign key.
|
|
DELETE FROM release_type_profiles WHERE id = $1 AND NOT is_default;
|
|
|
|
-- name: UpsertFollow :one
|
|
-- Following an artist already followed changes its profile. want_existing
|
|
-- only ever turns on here; the worker turns it off once applied, and turning
|
|
-- it on again re-reads the baseline so the existing releases are judged.
|
|
INSERT INTO follows (artist_mbid, artist_name, profile_id, followed_by, want_existing)
|
|
VALUES (sqlc.arg(artist_mbid), sqlc.arg(artist_name), sqlc.arg(profile_id),
|
|
sqlc.narg(followed_by), sqlc.arg(want_existing))
|
|
ON CONFLICT (artist_mbid) DO UPDATE
|
|
SET artist_name = EXCLUDED.artist_name,
|
|
profile_id = EXCLUDED.profile_id,
|
|
want_existing = follows.want_existing OR EXCLUDED.want_existing,
|
|
baseline_at = CASE WHEN EXCLUDED.want_existing THEN NULL ELSE follows.baseline_at END,
|
|
updated_at = now()
|
|
RETURNING *;
|
|
|
|
-- name: GetFollow :one
|
|
SELECT f.*, p.name AS profile_name
|
|
FROM follows f
|
|
JOIN release_type_profiles p ON p.id = f.profile_id
|
|
WHERE f.artist_mbid = $1;
|
|
|
|
-- name: DeleteFollow :execrows
|
|
DELETE FROM follows WHERE artist_mbid = $1;
|
|
|
|
-- name: ListFollows :many
|
|
SELECT f.artist_mbid, f.artist_name, f.profile_id, f.followed_at, f.baseline_at, f.checked_at,
|
|
p.name AS profile_name,
|
|
(SELECT a.id FROM artists a WHERE a.mbid = f.artist_mbid) AS artist_id,
|
|
(SELECT COUNT(*) FROM wants w
|
|
WHERE w.artist_mbid = f.artist_mbid AND w.fulfilled_at IS NULL)::bigint AS open_wants
|
|
FROM follows f
|
|
JOIN release_type_profiles p ON p.id = f.profile_id
|
|
ORDER BY lower(f.artist_name), f.artist_mbid;
|
|
|
|
-- name: ListFollowsDue :many
|
|
-- Follows whose discography has not been applied since @before, never-read
|
|
-- first, so a new follow's baseline is not stuck behind daily re-checks.
|
|
SELECT f.*
|
|
FROM follows f
|
|
WHERE f.baseline_at IS NULL OR f.checked_at IS NULL OR f.checked_at < sqlc.arg(before)
|
|
ORDER BY f.baseline_at NULLS FIRST, f.checked_at NULLS FIRST, f.artist_mbid
|
|
LIMIT sqlc.arg(max_rows);
|
|
|
|
-- name: MarkFollowChecked :exec
|
|
UPDATE follows
|
|
SET checked_at = now(),
|
|
baseline_at = COALESCE(baseline_at, now()),
|
|
want_existing = false,
|
|
updated_at = now()
|
|
WHERE artist_mbid = $1;
|
|
|
|
-- name: ListFollowGroups :many
|
|
-- An artist's release groups from the catalogue, each with whether the
|
|
-- follow has judged it before.
|
|
SELECT g.mbid, g.title, g.primary_type, g.secondary_types, g.first_release_date,
|
|
EXISTS (SELECT 1 FROM follow_seen_groups s
|
|
WHERE s.artist_mbid = sqlc.arg(artist_mbid) AND s.release_group_mbid = g.mbid) AS seen
|
|
FROM mb_artist_release_groups ag
|
|
JOIN mb_release_groups g ON g.mbid = ag.release_group_mbid
|
|
WHERE ag.artist_mbid = sqlc.arg(artist_mbid)
|
|
ORDER BY g.first_release_date, g.title;
|
|
|
|
-- name: MarkGroupSeen :exec
|
|
INSERT INTO follow_seen_groups (artist_mbid, release_group_mbid)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
-- name: LibraryHoldsReleaseGroup :one
|
|
-- Whether the library already has the group: an album tagged with it or
|
|
-- with one of its releases, or failing that an album of the artist's with
|
|
-- the same title. Only albums with a track on disk count.
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM albums a
|
|
WHERE (a.release_group_mbid = sqlc.arg(group_mbid)::text
|
|
OR a.mbid = sqlc.arg(group_mbid)
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = sqlc.arg(group_mbid))
|
|
OR (lower(a.title) = lower(sqlc.arg(title)::text)
|
|
AND a.artist_id IN (SELECT ar.id FROM artists ar WHERE ar.mbid = sqlc.arg(artist_mbid)::text)))
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id AND t.missing_since IS NULL)
|
|
);
|
|
|
|
-- name: WantReleaseGroup :one
|
|
-- Wants a release group, or returns the want already recorded for it. A
|
|
-- held want is not reopened here: the library has it.
|
|
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_type,
|
|
first_release_date, reason, created_by)
|
|
VALUES (sqlc.arg(release_group_mbid), sqlc.arg(artist_mbid), sqlc.arg(artist_name), sqlc.arg(title),
|
|
sqlc.arg(primary_type), sqlc.arg(first_release_date), sqlc.arg(reason), sqlc.narg(created_by))
|
|
ON CONFLICT (release_group_mbid) DO UPDATE SET updated_at = wants.updated_at
|
|
RETURNING *;
|
|
|
|
-- name: GetWantByReleaseGroup :one
|
|
SELECT * FROM wants WHERE release_group_mbid = $1;
|
|
|
|
-- name: ListOpenWants :many
|
|
SELECT * FROM wants
|
|
WHERE fulfilled_at IS NULL
|
|
ORDER BY wanted_at, id
|
|
LIMIT $1;
|
|
|
|
-- name: WantHoldings :many
|
|
-- Each library album that may answer a want, with what it has on disk:
|
|
-- tracks present, whether one arrived since the want, and whether a track
|
|
-- missing before the want is still missing.
|
|
SELECT a.id AS album_id,
|
|
COUNT(*) FILTER (WHERE t.missing_since IS NULL)::int AS present,
|
|
COALESCE(bool_or(t.missing_since IS NULL AND t.added_at > sqlc.arg(since)::timestamptz), false)::bool AS arrived_since,
|
|
COALESCE(bool_or(t.missing_since IS NOT NULL AND t.missing_since < sqlc.arg(since)::timestamptz), false)::bool AS missing_before,
|
|
-- Missing tracks no track on disk stands in for, by position or
|
|
-- title. A missing row whose song is back at a new path (#5245) is
|
|
-- covered; a lost bonus track is not.
|
|
COUNT(*) FILTER (WHERE t.missing_since IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1 FROM tracks p
|
|
WHERE p.album_id = a.id AND p.missing_since IS NULL
|
|
AND ((p.track_number = t.track_number AND p.disc_number IS NOT DISTINCT FROM t.disc_number)
|
|
OR lower(p.title) = lower(t.title))))::int AS uncovered_missing
|
|
FROM albums a
|
|
JOIN tracks t ON t.album_id = a.id
|
|
WHERE a.release_group_mbid = sqlc.arg(group_mbid)::text
|
|
OR a.mbid = sqlc.arg(group_mbid)
|
|
OR a.id = sqlc.narg(album_id)
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = sqlc.arg(group_mbid))
|
|
GROUP BY a.id
|
|
ORDER BY present DESC, a.id;
|
|
|
|
-- name: FulfillWant :exec
|
|
UPDATE wants
|
|
SET fulfilled_at = now(),
|
|
album_id = sqlc.arg(album_id),
|
|
updated_at = now()
|
|
WHERE id = sqlc.arg(id) AND fulfilled_at IS NULL;
|
|
|
|
-- name: RepointWant :exec
|
|
-- A want recorded under a release id before #5241 is moved to its group.
|
|
UPDATE wants SET release_group_mbid = $2, updated_at = now() WHERE id = $1;
|
|
|
|
-- name: DeleteWant :execrows
|
|
DELETE FROM wants WHERE id = $1;
|
|
|
|
-- name: ListWants :many
|
|
-- The Wanted page: open wants (all but missing ones, or only missing ones),
|
|
-- or what was held most recently.
|
|
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,
|
|
-- 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,
|
|
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
|
|
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date,
|
|
u.username AS requested_by,
|
|
(SELECT ar.id FROM artists ar WHERE ar.mbid = w.artist_mbid) AS library_artist_id,
|
|
-- The search schedule's last look (#5521), and the download running.
|
|
w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error,
|
|
-- Its latest download, running or ended, and what the import made of
|
|
-- it (#5522).
|
|
gr.id AS grab_id, gr.name AS grab_name, gr.grabbed_at, gr.origin AS grab_origin,
|
|
gr.quality AS grab_quality, gr.ended_at AS grab_ended_at, gr.end_reason AS grab_end_reason,
|
|
gr.progress AS grab_progress, gr.import_state AS grab_import_state,
|
|
gr.import_reason AS grab_import_reason, gr.end_detail AS grab_end_detail, gr.imported_at AS grab_imported_at,
|
|
gr.imported_files AS grab_imported_files
|
|
FROM wants w
|
|
LEFT JOIN users u ON u.id = w.created_by
|
|
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
|
|
LEFT JOIN grabs gr ON gr.id = (SELECT x.id FROM grabs x WHERE x.want_id = w.id
|
|
ORDER BY x.ended_at IS NULL DESC, x.grabbed_at DESC LIMIT 1)
|
|
WHERE CASE sqlc.arg(state)::text
|
|
WHEN 'held' THEN w.fulfilled_at IS NOT NULL
|
|
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
|
|
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
|
|
END
|
|
ORDER BY COALESCE(w.fulfilled_at, w.wanted_at) DESC, w.id
|
|
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
|
|
|
|
-- 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,
|
|
(SELECT COUNT(*) FROM follows)::bigint AS following,
|
|
-- The Wanted view's other tabs (#5522): downloads running, those
|
|
-- waiting on a person, and releases set aside.
|
|
(SELECT COUNT(*) FROM grabs WHERE ended_at IS NULL)::bigint AS activity,
|
|
(SELECT COUNT(*) FROM grabs
|
|
WHERE ended_at IS NULL AND import_state IN ('review', 'failed'))::bigint AS attention,
|
|
(SELECT COUNT(*) FROM release_blocks b
|
|
WHERE b.expires_at IS NULL OR b.expires_at > now())::bigint AS blocked
|
|
FROM wants;
|
|
|
|
-- The one-time listing of the library (#5567). ---------------------------
|
|
|
|
-- name: GetLibraryListing :one
|
|
SELECT library_listing_started_at, library_listed_at, library_unlisted
|
|
FROM acquisition_settings WHERE id = true;
|
|
|
|
-- name: StartLibraryListing :one
|
|
-- Marks the listing begun, once; its time is the cutoff for which albums
|
|
-- count as the library's.
|
|
UPDATE acquisition_settings
|
|
SET library_listing_started_at = COALESCE(library_listing_started_at, now())
|
|
WHERE id = true
|
|
RETURNING library_listing_started_at;
|
|
|
|
-- name: ListLibraryAlbums :execrows
|
|
-- Puts library albums on the list as held: each release group once, by the
|
|
-- album holding the most tracks. Only albums the library had at the cutoff,
|
|
-- and with album_id given only that album. A group already on the list is
|
|
-- left as it is. An album with tracks already missing is held from just
|
|
-- before the first loss, so re-acquisition still fetches it back.
|
|
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, reason, album_id, wanted_at, fulfilled_at)
|
|
SELECT DISTINCT ON (a.release_group_mbid)
|
|
a.release_group_mbid, ar.mbid, ar.name, a.title, 'library', a.id, now(),
|
|
COALESCE((SELECT MIN(t.missing_since) FROM tracks t WHERE t.album_id = a.id) - interval '1 second', now())
|
|
FROM albums a
|
|
JOIN artists ar ON ar.id = a.artist_id
|
|
WHERE a.release_group_mbid IS NOT NULL AND a.release_group_mbid <> ''
|
|
AND ar.mbid IS NOT NULL AND ar.mbid <> ''
|
|
AND a.created_at <= sqlc.arg(cutoff)::timestamptz
|
|
AND (sqlc.narg(album_id)::uuid IS NULL OR a.id = sqlc.narg(album_id)::uuid)
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
|
ORDER BY a.release_group_mbid,
|
|
(SELECT COUNT(*) FROM tracks t WHERE t.album_id = a.id AND t.missing_since IS NULL) DESC, a.id
|
|
ON CONFLICT (release_group_mbid) DO NOTHING;
|
|
|
|
-- name: RecordListedAlbums :exec
|
|
-- The albums ListLibraryAlbums takes are decided as listed by their tags,
|
|
-- so the matching that follows leaves them alone (#5569).
|
|
INSERT INTO library_album_matches (album_id, outcome, method, release_group_mbid)
|
|
SELECT a.id, 'listed', 'tags', a.release_group_mbid
|
|
FROM albums a
|
|
JOIN artists ar ON ar.id = a.artist_id
|
|
WHERE a.release_group_mbid IS NOT NULL AND a.release_group_mbid <> ''
|
|
AND ar.mbid IS NOT NULL AND ar.mbid <> ''
|
|
AND a.created_at <= sqlc.arg(cutoff)::timestamptz
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
|
ON CONFLICT (album_id) DO NOTHING;
|
|
|
|
-- name: ListAlbumsToMatch :many
|
|
-- Library albums from before the cutoff that the listing has not decided:
|
|
-- each is matched to MusicBrainz (#5569). An album row with no tracks is
|
|
-- not an album in the library.
|
|
SELECT a.id, a.title, a.mbid, a.release_group_mbid, ar.name AS artist_name, ar.mbid AS artist_mbid
|
|
FROM albums a
|
|
JOIN artists ar ON ar.id = a.artist_id
|
|
WHERE a.created_at <= sqlc.arg(cutoff)::timestamptz
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
|
AND NOT EXISTS (SELECT 1 FROM library_album_matches m WHERE m.album_id = a.id)
|
|
ORDER BY a.created_at, a.id
|
|
LIMIT sqlc.arg(max_rows);
|
|
|
|
-- name: ListAlbumTracksToMatch :many
|
|
-- What an album's tracks say about themselves, to check a match against.
|
|
SELECT title, disc_number, track_number, duration_ms, mbid
|
|
FROM tracks
|
|
WHERE album_id = $1
|
|
ORDER BY disc_number NULLS FIRST, track_number NULLS LAST, title;
|
|
|
|
-- name: SetAlbumReleaseGroup :exec
|
|
-- Names a matched album's release group, filling only what nothing named.
|
|
UPDATE albums SET release_group_mbid = sqlc.arg(release_group_mbid), updated_at = now()
|
|
WHERE id = sqlc.arg(id) AND (release_group_mbid IS NULL OR release_group_mbid = '');
|
|
|
|
-- name: InsertLibraryWant :execrows
|
|
-- Puts one matched album on the list as held, from just before its first
|
|
-- lost track if it has one. A group already on the list is left as it is.
|
|
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date,
|
|
reason, album_id, wanted_at, fulfilled_at)
|
|
VALUES (sqlc.arg(release_group_mbid), sqlc.arg(artist_mbid), sqlc.arg(artist_name), sqlc.arg(title),
|
|
sqlc.arg(primary_type), sqlc.arg(first_release_date), 'library', sqlc.arg(album_id)::uuid, now(),
|
|
COALESCE((SELECT MIN(t.missing_since) FROM tracks t WHERE t.album_id = sqlc.arg(album_id)::uuid)
|
|
- interval '1 second', now()))
|
|
ON CONFLICT (release_group_mbid) DO NOTHING;
|
|
|
|
-- name: RecordAlbumMatch :exec
|
|
INSERT INTO library_album_matches (album_id, outcome, method, release_group_mbid, detail)
|
|
VALUES (sqlc.arg(album_id), sqlc.arg(outcome), sqlc.arg(method), sqlc.arg(release_group_mbid), sqlc.arg(detail))
|
|
ON CONFLICT (album_id) DO UPDATE
|
|
SET outcome = EXCLUDED.outcome, method = EXCLUDED.method,
|
|
release_group_mbid = EXCLUDED.release_group_mbid, detail = EXCLUDED.detail, decided_at = now();
|
|
|
|
-- name: FinishLibraryListing :one
|
|
-- The listing is done: count the albums no match was found for.
|
|
UPDATE acquisition_settings
|
|
SET library_listed_at = now(),
|
|
library_unlisted = (SELECT COUNT(*) FROM library_album_matches WHERE outcome = 'unmatched')
|
|
WHERE id = true AND library_listed_at IS NULL
|
|
RETURNING library_listed_at, library_unlisted;
|
|
|
|
-- name: ListUnmatchedLibraryAlbums :many
|
|
-- The albums the listing could not match, for the Held tab.
|
|
SELECT m.album_id, a.title, ar.name AS artist_name, m.detail
|
|
FROM library_album_matches m
|
|
JOIN albums a ON a.id = m.album_id
|
|
JOIN artists ar ON ar.id = a.artist_id
|
|
WHERE m.outcome = 'unmatched'
|
|
ORDER BY ar.name, a.title
|
|
LIMIT sqlc.arg(max_rows);
|