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
@@ -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;