release / govulncheck (push) Successful in 15s
release / web (push) Failing after 1m21s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 4m57s
release / android (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m28s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
Minstrel keeps its own list of what it is to get, keyed by MusicBrainz release group. A want comes from an approved request, a new release by a followed artist, or an album whose files went missing, and is held once the library has a full edition of it. - Migration 0081: release_type_profiles (Standard seeded: Album, EP, Single, studio only), follows, follow_seen_groups and wants. lidarr_requests becomes requests with plain *_mbid columns; approved album and track requests move onto the wanted list, approved artist requests become follows, and the library's album artists start out followed. missing_reacquisitions and the retry settings go. - internal/wanted: follows are checked against MusicBrainz discographies daily. The first check sets a baseline, and only releases after it are wanted unless the follow asked for what already exists. Profiles filter by primary and secondary type, so live recordings stay out unless a profile ticks Live. - internal/requests (was lidarrrequests): approving puts the request on the wanted list, and an artist request can be approved on a chosen profile. A request completes when its want is held. - The re-acquisition sweeper wants a missing album back instead of asking Lidarr; searching for it is the search schedule's (#5521). - Web: an admin Wanted page (Wanted, Missing files, Held, Following), a follow control on artist pages, a release-type profiles card on Integrations, "Approve on a profile" on Requests, and want state on Missing files. The artist detail API returns the artist's MBID. - Android: the request body sends artist_mbid, album_mbid and track_mbid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
840 lines
23 KiB
Go
840 lines
23 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
|
|
FROM wants
|
|
`
|
|
|
|
type CountWantsRow struct {
|
|
Wanted int64
|
|
Missing int64
|
|
Held int64
|
|
Following 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,
|
|
)
|
|
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 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 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 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,
|
|
)
|
|
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 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 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,
|
|
); 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
|
|
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
|
|
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
|
|
}
|
|
|
|
// 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,
|
|
); 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 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 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
|
|
`
|
|
|
|
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,
|
|
)
|
|
return i, err
|
|
}
|