release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m18s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 5m33s
release / android (push) Successful in 5m47s
release / Build signed APK (releases and dev) (push) Successful in 6m4s
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
The second run's refusals were mostly albums holding a track twice (a two-track single with three files), which no edition can pair, and release names that differ only in "+" against "/". Tracks are now read once per disc, number and title, and "and", "&" and "+" count for nothing when titles are compared; an edition's own title also counts as the album's name. library_album_matches records which matcher version decided each album. Refusals by an older version are matched again, even after the listing is done, so an improvement reaches them without a migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1271 lines
37 KiB
Go
1271 lines
37 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 countAlbumsToRematch = `-- name: CountAlbumsToRematch :one
|
|
SELECT COUNT(*) FROM library_album_matches
|
|
WHERE outcome = 'unmatched' AND matcher < $1::int
|
|
`
|
|
|
|
// Refusals by an older matcher, which a finished listing goes back to.
|
|
func (q *Queries) CountAlbumsToRematch(ctx context.Context, matcher int32) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAlbumsToRematch, matcher)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, 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
|
|
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
|
|
`
|
|
|
|
type FinishLibraryListingRow struct {
|
|
LibraryListedAt pgtype.Timestamptz
|
|
LibraryUnlisted int32
|
|
}
|
|
|
|
// 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.
|
|
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 insertLibraryWant = `-- name: InsertLibraryWant :execrows
|
|
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date,
|
|
reason, album_id, wanted_at, fulfilled_at)
|
|
VALUES ($1, $2, $3, $4,
|
|
$5, $6, 'library', $7::uuid, now(),
|
|
COALESCE((SELECT MIN(t.missing_since) FROM tracks t WHERE t.album_id = $7::uuid)
|
|
- interval '1 second', now()))
|
|
ON CONFLICT (release_group_mbid) DO NOTHING
|
|
`
|
|
|
|
type InsertLibraryWantParams struct {
|
|
ReleaseGroupMbid string
|
|
ArtistMbid string
|
|
ArtistName string
|
|
Title string
|
|
PrimaryType string
|
|
FirstReleaseDate string
|
|
AlbumID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) InsertLibraryWant(ctx context.Context, arg InsertLibraryWantParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, insertLibraryWant,
|
|
arg.ReleaseGroupMbid,
|
|
arg.ArtistMbid,
|
|
arg.ArtistName,
|
|
arg.Title,
|
|
arg.PrimaryType,
|
|
arg.FirstReleaseDate,
|
|
arg.AlbumID,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
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 listAlbumTracksToMatch = `-- name: ListAlbumTracksToMatch :many
|
|
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
|
|
`
|
|
|
|
type ListAlbumTracksToMatchRow struct {
|
|
Title string
|
|
DiscNumber *int32
|
|
TrackNumber *int32
|
|
DurationMs int32
|
|
Mbid *string
|
|
}
|
|
|
|
// What an album's tracks say about themselves, to check a match against.
|
|
func (q *Queries) ListAlbumTracksToMatch(ctx context.Context, albumID pgtype.UUID) ([]ListAlbumTracksToMatchRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumTracksToMatch, albumID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumTracksToMatchRow
|
|
for rows.Next() {
|
|
var i ListAlbumTracksToMatchRow
|
|
if err := rows.Scan(
|
|
&i.Title,
|
|
&i.DiscNumber,
|
|
&i.TrackNumber,
|
|
&i.DurationMs,
|
|
&i.Mbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsToMatch = `-- name: ListAlbumsToMatch :many
|
|
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 <= $1::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 >= $2::int))
|
|
ORDER BY a.created_at, a.id
|
|
LIMIT $3
|
|
`
|
|
|
|
type ListAlbumsToMatchParams struct {
|
|
Cutoff pgtype.Timestamptz
|
|
Matcher int32
|
|
MaxRows int32
|
|
}
|
|
|
|
type ListAlbumsToMatchRow struct {
|
|
ID pgtype.UUID
|
|
Title string
|
|
Mbid *string
|
|
ReleaseGroupMbid *string
|
|
ArtistName string
|
|
ArtistMbid *string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListAlbumsToMatch(ctx context.Context, arg ListAlbumsToMatchParams) ([]ListAlbumsToMatchRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsToMatch, arg.Cutoff, arg.Matcher, arg.MaxRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumsToMatchRow
|
|
for rows.Next() {
|
|
var i ListAlbumsToMatchRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.Mbid,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistName,
|
|
&i.ArtistMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
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 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 listUnmatchedLibraryAlbums = `-- name: ListUnmatchedLibraryAlbums :many
|
|
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 $1
|
|
`
|
|
|
|
type ListUnmatchedLibraryAlbumsRow struct {
|
|
AlbumID pgtype.UUID
|
|
Title string
|
|
ArtistName string
|
|
Detail string
|
|
}
|
|
|
|
// The albums the listing could not match, for the Held tab.
|
|
func (q *Queries) ListUnmatchedLibraryAlbums(ctx context.Context, maxRows int32) ([]ListUnmatchedLibraryAlbumsRow, error) {
|
|
rows, err := q.db.Query(ctx, listUnmatchedLibraryAlbums, maxRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListUnmatchedLibraryAlbumsRow
|
|
for rows.Next() {
|
|
var i ListUnmatchedLibraryAlbumsRow
|
|
if err := rows.Scan(
|
|
&i.AlbumID,
|
|
&i.Title,
|
|
&i.ArtistName,
|
|
&i.Detail,
|
|
); 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 recordAlbumMatch = `-- name: RecordAlbumMatch :exec
|
|
INSERT INTO library_album_matches (album_id, outcome, method, release_group_mbid, detail, matcher)
|
|
VALUES ($1, $2, $3, $4, $5,
|
|
$6)
|
|
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()
|
|
`
|
|
|
|
type RecordAlbumMatchParams struct {
|
|
AlbumID pgtype.UUID
|
|
Outcome string
|
|
Method string
|
|
ReleaseGroupMbid string
|
|
Detail string
|
|
Matcher int32
|
|
}
|
|
|
|
func (q *Queries) RecordAlbumMatch(ctx context.Context, arg RecordAlbumMatchParams) error {
|
|
_, err := q.db.Exec(ctx, recordAlbumMatch,
|
|
arg.AlbumID,
|
|
arg.Outcome,
|
|
arg.Method,
|
|
arg.ReleaseGroupMbid,
|
|
arg.Detail,
|
|
arg.Matcher,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const recordListedAlbums = `-- name: RecordListedAlbums :exec
|
|
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 <= $1::timestamptz
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
|
ON CONFLICT (album_id) DO NOTHING
|
|
`
|
|
|
|
// The albums ListLibraryAlbums takes are decided as listed by their tags,
|
|
// so the matching that follows leaves them alone (#5569).
|
|
func (q *Queries) RecordListedAlbums(ctx context.Context, cutoff pgtype.Timestamptz) error {
|
|
_, err := q.db.Exec(ctx, recordListedAlbums, cutoff)
|
|
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, updated_at = now()
|
|
WHERE id = $2 AND (release_group_mbid IS NULL OR release_group_mbid = '')
|
|
`
|
|
|
|
type SetAlbumReleaseGroupParams struct {
|
|
ReleaseGroupMbid *string
|
|
ID pgtype.UUID
|
|
}
|
|
|
|
// Names a matched album's release group, filling only what nothing named.
|
|
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
|
|
}
|