feat: a wanted list — follow artists on release-type profiles; requests and re-acquisition become wants (M502 #5520)
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
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>
This commit is contained in:
@@ -1,582 +0,0 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: lidarr_requests.sql
|
||||
|
||||
package dbq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const approveLidarrRequest = `-- name: ApproveLidarrRequest :one
|
||||
UPDATE lidarr_requests
|
||||
SET status = 'approved',
|
||||
quality_profile_id = $2,
|
||||
root_folder_path = $3,
|
||||
decided_at = now(),
|
||||
decided_by = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status = 'pending'
|
||||
RETURNING id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at
|
||||
`
|
||||
|
||||
type ApproveLidarrRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
QualityProfileID *int32
|
||||
RootFolderPath *string
|
||||
DecidedBy pgtype.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) ApproveLidarrRequest(ctx context.Context, arg ApproveLidarrRequestParams) (LidarrRequest, error) {
|
||||
row := q.db.QueryRow(ctx, approveLidarrRequest,
|
||||
arg.ID,
|
||||
arg.QualityProfileID,
|
||||
arg.RootFolderPath,
|
||||
arg.DecidedBy,
|
||||
)
|
||||
var i LidarrRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const cancelLidarrRequest = `-- name: CancelLidarrRequest :one
|
||||
UPDATE lidarr_requests
|
||||
SET status = 'rejected',
|
||||
notes = 'cancelled by user',
|
||||
decided_at = now(),
|
||||
decided_by = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND user_id = $2 AND status = 'pending'
|
||||
RETURNING id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at
|
||||
`
|
||||
|
||||
type CancelLidarrRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
DecidedBy pgtype.UUID
|
||||
}
|
||||
|
||||
// $2 serves as both the ownership guard (user_id = $2) and the
|
||||
// decided_by audit field. The generated parameter struct names it
|
||||
// DecidedBy — callers must pass the cancelling user's UUID, and
|
||||
// a wrong UUID will silently no-op (zero rows updated) rather than
|
||||
// erroring at the SQL layer.
|
||||
func (q *Queries) CancelLidarrRequest(ctx context.Context, arg CancelLidarrRequestParams) (LidarrRequest, error) {
|
||||
row := q.db.QueryRow(ctx, cancelLidarrRequest, arg.ID, arg.DecidedBy)
|
||||
var i LidarrRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const completeLidarrRequest = `-- name: CompleteLidarrRequest :one
|
||||
UPDATE lidarr_requests
|
||||
SET status = 'completed',
|
||||
matched_track_id = $2,
|
||||
matched_album_id = $3,
|
||||
matched_artist_id = $4,
|
||||
completed_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status = 'approved'
|
||||
RETURNING id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at
|
||||
`
|
||||
|
||||
type CompleteLidarrRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
MatchedTrackID pgtype.UUID
|
||||
MatchedAlbumID pgtype.UUID
|
||||
MatchedArtistID pgtype.UUID
|
||||
}
|
||||
|
||||
// Reconciler transitions an approved request to completed when its
|
||||
// target track/album/artist has appeared in the library. Callers
|
||||
// should set ONLY the matched_*_id corresponding to the request's
|
||||
// kind and pass pgtype.UUID{} (Valid: false) for the others —
|
||||
// per-kind one-of-three is the contract, NULL for the others
|
||||
// preserves auditability if the matched row is later evicted.
|
||||
func (q *Queries) CompleteLidarrRequest(ctx context.Context, arg CompleteLidarrRequestParams) (LidarrRequest, error) {
|
||||
row := q.db.QueryRow(ctx, completeLidarrRequest,
|
||||
arg.ID,
|
||||
arg.MatchedTrackID,
|
||||
arg.MatchedAlbumID,
|
||||
arg.MatchedArtistID,
|
||||
)
|
||||
var i LidarrRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createLidarrRequest = `-- name: CreateLidarrRequest :one
|
||||
INSERT INTO lidarr_requests (
|
||||
user_id, kind,
|
||||
lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid,
|
||||
artist_name, album_title, track_title
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at
|
||||
`
|
||||
|
||||
type CreateLidarrRequestParams struct {
|
||||
UserID pgtype.UUID
|
||||
Kind LidarrRequestKind
|
||||
LidarrArtistMbid string
|
||||
LidarrAlbumMbid *string
|
||||
LidarrTrackMbid *string
|
||||
ArtistName string
|
||||
AlbumTitle *string
|
||||
TrackTitle *string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateLidarrRequest(ctx context.Context, arg CreateLidarrRequestParams) (LidarrRequest, error) {
|
||||
row := q.db.QueryRow(ctx, createLidarrRequest,
|
||||
arg.UserID,
|
||||
arg.Kind,
|
||||
arg.LidarrArtistMbid,
|
||||
arg.LidarrAlbumMbid,
|
||||
arg.LidarrTrackMbid,
|
||||
arg.ArtistName,
|
||||
arg.AlbumTitle,
|
||||
arg.TrackTitle,
|
||||
)
|
||||
var i LidarrRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getLidarrRequestByID = `-- name: GetLidarrRequestByID :one
|
||||
SELECT id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at FROM lidarr_requests WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLidarrRequestByID(ctx context.Context, id pgtype.UUID) (LidarrRequest, error) {
|
||||
row := q.db.QueryRow(ctx, getLidarrRequestByID, id)
|
||||
var i LidarrRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getNonTerminalRequestForMBID = `-- name: GetNonTerminalRequestForMBID :one
|
||||
SELECT id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at FROM lidarr_requests
|
||||
WHERE status IN ('pending', 'approved', 'completed')
|
||||
AND ((kind = 'artist' AND lidarr_artist_mbid = $1)
|
||||
OR (kind = 'album' AND lidarr_album_mbid = $1)
|
||||
OR (kind = 'track' AND lidarr_track_mbid = $1))
|
||||
ORDER BY requested_at ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
// Returns the oldest non-terminal (pending/approved/completed) request
|
||||
// whose own-kind MBID matches @mbid, or pgx.ErrNoRows when none. Create()
|
||||
// uses this to dedupe: a re-request for something already in flight
|
||||
// returns the existing row instead of inserting a duplicate. MBIDs are
|
||||
// globally unique per entity type, so the cross-column OR cannot
|
||||
// false-match (an artist MBID never collides with an album/track MBID).
|
||||
func (q *Queries) GetNonTerminalRequestForMBID(ctx context.Context, mbid string) (LidarrRequest, error) {
|
||||
row := q.db.QueryRow(ctx, getNonTerminalRequestForMBID, mbid)
|
||||
var i LidarrRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const hasNonTerminalRequestForMBID = `-- name: HasNonTerminalRequestForMBID :one
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM lidarr_requests
|
||||
WHERE status IN ('pending', 'approved', 'completed')
|
||||
AND ((kind = 'artist' AND lidarr_artist_mbid = $1)
|
||||
OR (kind = 'album' AND lidarr_album_mbid = $1)
|
||||
OR (kind = 'track' AND lidarr_track_mbid = $1))
|
||||
)
|
||||
`
|
||||
|
||||
// Returns true if any user has a pending/approved/completed request
|
||||
// whose MBID matches at the given level. Used to set the `requested`
|
||||
// flag on /api/lidarr/search responses. Terminal-status (rejected,
|
||||
// failed) rows do not count. The same MBID parameter is checked
|
||||
// against artist/album/track columns based on kind.
|
||||
func (q *Queries) HasNonTerminalRequestForMBID(ctx context.Context, mbid string) (bool, error) {
|
||||
row := q.db.QueryRow(ctx, hasNonTerminalRequestForMBID, mbid)
|
||||
var exists bool
|
||||
err := row.Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
const listApprovedLidarrRequestsForReconcile = `-- name: ListApprovedLidarrRequestsForReconcile :many
|
||||
SELECT id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at FROM lidarr_requests
|
||||
WHERE status = 'approved'
|
||||
ORDER BY decided_at ASC
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListApprovedLidarrRequestsForReconcile(ctx context.Context, limit int32) ([]LidarrRequest, error) {
|
||||
rows, err := q.db.Query(ctx, listApprovedLidarrRequestsForReconcile, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []LidarrRequest
|
||||
for rows.Next() {
|
||||
var i LidarrRequest
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLidarrRequestsByStatus = `-- name: ListLidarrRequestsByStatus :many
|
||||
SELECT id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at FROM lidarr_requests
|
||||
WHERE status = $1
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListLidarrRequestsByStatusParams struct {
|
||||
Status LidarrRequestStatus
|
||||
Limit int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListLidarrRequestsByStatus(ctx context.Context, arg ListLidarrRequestsByStatusParams) ([]LidarrRequest, error) {
|
||||
rows, err := q.db.Query(ctx, listLidarrRequestsByStatus, arg.Status, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []LidarrRequest
|
||||
for rows.Next() {
|
||||
var i LidarrRequest
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLidarrRequestsForUser = `-- name: ListLidarrRequestsForUser :many
|
||||
SELECT id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at FROM lidarr_requests
|
||||
WHERE user_id = $1
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListLidarrRequestsForUserParams struct {
|
||||
UserID pgtype.UUID
|
||||
Limit int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListLidarrRequestsForUser(ctx context.Context, arg ListLidarrRequestsForUserParams) ([]LidarrRequest, error) {
|
||||
rows, err := q.db.Query(ctx, listLidarrRequestsForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []LidarrRequest
|
||||
for rows.Next() {
|
||||
var i LidarrRequest
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const markLidarrRequestAddConfirmed = `-- name: MarkLidarrRequestAddConfirmed :exec
|
||||
UPDATE lidarr_requests
|
||||
SET lidarr_add_confirmed_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
AND status = 'approved'
|
||||
AND lidarr_add_confirmed_at IS NULL
|
||||
`
|
||||
|
||||
// Idempotently records that Lidarr accepted the add for an approved
|
||||
// request. The lidarr_add_confirmed_at IS NULL guard makes a redundant
|
||||
// reconciler retry a no-op rather than bumping updated_at every tick.
|
||||
func (q *Queries) MarkLidarrRequestAddConfirmed(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, markLidarrRequestAddConfirmed, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const rejectLidarrRequest = `-- name: RejectLidarrRequest :one
|
||||
UPDATE lidarr_requests
|
||||
SET status = 'rejected',
|
||||
notes = $2,
|
||||
decided_at = now(),
|
||||
decided_by = $3,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status = 'pending'
|
||||
RETURNING id, user_id, status, kind, lidarr_artist_mbid, lidarr_album_mbid, lidarr_track_mbid, artist_name, album_title, track_title, quality_profile_id, root_folder_path, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at, lidarr_add_confirmed_at
|
||||
`
|
||||
|
||||
type RejectLidarrRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
Notes *string
|
||||
DecidedBy pgtype.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) RejectLidarrRequest(ctx context.Context, arg RejectLidarrRequestParams) (LidarrRequest, error) {
|
||||
row := q.db.QueryRow(ctx, rejectLidarrRequest, arg.ID, arg.Notes, arg.DecidedBy)
|
||||
var i LidarrRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.LidarrArtistMbid,
|
||||
&i.LidarrAlbumMbid,
|
||||
&i.LidarrTrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.QualityProfileID,
|
||||
&i.RootFolderPath,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LidarrAddConfirmedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const setLidarrRequestAlbumMbid = `-- name: SetLidarrRequestAlbumMbid :exec
|
||||
UPDATE lidarr_requests
|
||||
SET lidarr_album_mbid = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
type SetLidarrRequestAlbumMbidParams struct {
|
||||
ID pgtype.UUID
|
||||
LidarrAlbumMbid *string
|
||||
}
|
||||
|
||||
// #5241: repoint a request at the album's release group. Requests made before
|
||||
// the release-group read named albums by MusicBrainz release id, which Lidarr
|
||||
// does not index; the reconciler rewrites them once it has resolved the group.
|
||||
func (q *Queries) SetLidarrRequestAlbumMbid(ctx context.Context, arg SetLidarrRequestAlbumMbidParams) error {
|
||||
_, err := q.db.Exec(ctx, setLidarrRequestAlbumMbid, arg.ID, arg.LidarrAlbumMbid)
|
||||
return err
|
||||
}
|
||||
@@ -233,24 +233,6 @@ func (q *Queries) MergeRepointContextualLikes(ctx context.Context, arg MergeRepo
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const mergeRepointLidarrRequests = `-- name: MergeRepointLidarrRequests :execrows
|
||||
UPDATE lidarr_requests SET matched_track_id = $1::uuid
|
||||
WHERE matched_track_id = $2::uuid
|
||||
`
|
||||
|
||||
type MergeRepointLidarrRequestsParams struct {
|
||||
SurvivorID pgtype.UUID
|
||||
LoserID pgtype.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) MergeRepointLidarrRequests(ctx context.Context, arg MergeRepointLidarrRequestsParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, mergeRepointLidarrRequests, arg.SurvivorID, arg.LoserID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const mergeRepointPlayEvents = `-- name: MergeRepointPlayEvents :execrows
|
||||
|
||||
UPDATE play_events SET track_id = $1::uuid WHERE track_id = $2::uuid
|
||||
@@ -321,6 +303,24 @@ func (q *Queries) MergeRepointPlaylistTracks(ctx context.Context, arg MergeRepoi
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const mergeRepointRequests = `-- name: MergeRepointRequests :execrows
|
||||
UPDATE requests SET matched_track_id = $1::uuid
|
||||
WHERE matched_track_id = $2::uuid
|
||||
`
|
||||
|
||||
type MergeRepointRequestsParams struct {
|
||||
SurvivorID pgtype.UUID
|
||||
LoserID pgtype.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) MergeRepointRequests(ctx context.Context, arg MergeRepointRequestsParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, mergeRepointRequests, arg.SurvivorID, arg.LoserID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const mergeRepointSkipEvents = `-- name: MergeRepointSkipEvents :execrows
|
||||
UPDATE skip_events SET track_id = $1::uuid WHERE track_id = $2::uuid
|
||||
`
|
||||
|
||||
+105
-77
@@ -99,92 +99,92 @@ func (ns NullLidarrQuarantineReason) Value() (driver.Value, error) {
|
||||
return string(ns.LidarrQuarantineReason), nil
|
||||
}
|
||||
|
||||
type LidarrRequestKind string
|
||||
type RequestKind string
|
||||
|
||||
const (
|
||||
LidarrRequestKindArtist LidarrRequestKind = "artist"
|
||||
LidarrRequestKindAlbum LidarrRequestKind = "album"
|
||||
LidarrRequestKindTrack LidarrRequestKind = "track"
|
||||
RequestKindArtist RequestKind = "artist"
|
||||
RequestKindAlbum RequestKind = "album"
|
||||
RequestKindTrack RequestKind = "track"
|
||||
)
|
||||
|
||||
func (e *LidarrRequestKind) Scan(src interface{}) error {
|
||||
func (e *RequestKind) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = LidarrRequestKind(s)
|
||||
*e = RequestKind(s)
|
||||
case string:
|
||||
*e = LidarrRequestKind(s)
|
||||
*e = RequestKind(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for LidarrRequestKind: %T", src)
|
||||
return fmt.Errorf("unsupported scan type for RequestKind: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullLidarrRequestKind struct {
|
||||
LidarrRequestKind LidarrRequestKind
|
||||
Valid bool // Valid is true if LidarrRequestKind is not NULL
|
||||
type NullRequestKind struct {
|
||||
RequestKind RequestKind
|
||||
Valid bool // Valid is true if RequestKind is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullLidarrRequestKind) Scan(value interface{}) error {
|
||||
func (ns *NullRequestKind) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.LidarrRequestKind, ns.Valid = "", false
|
||||
ns.RequestKind, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.LidarrRequestKind.Scan(value)
|
||||
return ns.RequestKind.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullLidarrRequestKind) Value() (driver.Value, error) {
|
||||
func (ns NullRequestKind) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.LidarrRequestKind), nil
|
||||
return string(ns.RequestKind), nil
|
||||
}
|
||||
|
||||
type LidarrRequestStatus string
|
||||
type RequestStatus string
|
||||
|
||||
const (
|
||||
LidarrRequestStatusPending LidarrRequestStatus = "pending"
|
||||
LidarrRequestStatusApproved LidarrRequestStatus = "approved"
|
||||
LidarrRequestStatusRejected LidarrRequestStatus = "rejected"
|
||||
LidarrRequestStatusCompleted LidarrRequestStatus = "completed"
|
||||
LidarrRequestStatusFailed LidarrRequestStatus = "failed"
|
||||
RequestStatusPending RequestStatus = "pending"
|
||||
RequestStatusApproved RequestStatus = "approved"
|
||||
RequestStatusRejected RequestStatus = "rejected"
|
||||
RequestStatusCompleted RequestStatus = "completed"
|
||||
RequestStatusFailed RequestStatus = "failed"
|
||||
)
|
||||
|
||||
func (e *LidarrRequestStatus) Scan(src interface{}) error {
|
||||
func (e *RequestStatus) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = LidarrRequestStatus(s)
|
||||
*e = RequestStatus(s)
|
||||
case string:
|
||||
*e = LidarrRequestStatus(s)
|
||||
*e = RequestStatus(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for LidarrRequestStatus: %T", src)
|
||||
return fmt.Errorf("unsupported scan type for RequestStatus: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullLidarrRequestStatus struct {
|
||||
LidarrRequestStatus LidarrRequestStatus
|
||||
Valid bool // Valid is true if LidarrRequestStatus is not NULL
|
||||
type NullRequestStatus struct {
|
||||
RequestStatus RequestStatus
|
||||
Valid bool // Valid is true if RequestStatus is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullLidarrRequestStatus) Scan(value interface{}) error {
|
||||
func (ns *NullRequestStatus) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.LidarrRequestStatus, ns.Valid = "", false
|
||||
ns.RequestStatus, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.LidarrRequestStatus.Scan(value)
|
||||
return ns.RequestStatus.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullLidarrRequestStatus) Value() (driver.Value, error) {
|
||||
func (ns NullRequestStatus) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.LidarrRequestStatus), nil
|
||||
return string(ns.RequestStatus), nil
|
||||
}
|
||||
|
||||
type AcoustidSetting struct {
|
||||
@@ -384,6 +384,24 @@ type FingerprintSetting struct {
|
||||
AutoResolve bool
|
||||
}
|
||||
|
||||
type Follow 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
|
||||
}
|
||||
|
||||
type FollowSeenGroup struct {
|
||||
ArtistMbid string
|
||||
ReleaseGroupMbid string
|
||||
SeenAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type GeneralLike struct {
|
||||
UserID pgtype.UUID
|
||||
TrackID pgtype.UUID
|
||||
@@ -443,31 +461,6 @@ type LidarrQuarantineAction struct {
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type LidarrRequest struct {
|
||||
ID pgtype.UUID
|
||||
UserID pgtype.UUID
|
||||
Status LidarrRequestStatus
|
||||
Kind LidarrRequestKind
|
||||
LidarrArtistMbid string
|
||||
LidarrAlbumMbid *string
|
||||
LidarrTrackMbid *string
|
||||
ArtistName string
|
||||
AlbumTitle *string
|
||||
TrackTitle *string
|
||||
QualityProfileID *int32
|
||||
RootFolderPath *string
|
||||
DecidedAt pgtype.Timestamptz
|
||||
DecidedBy pgtype.UUID
|
||||
Notes *string
|
||||
CompletedAt pgtype.Timestamptz
|
||||
MatchedTrackID pgtype.UUID
|
||||
MatchedAlbumID pgtype.UUID
|
||||
MatchedArtistID pgtype.UUID
|
||||
RequestedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
LidarrAddConfirmedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ListenbrainzSimilarRecording struct {
|
||||
SeedTrackID pgtype.UUID
|
||||
RecordingMbid string
|
||||
@@ -535,16 +528,6 @@ type MbReleaseTrack struct {
|
||||
RecordingMbid string
|
||||
}
|
||||
|
||||
type MissingReacquisition struct {
|
||||
AlbumID pgtype.UUID
|
||||
Attempts int32
|
||||
LastAttemptAt pgtype.Timestamptz
|
||||
LastRequestID pgtype.UUID
|
||||
GaveUpAt pgtype.Timestamptz
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type NetworkSetting struct {
|
||||
ID bool
|
||||
TrustedProxyHops int32
|
||||
@@ -636,14 +619,10 @@ type PlaylistTrack struct {
|
||||
}
|
||||
|
||||
type ReacquisitionSetting struct {
|
||||
ID bool
|
||||
Enabled bool
|
||||
GraceHours int32
|
||||
BackoffBaseHours int32
|
||||
BackoffMaxHours int32
|
||||
MaxAttempts int32
|
||||
MaxPerPass int32
|
||||
AutoApprove bool
|
||||
ID bool
|
||||
Enabled bool
|
||||
GraceHours int32
|
||||
MaxPerPass int32
|
||||
}
|
||||
|
||||
type RecommendationTuningAudit struct {
|
||||
@@ -673,6 +652,38 @@ type RegistrationSetting struct {
|
||||
Mode string
|
||||
}
|
||||
|
||||
type ReleaseTypeProfile struct {
|
||||
ID pgtype.UUID
|
||||
Name string
|
||||
PrimaryTypes []string
|
||||
SecondaryTypes []string
|
||||
IsDefault bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
ID pgtype.UUID
|
||||
UserID pgtype.UUID
|
||||
Status RequestStatus
|
||||
Kind RequestKind
|
||||
ArtistMbid string
|
||||
AlbumMbid *string
|
||||
TrackMbid *string
|
||||
ArtistName string
|
||||
AlbumTitle *string
|
||||
TrackTitle *string
|
||||
DecidedAt pgtype.Timestamptz
|
||||
DecidedBy pgtype.UUID
|
||||
Notes *string
|
||||
CompletedAt pgtype.Timestamptz
|
||||
MatchedTrackID pgtype.UUID
|
||||
MatchedAlbumID pgtype.UUID
|
||||
MatchedArtistID pgtype.UUID
|
||||
RequestedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ScanRun struct {
|
||||
ID pgtype.UUID
|
||||
StartedAt pgtype.Timestamptz
|
||||
@@ -950,6 +961,23 @@ type UserNotificationPref struct {
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Want struct {
|
||||
ID pgtype.UUID
|
||||
ReleaseGroupMbid string
|
||||
ArtistMbid string
|
||||
ArtistName string
|
||||
Title string
|
||||
PrimaryType string
|
||||
FirstReleaseDate string
|
||||
Reason string
|
||||
AlbumID pgtype.UUID
|
||||
CreatedBy pgtype.UUID
|
||||
WantedAt pgtype.Timestamptz
|
||||
FulfilledAt pgtype.Timestamptz
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type YouMightLikeAlbum struct {
|
||||
UserID pgtype.UUID
|
||||
AlbumID pgtype.UUID
|
||||
|
||||
@@ -11,27 +11,6 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const clearRecoveredReacquisitions = `-- name: ClearRecoveredReacquisitions :execrows
|
||||
DELETE FROM missing_reacquisitions r
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM tracks
|
||||
WHERE tracks.album_id = r.album_id
|
||||
AND tracks.missing_since IS NOT NULL
|
||||
)
|
||||
`
|
||||
|
||||
// Drops state for albums that no longer have any missing track — the files
|
||||
// came back, or the scanner adopted them at a new path (#2528). Deleting
|
||||
// rather than resetting counters means a future loss starts from a clean
|
||||
// budget, which is right: it is a new problem, not a continuation.
|
||||
func (q *Queries) ClearRecoveredReacquisitions(ctx context.Context) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, clearRecoveredReacquisitions)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const countAlbumsMissingWithoutMbid = `-- name: CountAlbumsMissingWithoutMbid :one
|
||||
SELECT COUNT(DISTINCT albums.id)::bigint
|
||||
FROM albums
|
||||
@@ -41,10 +20,9 @@ SELECT COUNT(DISTINCT albums.id)::bigint
|
||||
AND (albums.mbid IS NULL OR artists.mbid IS NULL)
|
||||
`
|
||||
|
||||
// Albums with missing files that can never be auto-requested because nothing
|
||||
// identifies them to MusicBrainz. Surfaced on the admin card so the gap is
|
||||
// visible: silently doing nothing for these would read as the feature being
|
||||
// broken.
|
||||
// Albums with missing files that can never be wanted automatically because
|
||||
// nothing identifies them to MusicBrainz. Surfaced on the admin card so the
|
||||
// gap is visible: silently doing nothing for these would read as broken.
|
||||
func (q *Queries) CountAlbumsMissingWithoutMbid(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countAlbumsMissingWithoutMbid)
|
||||
var column_1 int64
|
||||
@@ -52,29 +30,56 @@ func (q *Queries) CountAlbumsMissingWithoutMbid(ctx context.Context) (int64, err
|
||||
return column_1, err
|
||||
}
|
||||
|
||||
const getReacquisitionForAlbums = `-- name: GetReacquisitionForAlbums :many
|
||||
SELECT album_id, attempts, last_attempt_at, last_request_id, gave_up_at, created_at, updated_at FROM missing_reacquisitions WHERE album_id = ANY($1::uuid[])
|
||||
const getReacquisitionSettings = `-- name: GetReacquisitionSettings :one
|
||||
|
||||
SELECT id, enabled, grace_hours, max_per_pass FROM reacquisition_settings WHERE id = true
|
||||
`
|
||||
|
||||
// State for the admin missing-files surface, so each directory group can say
|
||||
// whether a re-acquisition is in flight, waiting, or given up.
|
||||
func (q *Queries) GetReacquisitionForAlbums(ctx context.Context, albumIds []pgtype.UUID) ([]MissingReacquisition, error) {
|
||||
rows, err := q.db.Query(ctx, getReacquisitionForAlbums, albumIds)
|
||||
// Re-acquisition of missing files (milestone #290, reworked for M502 #5520).
|
||||
// The unit is the album: an album whose files went missing becomes a want for
|
||||
// its release group, and the search schedule (#5521) looks for it.
|
||||
func (q *Queries) GetReacquisitionSettings(ctx context.Context) (ReacquisitionSetting, error) {
|
||||
row := q.db.QueryRow(ctx, getReacquisitionSettings)
|
||||
var i ReacquisitionSetting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Enabled,
|
||||
&i.GraceHours,
|
||||
&i.MaxPerPass,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWantsForAlbums = `-- name: GetWantsForAlbums :many
|
||||
SELECT a.id AS album_id, w.id AS want_id, w.wanted_at, w.fulfilled_at
|
||||
FROM albums a
|
||||
JOIN wants w ON w.album_id = a.id OR w.release_group_mbid = a.release_group_mbid
|
||||
WHERE a.id = ANY($1::uuid[])
|
||||
ORDER BY w.wanted_at DESC
|
||||
`
|
||||
|
||||
type GetWantsForAlbumsRow struct {
|
||||
AlbumID pgtype.UUID
|
||||
WantID pgtype.UUID
|
||||
WantedAt pgtype.Timestamptz
|
||||
FulfilledAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
// What the wanted list says about each album on the missing-files page.
|
||||
func (q *Queries) GetWantsForAlbums(ctx context.Context, albumIds []pgtype.UUID) ([]GetWantsForAlbumsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getWantsForAlbums, albumIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []MissingReacquisition
|
||||
var items []GetWantsForAlbumsRow
|
||||
for rows.Next() {
|
||||
var i MissingReacquisition
|
||||
var i GetWantsForAlbumsRow
|
||||
if err := rows.Scan(
|
||||
&i.AlbumID,
|
||||
&i.Attempts,
|
||||
&i.LastAttemptAt,
|
||||
&i.LastRequestID,
|
||||
&i.GaveUpAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.WantID,
|
||||
&i.WantedAt,
|
||||
&i.FulfilledAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,67 +91,35 @@ func (q *Queries) GetReacquisitionForAlbums(ctx context.Context, albumIds []pgty
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getReacquisitionSettings = `-- name: GetReacquisitionSettings :one
|
||||
|
||||
SELECT id, enabled, grace_hours, backoff_base_hours, backoff_max_hours, max_attempts, max_per_pass, auto_approve FROM reacquisition_settings WHERE id = true
|
||||
`
|
||||
|
||||
// Auto re-acquisition of missing files (milestone #290). The unit is the
|
||||
// album: Lidarr acquires releases, and grouping collapses "40 missing files"
|
||||
// into "3 albums to ask for".
|
||||
func (q *Queries) GetReacquisitionSettings(ctx context.Context) (ReacquisitionSetting, error) {
|
||||
row := q.db.QueryRow(ctx, getReacquisitionSettings)
|
||||
var i ReacquisitionSetting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Enabled,
|
||||
&i.GraceHours,
|
||||
&i.BackoffBaseHours,
|
||||
&i.BackoffMaxHours,
|
||||
&i.MaxAttempts,
|
||||
&i.MaxPerPass,
|
||||
&i.AutoApprove,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listAlbumsDueReacquisition = `-- name: ListAlbumsDueReacquisition :many
|
||||
SELECT albums.id AS album_id,
|
||||
albums.title AS album_title,
|
||||
albums.mbid AS album_mbid,
|
||||
SELECT albums.id AS album_id,
|
||||
albums.title AS album_title,
|
||||
albums.mbid AS album_mbid,
|
||||
albums.release_group_mbid AS album_release_group_mbid,
|
||||
artists.id AS artist_id,
|
||||
artists.name AS artist_name,
|
||||
artists.mbid AS artist_mbid,
|
||||
COUNT(tracks.id)::bigint AS missing_track_count,
|
||||
COALESCE(r.attempts, 0)::int AS attempts
|
||||
artists.name AS artist_name,
|
||||
artists.mbid AS artist_mbid,
|
||||
COUNT(tracks.id)::bigint AS missing_track_count,
|
||||
MAX(tracks.missing_since)::timestamptz AS last_missing_since
|
||||
FROM albums
|
||||
JOIN artists ON artists.id = albums.artist_id
|
||||
JOIN tracks ON tracks.album_id = albums.id
|
||||
LEFT JOIN missing_reacquisitions r ON r.album_id = albums.id
|
||||
WHERE tracks.missing_since IS NOT NULL
|
||||
AND tracks.missing_since <= now() - make_interval(hours => $1::int)
|
||||
AND albums.mbid IS NOT NULL
|
||||
AND artists.mbid IS NOT NULL
|
||||
AND (r.gave_up_at IS NULL)
|
||||
AND (
|
||||
r.last_attempt_at IS NULL
|
||||
OR r.last_attempt_at <= now() - make_interval(hours => LEAST(
|
||||
($2::int
|
||||
* POWER(2, GREATEST(COALESCE(r.attempts, 0) - 1, 0)))::int,
|
||||
$3::int))
|
||||
)
|
||||
GROUP BY albums.id, albums.title, albums.mbid, albums.release_group_mbid,
|
||||
artists.id, artists.name, artists.mbid, r.attempts, r.last_attempt_at
|
||||
ORDER BY r.last_attempt_at NULLS FIRST, albums.sort_title
|
||||
LIMIT $4
|
||||
artists.name, artists.mbid
|
||||
HAVING NOT EXISTS (
|
||||
SELECT 1 FROM wants w
|
||||
WHERE (w.album_id = albums.id OR w.release_group_mbid = albums.release_group_mbid)
|
||||
AND (w.fulfilled_at IS NULL OR w.fulfilled_at > MAX(tracks.missing_since)))
|
||||
ORDER BY MIN(tracks.missing_since), albums.sort_title
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListAlbumsDueReacquisitionParams struct {
|
||||
GraceHours int32
|
||||
BackoffBaseHours int32
|
||||
BackoffMaxHours int32
|
||||
PageLimit int32
|
||||
GraceHours int32
|
||||
PageLimit int32
|
||||
}
|
||||
|
||||
type ListAlbumsDueReacquisitionRow struct {
|
||||
@@ -154,37 +127,24 @@ type ListAlbumsDueReacquisitionRow struct {
|
||||
AlbumTitle string
|
||||
AlbumMbid *string
|
||||
AlbumReleaseGroupMbid *string
|
||||
ArtistID pgtype.UUID
|
||||
ArtistName string
|
||||
ArtistMbid *string
|
||||
MissingTrackCount int64
|
||||
Attempts int32
|
||||
LastMissingSince pgtype.Timestamptz
|
||||
}
|
||||
|
||||
// The sweeper's selection. An album qualifies when:
|
||||
//
|
||||
// - it still has at least one track whose file has been missing longer than
|
||||
// the grace window. Measured on missing_since, which reconcile never
|
||||
// re-stamps (#2523), so it is a genuine "gone since" clock rather than
|
||||
// "when we last noticed";
|
||||
// - MusicBrainz can name it. Both the album and its artist MBID are
|
||||
// required — Create rejects an album-kind request without them, and there
|
||||
// is nothing to ask Lidarr for anyway. Albums failing this are counted
|
||||
// separately (CountAlbumsMissingWithoutMbid) rather than vanishing;
|
||||
// - it has not spent its attempt budget (gave_up_at IS NULL);
|
||||
// - its backoff has elapsed: base * 2^(attempts-1) hours since the last
|
||||
// attempt, clamped to the configured maximum. First attempt (no row, or
|
||||
// last_attempt_at NULL) is always due.
|
||||
//
|
||||
// Oldest attempt first, never-attempted first, so a large loss drains in a
|
||||
// stable order across passes instead of re-picking the same head each time.
|
||||
// re-stamps (#2523), so it is a genuine "gone since" clock;
|
||||
// - MusicBrainz can name it: the album and its artist have MBIDs. Albums
|
||||
// failing this are counted separately (CountAlbumsMissingWithoutMbid);
|
||||
// - no want already covers it: none open, and none held since its files
|
||||
// went missing. A want held after the loss means the library has a full
|
||||
// copy despite the missing rows (#5245), so asking again would loop.
|
||||
func (q *Queries) ListAlbumsDueReacquisition(ctx context.Context, arg ListAlbumsDueReacquisitionParams) ([]ListAlbumsDueReacquisitionRow, error) {
|
||||
rows, err := q.db.Query(ctx, listAlbumsDueReacquisition,
|
||||
arg.GraceHours,
|
||||
arg.BackoffBaseHours,
|
||||
arg.BackoffMaxHours,
|
||||
arg.PageLimit,
|
||||
)
|
||||
rows, err := q.db.Query(ctx, listAlbumsDueReacquisition, arg.GraceHours, arg.PageLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,11 +157,10 @@ func (q *Queries) ListAlbumsDueReacquisition(ctx context.Context, arg ListAlbums
|
||||
&i.AlbumTitle,
|
||||
&i.AlbumMbid,
|
||||
&i.AlbumReleaseGroupMbid,
|
||||
&i.ArtistID,
|
||||
&i.ArtistName,
|
||||
&i.ArtistMbid,
|
||||
&i.MissingTrackCount,
|
||||
&i.Attempts,
|
||||
&i.LastMissingSince,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -213,101 +172,83 @@ func (q *Queries) ListAlbumsDueReacquisition(ctx context.Context, arg ListAlbums
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const markReacquisitionGaveUp = `-- name: MarkReacquisitionGaveUp :exec
|
||||
UPDATE missing_reacquisitions
|
||||
SET gave_up_at = now(),
|
||||
updated_at = now()
|
||||
WHERE album_id = $1
|
||||
AND gave_up_at IS NULL
|
||||
`
|
||||
|
||||
// Stamped when the attempt budget is spent. Stored as a timestamp rather than
|
||||
// inferred from `attempts >= max_attempts` so the verdict survives an operator
|
||||
// later raising the maximum, and so the admin surface can say when.
|
||||
func (q *Queries) MarkReacquisitionGaveUp(ctx context.Context, albumID pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, markReacquisitionGaveUp, albumID)
|
||||
return err
|
||||
}
|
||||
|
||||
const recordReacquisitionAttempt = `-- name: RecordReacquisitionAttempt :one
|
||||
INSERT INTO missing_reacquisitions (album_id, attempts, last_attempt_at, last_request_id)
|
||||
VALUES ($1, 1, now(), $2)
|
||||
ON CONFLICT (album_id) DO UPDATE
|
||||
SET attempts = missing_reacquisitions.attempts + 1,
|
||||
last_attempt_at = now(),
|
||||
last_request_id = COALESCE(EXCLUDED.last_request_id,
|
||||
missing_reacquisitions.last_request_id),
|
||||
updated_at = now()
|
||||
RETURNING album_id, attempts, last_attempt_at, last_request_id, gave_up_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type RecordReacquisitionAttemptParams struct {
|
||||
AlbumID pgtype.UUID
|
||||
LastRequestID pgtype.UUID
|
||||
}
|
||||
|
||||
// Bumps the attempt counter and stamps the clock the backoff measures from.
|
||||
// Upsert because the first attempt has no row yet.
|
||||
func (q *Queries) RecordReacquisitionAttempt(ctx context.Context, arg RecordReacquisitionAttemptParams) (MissingReacquisition, error) {
|
||||
row := q.db.QueryRow(ctx, recordReacquisitionAttempt, arg.AlbumID, arg.LastRequestID)
|
||||
var i MissingReacquisition
|
||||
err := row.Scan(
|
||||
&i.AlbumID,
|
||||
&i.Attempts,
|
||||
&i.LastAttemptAt,
|
||||
&i.LastRequestID,
|
||||
&i.GaveUpAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateReacquisitionSettings = `-- name: UpdateReacquisitionSettings :one
|
||||
UPDATE reacquisition_settings
|
||||
SET enabled = $1,
|
||||
grace_hours = $2,
|
||||
backoff_base_hours = $3,
|
||||
backoff_max_hours = $4,
|
||||
max_attempts = $5,
|
||||
max_per_pass = $6,
|
||||
auto_approve = $7
|
||||
SET enabled = $1,
|
||||
grace_hours = $2,
|
||||
max_per_pass = $3
|
||||
WHERE id = true
|
||||
RETURNING id, enabled, grace_hours, backoff_base_hours, backoff_max_hours, max_attempts, max_per_pass, auto_approve
|
||||
RETURNING id, enabled, grace_hours, max_per_pass
|
||||
`
|
||||
|
||||
type UpdateReacquisitionSettingsParams struct {
|
||||
Enabled bool
|
||||
GraceHours int32
|
||||
BackoffBaseHours int32
|
||||
BackoffMaxHours int32
|
||||
MaxAttempts int32
|
||||
MaxPerPass int32
|
||||
AutoApprove bool
|
||||
Enabled bool
|
||||
GraceHours int32
|
||||
MaxPerPass int32
|
||||
}
|
||||
|
||||
// Whole-row write from the admin card; the CHECKs in migration 0056 are the
|
||||
// validation, so a bad value fails loudly rather than being clamped silently.
|
||||
func (q *Queries) UpdateReacquisitionSettings(ctx context.Context, arg UpdateReacquisitionSettingsParams) (ReacquisitionSetting, error) {
|
||||
row := q.db.QueryRow(ctx, updateReacquisitionSettings,
|
||||
arg.Enabled,
|
||||
arg.GraceHours,
|
||||
arg.BackoffBaseHours,
|
||||
arg.BackoffMaxHours,
|
||||
arg.MaxAttempts,
|
||||
arg.MaxPerPass,
|
||||
arg.AutoApprove,
|
||||
)
|
||||
row := q.db.QueryRow(ctx, updateReacquisitionSettings, arg.Enabled, arg.GraceHours, arg.MaxPerPass)
|
||||
var i ReacquisitionSetting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Enabled,
|
||||
&i.GraceHours,
|
||||
&i.BackoffBaseHours,
|
||||
&i.BackoffMaxHours,
|
||||
&i.MaxAttempts,
|
||||
&i.MaxPerPass,
|
||||
&i.AutoApprove,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const wantMissingAlbum = `-- name: WantMissingAlbum :one
|
||||
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, reason, album_id)
|
||||
VALUES ($1, $2, $3,
|
||||
$4, 'missing', $5)
|
||||
ON CONFLICT (release_group_mbid) DO UPDATE
|
||||
SET reason = 'missing',
|
||||
album_id = EXCLUDED.album_id,
|
||||
wanted_at = now(),
|
||||
fulfilled_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE wants.fulfilled_at IS NOT NULL
|
||||
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 WantMissingAlbumParams struct {
|
||||
ReleaseGroupMbid string
|
||||
ArtistMbid string
|
||||
ArtistName string
|
||||
Title string
|
||||
AlbumID pgtype.UUID
|
||||
}
|
||||
|
||||
// Records an album with missing files as wanted. A held want for the same
|
||||
// group is reopened; an open one is left as it is, and no row comes back.
|
||||
func (q *Queries) WantMissingAlbum(ctx context.Context, arg WantMissingAlbumParams) (Want, error) {
|
||||
row := q.db.QueryRow(ctx, wantMissingAlbum,
|
||||
arg.ReleaseGroupMbid,
|
||||
arg.ArtistMbid,
|
||||
arg.ArtistName,
|
||||
arg.Title,
|
||||
arg.AlbumID,
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1142,9 +1142,9 @@ contributions AS (
|
||||
JOIN seeds ON seeds.artist_id = u.seed_artist_id
|
||||
WHERE NOT EXISTS (SELECT 1 FROM artists WHERE mbid = u.candidate_mbid)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_requests r
|
||||
SELECT 1 FROM requests r
|
||||
WHERE r.user_id = $1
|
||||
AND r.lidarr_artist_mbid = u.candidate_mbid
|
||||
AND r.artist_mbid = u.candidate_mbid
|
||||
AND r.status NOT IN ('rejected', 'failed')
|
||||
)
|
||||
-- Snoozed by this user and not yet expired (#2374). Time-boxed and
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: requests.sql
|
||||
|
||||
package dbq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const approveRequest = `-- name: ApproveRequest :one
|
||||
UPDATE requests
|
||||
SET status = 'approved',
|
||||
decided_at = now(),
|
||||
decided_by = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status = 'pending'
|
||||
RETURNING id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at
|
||||
`
|
||||
|
||||
type ApproveRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
DecidedBy pgtype.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) ApproveRequest(ctx context.Context, arg ApproveRequestParams) (Request, error) {
|
||||
row := q.db.QueryRow(ctx, approveRequest, arg.ID, arg.DecidedBy)
|
||||
var i Request
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const cancelRequest = `-- name: CancelRequest :one
|
||||
UPDATE requests
|
||||
SET status = 'rejected',
|
||||
notes = 'cancelled by user',
|
||||
decided_at = now(),
|
||||
decided_by = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND user_id = $2 AND status = 'pending'
|
||||
RETURNING id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at
|
||||
`
|
||||
|
||||
type CancelRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
DecidedBy pgtype.UUID
|
||||
}
|
||||
|
||||
// $2 serves as both the ownership guard (user_id = $2) and the
|
||||
// decided_by audit field. The generated parameter struct names it
|
||||
// DecidedBy — callers must pass the cancelling user's UUID, and
|
||||
// a wrong UUID will silently no-op (zero rows updated) rather than
|
||||
// erroring at the SQL layer.
|
||||
func (q *Queries) CancelRequest(ctx context.Context, arg CancelRequestParams) (Request, error) {
|
||||
row := q.db.QueryRow(ctx, cancelRequest, arg.ID, arg.DecidedBy)
|
||||
var i Request
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const completeRequest = `-- name: CompleteRequest :one
|
||||
UPDATE requests
|
||||
SET status = 'completed',
|
||||
matched_track_id = $2,
|
||||
matched_album_id = $3,
|
||||
matched_artist_id = $4,
|
||||
completed_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status = 'approved'
|
||||
RETURNING id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at
|
||||
`
|
||||
|
||||
type CompleteRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
MatchedTrackID pgtype.UUID
|
||||
MatchedAlbumID pgtype.UUID
|
||||
MatchedArtistID pgtype.UUID
|
||||
}
|
||||
|
||||
// Reconciler transitions an approved request to completed when its
|
||||
// target track/album/artist has appeared in the library. Callers
|
||||
// should set ONLY the matched_*_id corresponding to the request's
|
||||
// kind and pass pgtype.UUID{} (Valid: false) for the others —
|
||||
// per-kind one-of-three is the contract, NULL for the others
|
||||
// preserves auditability if the matched row is later evicted.
|
||||
func (q *Queries) CompleteRequest(ctx context.Context, arg CompleteRequestParams) (Request, error) {
|
||||
row := q.db.QueryRow(ctx, completeRequest,
|
||||
arg.ID,
|
||||
arg.MatchedTrackID,
|
||||
arg.MatchedAlbumID,
|
||||
arg.MatchedArtistID,
|
||||
)
|
||||
var i Request
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createRequest = `-- name: CreateRequest :one
|
||||
INSERT INTO requests (
|
||||
user_id, kind,
|
||||
artist_mbid, album_mbid, track_mbid,
|
||||
artist_name, album_title, track_title
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at
|
||||
`
|
||||
|
||||
type CreateRequestParams struct {
|
||||
UserID pgtype.UUID
|
||||
Kind RequestKind
|
||||
ArtistMbid string
|
||||
AlbumMbid *string
|
||||
TrackMbid *string
|
||||
ArtistName string
|
||||
AlbumTitle *string
|
||||
TrackTitle *string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateRequest(ctx context.Context, arg CreateRequestParams) (Request, error) {
|
||||
row := q.db.QueryRow(ctx, createRequest,
|
||||
arg.UserID,
|
||||
arg.Kind,
|
||||
arg.ArtistMbid,
|
||||
arg.AlbumMbid,
|
||||
arg.TrackMbid,
|
||||
arg.ArtistName,
|
||||
arg.AlbumTitle,
|
||||
arg.TrackTitle,
|
||||
)
|
||||
var i Request
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getNonTerminalRequestForMBID = `-- name: GetNonTerminalRequestForMBID :one
|
||||
SELECT id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at FROM requests
|
||||
WHERE status IN ('pending', 'approved', 'completed')
|
||||
AND ((kind = 'artist' AND artist_mbid = $1)
|
||||
OR (kind = 'album' AND album_mbid = $1)
|
||||
OR (kind = 'track' AND track_mbid = $1))
|
||||
ORDER BY requested_at ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
// Returns the oldest non-terminal (pending/approved/completed) request
|
||||
// whose own-kind MBID matches @mbid, or pgx.ErrNoRows when none. Create()
|
||||
// uses this to dedupe: a re-request for something already in flight
|
||||
// returns the existing row instead of inserting a duplicate.
|
||||
func (q *Queries) GetNonTerminalRequestForMBID(ctx context.Context, mbid string) (Request, error) {
|
||||
row := q.db.QueryRow(ctx, getNonTerminalRequestForMBID, mbid)
|
||||
var i Request
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getRequestByID = `-- name: GetRequestByID :one
|
||||
SELECT id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at FROM requests WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetRequestByID(ctx context.Context, id pgtype.UUID) (Request, error) {
|
||||
row := q.db.QueryRow(ctx, getRequestByID, id)
|
||||
var i Request
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const hasNonTerminalRequestForMBID = `-- name: HasNonTerminalRequestForMBID :one
|
||||
SELECT (EXISTS (
|
||||
SELECT 1 FROM requests r
|
||||
WHERE r.status IN ('pending', 'approved', 'completed')
|
||||
AND ((r.kind = 'artist' AND r.artist_mbid = $1)
|
||||
OR (r.kind = 'album' AND r.album_mbid = $1)
|
||||
OR (r.kind = 'track' AND r.track_mbid = $1))
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM wants w WHERE w.release_group_mbid = $1 AND w.fulfilled_at IS NULL
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM follows f WHERE f.artist_mbid = $1
|
||||
))::bool AS requested
|
||||
`
|
||||
|
||||
// Whether something is already on its way: a pending, approved or completed
|
||||
// request at the given level, an open want for the release group, or a
|
||||
// follow of the artist. Sets the `requested` flag on catalogue search.
|
||||
// Terminal-status (rejected, failed) requests do not count. MBIDs are unique
|
||||
// per entity type, so checking one id against every column cannot
|
||||
// false-match.
|
||||
func (q *Queries) HasNonTerminalRequestForMBID(ctx context.Context, mbid string) (bool, error) {
|
||||
row := q.db.QueryRow(ctx, hasNonTerminalRequestForMBID, mbid)
|
||||
var requested bool
|
||||
err := row.Scan(&requested)
|
||||
return requested, err
|
||||
}
|
||||
|
||||
const listApprovedRequestsForReconcile = `-- name: ListApprovedRequestsForReconcile :many
|
||||
SELECT id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at FROM requests
|
||||
WHERE status = 'approved'
|
||||
ORDER BY decided_at ASC
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListApprovedRequestsForReconcile(ctx context.Context, limit int32) ([]Request, error) {
|
||||
rows, err := q.db.Query(ctx, listApprovedRequestsForReconcile, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Request
|
||||
for rows.Next() {
|
||||
var i Request
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRequestsByStatus = `-- name: ListRequestsByStatus :many
|
||||
SELECT id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at FROM requests
|
||||
WHERE status = $1
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListRequestsByStatusParams struct {
|
||||
Status RequestStatus
|
||||
Limit int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListRequestsByStatus(ctx context.Context, arg ListRequestsByStatusParams) ([]Request, error) {
|
||||
rows, err := q.db.Query(ctx, listRequestsByStatus, arg.Status, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Request
|
||||
for rows.Next() {
|
||||
var i Request
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRequestsForUser = `-- name: ListRequestsForUser :many
|
||||
SELECT id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at FROM requests
|
||||
WHERE user_id = $1
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListRequestsForUserParams struct {
|
||||
UserID pgtype.UUID
|
||||
Limit int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListRequestsForUser(ctx context.Context, arg ListRequestsForUserParams) ([]Request, error) {
|
||||
rows, err := q.db.Query(ctx, listRequestsForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Request
|
||||
for rows.Next() {
|
||||
var i Request
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const rejectRequest = `-- name: RejectRequest :one
|
||||
UPDATE requests
|
||||
SET status = 'rejected',
|
||||
notes = $2,
|
||||
decided_at = now(),
|
||||
decided_by = $3,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status = 'pending'
|
||||
RETURNING id, user_id, status, kind, artist_mbid, album_mbid, track_mbid, artist_name, album_title, track_title, decided_at, decided_by, notes, completed_at, matched_track_id, matched_album_id, matched_artist_id, requested_at, updated_at
|
||||
`
|
||||
|
||||
type RejectRequestParams struct {
|
||||
ID pgtype.UUID
|
||||
Notes *string
|
||||
DecidedBy pgtype.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) RejectRequest(ctx context.Context, arg RejectRequestParams) (Request, error) {
|
||||
row := q.db.QueryRow(ctx, rejectRequest, arg.ID, arg.Notes, arg.DecidedBy)
|
||||
var i Request
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Status,
|
||||
&i.Kind,
|
||||
&i.ArtistMbid,
|
||||
&i.AlbumMbid,
|
||||
&i.TrackMbid,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.TrackTitle,
|
||||
&i.DecidedAt,
|
||||
&i.DecidedBy,
|
||||
&i.Notes,
|
||||
&i.CompletedAt,
|
||||
&i.MatchedTrackID,
|
||||
&i.MatchedAlbumID,
|
||||
&i.MatchedArtistID,
|
||||
&i.RequestedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const repointRequestsAlbumMbid = `-- name: RepointRequestsAlbumMbid :exec
|
||||
UPDATE requests
|
||||
SET album_mbid = $1,
|
||||
updated_at = now()
|
||||
WHERE album_mbid = $2
|
||||
`
|
||||
|
||||
type RepointRequestsAlbumMbidParams struct {
|
||||
NewMbid *string
|
||||
OldMbid *string
|
||||
}
|
||||
|
||||
// A want moved from a release id to its group (#5241) takes the requests
|
||||
// that name it along, so the reconciler finds it.
|
||||
func (q *Queries) RepointRequestsAlbumMbid(ctx context.Context, arg RepointRequestsAlbumMbidParams) error {
|
||||
_, err := q.db.Exec(ctx, repointRequestsAlbumMbid, arg.NewMbid, arg.OldMbid)
|
||||
return err
|
||||
}
|
||||
@@ -239,43 +239,6 @@ func (q *Queries) GetListenBrainzConfig(ctx context.Context, id pgtype.UUID) (Ge
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOldestAdmin = `-- name: GetOldestAdmin :one
|
||||
SELECT id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash FROM users
|
||||
WHERE is_admin = true
|
||||
ORDER BY created_at, id
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
// The account a system-initiated action is attributed to (milestone #290).
|
||||
// lidarr_requests.user_id is NOT NULL and a re-acquisition has no requesting
|
||||
// human, so the row is owned by the longest-standing admin: it keeps the
|
||||
// request auditable and puts it in the same admin queue as everything else,
|
||||
// without inventing a synthetic principal the rest of the schema would have
|
||||
// to understand. Ordered by id as a tiebreak so the choice is stable across
|
||||
// calls rather than depending on scan order.
|
||||
func (q *Queries) GetOldestAdmin(ctx context.Context) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getOldestAdmin)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
&i.ListenbrainzToken,
|
||||
&i.ListenbrainzEnabled,
|
||||
&i.DisplayName,
|
||||
&i.AutoApproveRequests,
|
||||
&i.Email,
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByAPITokenHash = `-- name: GetUserByAPITokenHash :one
|
||||
SELECT id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash FROM users WHERE api_token_hash = $1
|
||||
`
|
||||
|
||||
@@ -0,0 +1,839 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user