feat: data lifecycle rewrite, download clients, wanted list, and central catalog index
Ships the fresh-start schema cleanup: rebuilt explore catalog index pipeline (dump import, artifact fetch/build, incremental listen-count refresh), a new download subsystem (Lidarr/Prowlarr/qBittorrent/SABnzbd/ slskd/yt-dlp providers, staging, reconciliation, wanted list), and the supporting schema/query/store changes across backend and frontend. Also includes two smaller follow-ups: bump the central index's rebuild-after cadence from 90 to 180 days, and remove the Explore "library only" online/offline toggle entirely (frontend-only, no backend counterpart) rather than carry unused UI/state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
This commit is contained in:
@@ -6,8 +6,8 @@ RETURNING *;
|
||||
INSERT INTO audio_files (
|
||||
file_path, length_milliseconds, file_type_id, recording_id,
|
||||
sample_rate, bit_depth, channels, bitrate, file_size, basename,
|
||||
library_id, group_key, tag_status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
library_id, group_key, tag_status, modified_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetAudioFileGroupKey :one
|
||||
@@ -32,9 +32,23 @@ WHERE id = ?;
|
||||
|
||||
-- name: UpdateAudioFileRecording :exec
|
||||
UPDATE audio_files
|
||||
SET recording_id = ?, sample_rate = ?, bit_depth = ?, channels = ?, bitrate = ?, file_size = ?
|
||||
SET recording_id = ?, sample_rate = ?, bit_depth = ?, channels = ?, bitrate = ?, file_size = ?, length_milliseconds = ?, modified_at = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: UpdateAudioFileStat :exec
|
||||
-- Records the on-disk mtime/size without re-reading tags. Used to
|
||||
-- backfill the staleness baseline for files the scan skipped, and to
|
||||
-- re-baseline after YellowJacket's own tag writer rewrites a file.
|
||||
UPDATE audio_files
|
||||
SET modified_at = ?, file_size = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: GetLibraryMaxModifiedAt :one
|
||||
-- Newest recorded mtime in a library, for the startup soft scan. 0 when
|
||||
-- the library is empty or no row has a baseline yet.
|
||||
SELECT CAST(COALESCE(MAX(modified_at), 0) AS INTEGER) FROM audio_files
|
||||
WHERE library_id = ?;
|
||||
|
||||
-- name: DeleteAudioFile :exec
|
||||
DELETE FROM audio_files
|
||||
WHERE id = ?;
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
-- name: ListDownloadProviders :many
|
||||
SELECT id, kind, name, enabled, priority, settings, created_at
|
||||
FROM download_providers
|
||||
ORDER BY priority DESC, name;
|
||||
|
||||
-- name: GetDownloadProvider :one
|
||||
SELECT id, kind, name, enabled, priority, settings, created_at
|
||||
FROM download_providers
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: CreateDownloadProvider :one
|
||||
INSERT INTO download_providers (kind, name, enabled, priority, settings)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateDownloadProvider :exec
|
||||
UPDATE download_providers
|
||||
SET name = ?, enabled = ?, priority = ?, settings = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteDownloadProvider :exec
|
||||
DELETE FROM download_providers
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: CreateDownloadRequest :exec
|
||||
INSERT INTO download_requests (
|
||||
id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
|
||||
-- name: GetDownloadRequest :one
|
||||
SELECT id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state, error,
|
||||
created_at, updated_at
|
||||
FROM download_requests
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ListDownloadRequests :many
|
||||
SELECT id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state, error,
|
||||
created_at, updated_at
|
||||
FROM download_requests
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?;
|
||||
|
||||
-- name: ListLiveDownloadRequests :many
|
||||
SELECT id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state, error,
|
||||
created_at, updated_at
|
||||
FROM download_requests
|
||||
WHERE state NOT IN ('complete', 'cancelled', 'failed')
|
||||
ORDER BY created_at;
|
||||
|
||||
-- name: SetDownloadRequestState :exec
|
||||
UPDATE download_requests
|
||||
SET state = ?, error = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteDownloadRequest :exec
|
||||
DELETE FROM download_requests
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: CreateDownloadItem :exec
|
||||
INSERT INTO download_items (
|
||||
id, request_id, provider_id, transport_id, external_id,
|
||||
candidate, state, staging_dir, bytes_total
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
|
||||
-- name: GetDownloadItem :one
|
||||
SELECT id, request_id, provider_id, transport_id, external_id, candidate,
|
||||
state, staging_dir, bytes_done, bytes_total, imported_paths,
|
||||
error, created_at, updated_at
|
||||
FROM download_items
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ListDownloadItemsForRequest :many
|
||||
SELECT id, request_id, provider_id, transport_id, external_id, candidate,
|
||||
state, staging_dir, bytes_done, bytes_total, imported_paths,
|
||||
error, created_at, updated_at
|
||||
FROM download_items
|
||||
WHERE request_id = ?
|
||||
ORDER BY created_at;
|
||||
|
||||
-- name: ListLiveDownloadItems :many
|
||||
SELECT id, request_id, provider_id, transport_id, external_id, candidate,
|
||||
state, staging_dir, bytes_done, bytes_total, imported_paths,
|
||||
error, created_at, updated_at
|
||||
FROM download_items
|
||||
WHERE state NOT IN ('complete', 'cancelled', 'failed')
|
||||
ORDER BY created_at;
|
||||
|
||||
-- name: SetDownloadItemState :exec
|
||||
UPDATE download_items
|
||||
SET state = ?, error = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: SetDownloadItemProgress :exec
|
||||
UPDATE download_items
|
||||
SET bytes_done = ?, bytes_total = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: SetDownloadItemExternalID :exec
|
||||
UPDATE download_items
|
||||
SET external_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: SetDownloadItemImported :exec
|
||||
UPDATE download_items
|
||||
SET imported_paths = ?, state = 'complete', error = '',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteFinishedDownloadRequests :exec
|
||||
DELETE FROM download_requests
|
||||
WHERE state IN ('complete', 'cancelled', 'failed');
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Wants
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
-- name: UpsertDownloadWant :one
|
||||
-- Adding something already wanted is not an error and must not reset
|
||||
-- the retry clock, so the conflict path only refreshes display text and
|
||||
-- un-pauses nothing. scope and secondary are updated because asking
|
||||
-- again with a wider scope is a real change of intent.
|
||||
INSERT INTO download_wants (
|
||||
mbid, entity, library_id, artist, title, scope, secondary,
|
||||
parent_id, next_try_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(mbid, library_id) DO UPDATE SET
|
||||
artist = CASE WHEN excluded.artist <> '' THEN excluded.artist
|
||||
ELSE download_wants.artist END,
|
||||
title = CASE WHEN excluded.title <> '' THEN excluded.title
|
||||
ELSE download_wants.title END,
|
||||
scope = excluded.scope,
|
||||
secondary = excluded.secondary,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id;
|
||||
|
||||
-- name: GetDownloadWant :one
|
||||
SELECT * FROM download_wants WHERE id = ?;
|
||||
|
||||
-- name: GetDownloadWantByMBID :one
|
||||
SELECT * FROM download_wants WHERE mbid = ? AND library_id = ?;
|
||||
|
||||
-- name: ListDownloadWants :many
|
||||
SELECT * FROM download_wants
|
||||
ORDER BY
|
||||
CASE state WHEN 'wanted' THEN 0 WHEN 'paused' THEN 1 ELSE 2 END,
|
||||
artist, title;
|
||||
|
||||
-- name: ListDownloadWantsByEntity :many
|
||||
SELECT * FROM download_wants
|
||||
WHERE entity = ? AND state = ?
|
||||
ORDER BY id;
|
||||
|
||||
-- name: ListDueDownloadWants :many
|
||||
-- Everything the reconciler should act on this pass: wanted, not an
|
||||
-- artist subscription (those expand rather than download), and either
|
||||
-- never tried or past its backoff.
|
||||
SELECT * FROM download_wants
|
||||
WHERE state = 'wanted'
|
||||
AND entity <> 'artist'
|
||||
AND (next_try_at IS NULL OR next_try_at <= CURRENT_TIMESTAMP)
|
||||
ORDER BY attempts, created_at
|
||||
LIMIT ?;
|
||||
|
||||
-- name: ListChildDownloadWants :many
|
||||
SELECT * FROM download_wants WHERE parent_id = ? ORDER BY id;
|
||||
|
||||
-- name: SetDownloadWantState :exec
|
||||
UPDATE download_wants
|
||||
SET state = ?, last_error = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: RecordDownloadWantAttempt :exec
|
||||
UPDATE download_wants
|
||||
SET attempts = attempts + 1,
|
||||
last_error = ?,
|
||||
last_tried_at = CURRENT_TIMESTAMP,
|
||||
next_try_at = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: SatisfyDownloadWant :exec
|
||||
UPDATE download_wants
|
||||
SET state = 'satisfied', last_error = '', next_try_at = NULL,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: SetDownloadWantExternalIDs :exec
|
||||
UPDATE download_wants
|
||||
SET external_ids = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteDownloadWant :exec
|
||||
DELETE FROM download_wants WHERE id = ?;
|
||||
|
||||
-- name: DeleteSatisfiedDownloadWants :exec
|
||||
DELETE FROM download_wants WHERE state = 'satisfied';
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS libraries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
autotag_warning_acked INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
@@ -11,3 +11,6 @@ CREATE INDEX IF NOT EXISTS idx_artist_credit_artist_artist_id
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist_credit_artist_credit_id
|
||||
ON artist_credit_artist(credit_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_artist_credit_artist_unique
|
||||
ON artist_credit_artist(artist_id, credit_id);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS artist_images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
artist_mbid TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
source_url TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
is_primary INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
file_size INTEGER,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist_images_mbid
|
||||
ON artist_images(artist_mbid);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_artist_images_source
|
||||
ON artist_images(artist_mbid, source, source_url);
|
||||
@@ -2,6 +2,8 @@
|
||||
-- Sources: audiodb, fanart, wikidata-p18, wikipedia-lead, mb:artist-rels.
|
||||
-- No TTL — this data changes very rarely and is the backing store for
|
||||
-- the artist detail page.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS artist_metadata (
|
||||
mbid TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
@@ -9,4 +11,5 @@ CREATE TABLE IF NOT EXISTS artist_metadata (
|
||||
fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (mbid, source)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist_metadata_mbid ON artist_metadata(mbid);
|
||||
|
||||
@@ -3,3 +3,5 @@ CREATE TABLE IF NOT EXISTS artists (
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
mbid TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artists_mbid ON artists(mbid) WHERE mbid IS NOT NULL;
|
||||
|
||||
@@ -18,22 +18,28 @@ CREATE TABLE IF NOT EXISTS audio_files (
|
||||
'untagged', 'auto_matched', 'user_confirmed', 'user_skipped_permanent'
|
||||
)),
|
||||
group_key TEXT NOT NULL DEFAULT '',
|
||||
-- File mtime as a Unix timestamp in seconds, captured at import.
|
||||
-- Compared against the on-disk mtime during a scan to detect files
|
||||
-- another application retagged in place. 0 means "never recorded"
|
||||
-- (rows predating migration 47) and is treated as not-stale so an
|
||||
-- upgrade does not re-import the whole library.
|
||||
modified_at int NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(file_type_id) REFERENCES file_types(id),
|
||||
FOREIGN KEY(recording_id) REFERENCES recordings(id),
|
||||
FOREIGN KEY(library_id) REFERENCES libraries(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_files_basename
|
||||
ON audio_files(basename);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_files_group_key
|
||||
ON audio_files(group_key) WHERE group_key != '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_files_library_id
|
||||
ON audio_files(library_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_files_recording_id
|
||||
ON audio_files(recording_id);
|
||||
|
||||
-- idx_audio_files_library_id is created by migration 6 (not here) because
|
||||
-- on existing databases this schema file is a no-op (CREATE TABLE IF NOT EXISTS)
|
||||
-- and the library_id column doesn't exist until the migration adds it.
|
||||
--
|
||||
-- idx_audio_files_tag_status_untagged + idx_audio_files_group_key are
|
||||
-- created by migrations 31 and 32 for the same reason — on a pre-31
|
||||
-- database the partial index predicates (`WHERE tag_status = '...'`
|
||||
-- and `WHERE group_key != ''`) would reference columns that don't
|
||||
-- yet exist, since CREATE TABLE IF NOT EXISTS does not add columns
|
||||
-- to existing tables. sqlc still sees the columns above, and fresh
|
||||
-- DBs pick up the indexes inside the migrations.
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_files_tag_status_untagged
|
||||
ON audio_files(library_id) WHERE tag_status = 'untagged';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
-- One row per grab attempt against one candidate. A request can have
|
||||
-- several: the first pick stalls, the user picks another, or a
|
||||
-- search-only provider's candidate is fetched by a separate transport
|
||||
-- (in which case provider_id is the searcher and transport_id is the
|
||||
-- fetcher).
|
||||
--
|
||||
-- `candidate` is the full ranked Candidate as JSON. It is stored
|
||||
-- rather than re-derived because the provider's result set is
|
||||
-- ephemeral — a Soulseek peer that had the files an hour ago may be
|
||||
-- offline now, and the item still has to render in the UI and explain
|
||||
-- why it was chosen.
|
||||
--
|
||||
-- external_id holds a delegating manager's own identifier (a Lidarr
|
||||
-- queue id), which is how polling finds the record again after a
|
||||
-- restart.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS download_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
request_id TEXT NOT NULL,
|
||||
provider_id INTEGER NOT NULL,
|
||||
transport_id INTEGER,
|
||||
external_id TEXT NOT NULL DEFAULT '',
|
||||
candidate TEXT NOT NULL DEFAULT '{}',
|
||||
state TEXT NOT NULL DEFAULT 'queued'
|
||||
CHECK(state IN ('searching', 'found', 'queued', 'grabbing',
|
||||
'verifying', 'tagging', 'importing',
|
||||
'complete', 'cancelled', 'failed')),
|
||||
staging_dir TEXT NOT NULL DEFAULT '',
|
||||
bytes_done INTEGER NOT NULL DEFAULT 0,
|
||||
bytes_total INTEGER NOT NULL DEFAULT 0,
|
||||
-- imported_paths is a JSON array of the library paths the files
|
||||
-- ended up at, so an import can be undone without guessing.
|
||||
imported_paths TEXT NOT NULL DEFAULT '[]',
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(request_id) REFERENCES download_requests(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_items_live
|
||||
ON download_items(state)
|
||||
WHERE state NOT IN ('complete', 'cancelled', 'failed');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_items_request
|
||||
ON download_items(request_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_items_state
|
||||
ON download_items(state);
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Download clients the user has connected: an slskd daemon, a Lidarr
|
||||
-- instance, yt-dlp on PATH. One row per configured instance, so two
|
||||
-- Prowlarr servers or two Soulseek accounts coexist.
|
||||
--
|
||||
-- Secrets (API keys, passwords) are NOT stored here. They live in a
|
||||
-- 0600 file keyed by this row's id, so this table can be dumped into a
|
||||
-- bug report without redaction. `settings` holds only non-sensitive
|
||||
-- values (host, port, category, output format) as a JSON object.
|
||||
--
|
||||
-- `kind` names the adapter implementation and is looked up in the
|
||||
-- provider registry at startup; a row whose kind no longer exists is
|
||||
-- reported to the user rather than silently dropped.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS download_providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
kind TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
-- priority breaks ties between providers that found equally good
|
||||
-- candidates. Higher wins; 50 is the neutral default.
|
||||
priority INTEGER NOT NULL DEFAULT 50,
|
||||
settings TEXT NOT NULL DEFAULT '{}',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_providers_enabled
|
||||
ON download_providers(enabled);
|
||||
@@ -0,0 +1,49 @@
|
||||
-- One row per "go find me this", from the moment the user asks until
|
||||
-- the files are in the library or the attempt is abandoned.
|
||||
--
|
||||
-- release_mbid / release_group_mbid are the anchor: a request that
|
||||
-- carries one can be matched against a known tracklist at import time,
|
||||
-- which is what makes unattended completion safe. Free-text requests
|
||||
-- (both NULL) are always presented to the user for confirmation.
|
||||
--
|
||||
-- `expected` caches the anchor's tracklist as JSON so ranking and
|
||||
-- import do not have to re-resolve it, and so a request survives the
|
||||
-- explore index being rebuilt underneath it.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS download_requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
library_id INTEGER NOT NULL,
|
||||
-- source records where the request came from: 'explore-album',
|
||||
-- 'explore-artist', 'missing-album', 'wanted', 'manual'.
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
-- want_id is set when the reconciler raised this request from the
|
||||
-- wanted list, so the outcome can be written back to the want.
|
||||
-- NULL for one-off requests the user started by hand. Requests are
|
||||
-- disposable and wants are not, so the delete is a SET NULL rather
|
||||
-- than a cascade in either direction.
|
||||
want_id INTEGER REFERENCES download_wants(id) ON DELETE SET NULL,
|
||||
release_mbid TEXT,
|
||||
release_group_mbid TEXT,
|
||||
-- recording_mbid anchors a single-track request raised from a
|
||||
-- track-level want.
|
||||
recording_mbid TEXT,
|
||||
artist TEXT NOT NULL DEFAULT '',
|
||||
album TEXT NOT NULL DEFAULT '',
|
||||
query TEXT NOT NULL DEFAULT '',
|
||||
expected TEXT NOT NULL DEFAULT '[]',
|
||||
state TEXT NOT NULL DEFAULT 'searching'
|
||||
CHECK(state IN ('searching', 'found', 'queued', 'grabbing',
|
||||
'verifying', 'tagging', 'importing',
|
||||
'complete', 'cancelled', 'failed')),
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(library_id) REFERENCES libraries(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_requests_created
|
||||
ON download_requests(created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_requests_state
|
||||
ON download_requests(state);
|
||||
@@ -0,0 +1,76 @@
|
||||
-- One row per "go find me this", from the moment the user asks until
|
||||
-- the files are in the library or the attempt is abandoned.
|
||||
--
|
||||
-- release_mbid / release_group_mbid are the anchor: a request that
|
||||
-- carries one can be matched against a known tracklist at import time,
|
||||
-- which is what makes unattended completion safe. Free-text requests
|
||||
-- (both NULL) are always presented to the user for confirmation.
|
||||
--
|
||||
-- `expected` caches the anchor's tracklist as JSON so ranking and
|
||||
-- import do not have to re-resolve it, and so a request survives the
|
||||
-- explore index being rebuilt underneath it.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS download_wants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mbid TEXT NOT NULL,
|
||||
entity TEXT NOT NULL
|
||||
CHECK(entity IN ('artist', 'release-group', 'release', 'recording')),
|
||||
library_id INTEGER NOT NULL,
|
||||
|
||||
-- Display text, cached so the wanted list renders without touching
|
||||
-- the explore index. Neither is authoritative; the MBID is.
|
||||
artist TEXT NOT NULL DEFAULT '',
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
|
||||
scope TEXT NOT NULL DEFAULT 'future'
|
||||
CHECK(scope IN ('future', 'all')),
|
||||
|
||||
-- secondary controls whether an artist want's expansion includes
|
||||
-- compilations, live albums and remixes. Off by default: someone
|
||||
-- subscribing to an artist wants the albums, not six versions of
|
||||
-- the same greatest-hits package.
|
||||
secondary INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
state TEXT NOT NULL DEFAULT 'wanted'
|
||||
CHECK(state IN ('wanted', 'satisfied', 'paused')),
|
||||
|
||||
-- parent_id links a want the reconciler derived from an artist
|
||||
-- want. Deleting the artist takes its derived children with it,
|
||||
-- but children the user pinned themselves have no parent and stay.
|
||||
parent_id INTEGER,
|
||||
|
||||
-- Retry bookkeeping. attempts drives the backoff; last_error is
|
||||
-- the most recent reason it did not work out, which for a wanted
|
||||
-- item is information rather than a failure.
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
last_tried_at DATETIME,
|
||||
next_try_at DATETIME,
|
||||
|
||||
-- external_ids maps provider row ID to that provider's own
|
||||
-- identifier for this want, for clients that keep their own
|
||||
-- persistent list (a Lidarr artist ID). JSON object.
|
||||
external_ids TEXT NOT NULL DEFAULT '{}',
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- One want per thing per library. Asking twice is not two wants,
|
||||
-- and this is what lets an artist expansion re-run every reconcile
|
||||
-- without accumulating duplicates.
|
||||
UNIQUE(mbid, library_id),
|
||||
|
||||
FOREIGN KEY(library_id) REFERENCES libraries(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(parent_id) REFERENCES download_wants(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_wants_due
|
||||
ON download_wants(next_try_at)
|
||||
WHERE state = 'wanted';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_wants_entity
|
||||
ON download_wants(entity, state);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_download_wants_parent
|
||||
ON download_wants(parent_id);
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS explore_champion_fts USING fts5(
|
||||
title, artist_name, aliases,
|
||||
content='explore_index',
|
||||
content_rowid='id',
|
||||
tokenize='unicode61 remove_diacritics 2'
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
CREATE TABLE IF NOT EXISTS explore_index (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_type TEXT NOT NULL,
|
||||
mbid TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
artist_name TEXT NOT NULL,
|
||||
artist_mbid TEXT NOT NULL,
|
||||
aliases TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Popularity signals, derived from the ListenBrainz listens dump.
|
||||
popularity INTEGER NOT NULL DEFAULT 0,
|
||||
listener_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Recording-specific fields.
|
||||
duration INTEGER NOT NULL DEFAULT 0,
|
||||
caa_release_mbid TEXT NOT NULL DEFAULT '',
|
||||
release_name TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Release-group-specific fields.
|
||||
primary_type TEXT NOT NULL DEFAULT '',
|
||||
secondary_types TEXT NOT NULL DEFAULT '',
|
||||
release_date TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Artist-specific fields.
|
||||
artist_type TEXT NOT NULL DEFAULT '',
|
||||
country TEXT NOT NULL DEFAULT '',
|
||||
disambiguation TEXT NOT NULL DEFAULT '',
|
||||
sort_name TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Personalization flags.
|
||||
in_library INTEGER NOT NULL DEFAULT 0,
|
||||
is_similar INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Cross-reference to local library tables. NULL when the
|
||||
-- entity has no corresponding row in the library.
|
||||
local_artist_id INTEGER,
|
||||
local_release_group_id INTEGER,
|
||||
local_recording_id INTEGER,
|
||||
|
||||
-- Set once an artist's full discography (release groups +
|
||||
-- recordings) has been fetched, so EnsureArtistDiscography can
|
||||
-- skip artists the catalog already covers.
|
||||
discog_fetched INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
|
||||
UNIQUE(mbid)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_explore_artist_lower
|
||||
ON explore_index(LOWER(artist_name))
|
||||
WHERE popularity > 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_explore_caa_release
|
||||
ON explore_index(caa_release_mbid)
|
||||
WHERE entity_type = 'release_group' AND caa_release_mbid != '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_explore_index_artist_mbid
|
||||
ON explore_index(artist_mbid, entity_type, popularity DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_explore_index_entity_pop
|
||||
ON explore_index(entity_type, popularity DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_explore_title_lower
|
||||
ON explore_index(LOWER(title))
|
||||
WHERE popularity > 0;
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS explore_index_fts USING fts5(
|
||||
title, artist_name, aliases,
|
||||
content='explore_index',
|
||||
content_rowid='id',
|
||||
tokenize='unicode61 remove_diacritics 2'
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS explore_index_meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
@@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS file_types (
|
||||
extension text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
-- Seed rows: the supported audio formats, referenced by
|
||||
-- audio_files.file_type_id.
|
||||
INSERT OR IGNORE INTO file_types (id, extension) VALUES (0, '.mp3');
|
||||
INSERT OR IGNORE INTO file_types (id, extension) VALUES (1, '.flac');
|
||||
INSERT OR IGNORE INTO file_types (id, extension) VALUES (2, '.ogg');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
-- Short-lived HTTP response cache (search results, MB/LB lookups, etc).
|
||||
-- For long-lived enrichment data keyed by MBID, see artist_metadata.sql.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS http_cache (
|
||||
url_key TEXT PRIMARY KEY,
|
||||
response BLOB NOT NULL,
|
||||
@@ -7,5 +9,7 @@ CREATE TABLE IF NOT EXISTS http_cache (
|
||||
entity_mbid TEXT NOT NULL DEFAULT '',
|
||||
entity_type TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_http_cache_expires ON http_cache(expires_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_http_cache_mbid ON http_cache(entity_mbid);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
-- Rows are written when a durable job enters the paused state and
|
||||
-- deleted on resume, cancel, or completion — this is not a job history
|
||||
-- table, and it stays at zero rows in the common case.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS job_state (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- One row per "go find me this", from the moment the user asks until
|
||||
-- the files are in the library or the attempt is abandoned.
|
||||
--
|
||||
-- release_mbid / release_group_mbid are the anchor: a request that
|
||||
-- carries one can be matched against a known tracklist at import time,
|
||||
-- which is what makes unattended completion safe. Free-text requests
|
||||
-- (both NULL) are always presented to the user for confirmation.
|
||||
--
|
||||
-- `expected` caches the anchor's tracklist as JSON so ranking and
|
||||
-- import do not have to re-resolve it, and so a request survives the
|
||||
-- explore index being rebuilt underneath it.
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS libraries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
autotag_warning_acked INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
@@ -7,6 +7,8 @@
|
||||
-- tokenised inverted index, so it stays compact even for large
|
||||
-- libraries. contentless_delete=1 lets us delete/reinsert a single
|
||||
-- row when a track's lyrics change (scan update or LRCLIB backfill).
|
||||
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS lyrics_index USING fts5(
|
||||
lyrics,
|
||||
content='',
|
||||
|
||||
@@ -6,4 +6,5 @@ CREATE TABLE IF NOT EXISTS player_state (
|
||||
last_position_seconds INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- Singleton row: player state is a single mutable record.
|
||||
INSERT OR IGNORE INTO player_state (id) VALUES (1);
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS playlist_tracks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
playlist_id INTEGER NOT NULL,
|
||||
audio_file_id INTEGER,
|
||||
position INTEGER NOT NULL,
|
||||
phantom_title TEXT,
|
||||
phantom_artist TEXT,
|
||||
phantom_album TEXT,
|
||||
phantom_duration_ms INTEGER,
|
||||
phantom_genre TEXT,
|
||||
phantom_cover_art_path TEXT,
|
||||
phantom_file_path TEXT,
|
||||
FOREIGN KEY(playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_playlist_tracks_playlist_id
|
||||
ON playlist_tracks(playlist_id);
|
||||
CREATE TABLE IF NOT EXISTS "playlist_tracks" (
|
||||
id INTEGER PRIMARY KEY,
|
||||
playlist_id INTEGER NOT NULL,
|
||||
audio_file_id INTEGER,
|
||||
position INTEGER NOT NULL,
|
||||
phantom_title TEXT,
|
||||
phantom_artist TEXT,
|
||||
phantom_album TEXT,
|
||||
phantom_duration_ms INTEGER,
|
||||
phantom_genre TEXT,
|
||||
phantom_cover_art_path TEXT, phantom_file_path TEXT,
|
||||
FOREIGN KEY(playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_playlist_tracks_audio_file_id
|
||||
ON playlist_tracks(audio_file_id);
|
||||
ON playlist_tracks(audio_file_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_playlist_tracks_playlist_id
|
||||
ON playlist_tracks(playlist_id);
|
||||
|
||||
@@ -8,4 +8,5 @@ CREATE TABLE IF NOT EXISTS queue (
|
||||
FOREIGN KEY(source_playlist_id) REFERENCES playlists(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Singleton row: there is exactly one playback queue.
|
||||
INSERT OR IGNORE INTO queue (id) VALUES (1);
|
||||
|
||||
+3
-3
@@ -7,8 +7,8 @@ CREATE TABLE IF NOT EXISTS recording_genres (
|
||||
UNIQUE(recording_id, genre_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_genres_recording_id
|
||||
ON recording_genres(recording_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_genres_genre_id
|
||||
ON recording_genres(genre_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_genres_recording_id
|
||||
ON recording_genres(recording_id);
|
||||
@@ -15,3 +15,5 @@ CREATE TABLE IF NOT EXISTS recordings (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recordings_artist_credit_id
|
||||
ON recordings(artist_credit_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recordings_mbid ON recordings(mbid) WHERE mbid IS NOT NULL;
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS release_groups (
|
||||
CREATE TABLE IF NOT EXISTS "release_groups" (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
cover_art_id INTEGER,
|
||||
album_artist_credit_id INTEGER,
|
||||
-- year is the *technical release year* of the album as it lives
|
||||
-- in the user's library — typically the file's ID3 year tag,
|
||||
-- which for remasters/reissues is the reissue year.
|
||||
year INTEGER,
|
||||
-- original_year is the album's *first-release-date* year sourced
|
||||
-- from MusicBrainz' release-group.first-release-date. For a 2010
|
||||
-- remaster of a 1973 album, year=2010 and original_year=1973.
|
||||
-- Populated by autotag apply; NULL until the user accepts a
|
||||
-- candidate (or for libraries that have never been autotagged).
|
||||
-- Reads should COALESCE(original_year, year) to get the
|
||||
-- preferred user-facing year.
|
||||
original_year INTEGER,
|
||||
total_tracks INTEGER,
|
||||
total_discs INTEGER,
|
||||
mbid TEXT,
|
||||
total_discs INTEGER, mbid TEXT, original_year INTEGER,
|
||||
FOREIGN KEY(cover_art_id) REFERENCES cover_art(id),
|
||||
FOREIGN KEY(album_artist_credit_id) REFERENCES artist_credit(id),
|
||||
UNIQUE(name, album_artist_credit_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_release_groups_cover_art_id
|
||||
ON release_groups(cover_art_id);
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_release_groups_album_artist_credit_id
|
||||
ON release_groups(album_artist_credit_id);
|
||||
ON release_groups(album_artist_credit_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_release_groups_cover_art_id
|
||||
ON release_groups(cover_art_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_release_groups_mbid ON release_groups(mbid) WHERE mbid IS NOT NULL;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS release_to_rg (
|
||||
release_mbid TEXT PRIMARY KEY,
|
||||
rg_mbid TEXT NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS search_clicks (
|
||||
query TEXT NOT NULL,
|
||||
entity_mbid TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
click_count INTEGER NOT NULL DEFAULT 1,
|
||||
last_clicked DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (query, entity_mbid)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_search_clicks_query
|
||||
ON search_clicks(query);
|
||||
@@ -1,9 +1,9 @@
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
||||
file_path,
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
content='',
|
||||
contentless_delete=1,
|
||||
tokenize='unicode61 remove_diacritics 2'
|
||||
);
|
||||
file_path,
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
content='',
|
||||
contentless_delete=1,
|
||||
tokenize='unicode61 remove_diacritics 2'
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS similar_artist_map (
|
||||
source_artist_mbid TEXT NOT NULL,
|
||||
similar_artist_mbid TEXT NOT NULL,
|
||||
similar_artist_name TEXT NOT NULL,
|
||||
score INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (source_artist_mbid, similar_artist_mbid)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_similar_artist_map_source
|
||||
ON similar_artist_map(source_artist_mbid);
|
||||
@@ -10,6 +10,8 @@
|
||||
-- CASCADE ties the blob's lifetime to its tagging_items row: when a
|
||||
-- group's tracks change, the scan path deletes the old group_key row
|
||||
-- (and SQLite, with foreign_keys = ON, drops the stale blob with it).
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tagging_candidates (
|
||||
group_key TEXT PRIMARY KEY,
|
||||
candidates TEXT NOT NULL,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
CREATE VIEW IF NOT EXISTS track_metadata AS
|
||||
SELECT
|
||||
af.id,
|
||||
af.file_path,
|
||||
af.length_milliseconds,
|
||||
COALESCE(r.name, '') AS title,
|
||||
COALESCE(ac.text, '') AS artist_name,
|
||||
r.track_number,
|
||||
r.disc_number,
|
||||
COALESCE(rg.name, '') AS album,
|
||||
CAST(COALESCE(
|
||||
(SELECT GROUP_CONCAT(g.name, '||')
|
||||
FROM recording_genres rg_sub
|
||||
JOIN genres g ON rg_sub.genre_id = g.id
|
||||
WHERE rg_sub.recording_id = r.id),
|
||||
''
|
||||
) AS TEXT) AS genre,
|
||||
COALESCE(rg.original_year, rg.year, r.year, 0) AS year,
|
||||
COALESCE(rg.year, r.year, 0) AS release_year,
|
||||
COALESCE(r.composer, '') AS composer,
|
||||
COALESCE(ft.extension, '') AS file_type,
|
||||
af.sample_rate,
|
||||
af.bit_depth,
|
||||
af.channels,
|
||||
af.bitrate,
|
||||
af.file_size,
|
||||
af.library_id,
|
||||
af.play_count,
|
||||
af.last_played,
|
||||
COALESCE(ca.file_path, '') AS cover_art_path,
|
||||
COALESCE(a.mbid, '') AS artist_mbid,
|
||||
COALESCE(rg.mbid, '') AS release_group_mbid,
|
||||
COALESCE(r.mbid, '') AS recording_mbid
|
||||
FROM audio_files af
|
||||
LEFT JOIN recordings r ON af.recording_id = r.id
|
||||
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
|
||||
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
|
||||
LEFT JOIN artists a ON a.id = aca.artist_id
|
||||
LEFT JOIN (
|
||||
SELECT recording_id,
|
||||
MIN(release_group_id) AS release_group_id
|
||||
FROM release_group_recordings
|
||||
GROUP BY recording_id
|
||||
) rgr ON r.id = rgr.recording_id
|
||||
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
|
||||
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
|
||||
LEFT JOIN file_types ft ON af.file_type_id = ft.id;
|
||||
@@ -1,52 +0,0 @@
|
||||
CREATE VIEW IF NOT EXISTS track_metadata AS
|
||||
SELECT
|
||||
af.id,
|
||||
af.file_path,
|
||||
af.length_milliseconds,
|
||||
COALESCE(r.name, '') AS title,
|
||||
COALESCE(ac.text, '') AS artist_name,
|
||||
r.track_number,
|
||||
r.disc_number,
|
||||
COALESCE(rg.name, '') AS album,
|
||||
CAST(COALESCE(
|
||||
(SELECT GROUP_CONCAT(g.name, '||')
|
||||
FROM recording_genres rg_sub
|
||||
JOIN genres g ON rg_sub.genre_id = g.id
|
||||
WHERE rg_sub.recording_id = r.id),
|
||||
''
|
||||
) AS TEXT) AS genre,
|
||||
-- Year defaults to the release group's original release year
|
||||
-- (MusicBrainz first-release-date) so a 1973 album shows as
|
||||
-- 1973 even if the user owns the 2010 remaster. Falls back
|
||||
-- to release-group year (file tag), then to recording year.
|
||||
-- See release_groups.original_year for full semantics.
|
||||
COALESCE(rg.original_year, rg.year, r.year, 0) AS year,
|
||||
COALESCE(rg.year, r.year, 0) AS release_year,
|
||||
COALESCE(r.composer, '') AS composer,
|
||||
COALESCE(ft.extension, '') AS file_type,
|
||||
af.sample_rate,
|
||||
af.bit_depth,
|
||||
af.channels,
|
||||
af.bitrate,
|
||||
af.file_size,
|
||||
af.library_id,
|
||||
af.play_count,
|
||||
af.last_played,
|
||||
COALESCE(ca.file_path, '') AS cover_art_path,
|
||||
COALESCE(a.mbid, '') AS artist_mbid,
|
||||
COALESCE(rg.mbid, '') AS release_group_mbid,
|
||||
COALESCE(r.mbid, '') AS recording_mbid
|
||||
FROM audio_files af
|
||||
LEFT JOIN recordings r ON af.recording_id = r.id
|
||||
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
|
||||
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
|
||||
LEFT JOIN artists a ON a.id = aca.artist_id
|
||||
LEFT JOIN (
|
||||
SELECT recording_id,
|
||||
MIN(release_group_id) AS release_group_id
|
||||
FROM release_group_recordings
|
||||
GROUP BY recording_id
|
||||
) rgr ON r.id = rgr.recording_id
|
||||
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
|
||||
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
|
||||
LEFT JOIN file_types ft ON af.file_type_id = ft.id;
|
||||
@@ -35,7 +35,7 @@ func (q *Queries) CountAudioFilesByLibrary(ctx context.Context, libraryID int64)
|
||||
|
||||
const createAudioFile = `-- name: CreateAudioFile :one
|
||||
INSERT INTO audio_files (file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key
|
||||
RETURNING id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key, modified_at
|
||||
`
|
||||
|
||||
type CreateAudioFileParams struct {
|
||||
@@ -84,6 +84,7 @@ func (q *Queries) CreateAudioFile(ctx context.Context, arg CreateAudioFileParams
|
||||
&i.LastPlayed,
|
||||
&i.TagStatus,
|
||||
&i.GroupKey,
|
||||
&i.ModifiedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -92,9 +93,9 @@ const createAudioFileWithGroupKey = `-- name: CreateAudioFileWithGroupKey :one
|
||||
INSERT INTO audio_files (
|
||||
file_path, length_milliseconds, file_type_id, recording_id,
|
||||
sample_rate, bit_depth, channels, bitrate, file_size, basename,
|
||||
library_id, group_key, tag_status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key
|
||||
library_id, group_key, tag_status, modified_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key, modified_at
|
||||
`
|
||||
|
||||
type CreateAudioFileWithGroupKeyParams struct {
|
||||
@@ -111,6 +112,7 @@ type CreateAudioFileWithGroupKeyParams struct {
|
||||
LibraryID int64
|
||||
GroupKey string
|
||||
TagStatus string
|
||||
ModifiedAt int64
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAudioFileWithGroupKey(ctx context.Context, arg CreateAudioFileWithGroupKeyParams) (AudioFile, error) {
|
||||
@@ -128,6 +130,7 @@ func (q *Queries) CreateAudioFileWithGroupKey(ctx context.Context, arg CreateAud
|
||||
arg.LibraryID,
|
||||
arg.GroupKey,
|
||||
arg.TagStatus,
|
||||
arg.ModifiedAt,
|
||||
)
|
||||
var i AudioFile
|
||||
err := row.Scan(
|
||||
@@ -147,6 +150,7 @@ func (q *Queries) CreateAudioFileWithGroupKey(ctx context.Context, arg CreateAud
|
||||
&i.LastPlayed,
|
||||
&i.TagStatus,
|
||||
&i.GroupKey,
|
||||
&i.ModifiedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -203,7 +207,7 @@ func (q *Queries) GetAllAudioFilePaths(ctx context.Context) ([]GetAllAudioFilePa
|
||||
}
|
||||
|
||||
const getAllAudioFiles = `-- name: GetAllAudioFiles :many
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key FROM audio_files
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key, modified_at FROM audio_files
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllAudioFiles(ctx context.Context) ([]AudioFile, error) {
|
||||
@@ -232,6 +236,7 @@ func (q *Queries) GetAllAudioFiles(ctx context.Context) ([]AudioFile, error) {
|
||||
&i.LastPlayed,
|
||||
&i.TagStatus,
|
||||
&i.GroupKey,
|
||||
&i.ModifiedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -527,7 +532,7 @@ func (q *Queries) GetAllTracksWithFullMetadataByLibrary(ctx context.Context, lib
|
||||
}
|
||||
|
||||
const getAudioFile = `-- name: GetAudioFile :one
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key FROM audio_files
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key, modified_at FROM audio_files
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
@@ -551,12 +556,13 @@ func (q *Queries) GetAudioFile(ctx context.Context, id int64) (AudioFile, error)
|
||||
&i.LastPlayed,
|
||||
&i.TagStatus,
|
||||
&i.GroupKey,
|
||||
&i.ModifiedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAudioFileByPath = `-- name: GetAudioFileByPath :one
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key FROM audio_files
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key, modified_at FROM audio_files
|
||||
WHERE file_path = ? LIMIT 1
|
||||
`
|
||||
|
||||
@@ -580,6 +586,7 @@ func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (Audi
|
||||
&i.LastPlayed,
|
||||
&i.TagStatus,
|
||||
&i.GroupKey,
|
||||
&i.ModifiedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -597,7 +604,7 @@ func (q *Queries) GetAudioFileGroupKey(ctx context.Context, id int64) (string, e
|
||||
}
|
||||
|
||||
const getAudioFilesByLibrary = `-- name: GetAudioFilesByLibrary :many
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key FROM audio_files WHERE library_id = ?
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key, modified_at FROM audio_files WHERE library_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetAudioFilesByLibrary(ctx context.Context, libraryID int64) ([]AudioFile, error) {
|
||||
@@ -626,6 +633,7 @@ func (q *Queries) GetAudioFilesByLibrary(ctx context.Context, libraryID int64) (
|
||||
&i.LastPlayed,
|
||||
&i.TagStatus,
|
||||
&i.GroupKey,
|
||||
&i.ModifiedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -854,7 +862,7 @@ func (q *Queries) GetAudioFilesByReleaseGroupByLibrary(ctx context.Context, arg
|
||||
}
|
||||
|
||||
const getAudioFilesNeedingMetadata = `-- name: GetAudioFilesNeedingMetadata :many
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key FROM audio_files
|
||||
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id, play_count, last_played, tag_status, group_key, modified_at FROM audio_files
|
||||
WHERE recording_id = 0
|
||||
`
|
||||
|
||||
@@ -884,6 +892,7 @@ func (q *Queries) GetAudioFilesNeedingMetadata(ctx context.Context) ([]AudioFile
|
||||
&i.LastPlayed,
|
||||
&i.TagStatus,
|
||||
&i.GroupKey,
|
||||
&i.ModifiedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -898,6 +907,20 @@ func (q *Queries) GetAudioFilesNeedingMetadata(ctx context.Context) ([]AudioFile
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLibraryMaxModifiedAt = `-- name: GetLibraryMaxModifiedAt :one
|
||||
SELECT CAST(COALESCE(MAX(modified_at), 0) AS INTEGER) FROM audio_files
|
||||
WHERE library_id = ?
|
||||
`
|
||||
|
||||
// Newest recorded mtime in a library, for the startup soft scan. 0 when
|
||||
// the library is empty or no row has a baseline yet.
|
||||
func (q *Queries) GetLibraryMaxModifiedAt(ctx context.Context, libraryID int64) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, getLibraryMaxModifiedAt, libraryID)
|
||||
var column_1 int64
|
||||
err := row.Scan(&column_1)
|
||||
return column_1, err
|
||||
}
|
||||
|
||||
const getRandomAudioFilePath = `-- name: GetRandomAudioFilePath :one
|
||||
SELECT file_path FROM audio_files
|
||||
ORDER BY RANDOM()
|
||||
@@ -1139,18 +1162,20 @@ func (q *Queries) UpdateAudioFile(ctx context.Context, arg UpdateAudioFileParams
|
||||
|
||||
const updateAudioFileRecording = `-- name: UpdateAudioFileRecording :exec
|
||||
UPDATE audio_files
|
||||
SET recording_id = ?, sample_rate = ?, bit_depth = ?, channels = ?, bitrate = ?, file_size = ?
|
||||
SET recording_id = ?, sample_rate = ?, bit_depth = ?, channels = ?, bitrate = ?, file_size = ?, length_milliseconds = ?, modified_at = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateAudioFileRecordingParams struct {
|
||||
RecordingID int64
|
||||
SampleRate int64
|
||||
BitDepth int64
|
||||
Channels int64
|
||||
Bitrate int64
|
||||
FileSize int64
|
||||
ID int64
|
||||
RecordingID int64
|
||||
SampleRate int64
|
||||
BitDepth int64
|
||||
Channels int64
|
||||
Bitrate int64
|
||||
FileSize int64
|
||||
LengthMilliseconds int64
|
||||
ModifiedAt int64
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAudioFileRecording(ctx context.Context, arg UpdateAudioFileRecordingParams) error {
|
||||
@@ -1161,7 +1186,29 @@ func (q *Queries) UpdateAudioFileRecording(ctx context.Context, arg UpdateAudioF
|
||||
arg.Channels,
|
||||
arg.Bitrate,
|
||||
arg.FileSize,
|
||||
arg.LengthMilliseconds,
|
||||
arg.ModifiedAt,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateAudioFileStat = `-- name: UpdateAudioFileStat :exec
|
||||
UPDATE audio_files
|
||||
SET modified_at = ?, file_size = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateAudioFileStatParams struct {
|
||||
ModifiedAt int64
|
||||
FileSize int64
|
||||
ID int64
|
||||
}
|
||||
|
||||
// Records the on-disk mtime/size without re-reading tags. Used to
|
||||
// backfill the staleness baseline for files the scan skipped, and to
|
||||
// re-baseline after YellowJacket's own tag writer rewrites a file.
|
||||
func (q *Queries) UpdateAudioFileStat(ctx context.Context, arg UpdateAudioFileStatParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateAudioFileStat, arg.ModifiedAt, arg.FileSize, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,959 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: download.sql
|
||||
|
||||
package sqlcgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createDownloadItem = `-- name: CreateDownloadItem :exec
|
||||
INSERT INTO download_items (
|
||||
id, request_id, provider_id, transport_id, external_id,
|
||||
candidate, state, staging_dir, bytes_total
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type CreateDownloadItemParams struct {
|
||||
ID string
|
||||
RequestID string
|
||||
ProviderID int64
|
||||
TransportID sql.NullInt64
|
||||
ExternalID string
|
||||
Candidate string
|
||||
State string
|
||||
StagingDir string
|
||||
BytesTotal int64
|
||||
}
|
||||
|
||||
func (q *Queries) CreateDownloadItem(ctx context.Context, arg CreateDownloadItemParams) error {
|
||||
_, err := q.db.ExecContext(ctx, createDownloadItem,
|
||||
arg.ID,
|
||||
arg.RequestID,
|
||||
arg.ProviderID,
|
||||
arg.TransportID,
|
||||
arg.ExternalID,
|
||||
arg.Candidate,
|
||||
arg.State,
|
||||
arg.StagingDir,
|
||||
arg.BytesTotal,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const createDownloadProvider = `-- name: CreateDownloadProvider :one
|
||||
INSERT INTO download_providers (kind, name, enabled, priority, settings)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateDownloadProviderParams struct {
|
||||
Kind string
|
||||
Name string
|
||||
Enabled int64
|
||||
Priority int64
|
||||
Settings string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateDownloadProvider(ctx context.Context, arg CreateDownloadProviderParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, createDownloadProvider,
|
||||
arg.Kind,
|
||||
arg.Name,
|
||||
arg.Enabled,
|
||||
arg.Priority,
|
||||
arg.Settings,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const createDownloadRequest = `-- name: CreateDownloadRequest :exec
|
||||
INSERT INTO download_requests (
|
||||
id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type CreateDownloadRequestParams struct {
|
||||
ID string
|
||||
LibraryID int64
|
||||
Source string
|
||||
WantID sql.NullInt64
|
||||
ReleaseMbid sql.NullString
|
||||
ReleaseGroupMbid sql.NullString
|
||||
RecordingMbid sql.NullString
|
||||
Artist string
|
||||
Album string
|
||||
Query string
|
||||
Expected string
|
||||
State string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateDownloadRequest(ctx context.Context, arg CreateDownloadRequestParams) error {
|
||||
_, err := q.db.ExecContext(ctx, createDownloadRequest,
|
||||
arg.ID,
|
||||
arg.LibraryID,
|
||||
arg.Source,
|
||||
arg.WantID,
|
||||
arg.ReleaseMbid,
|
||||
arg.ReleaseGroupMbid,
|
||||
arg.RecordingMbid,
|
||||
arg.Artist,
|
||||
arg.Album,
|
||||
arg.Query,
|
||||
arg.Expected,
|
||||
arg.State,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDownloadProvider = `-- name: DeleteDownloadProvider :exec
|
||||
DELETE FROM download_providers
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteDownloadProvider(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteDownloadProvider, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDownloadRequest = `-- name: DeleteDownloadRequest :exec
|
||||
DELETE FROM download_requests
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteDownloadRequest(ctx context.Context, id string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteDownloadRequest, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDownloadWant = `-- name: DeleteDownloadWant :exec
|
||||
DELETE FROM download_wants WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteDownloadWant(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteDownloadWant, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteFinishedDownloadRequests = `-- name: DeleteFinishedDownloadRequests :exec
|
||||
DELETE FROM download_requests
|
||||
WHERE state IN ('complete', 'cancelled', 'failed')
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteFinishedDownloadRequests(ctx context.Context) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteFinishedDownloadRequests)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteSatisfiedDownloadWants = `-- name: DeleteSatisfiedDownloadWants :exec
|
||||
DELETE FROM download_wants WHERE state = 'satisfied'
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSatisfiedDownloadWants(ctx context.Context) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteSatisfiedDownloadWants)
|
||||
return err
|
||||
}
|
||||
|
||||
const getDownloadItem = `-- name: GetDownloadItem :one
|
||||
SELECT id, request_id, provider_id, transport_id, external_id, candidate,
|
||||
state, staging_dir, bytes_done, bytes_total, imported_paths,
|
||||
error, created_at, updated_at
|
||||
FROM download_items
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetDownloadItem(ctx context.Context, id string) (DownloadItem, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDownloadItem, id)
|
||||
var i DownloadItem
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.RequestID,
|
||||
&i.ProviderID,
|
||||
&i.TransportID,
|
||||
&i.ExternalID,
|
||||
&i.Candidate,
|
||||
&i.State,
|
||||
&i.StagingDir,
|
||||
&i.BytesDone,
|
||||
&i.BytesTotal,
|
||||
&i.ImportedPaths,
|
||||
&i.Error,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDownloadProvider = `-- name: GetDownloadProvider :one
|
||||
SELECT id, kind, name, enabled, priority, settings, created_at
|
||||
FROM download_providers
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetDownloadProvider(ctx context.Context, id int64) (DownloadProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDownloadProvider, id)
|
||||
var i DownloadProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Kind,
|
||||
&i.Name,
|
||||
&i.Enabled,
|
||||
&i.Priority,
|
||||
&i.Settings,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDownloadRequest = `-- name: GetDownloadRequest :one
|
||||
SELECT id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state, error,
|
||||
created_at, updated_at
|
||||
FROM download_requests
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetDownloadRequest(ctx context.Context, id string) (DownloadRequest, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDownloadRequest, id)
|
||||
var i DownloadRequest
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.LibraryID,
|
||||
&i.Source,
|
||||
&i.WantID,
|
||||
&i.ReleaseMbid,
|
||||
&i.ReleaseGroupMbid,
|
||||
&i.RecordingMbid,
|
||||
&i.Artist,
|
||||
&i.Album,
|
||||
&i.Query,
|
||||
&i.Expected,
|
||||
&i.State,
|
||||
&i.Error,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDownloadWant = `-- name: GetDownloadWant :one
|
||||
SELECT id, mbid, entity, library_id, artist, title, scope, secondary, state, parent_id, attempts, last_error, last_tried_at, next_try_at, external_ids, created_at, updated_at FROM download_wants WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetDownloadWant(ctx context.Context, id int64) (DownloadWant, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDownloadWant, id)
|
||||
var i DownloadWant
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Mbid,
|
||||
&i.Entity,
|
||||
&i.LibraryID,
|
||||
&i.Artist,
|
||||
&i.Title,
|
||||
&i.Scope,
|
||||
&i.Secondary,
|
||||
&i.State,
|
||||
&i.ParentID,
|
||||
&i.Attempts,
|
||||
&i.LastError,
|
||||
&i.LastTriedAt,
|
||||
&i.NextTryAt,
|
||||
&i.ExternalIds,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDownloadWantByMBID = `-- name: GetDownloadWantByMBID :one
|
||||
SELECT id, mbid, entity, library_id, artist, title, scope, secondary, state, parent_id, attempts, last_error, last_tried_at, next_try_at, external_ids, created_at, updated_at FROM download_wants WHERE mbid = ? AND library_id = ?
|
||||
`
|
||||
|
||||
type GetDownloadWantByMBIDParams struct {
|
||||
Mbid string
|
||||
LibraryID int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetDownloadWantByMBID(ctx context.Context, arg GetDownloadWantByMBIDParams) (DownloadWant, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDownloadWantByMBID, arg.Mbid, arg.LibraryID)
|
||||
var i DownloadWant
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Mbid,
|
||||
&i.Entity,
|
||||
&i.LibraryID,
|
||||
&i.Artist,
|
||||
&i.Title,
|
||||
&i.Scope,
|
||||
&i.Secondary,
|
||||
&i.State,
|
||||
&i.ParentID,
|
||||
&i.Attempts,
|
||||
&i.LastError,
|
||||
&i.LastTriedAt,
|
||||
&i.NextTryAt,
|
||||
&i.ExternalIds,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listChildDownloadWants = `-- name: ListChildDownloadWants :many
|
||||
SELECT id, mbid, entity, library_id, artist, title, scope, secondary, state, parent_id, attempts, last_error, last_tried_at, next_try_at, external_ids, created_at, updated_at FROM download_wants WHERE parent_id = ? ORDER BY id
|
||||
`
|
||||
|
||||
func (q *Queries) ListChildDownloadWants(ctx context.Context, parentID sql.NullInt64) ([]DownloadWant, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listChildDownloadWants, parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadWant
|
||||
for rows.Next() {
|
||||
var i DownloadWant
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Mbid,
|
||||
&i.Entity,
|
||||
&i.LibraryID,
|
||||
&i.Artist,
|
||||
&i.Title,
|
||||
&i.Scope,
|
||||
&i.Secondary,
|
||||
&i.State,
|
||||
&i.ParentID,
|
||||
&i.Attempts,
|
||||
&i.LastError,
|
||||
&i.LastTriedAt,
|
||||
&i.NextTryAt,
|
||||
&i.ExternalIds,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDownloadItemsForRequest = `-- name: ListDownloadItemsForRequest :many
|
||||
SELECT id, request_id, provider_id, transport_id, external_id, candidate,
|
||||
state, staging_dir, bytes_done, bytes_total, imported_paths,
|
||||
error, created_at, updated_at
|
||||
FROM download_items
|
||||
WHERE request_id = ?
|
||||
ORDER BY created_at
|
||||
`
|
||||
|
||||
func (q *Queries) ListDownloadItemsForRequest(ctx context.Context, requestID string) ([]DownloadItem, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDownloadItemsForRequest, requestID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadItem
|
||||
for rows.Next() {
|
||||
var i DownloadItem
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.RequestID,
|
||||
&i.ProviderID,
|
||||
&i.TransportID,
|
||||
&i.ExternalID,
|
||||
&i.Candidate,
|
||||
&i.State,
|
||||
&i.StagingDir,
|
||||
&i.BytesDone,
|
||||
&i.BytesTotal,
|
||||
&i.ImportedPaths,
|
||||
&i.Error,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDownloadProviders = `-- name: ListDownloadProviders :many
|
||||
SELECT id, kind, name, enabled, priority, settings, created_at
|
||||
FROM download_providers
|
||||
ORDER BY priority DESC, name
|
||||
`
|
||||
|
||||
func (q *Queries) ListDownloadProviders(ctx context.Context) ([]DownloadProvider, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDownloadProviders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadProvider
|
||||
for rows.Next() {
|
||||
var i DownloadProvider
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Kind,
|
||||
&i.Name,
|
||||
&i.Enabled,
|
||||
&i.Priority,
|
||||
&i.Settings,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDownloadRequests = `-- name: ListDownloadRequests :many
|
||||
SELECT id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state, error,
|
||||
created_at, updated_at
|
||||
FROM download_requests
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
func (q *Queries) ListDownloadRequests(ctx context.Context, limit int64) ([]DownloadRequest, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDownloadRequests, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadRequest
|
||||
for rows.Next() {
|
||||
var i DownloadRequest
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.LibraryID,
|
||||
&i.Source,
|
||||
&i.WantID,
|
||||
&i.ReleaseMbid,
|
||||
&i.ReleaseGroupMbid,
|
||||
&i.RecordingMbid,
|
||||
&i.Artist,
|
||||
&i.Album,
|
||||
&i.Query,
|
||||
&i.Expected,
|
||||
&i.State,
|
||||
&i.Error,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDownloadWants = `-- name: ListDownloadWants :many
|
||||
SELECT id, mbid, entity, library_id, artist, title, scope, secondary, state, parent_id, attempts, last_error, last_tried_at, next_try_at, external_ids, created_at, updated_at FROM download_wants
|
||||
ORDER BY
|
||||
CASE state WHEN 'wanted' THEN 0 WHEN 'paused' THEN 1 ELSE 2 END,
|
||||
artist, title
|
||||
`
|
||||
|
||||
func (q *Queries) ListDownloadWants(ctx context.Context) ([]DownloadWant, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDownloadWants)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadWant
|
||||
for rows.Next() {
|
||||
var i DownloadWant
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Mbid,
|
||||
&i.Entity,
|
||||
&i.LibraryID,
|
||||
&i.Artist,
|
||||
&i.Title,
|
||||
&i.Scope,
|
||||
&i.Secondary,
|
||||
&i.State,
|
||||
&i.ParentID,
|
||||
&i.Attempts,
|
||||
&i.LastError,
|
||||
&i.LastTriedAt,
|
||||
&i.NextTryAt,
|
||||
&i.ExternalIds,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDownloadWantsByEntity = `-- name: ListDownloadWantsByEntity :many
|
||||
SELECT id, mbid, entity, library_id, artist, title, scope, secondary, state, parent_id, attempts, last_error, last_tried_at, next_try_at, external_ids, created_at, updated_at FROM download_wants
|
||||
WHERE entity = ? AND state = ?
|
||||
ORDER BY id
|
||||
`
|
||||
|
||||
type ListDownloadWantsByEntityParams struct {
|
||||
Entity string
|
||||
State string
|
||||
}
|
||||
|
||||
func (q *Queries) ListDownloadWantsByEntity(ctx context.Context, arg ListDownloadWantsByEntityParams) ([]DownloadWant, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDownloadWantsByEntity, arg.Entity, arg.State)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadWant
|
||||
for rows.Next() {
|
||||
var i DownloadWant
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Mbid,
|
||||
&i.Entity,
|
||||
&i.LibraryID,
|
||||
&i.Artist,
|
||||
&i.Title,
|
||||
&i.Scope,
|
||||
&i.Secondary,
|
||||
&i.State,
|
||||
&i.ParentID,
|
||||
&i.Attempts,
|
||||
&i.LastError,
|
||||
&i.LastTriedAt,
|
||||
&i.NextTryAt,
|
||||
&i.ExternalIds,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDueDownloadWants = `-- name: ListDueDownloadWants :many
|
||||
SELECT id, mbid, entity, library_id, artist, title, scope, secondary, state, parent_id, attempts, last_error, last_tried_at, next_try_at, external_ids, created_at, updated_at FROM download_wants
|
||||
WHERE state = 'wanted'
|
||||
AND entity <> 'artist'
|
||||
AND (next_try_at IS NULL OR next_try_at <= CURRENT_TIMESTAMP)
|
||||
ORDER BY attempts, created_at
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
// Everything the reconciler should act on this pass: wanted, not an
|
||||
// artist subscription (those expand rather than download), and either
|
||||
// never tried or past its backoff.
|
||||
func (q *Queries) ListDueDownloadWants(ctx context.Context, limit int64) ([]DownloadWant, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDueDownloadWants, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadWant
|
||||
for rows.Next() {
|
||||
var i DownloadWant
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Mbid,
|
||||
&i.Entity,
|
||||
&i.LibraryID,
|
||||
&i.Artist,
|
||||
&i.Title,
|
||||
&i.Scope,
|
||||
&i.Secondary,
|
||||
&i.State,
|
||||
&i.ParentID,
|
||||
&i.Attempts,
|
||||
&i.LastError,
|
||||
&i.LastTriedAt,
|
||||
&i.NextTryAt,
|
||||
&i.ExternalIds,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLiveDownloadItems = `-- name: ListLiveDownloadItems :many
|
||||
SELECT id, request_id, provider_id, transport_id, external_id, candidate,
|
||||
state, staging_dir, bytes_done, bytes_total, imported_paths,
|
||||
error, created_at, updated_at
|
||||
FROM download_items
|
||||
WHERE state NOT IN ('complete', 'cancelled', 'failed')
|
||||
ORDER BY created_at
|
||||
`
|
||||
|
||||
func (q *Queries) ListLiveDownloadItems(ctx context.Context) ([]DownloadItem, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listLiveDownloadItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadItem
|
||||
for rows.Next() {
|
||||
var i DownloadItem
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.RequestID,
|
||||
&i.ProviderID,
|
||||
&i.TransportID,
|
||||
&i.ExternalID,
|
||||
&i.Candidate,
|
||||
&i.State,
|
||||
&i.StagingDir,
|
||||
&i.BytesDone,
|
||||
&i.BytesTotal,
|
||||
&i.ImportedPaths,
|
||||
&i.Error,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLiveDownloadRequests = `-- name: ListLiveDownloadRequests :many
|
||||
SELECT id, library_id, source, want_id, release_mbid, release_group_mbid,
|
||||
recording_mbid, artist, album, query, expected, state, error,
|
||||
created_at, updated_at
|
||||
FROM download_requests
|
||||
WHERE state NOT IN ('complete', 'cancelled', 'failed')
|
||||
ORDER BY created_at
|
||||
`
|
||||
|
||||
func (q *Queries) ListLiveDownloadRequests(ctx context.Context) ([]DownloadRequest, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listLiveDownloadRequests)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DownloadRequest
|
||||
for rows.Next() {
|
||||
var i DownloadRequest
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.LibraryID,
|
||||
&i.Source,
|
||||
&i.WantID,
|
||||
&i.ReleaseMbid,
|
||||
&i.ReleaseGroupMbid,
|
||||
&i.RecordingMbid,
|
||||
&i.Artist,
|
||||
&i.Album,
|
||||
&i.Query,
|
||||
&i.Expected,
|
||||
&i.State,
|
||||
&i.Error,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const recordDownloadWantAttempt = `-- name: RecordDownloadWantAttempt :exec
|
||||
UPDATE download_wants
|
||||
SET attempts = attempts + 1,
|
||||
last_error = ?,
|
||||
last_tried_at = CURRENT_TIMESTAMP,
|
||||
next_try_at = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type RecordDownloadWantAttemptParams struct {
|
||||
LastError string
|
||||
NextTryAt sql.NullTime
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) RecordDownloadWantAttempt(ctx context.Context, arg RecordDownloadWantAttemptParams) error {
|
||||
_, err := q.db.ExecContext(ctx, recordDownloadWantAttempt, arg.LastError, arg.NextTryAt, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const satisfyDownloadWant = `-- name: SatisfyDownloadWant :exec
|
||||
UPDATE download_wants
|
||||
SET state = 'satisfied', last_error = '', next_try_at = NULL,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) SatisfyDownloadWant(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, satisfyDownloadWant, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const setDownloadItemExternalID = `-- name: SetDownloadItemExternalID :exec
|
||||
UPDATE download_items
|
||||
SET external_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type SetDownloadItemExternalIDParams struct {
|
||||
ExternalID string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) SetDownloadItemExternalID(ctx context.Context, arg SetDownloadItemExternalIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDownloadItemExternalID, arg.ExternalID, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const setDownloadItemImported = `-- name: SetDownloadItemImported :exec
|
||||
UPDATE download_items
|
||||
SET imported_paths = ?, state = 'complete', error = '',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type SetDownloadItemImportedParams struct {
|
||||
ImportedPaths string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) SetDownloadItemImported(ctx context.Context, arg SetDownloadItemImportedParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDownloadItemImported, arg.ImportedPaths, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const setDownloadItemProgress = `-- name: SetDownloadItemProgress :exec
|
||||
UPDATE download_items
|
||||
SET bytes_done = ?, bytes_total = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type SetDownloadItemProgressParams struct {
|
||||
BytesDone int64
|
||||
BytesTotal int64
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) SetDownloadItemProgress(ctx context.Context, arg SetDownloadItemProgressParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDownloadItemProgress, arg.BytesDone, arg.BytesTotal, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const setDownloadItemState = `-- name: SetDownloadItemState :exec
|
||||
UPDATE download_items
|
||||
SET state = ?, error = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type SetDownloadItemStateParams struct {
|
||||
State string
|
||||
Error string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) SetDownloadItemState(ctx context.Context, arg SetDownloadItemStateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDownloadItemState, arg.State, arg.Error, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const setDownloadRequestState = `-- name: SetDownloadRequestState :exec
|
||||
UPDATE download_requests
|
||||
SET state = ?, error = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type SetDownloadRequestStateParams struct {
|
||||
State string
|
||||
Error string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) SetDownloadRequestState(ctx context.Context, arg SetDownloadRequestStateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDownloadRequestState, arg.State, arg.Error, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const setDownloadWantExternalIDs = `-- name: SetDownloadWantExternalIDs :exec
|
||||
UPDATE download_wants
|
||||
SET external_ids = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type SetDownloadWantExternalIDsParams struct {
|
||||
ExternalIds string
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) SetDownloadWantExternalIDs(ctx context.Context, arg SetDownloadWantExternalIDsParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDownloadWantExternalIDs, arg.ExternalIds, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const setDownloadWantState = `-- name: SetDownloadWantState :exec
|
||||
UPDATE download_wants
|
||||
SET state = ?, last_error = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type SetDownloadWantStateParams struct {
|
||||
State string
|
||||
LastError string
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) SetDownloadWantState(ctx context.Context, arg SetDownloadWantStateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDownloadWantState, arg.State, arg.LastError, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateDownloadProvider = `-- name: UpdateDownloadProvider :exec
|
||||
UPDATE download_providers
|
||||
SET name = ?, enabled = ?, priority = ?, settings = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateDownloadProviderParams struct {
|
||||
Name string
|
||||
Enabled int64
|
||||
Priority int64
|
||||
Settings string
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateDownloadProvider(ctx context.Context, arg UpdateDownloadProviderParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateDownloadProvider,
|
||||
arg.Name,
|
||||
arg.Enabled,
|
||||
arg.Priority,
|
||||
arg.Settings,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertDownloadWant = `-- name: UpsertDownloadWant :one
|
||||
|
||||
INSERT INTO download_wants (
|
||||
mbid, entity, library_id, artist, title, scope, secondary,
|
||||
parent_id, next_try_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(mbid, library_id) DO UPDATE SET
|
||||
artist = CASE WHEN excluded.artist <> '' THEN excluded.artist
|
||||
ELSE download_wants.artist END,
|
||||
title = CASE WHEN excluded.title <> '' THEN excluded.title
|
||||
ELSE download_wants.title END,
|
||||
scope = excluded.scope,
|
||||
secondary = excluded.secondary,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type UpsertDownloadWantParams struct {
|
||||
Mbid string
|
||||
Entity string
|
||||
LibraryID int64
|
||||
Artist string
|
||||
Title string
|
||||
Scope string
|
||||
Secondary int64
|
||||
ParentID sql.NullInt64
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Wants
|
||||
// ---------------------------------------------------------------------
|
||||
// Adding something already wanted is not an error and must not reset
|
||||
// the retry clock, so the conflict path only refreshes display text and
|
||||
// un-pauses nothing. scope and secondary are updated because asking
|
||||
// again with a wider scope is a real change of intent.
|
||||
func (q *Queries) UpsertDownloadWant(ctx context.Context, arg UpsertDownloadWantParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, upsertDownloadWant,
|
||||
arg.Mbid,
|
||||
arg.Entity,
|
||||
arg.LibraryID,
|
||||
arg.Artist,
|
||||
arg.Title,
|
||||
arg.Scope,
|
||||
arg.Secondary,
|
||||
arg.ParentID,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
@@ -26,6 +26,20 @@ type ArtistCreditArtist struct {
|
||||
CreditID int64
|
||||
}
|
||||
|
||||
type ArtistImage struct {
|
||||
ID int64
|
||||
ArtistMbid string
|
||||
Source string
|
||||
SourceUrl string
|
||||
FilePath string
|
||||
IsPrimary int64
|
||||
SortOrder int64
|
||||
Width sql.NullInt64
|
||||
Height sql.NullInt64
|
||||
FileSize sql.NullInt64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ArtistMetadatum struct {
|
||||
Mbid string
|
||||
Source string
|
||||
@@ -50,6 +64,7 @@ type AudioFile struct {
|
||||
LastPlayed sql.NullTime
|
||||
TagStatus string
|
||||
GroupKey string
|
||||
ModifiedAt int64
|
||||
}
|
||||
|
||||
type CoverArt struct {
|
||||
@@ -59,6 +74,116 @@ type CoverArt struct {
|
||||
MimeType string
|
||||
}
|
||||
|
||||
type DownloadItem struct {
|
||||
ID string
|
||||
RequestID string
|
||||
ProviderID int64
|
||||
TransportID sql.NullInt64
|
||||
ExternalID string
|
||||
Candidate string
|
||||
State string
|
||||
StagingDir string
|
||||
BytesDone int64
|
||||
BytesTotal int64
|
||||
ImportedPaths string
|
||||
Error string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type DownloadProvider struct {
|
||||
ID int64
|
||||
Kind string
|
||||
Name string
|
||||
Enabled int64
|
||||
Priority int64
|
||||
Settings string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type DownloadRequest struct {
|
||||
ID string
|
||||
LibraryID int64
|
||||
Source string
|
||||
WantID sql.NullInt64
|
||||
ReleaseMbid sql.NullString
|
||||
ReleaseGroupMbid sql.NullString
|
||||
RecordingMbid sql.NullString
|
||||
Artist string
|
||||
Album string
|
||||
Query string
|
||||
Expected string
|
||||
State string
|
||||
Error string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type DownloadWant struct {
|
||||
ID int64
|
||||
Mbid string
|
||||
Entity string
|
||||
LibraryID int64
|
||||
Artist string
|
||||
Title string
|
||||
Scope string
|
||||
Secondary int64
|
||||
State string
|
||||
ParentID sql.NullInt64
|
||||
Attempts int64
|
||||
LastError string
|
||||
LastTriedAt sql.NullTime
|
||||
NextTryAt sql.NullTime
|
||||
ExternalIds string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ExploreChampionFt struct {
|
||||
Title string
|
||||
ArtistName string
|
||||
Aliases string
|
||||
}
|
||||
|
||||
type ExploreIndex struct {
|
||||
ID int64
|
||||
EntityType string
|
||||
Mbid string
|
||||
Title string
|
||||
ArtistName string
|
||||
ArtistMbid string
|
||||
Aliases string
|
||||
Popularity int64
|
||||
ListenerCount int64
|
||||
Duration int64
|
||||
CaaReleaseMbid string
|
||||
ReleaseName string
|
||||
PrimaryType string
|
||||
SecondaryTypes string
|
||||
ReleaseDate string
|
||||
ArtistType string
|
||||
Country string
|
||||
Disambiguation string
|
||||
SortName string
|
||||
InLibrary int64
|
||||
IsSimilar int64
|
||||
LocalArtistID sql.NullInt64
|
||||
LocalReleaseGroupID sql.NullInt64
|
||||
LocalRecordingID sql.NullInt64
|
||||
DiscogFetched int64
|
||||
}
|
||||
|
||||
type ExploreIndexFt struct {
|
||||
Title string
|
||||
ArtistName string
|
||||
Aliases string
|
||||
}
|
||||
|
||||
type ExploreIndexMetum struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type FileType struct {
|
||||
ID int64
|
||||
Extension string
|
||||
@@ -176,10 +301,10 @@ type ReleaseGroup struct {
|
||||
CoverArtID sql.NullInt64
|
||||
AlbumArtistCreditID sql.NullInt64
|
||||
Year sql.NullInt64
|
||||
OriginalYear sql.NullInt64
|
||||
TotalTracks sql.NullInt64
|
||||
TotalDiscs sql.NullInt64
|
||||
Mbid sql.NullString
|
||||
OriginalYear sql.NullInt64
|
||||
}
|
||||
|
||||
type ReleaseGroupRecording struct {
|
||||
@@ -190,6 +315,19 @@ type ReleaseGroupRecording struct {
|
||||
DiscNumber sql.NullInt64
|
||||
}
|
||||
|
||||
type ReleaseToRg struct {
|
||||
ReleaseMbid string
|
||||
RgMbid string
|
||||
}
|
||||
|
||||
type SearchClick struct {
|
||||
Query string
|
||||
EntityMbid string
|
||||
EntityType string
|
||||
ClickCount int64
|
||||
LastClicked time.Time
|
||||
}
|
||||
|
||||
type SearchIndex struct {
|
||||
FilePath string
|
||||
Title string
|
||||
@@ -197,6 +335,13 @@ type SearchIndex struct {
|
||||
Album string
|
||||
}
|
||||
|
||||
type SimilarArtistMap struct {
|
||||
SourceArtistMbid string
|
||||
SimilarArtistMbid string
|
||||
SimilarArtistName string
|
||||
Score int64
|
||||
}
|
||||
|
||||
type TaggingCandidate struct {
|
||||
GroupKey string
|
||||
Candidates string
|
||||
|
||||
@@ -23,7 +23,7 @@ func (q *Queries) CountReleaseGroupRecordings(ctx context.Context, releaseGroupI
|
||||
|
||||
const createReleaseGroup = `-- name: CreateReleaseGroup :one
|
||||
INSERT INTO release_groups (name) VALUES (?)
|
||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, original_year, total_tracks, total_discs, mbid
|
||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year
|
||||
`
|
||||
|
||||
func (q *Queries) CreateReleaseGroup(ctx context.Context, name string) (ReleaseGroup, error) {
|
||||
@@ -35,10 +35,10 @@ func (q *Queries) CreateReleaseGroup(ctx context.Context, name string) (ReleaseG
|
||||
&i.CoverArtID,
|
||||
&i.AlbumArtistCreditID,
|
||||
&i.Year,
|
||||
&i.OriginalYear,
|
||||
&i.TotalTracks,
|
||||
&i.TotalDiscs,
|
||||
&i.Mbid,
|
||||
&i.OriginalYear,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -47,7 +47,7 @@ const createReleaseGroupFull = `-- name: CreateReleaseGroupFull :one
|
||||
INSERT INTO release_groups (
|
||||
name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, original_year, total_tracks, total_discs, mbid
|
||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year
|
||||
`
|
||||
|
||||
type CreateReleaseGroupFullParams struct {
|
||||
@@ -75,10 +75,10 @@ func (q *Queries) CreateReleaseGroupFull(ctx context.Context, arg CreateReleaseG
|
||||
&i.CoverArtID,
|
||||
&i.AlbumArtistCreditID,
|
||||
&i.Year,
|
||||
&i.OriginalYear,
|
||||
&i.TotalTracks,
|
||||
&i.TotalDiscs,
|
||||
&i.Mbid,
|
||||
&i.OriginalYear,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -432,7 +432,7 @@ func (q *Queries) GetAllAlbumsWithDetailsByLibrary(ctx context.Context, libraryI
|
||||
}
|
||||
|
||||
const getAllReleaseGroups = `-- name: GetAllReleaseGroups :many
|
||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, original_year, total_tracks, total_discs, mbid FROM release_groups
|
||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year FROM release_groups
|
||||
ORDER BY name
|
||||
`
|
||||
|
||||
@@ -451,10 +451,10 @@ func (q *Queries) GetAllReleaseGroups(ctx context.Context) ([]ReleaseGroup, erro
|
||||
&i.CoverArtID,
|
||||
&i.AlbumArtistCreditID,
|
||||
&i.Year,
|
||||
&i.OriginalYear,
|
||||
&i.TotalTracks,
|
||||
&i.TotalDiscs,
|
||||
&i.Mbid,
|
||||
&i.OriginalYear,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -470,7 +470,7 @@ func (q *Queries) GetAllReleaseGroups(ctx context.Context) ([]ReleaseGroup, erro
|
||||
}
|
||||
|
||||
const getReleaseGroup = `-- name: GetReleaseGroup :one
|
||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, original_year, total_tracks, total_discs, mbid FROM release_groups
|
||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year FROM release_groups
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
@@ -483,16 +483,16 @@ func (q *Queries) GetReleaseGroup(ctx context.Context, id int64) (ReleaseGroup,
|
||||
&i.CoverArtID,
|
||||
&i.AlbumArtistCreditID,
|
||||
&i.Year,
|
||||
&i.OriginalYear,
|
||||
&i.TotalTracks,
|
||||
&i.TotalDiscs,
|
||||
&i.Mbid,
|
||||
&i.OriginalYear,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getReleaseGroupByNameAndArtist = `-- name: GetReleaseGroupByNameAndArtist :one
|
||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, original_year, total_tracks, total_discs, mbid FROM release_groups
|
||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year FROM release_groups
|
||||
WHERE name = ? AND album_artist_credit_id = ? LIMIT 1
|
||||
`
|
||||
|
||||
@@ -510,10 +510,10 @@ func (q *Queries) GetReleaseGroupByNameAndArtist(ctx context.Context, arg GetRel
|
||||
&i.CoverArtID,
|
||||
&i.AlbumArtistCreditID,
|
||||
&i.Year,
|
||||
&i.OriginalYear,
|
||||
&i.TotalTracks,
|
||||
&i.TotalDiscs,
|
||||
&i.Mbid,
|
||||
&i.OriginalYear,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -574,7 +574,7 @@ VALUES (?, ?, ?)
|
||||
ON CONFLICT(name, album_artist_credit_id) DO UPDATE SET
|
||||
album_artist_credit_id = COALESCE(excluded.album_artist_credit_id, release_groups.album_artist_credit_id),
|
||||
year = COALESCE(excluded.year, release_groups.year)
|
||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, original_year, total_tracks, total_discs, mbid
|
||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year
|
||||
`
|
||||
|
||||
type UpsertReleaseGroupParams struct {
|
||||
@@ -592,10 +592,10 @@ func (q *Queries) UpsertReleaseGroup(ctx context.Context, arg UpsertReleaseGroup
|
||||
&i.CoverArtID,
|
||||
&i.AlbumArtistCreditID,
|
||||
&i.Year,
|
||||
&i.OriginalYear,
|
||||
&i.TotalTracks,
|
||||
&i.TotalDiscs,
|
||||
&i.Mbid,
|
||||
&i.OriginalYear,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user