-- 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), -- the held ones, or all of them; artist_mbid narrows to one artist's page. Ordered like the library (#5572): artists A–Z by the -- library's sort name when it holds the artist, each artist's albums by -- release date, so the page groups them as they arrive. q narrows to an -- 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, -- 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, la.id AS library_artist_id, COUNT(*) OVER () AS total, -- 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 LATERAL (SELECT ar.id, ar.sort_name FROM artists ar WHERE ar.mbid = w.artist_mbid LIMIT 1) la ON true 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 'all' THEN true 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 AND (sqlc.arg(artist_mbid)::text = '' OR w.artist_mbid = sqlc.arg(artist_mbid)::text) AND (sqlc.arg(q)::text = '' OR strpos(lower(w.artist_name), lower(sqlc.arg(q)::text)) > 0 OR strpos(lower(w.title), lower(sqlc.arg(q)::text)) > 0) ORDER BY lower(COALESCE(NULLIF(la.sort_name, ''), w.artist_name)), w.artist_mbid, NULLIF(COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, ''), '') NULLS LAST, lower(w.title), w.id LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip); -- name: ListWantArtists :many -- The Wanted view's main page (#5572): every artist on the list, A–Z like -- the library, with how many of their albums are held of how many are on -- the list. q matches the artist's name or any of their albums' titles; -- artist_mbid narrows to one artist, for their page's header. WITH a AS ( SELECT w.artist_mbid, (array_agg(w.artist_name ORDER BY w.wanted_at DESC))[1] AS artist_name, COUNT(*) FILTER (WHERE w.fulfilled_at IS NOT NULL)::int AS held, COUNT(*) FILTER (WHERE w.fulfilled_at IS NULL AND w.reason <> 'missing')::int AS wanted, COUNT(*) FILTER (WHERE w.fulfilled_at IS NULL AND w.reason = 'missing')::int AS missing, COUNT(*)::int AS albums, bool_or(sqlc.arg(q)::text <> '' AND strpos(lower(w.title), lower(sqlc.arg(q)::text)) > 0) AS title_match FROM wants w WHERE sqlc.arg(artist_mbid)::text = '' OR w.artist_mbid = sqlc.arg(artist_mbid)::text GROUP BY w.artist_mbid ) SELECT a.artist_mbid, a.artist_name::text AS artist_name, a.held, a.wanted, a.missing, a.albums, COALESCE(NULLIF(la.sort_name, ''), a.artist_name)::text AS sort_name, la.id AS library_artist_id, (f.artist_mbid IS NOT NULL)::bool AS following, (SELECT COUNT(*) FROM grabs g JOIN wants x ON x.id = g.want_id WHERE x.artist_mbid = a.artist_mbid AND g.ended_at IS NULL)::int AS downloading, COUNT(*) OVER () AS total FROM a LEFT JOIN LATERAL (SELECT ar.id, ar.sort_name FROM artists ar WHERE ar.mbid = a.artist_mbid LIMIT 1) la ON true LEFT JOIN follows f ON f.artist_mbid = a.artist_mbid WHERE sqlc.arg(q)::text = '' OR strpos(lower(a.artist_name), lower(sqlc.arg(q)::text)) > 0 OR a.title_match ORDER BY lower(COALESCE(NULLIF(la.sort_name, ''), a.artist_name)), a.artist_mbid LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip); -- name: GetWantAlbum :one -- What the Wanted view's track list needs of a want (#5572). SELECT id, release_group_mbid, album_id FROM wants WHERE id = $1; -- name: ListLibraryAlbumTracks :many -- A held or missing album's own tracks, for the Wanted view's track list; -- missing says its file is gone. SELECT title, disc_number, track_number, duration_ms, (missing_since IS NOT NULL)::bool AS missing FROM tracks WHERE album_id = $1 ORDER BY disc_number NULLS FIRST, track_number NULLS LAST, title; -- 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, COUNT(DISTINCT artist_mbid)::bigint AS artists, (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, -- or that an older matcher refused: 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 AND (m.outcome = 'listed' OR m.matcher >= sqlc.arg(matcher)::int)) ORDER BY a.created_at, a.id LIMIT sqlc.arg(max_rows); -- name: CountAlbumsToRematch :one -- Refusals by an older matcher, which a finished listing goes back to. SELECT COUNT(*) FROM library_album_matches WHERE outcome = 'unmatched' AND matcher < sqlc.arg(matcher)::int; -- 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, matcher) VALUES (sqlc.arg(album_id), sqlc.arg(outcome), sqlc.arg(method), sqlc.arg(release_group_mbid), sqlc.arg(detail), sqlc.arg(matcher)) ON CONFLICT (album_id) DO UPDATE SET outcome = EXCLUDED.outcome, method = EXCLUDED.method, release_group_mbid = EXCLUDED.release_group_mbid, detail = EXCLUDED.detail, matcher = EXCLUDED.matcher, decided_at = now(); -- name: FinishLibraryListing :one -- The listing is done, or done again after a newer matcher's retries: -- count the albums no match was found for. The first finish is kept. UPDATE acquisition_settings SET library_listed_at = COALESCE(library_listed_at, now()), library_unlisted = (SELECT COUNT(*) FROM library_album_matches WHERE outcome = 'unmatched') WHERE id = true 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);