feat: data lifecycle rewrite, download clients, wanted list, and central catalog index
Build & publish Arch package / arch-package (push) Successful in 2m12s
Search index maintenance / maintain-index (push) Successful in 2h22m28s

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:
2026-08-06 17:12:01 -04:00
co-authored by Claude Sonnet 5
parent d0d86f85d5
commit e190fd75b9
165 changed files with 31088 additions and 5192 deletions
+17 -3
View File
@@ -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 = ?;
+203
View File
@@ -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);
+2
View File
@@ -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;
+17 -11
View File
@@ -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);
+1
View File
@@ -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);
@@ -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;
+64 -17
View File
@@ -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
}
+146 -1
View File
@@ -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
}