release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m24s
release / go (push) Successful in 1m44s
release / integration (push) Successful in 5m19s
release / android (push) Successful in 5m31s
release / Build signed APK (releases and dev) (push) Successful in 5m35s
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
A one-time job in the wanted worker lists every album in the library as a held want, one per release group, with a record that it happened (acquisition_settings.library_listed_at). Albums whose tags name only a release are named on MusicBrainz first, 120 a pass; what nothing names is counted and shown on the Held tab. Re-acquisition now wants back only albums on the list. Taking a held album off the list leaves its files, and it is never fetched back; albums put in the library folder by hand afterwards are not added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1075 lines
32 KiB
Go
1075 lines
32 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: wanted.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const clearDefaultReleaseTypeProfile = `-- name: ClearDefaultReleaseTypeProfile :exec
|
|
UPDATE release_type_profiles SET is_default = false, updated_at = now() WHERE is_default
|
|
`
|
|
|
|
func (q *Queries) ClearDefaultReleaseTypeProfile(ctx context.Context) error {
|
|
_, err := q.db.Exec(ctx, clearDefaultReleaseTypeProfile)
|
|
return err
|
|
}
|
|
|
|
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,
|
|
(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
|
|
`
|
|
|
|
type CountWantsRow struct {
|
|
Wanted int64
|
|
Missing int64
|
|
Held int64
|
|
Following int64
|
|
Activity int64
|
|
Attention int64
|
|
Blocked int64
|
|
}
|
|
|
|
func (q *Queries) CountWants(ctx context.Context) (CountWantsRow, error) {
|
|
row := q.db.QueryRow(ctx, countWants)
|
|
var i CountWantsRow
|
|
err := row.Scan(
|
|
&i.Wanted,
|
|
&i.Missing,
|
|
&i.Held,
|
|
&i.Following,
|
|
&i.Activity,
|
|
&i.Attention,
|
|
&i.Blocked,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createReleaseTypeProfile = `-- name: CreateReleaseTypeProfile :one
|
|
INSERT INTO release_type_profiles (name, primary_types, secondary_types)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, name, primary_types, secondary_types, is_default, created_at, updated_at
|
|
`
|
|
|
|
type CreateReleaseTypeProfileParams struct {
|
|
Name string
|
|
PrimaryTypes []string
|
|
SecondaryTypes []string
|
|
}
|
|
|
|
func (q *Queries) CreateReleaseTypeProfile(ctx context.Context, arg CreateReleaseTypeProfileParams) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, createReleaseTypeProfile, arg.Name, arg.PrimaryTypes, arg.SecondaryTypes)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteFollow = `-- name: DeleteFollow :execrows
|
|
DELETE FROM follows WHERE artist_mbid = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteFollow(ctx context.Context, artistMbid string) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteFollow, artistMbid)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const deleteReleaseTypeProfile = `-- name: DeleteReleaseTypeProfile :execrows
|
|
DELETE FROM release_type_profiles WHERE id = $1 AND NOT is_default
|
|
`
|
|
|
|
// The default stays; one in use is refused by the follows foreign key.
|
|
func (q *Queries) DeleteReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteReleaseTypeProfile, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const deleteWant = `-- name: DeleteWant :execrows
|
|
DELETE FROM wants WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWant(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteWant, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const finishLibraryListing = `-- name: FinishLibraryListing :one
|
|
UPDATE acquisition_settings s
|
|
SET library_listed_at = now(),
|
|
library_unlisted = (
|
|
SELECT COUNT(*) FROM albums a
|
|
WHERE a.created_at <= s.library_listing_started_at
|
|
AND NOT EXISTS (SELECT 1 FROM wants w
|
|
WHERE w.album_id = a.id
|
|
OR (a.release_group_mbid <> '' AND w.release_group_mbid = a.release_group_mbid)))
|
|
WHERE s.id = true AND s.library_listed_at IS NULL
|
|
RETURNING library_listed_at, library_unlisted
|
|
`
|
|
|
|
type FinishLibraryListingRow struct {
|
|
LibraryListedAt pgtype.Timestamptz
|
|
LibraryUnlisted int32
|
|
}
|
|
|
|
// The listing is done: count the albums from before the cutoff it could not
|
|
// list, because nothing names them to MusicBrainz.
|
|
func (q *Queries) FinishLibraryListing(ctx context.Context) (FinishLibraryListingRow, error) {
|
|
row := q.db.QueryRow(ctx, finishLibraryListing)
|
|
var i FinishLibraryListingRow
|
|
err := row.Scan(&i.LibraryListedAt, &i.LibraryUnlisted)
|
|
return i, err
|
|
}
|
|
|
|
const fulfillWant = `-- name: FulfillWant :exec
|
|
UPDATE wants
|
|
SET fulfilled_at = now(),
|
|
album_id = $1,
|
|
updated_at = now()
|
|
WHERE id = $2 AND fulfilled_at IS NULL
|
|
`
|
|
|
|
type FulfillWantParams struct {
|
|
AlbumID pgtype.UUID
|
|
ID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) FulfillWant(ctx context.Context, arg FulfillWantParams) error {
|
|
_, err := q.db.Exec(ctx, fulfillWant, arg.AlbumID, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const getDefaultReleaseTypeProfile = `-- name: GetDefaultReleaseTypeProfile :one
|
|
SELECT id, name, primary_types, secondary_types, is_default, created_at, updated_at FROM release_type_profiles WHERE is_default
|
|
`
|
|
|
|
func (q *Queries) GetDefaultReleaseTypeProfile(ctx context.Context) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, getDefaultReleaseTypeProfile)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getFollow = `-- name: GetFollow :one
|
|
SELECT f.artist_mbid, f.artist_name, f.profile_id, f.followed_by, f.followed_at, f.baseline_at, f.want_existing, f.checked_at, f.updated_at, p.name AS profile_name
|
|
FROM follows f
|
|
JOIN release_type_profiles p ON p.id = f.profile_id
|
|
WHERE f.artist_mbid = $1
|
|
`
|
|
|
|
type GetFollowRow 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
|
|
ProfileName string
|
|
}
|
|
|
|
func (q *Queries) GetFollow(ctx context.Context, artistMbid string) (GetFollowRow, error) {
|
|
row := q.db.QueryRow(ctx, getFollow, artistMbid)
|
|
var i GetFollowRow
|
|
err := row.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedBy,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.WantExisting,
|
|
&i.CheckedAt,
|
|
&i.UpdatedAt,
|
|
&i.ProfileName,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getLibraryListing = `-- name: GetLibraryListing :one
|
|
|
|
SELECT library_listing_started_at, library_listed_at, library_unlisted
|
|
FROM acquisition_settings WHERE id = true
|
|
`
|
|
|
|
type GetLibraryListingRow struct {
|
|
LibraryListingStartedAt pgtype.Timestamptz
|
|
LibraryListedAt pgtype.Timestamptz
|
|
LibraryUnlisted int32
|
|
}
|
|
|
|
// The one-time listing of the library (#5567). ---------------------------
|
|
func (q *Queries) GetLibraryListing(ctx context.Context) (GetLibraryListingRow, error) {
|
|
row := q.db.QueryRow(ctx, getLibraryListing)
|
|
var i GetLibraryListingRow
|
|
err := row.Scan(&i.LibraryListingStartedAt, &i.LibraryListedAt, &i.LibraryUnlisted)
|
|
return i, err
|
|
}
|
|
|
|
const getReleaseTypeProfile = `-- name: GetReleaseTypeProfile :one
|
|
SELECT id, name, primary_types, secondary_types, is_default, created_at, updated_at FROM release_type_profiles WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, getReleaseTypeProfile, id)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
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
|
|
`
|
|
|
|
func (q *Queries) GetWantByReleaseGroup(ctx context.Context, releaseGroupMbid string) (Want, error) {
|
|
row := q.db.QueryRow(ctx, getWantByReleaseGroup, releaseGroupMbid)
|
|
var i Want
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.FirstReleaseDate,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.CreatedBy,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const libraryHoldsReleaseGroup = `-- name: LibraryHoldsReleaseGroup :one
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM albums a
|
|
WHERE (a.release_group_mbid = $1::text
|
|
OR a.mbid = $1
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = $1)
|
|
OR (lower(a.title) = lower($2::text)
|
|
AND a.artist_id IN (SELECT ar.id FROM artists ar WHERE ar.mbid = $3::text)))
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id AND t.missing_since IS NULL)
|
|
)
|
|
`
|
|
|
|
type LibraryHoldsReleaseGroupParams struct {
|
|
GroupMbid string
|
|
Title string
|
|
ArtistMbid string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) LibraryHoldsReleaseGroup(ctx context.Context, arg LibraryHoldsReleaseGroupParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, libraryHoldsReleaseGroup, arg.GroupMbid, arg.Title, arg.ArtistMbid)
|
|
var exists bool
|
|
err := row.Scan(&exists)
|
|
return exists, err
|
|
}
|
|
|
|
const listFollowGroups = `-- name: ListFollowGroups :many
|
|
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 = $1 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 = $1
|
|
ORDER BY g.first_release_date, g.title
|
|
`
|
|
|
|
type ListFollowGroupsRow struct {
|
|
Mbid string
|
|
Title string
|
|
PrimaryType string
|
|
SecondaryTypes []string
|
|
FirstReleaseDate string
|
|
Seen bool
|
|
}
|
|
|
|
// An artist's release groups from the catalogue, each with whether the
|
|
// follow has judged it before.
|
|
func (q *Queries) ListFollowGroups(ctx context.Context, artistMbid string) ([]ListFollowGroupsRow, error) {
|
|
rows, err := q.db.Query(ctx, listFollowGroups, artistMbid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListFollowGroupsRow
|
|
for rows.Next() {
|
|
var i ListFollowGroupsRow
|
|
if err := rows.Scan(
|
|
&i.Mbid,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.SecondaryTypes,
|
|
&i.FirstReleaseDate,
|
|
&i.Seen,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listFollows = `-- 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
|
|
`
|
|
|
|
type ListFollowsRow struct {
|
|
ArtistMbid string
|
|
ArtistName string
|
|
ProfileID pgtype.UUID
|
|
FollowedAt pgtype.Timestamptz
|
|
BaselineAt pgtype.Timestamptz
|
|
CheckedAt pgtype.Timestamptz
|
|
ProfileName string
|
|
ArtistID pgtype.UUID
|
|
OpenWants int64
|
|
}
|
|
|
|
func (q *Queries) ListFollows(ctx context.Context) ([]ListFollowsRow, error) {
|
|
rows, err := q.db.Query(ctx, listFollows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListFollowsRow
|
|
for rows.Next() {
|
|
var i ListFollowsRow
|
|
if err := rows.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.CheckedAt,
|
|
&i.ProfileName,
|
|
&i.ArtistID,
|
|
&i.OpenWants,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listFollowsDue = `-- name: ListFollowsDue :many
|
|
SELECT f.artist_mbid, f.artist_name, f.profile_id, f.followed_by, f.followed_at, f.baseline_at, f.want_existing, f.checked_at, f.updated_at
|
|
FROM follows f
|
|
WHERE f.baseline_at IS NULL OR f.checked_at IS NULL OR f.checked_at < $1
|
|
ORDER BY f.baseline_at NULLS FIRST, f.checked_at NULLS FIRST, f.artist_mbid
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListFollowsDueParams struct {
|
|
Before pgtype.Timestamptz
|
|
MaxRows int32
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListFollowsDue(ctx context.Context, arg ListFollowsDueParams) ([]Follow, error) {
|
|
rows, err := q.db.Query(ctx, listFollowsDue, arg.Before, arg.MaxRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Follow
|
|
for rows.Next() {
|
|
var i Follow
|
|
if err := rows.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedBy,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.WantExisting,
|
|
&i.CheckedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listLibraryAlbums = `-- name: ListLibraryAlbums :execrows
|
|
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 <= $1::timestamptz
|
|
AND ($2::uuid IS NULL OR a.id = $2::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
|
|
`
|
|
|
|
type ListLibraryAlbumsParams struct {
|
|
Cutoff pgtype.Timestamptz
|
|
AlbumID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListLibraryAlbums(ctx context.Context, arg ListLibraryAlbumsParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, listLibraryAlbums, arg.Cutoff, arg.AlbumID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const listLibraryAlbumsToName = `-- name: ListLibraryAlbumsToName :many
|
|
SELECT a.id, a.title, a.mbid FROM albums a
|
|
WHERE a.release_group_mbid IS NULL AND a.mbid IS NOT NULL AND a.mbid <> ''
|
|
AND a.created_at <= $1::timestamptz
|
|
ORDER BY a.created_at, a.id
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListLibraryAlbumsToNameParams struct {
|
|
Cutoff pgtype.Timestamptz
|
|
MaxRows int32
|
|
}
|
|
|
|
type ListLibraryAlbumsToNameRow struct {
|
|
ID pgtype.UUID
|
|
Title string
|
|
Mbid *string
|
|
}
|
|
|
|
// Library albums from before the cutoff that carry a release id but no
|
|
// release group yet: the listing asks MusicBrainz for each. ” marks one
|
|
// MusicBrainz could not name, so it is asked once.
|
|
func (q *Queries) ListLibraryAlbumsToName(ctx context.Context, arg ListLibraryAlbumsToNameParams) ([]ListLibraryAlbumsToNameRow, error) {
|
|
rows, err := q.db.Query(ctx, listLibraryAlbumsToName, arg.Cutoff, arg.MaxRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListLibraryAlbumsToNameRow
|
|
for rows.Next() {
|
|
var i ListLibraryAlbumsToNameRow
|
|
if err := rows.Scan(&i.ID, &i.Title, &i.Mbid); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
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
|
|
WHERE fulfilled_at IS NULL
|
|
ORDER BY wanted_at, id
|
|
LIMIT $1
|
|
`
|
|
|
|
func (q *Queries) ListOpenWants(ctx context.Context, limit int32) ([]Want, error) {
|
|
rows, err := q.db.Query(ctx, listOpenWants, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Want
|
|
for rows.Next() {
|
|
var i Want
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.FirstReleaseDate,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.CreatedBy,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listReleaseTypeProfiles = `-- name: ListReleaseTypeProfiles :many
|
|
|
|
SELECT p.id, p.name, p.primary_types, p.secondary_types, p.is_default, p.created_at, p.updated_at,
|
|
(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
|
|
`
|
|
|
|
type ListReleaseTypeProfilesRow struct {
|
|
ID pgtype.UUID
|
|
Name string
|
|
PrimaryTypes []string
|
|
SecondaryTypes []string
|
|
IsDefault bool
|
|
CreatedAt pgtype.Timestamptz
|
|
UpdatedAt pgtype.Timestamptz
|
|
FollowCount int64
|
|
}
|
|
|
|
// The wanted list (M502 #5520): release-type profiles, follows and wants.
|
|
func (q *Queries) ListReleaseTypeProfiles(ctx context.Context) ([]ListReleaseTypeProfilesRow, error) {
|
|
rows, err := q.db.Query(ctx, listReleaseTypeProfiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListReleaseTypeProfilesRow
|
|
for rows.Next() {
|
|
var i ListReleaseTypeProfilesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.FollowCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
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,
|
|
-- 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 $1::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 $3 OFFSET $2
|
|
`
|
|
|
|
type ListWantsParams struct {
|
|
State string
|
|
Skip int32
|
|
MaxRows int32
|
|
}
|
|
|
|
type ListWantsRow struct {
|
|
ID pgtype.UUID
|
|
ReleaseGroupMbid string
|
|
ArtistMbid string
|
|
ArtistName string
|
|
Title string
|
|
Reason string
|
|
AlbumID pgtype.UUID
|
|
WantedAt pgtype.Timestamptz
|
|
FulfilledAt pgtype.Timestamptz
|
|
PrimaryType string
|
|
SecondaryTypes []string
|
|
FirstReleaseDate string
|
|
RequestedBy *string
|
|
LibraryArtistID pgtype.UUID
|
|
SearchedAt pgtype.Timestamptz
|
|
SearchOutcome string
|
|
SearchConsidered int32
|
|
BestRelease string
|
|
BestVerdict string
|
|
SearchError string
|
|
GrabID pgtype.UUID
|
|
GrabName *string
|
|
GrabbedAt pgtype.Timestamptz
|
|
GrabOrigin *string
|
|
GrabQuality *string
|
|
GrabEndedAt pgtype.Timestamptz
|
|
GrabEndReason *string
|
|
GrabProgress *float32
|
|
GrabImportState *string
|
|
GrabImportReason *string
|
|
GrabEndDetail *string
|
|
GrabImportedAt pgtype.Timestamptz
|
|
GrabImportedFiles *int32
|
|
}
|
|
|
|
// The Wanted page: open wants (all but missing ones, or only missing ones),
|
|
// or what was held most recently.
|
|
func (q *Queries) ListWants(ctx context.Context, arg ListWantsParams) ([]ListWantsRow, error) {
|
|
rows, err := q.db.Query(ctx, listWants, arg.State, arg.Skip, arg.MaxRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListWantsRow
|
|
for rows.Next() {
|
|
var i ListWantsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.PrimaryType,
|
|
&i.SecondaryTypes,
|
|
&i.FirstReleaseDate,
|
|
&i.RequestedBy,
|
|
&i.LibraryArtistID,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
&i.GrabID,
|
|
&i.GrabName,
|
|
&i.GrabbedAt,
|
|
&i.GrabOrigin,
|
|
&i.GrabQuality,
|
|
&i.GrabEndedAt,
|
|
&i.GrabEndReason,
|
|
&i.GrabProgress,
|
|
&i.GrabImportState,
|
|
&i.GrabImportReason,
|
|
&i.GrabEndDetail,
|
|
&i.GrabImportedAt,
|
|
&i.GrabImportedFiles,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const markFollowChecked = `-- 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
|
|
`
|
|
|
|
func (q *Queries) MarkFollowChecked(ctx context.Context, artistMbid string) error {
|
|
_, err := q.db.Exec(ctx, markFollowChecked, artistMbid)
|
|
return err
|
|
}
|
|
|
|
const markGroupSeen = `-- name: MarkGroupSeen :exec
|
|
INSERT INTO follow_seen_groups (artist_mbid, release_group_mbid)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type MarkGroupSeenParams struct {
|
|
ArtistMbid string
|
|
ReleaseGroupMbid string
|
|
}
|
|
|
|
func (q *Queries) MarkGroupSeen(ctx context.Context, arg MarkGroupSeenParams) error {
|
|
_, err := q.db.Exec(ctx, markGroupSeen, arg.ArtistMbid, arg.ReleaseGroupMbid)
|
|
return err
|
|
}
|
|
|
|
const repointWant = `-- name: RepointWant :exec
|
|
UPDATE wants SET release_group_mbid = $2, updated_at = now() WHERE id = $1
|
|
`
|
|
|
|
type RepointWantParams struct {
|
|
ID pgtype.UUID
|
|
ReleaseGroupMbid string
|
|
}
|
|
|
|
// A want recorded under a release id before #5241 is moved to its group.
|
|
func (q *Queries) RepointWant(ctx context.Context, arg RepointWantParams) error {
|
|
_, err := q.db.Exec(ctx, repointWant, arg.ID, arg.ReleaseGroupMbid)
|
|
return err
|
|
}
|
|
|
|
const setAlbumReleaseGroup = `-- name: SetAlbumReleaseGroup :exec
|
|
UPDATE albums SET release_group_mbid = $1
|
|
WHERE id = $2 AND release_group_mbid IS NULL
|
|
`
|
|
|
|
type SetAlbumReleaseGroupParams struct {
|
|
ReleaseGroupMbid *string
|
|
ID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) SetAlbumReleaseGroup(ctx context.Context, arg SetAlbumReleaseGroupParams) error {
|
|
_, err := q.db.Exec(ctx, setAlbumReleaseGroup, arg.ReleaseGroupMbid, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const setDefaultReleaseTypeProfile = `-- name: SetDefaultReleaseTypeProfile :execrows
|
|
UPDATE release_type_profiles SET is_default = true, updated_at = now() WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) SetDefaultReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, setDefaultReleaseTypeProfile, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const startLibraryListing = `-- name: StartLibraryListing :one
|
|
UPDATE acquisition_settings
|
|
SET library_listing_started_at = COALESCE(library_listing_started_at, now())
|
|
WHERE id = true
|
|
RETURNING library_listing_started_at
|
|
`
|
|
|
|
// Marks the listing begun, once; its time is the cutoff for which albums
|
|
// count as the library's.
|
|
func (q *Queries) StartLibraryListing(ctx context.Context) (pgtype.Timestamptz, error) {
|
|
row := q.db.QueryRow(ctx, startLibraryListing)
|
|
var library_listing_started_at pgtype.Timestamptz
|
|
err := row.Scan(&library_listing_started_at)
|
|
return library_listing_started_at, err
|
|
}
|
|
|
|
const updateReleaseTypeProfile = `-- name: UpdateReleaseTypeProfile :one
|
|
UPDATE release_type_profiles
|
|
SET name = $2,
|
|
primary_types = $3,
|
|
secondary_types = $4,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, name, primary_types, secondary_types, is_default, created_at, updated_at
|
|
`
|
|
|
|
type UpdateReleaseTypeProfileParams struct {
|
|
ID pgtype.UUID
|
|
Name string
|
|
PrimaryTypes []string
|
|
SecondaryTypes []string
|
|
}
|
|
|
|
func (q *Queries) UpdateReleaseTypeProfile(ctx context.Context, arg UpdateReleaseTypeProfileParams) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, updateReleaseTypeProfile,
|
|
arg.ID,
|
|
arg.Name,
|
|
arg.PrimaryTypes,
|
|
arg.SecondaryTypes,
|
|
)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertFollow = `-- name: UpsertFollow :one
|
|
INSERT INTO follows (artist_mbid, artist_name, profile_id, followed_by, want_existing)
|
|
VALUES ($1, $2, $3,
|
|
$4, $5)
|
|
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 artist_mbid, artist_name, profile_id, followed_by, followed_at, baseline_at, want_existing, checked_at, updated_at
|
|
`
|
|
|
|
type UpsertFollowParams struct {
|
|
ArtistMbid string
|
|
ArtistName string
|
|
ProfileID pgtype.UUID
|
|
FollowedBy pgtype.UUID
|
|
WantExisting bool
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) UpsertFollow(ctx context.Context, arg UpsertFollowParams) (Follow, error) {
|
|
row := q.db.QueryRow(ctx, upsertFollow,
|
|
arg.ArtistMbid,
|
|
arg.ArtistName,
|
|
arg.ProfileID,
|
|
arg.FollowedBy,
|
|
arg.WantExisting,
|
|
)
|
|
var i Follow
|
|
err := row.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedBy,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.WantExisting,
|
|
&i.CheckedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const wantHoldings = `-- name: WantHoldings :many
|
|
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 > $1::timestamptz), false)::bool AS arrived_since,
|
|
COALESCE(bool_or(t.missing_since IS NOT NULL AND t.missing_since < $1::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 = $2::text
|
|
OR a.mbid = $2
|
|
OR a.id = $3
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = $2)
|
|
GROUP BY a.id
|
|
ORDER BY present DESC, a.id
|
|
`
|
|
|
|
type WantHoldingsParams struct {
|
|
Since pgtype.Timestamptz
|
|
GroupMbid string
|
|
AlbumID pgtype.UUID
|
|
}
|
|
|
|
type WantHoldingsRow struct {
|
|
AlbumID pgtype.UUID
|
|
Present int32
|
|
ArrivedSince bool
|
|
MissingBefore bool
|
|
UncoveredMissing int32
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) WantHoldings(ctx context.Context, arg WantHoldingsParams) ([]WantHoldingsRow, error) {
|
|
rows, err := q.db.Query(ctx, wantHoldings, arg.Since, arg.GroupMbid, arg.AlbumID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WantHoldingsRow
|
|
for rows.Next() {
|
|
var i WantHoldingsRow
|
|
if err := rows.Scan(
|
|
&i.AlbumID,
|
|
&i.Present,
|
|
&i.ArrivedSince,
|
|
&i.MissingBefore,
|
|
&i.UncoveredMissing,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const wantReleaseGroup = `-- name: WantReleaseGroup :one
|
|
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_type,
|
|
first_release_date, reason, created_by)
|
|
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
|
|
`
|
|
|
|
type WantReleaseGroupParams struct {
|
|
ReleaseGroupMbid string
|
|
ArtistMbid string
|
|
ArtistName string
|
|
Title string
|
|
PrimaryType string
|
|
FirstReleaseDate string
|
|
Reason string
|
|
CreatedBy pgtype.UUID
|
|
}
|
|
|
|
// Wants a release group, or returns the want already recorded for it. A
|
|
// held want is not reopened here: the library has it.
|
|
func (q *Queries) WantReleaseGroup(ctx context.Context, arg WantReleaseGroupParams) (Want, error) {
|
|
row := q.db.QueryRow(ctx, wantReleaseGroup,
|
|
arg.ReleaseGroupMbid,
|
|
arg.ArtistMbid,
|
|
arg.ArtistName,
|
|
arg.Title,
|
|
arg.PrimaryType,
|
|
arg.FirstReleaseDate,
|
|
arg.Reason,
|
|
arg.CreatedBy,
|
|
)
|
|
var i Want
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.FirstReleaseDate,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.CreatedBy,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
)
|
|
return i, err
|
|
}
|