funoteka 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +90 -0
- package/CHANGELOG.md +104 -0
- package/DEPLOY.md +375 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/deploy/answers.schema.json +151 -0
- package/deploy/build.mjs +88 -0
- package/deploy/check-publishable.mjs +246 -0
- package/deploy/smoke.mjs +681 -0
- package/deploy/systemd/funoteka.env.example +41 -0
- package/deploy/systemd/funoteka.service +77 -0
- package/deploy/windows/funoteka-service.xml +58 -0
- package/deploy/windows/install-service.ps1 +158 -0
- package/dist/annotation/store.js +67 -0
- package/dist/api/admin-export.js +227 -0
- package/dist/api/admin-guard.js +181 -0
- package/dist/api/admin-library.js +113 -0
- package/dist/api/admin.js +1162 -0
- package/dist/api/annotation.js +193 -0
- package/dist/api/artistinfo.js +140 -0
- package/dist/api/audit.js +32 -0
- package/dist/api/auth.js +145 -0
- package/dist/api/bookmark.js +108 -0
- package/dist/api/browse.js +1241 -0
- package/dist/api/config-file.js +202 -0
- package/dist/api/config.js +285 -0
- package/dist/api/cover.js +304 -0
- package/dist/api/download.js +112 -0
- package/dist/api/envelope.js +162 -0
- package/dist/api/extensions.js +43 -0
- package/dist/api/genre.js +81 -0
- package/dist/api/health.js +35 -0
- package/dist/api/history.js +374 -0
- package/dist/api/idempotency.js +66 -0
- package/dist/api/keys.js +90 -0
- package/dist/api/log-file.js +65 -0
- package/dist/api/meta.js +1410 -0
- package/dist/api/playlist.js +311 -0
- package/dist/api/rescan.js +196 -0
- package/dist/api/roots.js +161 -0
- package/dist/api/router.js +133 -0
- package/dist/api/scanner.js +163 -0
- package/dist/api/search.js +82 -0
- package/dist/api/server.js +643 -0
- package/dist/api/settings.js +67 -0
- package/dist/api/stream.js +442 -0
- package/dist/api/stubs.js +183 -0
- package/dist/api/transcode.js +678 -0
- package/dist/api/user.js +73 -0
- package/dist/api/virtual.js +362 -0
- package/dist/api/visibility.js +21 -0
- package/dist/artist/apply.js +752 -0
- package/dist/artist/credit.js +95 -0
- package/dist/artist/folder.js +65 -0
- package/dist/artist/name.js +205 -0
- package/dist/artist/nfo.js +101 -0
- package/dist/artist/translit.js +79 -0
- package/dist/bookmark/store.js +54 -0
- package/dist/classify/classify.js +405 -0
- package/dist/classify/collision-name.js +109 -0
- package/dist/classify/folder-name.js +466 -0
- package/dist/classify/roles.js +277 -0
- package/dist/classify/shelf-name.js +178 -0
- package/dist/classify/tree.js +43 -0
- package/dist/cli/args.js +169 -0
- package/dist/cli/daemon.js +192 -0
- package/dist/cli/entry.js +25 -0
- package/dist/cli/keys.js +52 -0
- package/dist/cli.js +638 -0
- package/dist/cover/pick.js +58 -0
- package/dist/cover/picture.js +111 -0
- package/dist/cue/engine.js +759 -0
- package/dist/cue/match.js +142 -0
- package/dist/cue/parse.js +182 -0
- package/dist/cue/plan.js +286 -0
- package/dist/cue/track-name.js +120 -0
- package/dist/db/index.js +163 -0
- package/dist/db/issue.js +73 -0
- package/dist/db/ledger.js +84 -0
- package/dist/db/migrations/001_init.sql +196 -0
- package/dist/db/migrations/002_cue_track_flexible.sql +35 -0
- package/dist/db/migrations/003_cue_metadata.sql +14 -0
- package/dist/db/migrations/004_cue_performer.sql +12 -0
- package/dist/db/migrations/005_incremental.sql +23 -0
- package/dist/db/migrations/006_tags.sql +57 -0
- package/dist/db/migrations/007_artist_credit.sql +35 -0
- package/dist/db/migrations/008_issue_stage.sql +39 -0
- package/dist/db/migrations/009_cue_title.sql +20 -0
- package/dist/db/migrations/010_duration_source.sql +31 -0
- package/dist/db/migrations/011_track_title_source.sql +28 -0
- package/dist/db/migrations/012_file_container.sql +25 -0
- package/dist/db/migrations/013_cover_art.sql +28 -0
- package/dist/db/migrations/014_album_year.sql +17 -0
- package/dist/db/migrations/015_probe_method.sql +14 -0
- package/dist/db/migrations/016_tags_method.sql +20 -0
- package/dist/db/migrations/017_cover_indirect.sql +23 -0
- package/dist/db/migrations/018_release_year.sql +14 -0
- package/dist/db/migrations/019_file_tag_name_file_value.sql +20 -0
- package/dist/db/migrations/020_analyze_after_index.sql +16 -0
- package/dist/db/migrations/021_cue_index_files.sql +15 -0
- package/dist/db/migrations/022_sidecar_text.sql +19 -0
- package/dist/db/migrations/023_track_credit.sql +23 -0
- package/dist/db/migrations/024_playlists.sql +63 -0
- package/dist/db/migrations/025_playlist_sequence.sql +27 -0
- package/dist/db/migrations/026_playlist_source_file.sql +19 -0
- package/dist/db/migrations/027_lookup_indexes.sql +30 -0
- package/dist/db/migrations/028_annotations.sql +47 -0
- package/dist/db/migrations/029_scan_state_root_run.sql +28 -0
- package/dist/db/migrations/030_history.sql +108 -0
- package/dist/db/migrations/031_queue_position_and_plays.sql +46 -0
- package/dist/db/migrations/032_bookmarks.sql +27 -0
- package/dist/db/migrations/033_bookmark_order.sql +14 -0
- package/dist/db/migrations/034_junk.sql +33 -0
- package/dist/db/migrations/035_api_key.sql +38 -0
- package/dist/db/migrations/036_file_tag_first.sql +88 -0
- package/dist/db/migrations/037_admin_idempotency.sql +31 -0
- package/dist/db/migrations/038_cue_audio_index.sql +24 -0
- package/dist/db/sweep.js +109 -0
- package/dist/history/store.js +151 -0
- package/dist/inventory/inventory.js +294 -0
- package/dist/junk/marks.js +143 -0
- package/dist/junk/rule.js +93 -0
- package/dist/mcp/client.js +48 -0
- package/dist/mcp/server.js +175 -0
- package/dist/mcp/tools.js +348 -0
- package/dist/playlist/files.js +116 -0
- package/dist/playlist/import.js +260 -0
- package/dist/playlist/store.js +240 -0
- package/dist/probe/ffprobe.js +179 -0
- package/dist/run.js +120 -0
- package/dist/scan/kinds.js +49 -0
- package/dist/scan/scan.js +503 -0
- package/dist/scan/schedule.js +132 -0
- package/dist/scan/settle.js +172 -0
- package/dist/scan/walk.js +132 -0
- package/dist/search/index.js +41 -0
- package/dist/search/query.js +28 -0
- package/dist/stream/flac.js +541 -0
- package/dist/stream/mpeg.js +179 -0
- package/dist/stream/recode.js +393 -0
- package/dist/stream/rewrite.js +163 -0
- package/dist/stream/segment.js +624 -0
- package/dist/tags/apply.js +703 -0
- package/dist/tags/encode.js +164 -0
- package/dist/tags/first.js +130 -0
- package/dist/tags/flac.js +196 -0
- package/dist/tags/genres.js +123 -0
- package/dist/tags/id3v1.js +209 -0
- package/dist/tags/id3v2.js +754 -0
- package/dist/tags/mp4.js +534 -0
- package/dist/tags/mpeg.js +355 -0
- package/dist/tags/ogg.js +463 -0
- package/dist/tags/picture.js +71 -0
- package/dist/tags/read.js +269 -0
- package/dist/tags/types.js +51 -0
- package/dist/tags/vorbis-comment.js +155 -0
- package/dist/text/encoding.js +384 -0
- package/dist/text/site-name.js +37 -0
- package/dist/util/child.js +21 -0
- package/dist/util/names.js +97 -0
- package/funoteka.json.example +27 -0
- package/package.json +55 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- The cue's own metadata, which was parsed and then dropped on the floor.
|
|
2
|
+
--
|
|
3
|
+
-- wiki:3499 §8: "CATALOG in cue is the source of the catalogue number, with
|
|
4
|
+
-- priority over the folder name". The parser has always filled `doc.rem`, but
|
|
5
|
+
-- nothing read it and there was nowhere to put it, so every album got the raw
|
|
6
|
+
-- folder name — `[2026-08-28] VA - A State Of Trance_Ibiza 2026 (...)[ARDI4701]`
|
|
7
|
+
-- when the cue plainly says `TITLE "A State Of Trance: Ibiza 2026"`.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE cue ADD COLUMN catalog TEXT;
|
|
10
|
+
|
|
11
|
+
-- The whole REM block, so a later stage can reach DATE / GENRE / DISCID /
|
|
12
|
+
-- COMMENT without another migration. JSON, because REM keys are open-ended —
|
|
13
|
+
-- real rips carry whatever the ripper felt like.
|
|
14
|
+
ALTER TABLE cue ADD COLUMN rem_json TEXT;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- The cue's album-level PERFORMER, which was parsed and then dropped.
|
|
2
|
+
--
|
|
3
|
+
-- `applyCues` has always filled `doc.performer` — in a cue sheet the performer
|
|
4
|
+
-- written above the first TRACK names the record's artist, and the per-track
|
|
5
|
+
-- ones below it name each track — but there was nowhere to put it. The result
|
|
6
|
+
-- was an artist stage with nothing to go on: the per-track PERFORMER survived
|
|
7
|
+
-- in `cue_track`, while the album's own went in the bin.
|
|
8
|
+
--
|
|
9
|
+
-- Nullable, and legitimately so: a cue need not name a performer at all, and a
|
|
10
|
+
-- compilation often names only the tracks.
|
|
11
|
+
|
|
12
|
+
ALTER TABLE cue ADD COLUMN performer TEXT;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Incremental rescan (plan:32 этап 6, requirements:39 §8).
|
|
2
|
+
|
|
3
|
+
-- scan_state is the ledger of the last observation of each file: a scan writes
|
|
4
|
+
-- what it saw and whether that differed from the run before it. The verdict has
|
|
5
|
+
-- to be taken during the walk, not read back afterwards — scan writes the file
|
|
6
|
+
-- row and this ledger from the same observation, so comparing the two later
|
|
7
|
+
-- always agrees and tells nothing.
|
|
8
|
+
--
|
|
9
|
+
-- Defaults to 1 (changed): assuming movement costs a re-read, assuming stillness
|
|
10
|
+
-- costs correctness.
|
|
11
|
+
ALTER TABLE scan_state ADD COLUMN changed INTEGER NOT NULL DEFAULT 1;
|
|
12
|
+
|
|
13
|
+
-- The "did this run see it" stamp `file` has carried since 001, extended to the
|
|
14
|
+
-- rows a rescan must also let go of. Without it, dropping what is no longer on
|
|
15
|
+
-- disk means matching path prefixes in SQL, which is both slower and wrong at
|
|
16
|
+
-- the boundaries (`Album` would swallow `Album 2`).
|
|
17
|
+
ALTER TABLE folder ADD COLUMN last_seen_run_id INTEGER REFERENCES scan_run(id);
|
|
18
|
+
ALTER TABLE album ADD COLUMN last_seen_run_id INTEGER REFERENCES scan_run(id);
|
|
19
|
+
ALTER TABLE release ADD COLUMN last_seen_run_id INTEGER REFERENCES scan_run(id);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX idx_folder_last_seen ON folder (root_id, last_seen_run_id);
|
|
22
|
+
CREATE INDEX idx_album_last_seen ON album (root_id, last_seen_run_id);
|
|
23
|
+
CREATE INDEX idx_release_last_seen ON release (root_id, last_seen_run_id);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
-- Tags, and where a title came from ([[task:2667]]).
|
|
2
|
+
--
|
|
3
|
+
-- Until now the collection's own metadata was parsed and dropped: `file` knew
|
|
4
|
+
-- a file's size and mtime and nothing about what it called itself. 279 of 291
|
|
5
|
+
-- files across four sample roots show as `(untitled)` / `--:--` while carrying
|
|
6
|
+
-- perfectly good ID3 and Vorbis comments.
|
|
7
|
+
|
|
8
|
+
-- The encoding a tag's text was read as, and how sure the reader was.
|
|
9
|
+
--
|
|
10
|
+
-- The same pair `cue` carries, for the same reason and with the same shape: a
|
|
11
|
+
-- ripper that writes CP1251 bytes under a Latin-1 declaration produces a title
|
|
12
|
+
-- that looks like a title, and "I guessed windows-1251" is the finding that
|
|
13
|
+
-- makes it visible. CERTAIN means nothing was inferred.
|
|
14
|
+
ALTER TABLE file ADD COLUMN encoding TEXT;
|
|
15
|
+
ALTER TABLE file ADD COLUMN encoding_confidence REAL;
|
|
16
|
+
|
|
17
|
+
-- Which run last read this file's tags.
|
|
18
|
+
--
|
|
19
|
+
-- The ledger alone is not enough to make this stage incremental. `scan_state`
|
|
20
|
+
-- says a file's bytes moved or did not; it cannot say whether anything ever
|
|
21
|
+
-- *read* them, because until this migration nothing did. And "no rows in
|
|
22
|
+
-- file_tag" cannot stand in for it either: a file with no tags at all is a
|
|
23
|
+
-- completely ordinary thing — an untagged rip — and it would be re-read on
|
|
24
|
+
-- every scan forever while never producing a row to show for it. So the read
|
|
25
|
+
-- is stamped, and NULL means never read.
|
|
26
|
+
--
|
|
27
|
+
-- Nullable on purpose: every file already on disk has NULL here, which is
|
|
28
|
+
-- exactly the backfill condition - the tags of the existing collection get read
|
|
29
|
+
-- once, on the first scan after this migration.
|
|
30
|
+
ALTER TABLE file ADD COLUMN tags_read_run_id INTEGER REFERENCES scan_run(id);
|
|
31
|
+
|
|
32
|
+
-- Where an album's or release's title came from: 'folder' | 'cue' | 'tag'.
|
|
33
|
+
--
|
|
34
|
+
-- A source, written down, instead of a guess from emptiness. `album.title` is
|
|
35
|
+
-- never NULL — classify fills it from the folder name or the file's stem — so
|
|
36
|
+
-- a tag cannot decide whether it is looking at a placeholder or at a name a cue
|
|
37
|
+
-- deliberately chose. With the source recorded, the priority is explicit: a tag
|
|
38
|
+
-- overrides the folder and never overrides a cue.
|
|
39
|
+
ALTER TABLE album ADD COLUMN title_source TEXT;
|
|
40
|
+
ALTER TABLE release ADD COLUMN title_source TEXT;
|
|
41
|
+
|
|
42
|
+
-- One tag of one file.
|
|
43
|
+
--
|
|
44
|
+
-- `position` exists because Vorbis comments repeat a name: two ARTIST lines in
|
|
45
|
+
-- one file is how a collaboration is written, and collapsing them would keep
|
|
46
|
+
-- one name and silently drop the other.
|
|
47
|
+
CREATE TABLE file_tag (
|
|
48
|
+
file_id INTEGER NOT NULL REFERENCES file(id) ON DELETE CASCADE,
|
|
49
|
+
name TEXT NOT NULL, -- folded to lower case, as the reader reports it
|
|
50
|
+
value TEXT NOT NULL,
|
|
51
|
+
position INTEGER NOT NULL,
|
|
52
|
+
PRIMARY KEY (file_id, name, position)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
-- Reading a tag by name is the only lookup the filling stages do: "the ALBUM
|
|
56
|
+
-- of this file", "the ARTIST of this file".
|
|
57
|
+
CREATE INDEX idx_file_tag_name ON file_tag (name);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- An artist credit is a list, not a name ([[task:2684]]).
|
|
2
|
+
--
|
|
3
|
+
-- Until now an album pointed at exactly one artist. A collaboration is not one
|
|
4
|
+
-- artist: `Cock E.S.P. + Thirdorgan`, `Suffering Bastard + Cock E.S.P.` — and on
|
|
5
|
+
-- the sample this showed up as 11 of 28 albums with no artist at all, because
|
|
6
|
+
-- the per-track ARTIST values inside a split disagree and no majority exists
|
|
7
|
+
-- (`2011 - Demonologists + Cock E.S.P.` is 10 to 1, and calling that album
|
|
8
|
+
-- `Cock E.S.P.` would be confidently wrong).
|
|
9
|
+
--
|
|
10
|
+
-- The shape is OpenSubsonic's `artists[]` with `joinPhrase`: an ordered list,
|
|
11
|
+
-- each entry carrying the phrase that joined it to the one before.
|
|
12
|
+
|
|
13
|
+
CREATE TABLE artist_credit (
|
|
14
|
+
album_id INTEGER NOT NULL REFERENCES album(id) ON DELETE CASCADE,
|
|
15
|
+
position INTEGER NOT NULL,
|
|
16
|
+
artist_id INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE,
|
|
17
|
+
-- Verbatim, whitespace and all, so `a + b` rebuilds as `a + b` and not
|
|
18
|
+
-- `a+b`. Empty for the first entry: nothing joined it to anything.
|
|
19
|
+
join_phrase TEXT NOT NULL DEFAULT '',
|
|
20
|
+
PRIMARY KEY (album_id, position)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- What the source said, before it was read as a list.
|
|
24
|
+
--
|
|
25
|
+
-- The list is lossless — it reassembles exactly — so this is not a backup. It
|
|
26
|
+
-- is the thing a human audits: a rule that splits `Smell & Quim` into two
|
|
27
|
+
-- artists cannot be argued with unless the original is still on the row, and
|
|
28
|
+
-- the rule that split it is a guess that has to stay visible.
|
|
29
|
+
ALTER TABLE album ADD COLUMN credit_raw TEXT;
|
|
30
|
+
ALTER TABLE album ADD COLUMN credit_source TEXT; -- 'cue' | 'folder' | 'tag'
|
|
31
|
+
|
|
32
|
+
-- Pruning reads this table: an artist named only by a credit is still an
|
|
33
|
+
-- artist. Without that, `pruneArtists` would delete the row and the cascade
|
|
34
|
+
-- above would silently take the credit with it.
|
|
35
|
+
CREATE INDEX idx_artist_credit_artist ON artist_credit (artist_id);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- Which stage filed each issue, so a stage can clear its own rows.
|
|
2
|
+
--
|
|
3
|
+
-- Most stages derive their report from the present: `cues` and `artists` walk
|
|
4
|
+
-- every root on every run, so an earlier run's rows are dead weight — every
|
|
5
|
+
-- reader scopes by run, and nothing removed them. Without ownership, a
|
|
6
|
+
-- collection rescanned daily grows this table by one run's worth per day, and
|
|
7
|
+
-- any reader that forgets the scope counts duplicates.
|
|
8
|
+
--
|
|
9
|
+
-- Two stages are not like that. `scan` records a healed root path and a twin
|
|
10
|
+
-- whose rows were dropped, which happen once and raise nothing on a later run;
|
|
11
|
+
-- `tags` reads only the files the ledger reports changed, so a row describes the
|
|
12
|
+
-- last time that file was read. Clearing those would not replace the rows with
|
|
13
|
+
-- an equal report — it would delete the only record that the thing happened.
|
|
14
|
+
--
|
|
15
|
+
-- Ownership is by stage and never by a list of kinds: a hand-kept list is a list
|
|
16
|
+
-- that drifts, and the cue stage's kinds are born in two files.
|
|
17
|
+
|
|
18
|
+
ALTER TABLE issue ADD COLUMN stage TEXT NOT NULL DEFAULT '';
|
|
19
|
+
|
|
20
|
+
-- Rows that predate the column. The two stages that clear replace theirs on the
|
|
21
|
+
-- next run, so only `scan` and `tags` rows survive this mapping — and those are
|
|
22
|
+
-- the two the rules below get right.
|
|
23
|
+
--
|
|
24
|
+
-- The empty default exists for this backfill alone. Every writer binds a stage,
|
|
25
|
+
-- and the value it binds is a union type, so omitting one is a compile error
|
|
26
|
+
-- rather than a row no `clearIssues` will ever match.
|
|
27
|
+
UPDATE issue SET stage = CASE
|
|
28
|
+
WHEN kind LIKE 'artist-%' THEN 'artists'
|
|
29
|
+
WHEN kind LIKE 'tag-%' THEN 'tags'
|
|
30
|
+
WHEN kind LIKE 'cue-%' THEN 'cues'
|
|
31
|
+
WHEN kind IN ('ripper-marker-titles', 'track-without-index',
|
|
32
|
+
'album-without-audio', 'track-count-mismatch') THEN 'cues'
|
|
33
|
+
ELSE 'scan'
|
|
34
|
+
END;
|
|
35
|
+
|
|
36
|
+
-- Serves both scopes a clear uses: the stage alone (a prefix of this index) and
|
|
37
|
+
-- the per-file delete `tags` does, which is one statement per file read and
|
|
38
|
+
-- would otherwise scan the stage's rows each time.
|
|
39
|
+
CREATE INDEX idx_issue_stage ON issue (stage, root_id, rel_path);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- The cue's own TITLE, which was parsed and then dropped on the floor.
|
|
2
|
+
--
|
|
3
|
+
-- 003 did exactly this for CATALOG, and gave the reason in almost these words.
|
|
4
|
+
-- It stopped one column short: `doc.title` is read on every run — it is what
|
|
5
|
+
-- names a release better than the folder name does — and nothing persisted it,
|
|
6
|
+
-- so the moment a run ended the only record of what a cue said was gone.
|
|
7
|
+
--
|
|
8
|
+
-- That is visible wherever the priority chain *turns a title away*. Both of the
|
|
9
|
+
-- Wall's cues write `The Wall [Disc 1]` and `The Wall [1994 Remaster](Disc 2)`:
|
|
10
|
+
-- EAC writes a disc's own label into TITLE, each label names a disc rather than
|
|
11
|
+
-- the record, and the release keeps its folder name. The decision is right and
|
|
12
|
+
-- is reported as an issue — but a refusal is a judgement about a value, and
|
|
13
|
+
-- without this column the value did not survive the judgement, so there was
|
|
14
|
+
-- nothing left to review the decision against.
|
|
15
|
+
--
|
|
16
|
+
-- Kept for every cue, not only the turned-away ones. Which titles are refused
|
|
17
|
+
-- is a rule that will move, and a column that only holds the refusals of today's
|
|
18
|
+
-- rule cannot show what tomorrow's would have done differently.
|
|
19
|
+
|
|
20
|
+
ALTER TABLE cue ADD COLUMN title TEXT;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Where an `audio_probe` length came from, measured or worked out.
|
|
2
|
+
--
|
|
3
|
+
-- The project's rule is that a value has to say whether it is knowledge or a
|
|
4
|
+
-- guess — `title_source`, `ambiguous`, `encoding_confidence`, and
|
|
5
|
+
-- `TagRead.durationRefused` are all that rule applied elsewhere. Duration was
|
|
6
|
+
-- the one place it was not: `probeFile` puts ffprobe's `format.duration` into
|
|
7
|
+
-- this table the same way the tag readers put the container's own statement
|
|
8
|
+
-- there, and the two are not the same kind of answer.
|
|
9
|
+
--
|
|
10
|
+
-- They differ exactly where it matters. ffprobe reads a length when the
|
|
11
|
+
-- container states one, but an mp3 that states no frame count — no Xing/Info
|
|
12
|
+
-- header — leaves it nothing to read, and it derives the number from the file
|
|
13
|
+
-- size and the bitrate instead. Measured on the collection's own
|
|
14
|
+
-- `02. 218 Tracks.mp3`: the stored 165 778 ms is `size * 8 / bitrate` to the
|
|
15
|
+
-- millisecond. That the value happens to be close to the truth does not make
|
|
16
|
+
-- it a measurement, and nothing downstream could tell.
|
|
17
|
+
--
|
|
18
|
+
-- Two values, because two things are worth telling apart and no more:
|
|
19
|
+
--
|
|
20
|
+
-- 'container' the file's own bytes state it — FLAC STREAMINFO, MP4 mvhd, an
|
|
21
|
+
-- mp3's stated frame count, or a walk over an mp3's frames.
|
|
22
|
+
-- Everything the tag stage seeds is this.
|
|
23
|
+
-- 'ffprobe' ffprobe's answer, which is a reading for some containers and
|
|
24
|
+
-- a derivation for others. It is only ever reached for a file
|
|
25
|
+
-- the reader could not measure, and it is named rather than
|
|
26
|
+
-- judged: `tag-duration-estimated` carries the detail.
|
|
27
|
+
--
|
|
28
|
+
-- NULL for rows written before this column, which is honest — they were
|
|
29
|
+
-- written when the distinction was not being made.
|
|
30
|
+
|
|
31
|
+
ALTER TABLE audio_probe ADD COLUMN duration_source TEXT;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Where a track's title came from — the cue, the file's tag, or its name.
|
|
2
|
+
--
|
|
3
|
+
-- The project's rule is that a value has to say whether it is knowledge or a
|
|
4
|
+
-- guess — `title_source` on album and release, `duration_source` on
|
|
5
|
+
-- `audio_probe`, `encoding_confidence`, `ambiguous`. Track titles were the one
|
|
6
|
+
-- place the chain could not say: three sources now feed it and two of them are
|
|
7
|
+
-- weaker than the first.
|
|
8
|
+
--
|
|
9
|
+
-- The distinction is not academic. A cue is a document about *this* record and
|
|
10
|
+
-- a tag is a statement somebody made about the track, while a title read off a
|
|
11
|
+
-- file name is a hint the filesystem happens to carry — `back to archangelsk`
|
|
12
|
+
-- written in lower case, with the artist's name and the release group cut off
|
|
13
|
+
-- it by a rule that guessed where they ended. That is a name worth having
|
|
14
|
+
-- where there is nothing else, and worth being able to tell apart from one
|
|
15
|
+
-- somebody wrote.
|
|
16
|
+
--
|
|
17
|
+
-- 'cue' a TITLE in a cue that describes these files. The authority.
|
|
18
|
+
-- 'tag' the file's own TITLE tag. Beats the folder, loses to the cue.
|
|
19
|
+
-- 'name' read off the file name (see `cue/track-name.ts`). The last word,
|
|
20
|
+
-- and the one that says out loud that it is a guess.
|
|
21
|
+
--
|
|
22
|
+
-- NULL where no source named the track — which includes a source that spoke and
|
|
23
|
+
-- was rejected as a ripper's marker (`(empty)`, `(silence)`), because the value
|
|
24
|
+
-- the row would carry is not a name at all. Also NULL for rows written before
|
|
25
|
+
-- this column, which is honest: they were written when the distinction was not
|
|
26
|
+
-- being made.
|
|
27
|
+
|
|
28
|
+
ALTER TABLE track ADD COLUMN title_source TEXT;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- The container the tag reader recognised in a file, NULL when it recognised
|
|
2
|
+
-- none ([[task:2706]]).
|
|
3
|
+
--
|
|
4
|
+
-- `tag-format-unknown` is an aggregate on purpose — one finding per root and
|
|
5
|
+
-- extension, because 309 `m4a` files are a fact about the collection and not 309
|
|
6
|
+
-- facts — and it was counted over the files the stage opened *this run*. That
|
|
7
|
+
-- made the number a property of the run rather than of the collection: a second
|
|
8
|
+
-- run opens nothing, so it was not recomputed at all, and a run that opened one
|
|
9
|
+
-- new file reported one while an older row still claimed two.
|
|
10
|
+
--
|
|
11
|
+
-- Counting it over the whole collection needs the reader's verdict to outlive
|
|
12
|
+
-- the run that reached it, which is what this column is. Read its NULL two ways,
|
|
13
|
+
-- told apart by `tags_read_run_id`: never read, and read with nothing
|
|
14
|
+
-- recognised. A file whose ripper wrote no tags at all is neither — its
|
|
15
|
+
-- container is named and it has simply stated nothing, which is a different
|
|
16
|
+
-- thing from a format nothing here can open.
|
|
17
|
+
ALTER TABLE file ADD COLUMN tags_container TEXT;
|
|
18
|
+
|
|
19
|
+
-- Every file already on disk predates the column, so its NULL means "not
|
|
20
|
+
-- recorded", which is not the same as "the reader recognised nothing". Left
|
|
21
|
+
-- alone, the whole collection would be counted as a format nothing can read.
|
|
22
|
+
-- Clearing the stamp is what makes the next scan open those files again and
|
|
23
|
+
-- write the verdict down — the same backfill migration 006 chose, for the same
|
|
24
|
+
-- reason and at the same cost: one full read of the collection's tags.
|
|
25
|
+
UPDATE file SET tags_read_run_id = NULL;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- The cover a file carries, as a place in the file rather than a copy.
|
|
2
|
+
--
|
|
3
|
+
-- A picture inside a file is the answer for a record whose folder holds none —
|
|
4
|
+
-- measured on the live collection, 54 of the 59 albums without a folder image
|
|
5
|
+
-- carry their art inside their files, 47 of them as an MP4 `covr` and 7 as an
|
|
6
|
+
-- ID3v2 `APIC`. Copying those bytes into the meta layer would mean gigabytes of
|
|
7
|
+
-- something already on the disk, and a copy that goes stale the moment somebody
|
|
8
|
+
-- edits the file's tags — so what is written down is where the image is.
|
|
9
|
+
--
|
|
10
|
+
-- `picture_type` is the file's own numbering, where 3 is a front cover: it says
|
|
11
|
+
-- which picture this is, and it is what `betterPicture` chose between when a
|
|
12
|
+
-- file carried more than one.
|
|
13
|
+
CREATE TABLE cover_art (
|
|
14
|
+
file_id INTEGER PRIMARY KEY REFERENCES file(id) ON DELETE CASCADE,
|
|
15
|
+
mime TEXT NOT NULL,
|
|
16
|
+
picture_type INTEGER NOT NULL,
|
|
17
|
+
offset INTEGER NOT NULL,
|
|
18
|
+
length INTEGER NOT NULL
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- A table a stage derives is empty until that stage runs again, and the tags
|
|
22
|
+
-- stage only reads files the ledger reports as moved or never read — so on the
|
|
23
|
+
-- scan after this migration every file would keep its old `tags_read_run_id`,
|
|
24
|
+
-- the stage would skip all of them, and this table would stay empty while the
|
|
25
|
+
-- collection it describes is full of covers. Clearing the stamp is what makes
|
|
26
|
+
-- the next scan read them once more; it costs one full read of the collection
|
|
27
|
+
-- and its absence costs the feature.
|
|
28
|
+
UPDATE file SET tags_read_run_id = NULL WHERE kind = 'audio';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- The year a record was released, which the classifier has been parsing and
|
|
2
|
+
-- throwing away.
|
|
3
|
+
--
|
|
4
|
+
-- `parseFolderName` reads a folder's name once for the pieces a collector puts
|
|
5
|
+
-- in it — the year, the artist, the format note — and only the title has ever
|
|
6
|
+
-- been used (`classify.ts`). A release called `1988. Группа крови` was filed
|
|
7
|
+
-- under `Группа крови` and the 1988 was lost, which is why no client could show
|
|
8
|
+
-- a release date: 236 of the 241 albums in the live collection name a year in
|
|
9
|
+
-- their path, and the meta layer held none of them.
|
|
10
|
+
--
|
|
11
|
+
-- `year_source` records which statement the number is, the way `title_source`
|
|
12
|
+
-- does: `folder` when the name slot carried it, `tag` when the only thing that
|
|
13
|
+
-- said so was a file's own DATE. Local sources own the year until an external
|
|
14
|
+
-- one arrives (v1.5, brainstorm:190 Q13) — and the folder outranks the tag for
|
|
15
|
+
-- the same reason it outranks it for a title: it is what the collector wrote.
|
|
16
|
+
ALTER TABLE album ADD COLUMN year INTEGER;
|
|
17
|
+
ALTER TABLE album ADD COLUMN year_source TEXT;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Which reading of a file a probe row is.
|
|
2
|
+
--
|
|
3
|
+
-- A row in this table is an answer about a file *as read by a particular
|
|
4
|
+
-- method*, and until now nothing in the row said which one — so a change of
|
|
5
|
+
-- method left readings behind that were no longer readings, and the collection
|
|
6
|
+
-- carried them as facts. (It did: `codec` was filled with the container's name
|
|
7
|
+
-- rather than a codec, `mp4` and `id3v2`, for two thousand files.)
|
|
8
|
+
--
|
|
9
|
+
-- Zero is "a method older than the one this code reads with", which is what
|
|
10
|
+
-- every row written before this migration is. `PROBE_METHOD` in
|
|
11
|
+
-- `src/probe/ffprobe.ts` is the number rows carry from here on, and raising it
|
|
12
|
+
-- is how a change of method is announced: every row below it stops being an
|
|
13
|
+
-- answer, and the next scan takes those files again.
|
|
14
|
+
ALTER TABLE audio_probe ADD COLUMN probe_method INTEGER NOT NULL DEFAULT 0;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Which reading of a file's tags a file row is.
|
|
2
|
+
--
|
|
3
|
+
-- The same defect `probe_method` was added in 015 to fix, one table over. A file
|
|
4
|
+
-- row carries the verdict of whichever reader last opened it — `tags_container`,
|
|
5
|
+
-- from 012, is that verdict — and until now nothing said *which* reader it was.
|
|
6
|
+
-- So a change of reader left verdicts behind that were no longer verdicts, and
|
|
7
|
+
-- the collection carried them as facts.
|
|
8
|
+
--
|
|
9
|
+
-- It did, and this migration is the repair. The Ogg reader landed on 2026-09-13
|
|
10
|
+
-- and the eighty `.ogg` files of the collection stayed read-and-empty: an
|
|
11
|
+
-- earlier scan had stamped them with a reader that did not know the format, the
|
|
12
|
+
-- files had not moved since, and the stage's own incrementality is what kept the
|
|
13
|
+
-- new reader away from them. Nothing was wrong with the reader; nothing could
|
|
14
|
+
-- reach it. The FLAC sweep before it had the same shape and was fixed by hand.
|
|
15
|
+
--
|
|
16
|
+
-- Zero is "a method older than the one this code reads with", which is what
|
|
17
|
+
-- every row written before this migration is. `TAGS_METHOD` in `src/tags/read.ts`
|
|
18
|
+
-- is the number rows carry from here on, and raising it is how a change of
|
|
19
|
+
-- reader is announced: every file below it is read again, once.
|
|
20
|
+
ALTER TABLE file ADD COLUMN tags_method INTEGER NOT NULL DEFAULT 0;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- What kind of place a cover's `offset` and `length` name.
|
|
2
|
+
--
|
|
3
|
+
-- Until now there was one kind and no need to say so: the range was the image,
|
|
4
|
+
-- a run of the audio file a client could be sent. A picture carried as a
|
|
5
|
+
-- `METADATA_BLOCK_PICTURE` comment — which is how an Ogg file holds a cover at
|
|
6
|
+
-- all, and how all eighty of this collection's `.ogg` files hold theirs — is a
|
|
7
|
+
-- different kind of place. It is base64 of a picture block inside a packet that
|
|
8
|
+
-- the lacing rule scatters across pages, so no range of the file *is* the image;
|
|
9
|
+
-- what can be written down is a range worth reading.
|
|
10
|
+
--
|
|
11
|
+
-- So the column says which of the two a row means:
|
|
12
|
+
--
|
|
13
|
+
-- 'image' [offset, length) is the picture. Every row written before this
|
|
14
|
+
-- migration, and every reader that names a picture's own bytes —
|
|
15
|
+
-- MP4 `covr`, ID3v2 `APIC`, a FLAC PICTURE block.
|
|
16
|
+
-- 'indirect' [offset, length) is a region that holds the picture and must be
|
|
17
|
+
-- read and parsed to get it. `src/cover/picture.ts` does that.
|
|
18
|
+
--
|
|
19
|
+
-- The picture was already being read and thrown away by this collection's
|
|
20
|
+
-- reader — it arrived as just another comment and left as a tag of a hundred and
|
|
21
|
+
-- eighty kilobytes of base64 — so nothing here is a new discovery. What is new
|
|
22
|
+
-- is that it goes somewhere useful.
|
|
23
|
+
ALTER TABLE cover_art ADD COLUMN kind TEXT NOT NULL DEFAULT 'image';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- The year a box was released, which the classifier has been reading and
|
|
2
|
+
-- throwing away.
|
|
3
|
+
--
|
|
4
|
+
-- `014_album_year.sql` gave the year a column on `album` because the folder name
|
|
5
|
+
-- states it and nothing kept it. A box is the case that column cannot hold: its
|
|
6
|
+
-- discs are named `CD1 ● Альбом`, `CD2 ● …` — the year is in the *box* folder's
|
|
7
|
+
-- name (`1988 ● Группа крови (2019, Maschina Records, MKK881CD, 3CD)`), and the
|
|
8
|
+
-- disc rows state nothing. So every record of every box came back with no year,
|
|
9
|
+
-- while the name the client was shown began with one.
|
|
10
|
+
--
|
|
11
|
+
-- `release` is the row that owns the box folder, so the year goes here, beside
|
|
12
|
+
-- the title it was parsed from. Nothing is backfilled: the next scan writes it,
|
|
13
|
+
-- the same way the title is written on every run.
|
|
14
|
+
ALTER TABLE release ADD COLUMN year INTEGER;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- The genre a file states, findable without reading the file's row.
|
|
2
|
+
--
|
|
3
|
+
-- `ALBUM_GENRES` drives from `file_tag` filtered by name — 2702 rows on this
|
|
4
|
+
-- collection — and then reads each row to get its `file_id` and its value.
|
|
5
|
+
-- `idx_file_tag_name` answers the name and stops there, so every one of those
|
|
6
|
+
-- rows costs a table read. Carrying `file_id` and `value` in the index makes the
|
|
7
|
+
-- scan covering, and measured on a copy of the live collection, warm and
|
|
8
|
+
-- interleaved with the baseline so that drift touches both equally, it halves the
|
|
9
|
+
-- join: 8.32 ms to 4.18. `ALBUM_GENRES` as a whole goes from 14.98 to 12.11 ms,
|
|
10
|
+
-- the window function being what is left.
|
|
11
|
+
--
|
|
12
|
+
-- It is not free: the index carries every tag value, and `VACUUM`ed the database
|
|
13
|
+
-- grows from 12.3 MB to 14.2. Fifteen per cent of the meta layer for about three
|
|
14
|
+
-- milliseconds on a listing route. That is a small win and it is written down as
|
|
15
|
+
-- one rather than dressed up.
|
|
16
|
+
--
|
|
17
|
+
-- `(name, file_id)` without the value was measured too and buys nothing — 8.05
|
|
18
|
+
-- against 8.32 — because what the join saves is the table read, and the value is
|
|
19
|
+
-- what it wanted from the table.
|
|
20
|
+
CREATE INDEX idx_file_tag_name_file_value ON file_tag (name, file_id, value);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- The statistics a new index invalidates, refreshed.
|
|
2
|
+
--
|
|
3
|
+
-- `019_file_tag_name_file_value.sql` added an index, and a migration is not a
|
|
4
|
+
-- scan: `runStages` refreshes the planner's statistics at the end of every run
|
|
5
|
+
-- (`run.ts`, the 'planner' stage) and it never ran here. So SQLite kept the
|
|
6
|
+
-- estimates it had made while that index did not exist and chose its plan for the
|
|
7
|
+
-- listing accordingly. Measured on a copy of the live collection, 500-record
|
|
8
|
+
-- `getAlbumList2` went from 29.8 ms to 68.6 ms the moment the index appeared, and
|
|
9
|
+
-- back to 24.7 ms once `ANALYZE` had run — better than it was without the index at
|
|
10
|
+
-- all, which is what the index was for.
|
|
11
|
+
--
|
|
12
|
+
-- This is the trap `run.ts` already describes, arriving through a door that file
|
|
13
|
+
-- does not cover: the planner's numbers are derived from the data, and anything
|
|
14
|
+
-- that changes the data *or the graph above it* has to say so. A future migration
|
|
15
|
+
-- that adds an index owes the same line.
|
|
16
|
+
ANALYZE;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Which FILE each cue mark was written in.
|
|
2
|
+
--
|
|
3
|
+
-- `cue_track` carried one `file_index` — the file the track opens — and two times,
|
|
4
|
+
-- and nothing said which file each time was counted from. A cue writes the pregap
|
|
5
|
+
-- of a track that opens a file at the *end* of the file before it, so `INDEX 00`
|
|
6
|
+
-- can sit in one FILE and `INDEX 01` in the next; the row then read
|
|
7
|
+
-- `index00_ms 895173, index01_ms 0`, which says the track ends before it begins.
|
|
8
|
+
-- Measured on this collection: 31 rows of 1001, across six cues.
|
|
9
|
+
--
|
|
10
|
+
-- Nothing in v1 reads those two columns directly, and every segmented album here
|
|
11
|
+
-- has a single `file_index`, so no output changes. The columns are added because
|
|
12
|
+
-- a schema that cannot say what its numbers are counted from is a schema the next
|
|
13
|
+
-- reader — the MCP surface v1.1 — would read as a lie.
|
|
14
|
+
ALTER TABLE cue_track ADD COLUMN index00_file_index INTEGER;
|
|
15
|
+
ALTER TABLE cue_track ADD COLUMN index01_file_index INTEGER;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- The text a sidecar file holds, decoded.
|
|
2
|
+
--
|
|
3
|
+
-- An `.nfo` and an EAC `.log` are the record's own documentation, and a FLAC rip
|
|
4
|
+
-- already carries both *inside* itself: `logfile` and `cuesheet` are tags that 41
|
|
5
|
+
-- files of this collection state. A rip that keeps them as files beside the audio
|
|
6
|
+
-- had them nowhere — the scanner recorded their kind and their size and never
|
|
7
|
+
-- opened them, so `file.encoding` was NULL for all 417 of them and the meta layer
|
|
8
|
+
-- could not be read across the two shapes (task:2757).
|
|
9
|
+
--
|
|
10
|
+
-- The text is here rather than in `file_tag`, and that is the point of a table of
|
|
11
|
+
-- its own. A tag is something a file states about itself and is queried by name;
|
|
12
|
+
-- a whole document under a name is what once put 562 KB of base64 into
|
|
13
|
+
-- `file_tag`, one value at a time (TAGS_METHOD 2). These run to 15 KB each. The
|
|
14
|
+
-- encoding and how much it was a guess are the `file` row's, beside every other
|
|
15
|
+
-- file's.
|
|
16
|
+
CREATE TABLE sidecar_text (
|
|
17
|
+
file_id INTEGER PRIMARY KEY REFERENCES file(id) ON DELETE CASCADE,
|
|
18
|
+
text TEXT NOT NULL
|
|
19
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Who a track is by, when that is not who the record is by.
|
|
2
|
+
--
|
|
3
|
+
-- An album's credit is a list with join phrases (`artist_credit`), because a
|
|
4
|
+
-- record can be by two people and the source says which word joined them. A
|
|
5
|
+
-- track's is the same shape, for the same reason — and `track.artist_id` cannot
|
|
6
|
+
-- hold it: one column holds one artist, and `Кино & Джоанна Стингрей` is two.
|
|
7
|
+
--
|
|
8
|
+
-- What this is for, measured on the live collection: of 2626 tracks whose cue
|
|
9
|
+
-- states a performer, 1862 state the record's own credit again, 218 differ —
|
|
10
|
+
-- and 534 have no album row at all, so for them the performer is not a
|
|
11
|
+
-- refinement of the record's credit but the only credit that exists (task:2729).
|
|
12
|
+
--
|
|
13
|
+
-- `position` is the order the source stated, and `join_phrase` is the word it
|
|
14
|
+
-- joined them with, exactly as `artist_credit` keeps both.
|
|
15
|
+
CREATE TABLE track_credit (
|
|
16
|
+
track_id INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE,
|
|
17
|
+
position INTEGER NOT NULL,
|
|
18
|
+
artist_id INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE,
|
|
19
|
+
join_phrase TEXT,
|
|
20
|
+
PRIMARY KEY (track_id, position)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX track_credit_artist ON track_credit (artist_id);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
-- Server-side playlists: the one thing in this database that is not derived
|
|
2
|
+
-- from the filesystem.
|
|
3
|
+
--
|
|
4
|
+
-- Every other table here is an overlay over the collection — a row exists
|
|
5
|
+
-- because a file exists, and a scan can rebuild all of it. A playlist is the
|
|
6
|
+
-- listener's own statement and is nowhere on disk: it is the first thing the
|
|
7
|
+
-- API writes, and the first thing a scan must leave alone.
|
|
8
|
+
--
|
|
9
|
+
-- That is why the two tables live in the meta layer rather than beside it. They
|
|
10
|
+
-- share the one file with the classified model, and the model's rule — the
|
|
11
|
+
-- scanner is the only writer — is narrowed rather than broken: the scanner
|
|
12
|
+
-- writes the model, and this is the one part of the file it never touches.
|
|
13
|
+
|
|
14
|
+
CREATE TABLE playlist (
|
|
15
|
+
id INTEGER PRIMARY KEY,
|
|
16
|
+
name TEXT NOT NULL,
|
|
17
|
+
comment TEXT,
|
|
18
|
+
-- The protocol's `public`, kept although one listener cannot tell the
|
|
19
|
+
-- difference: there is no second account to share with, and a server that
|
|
20
|
+
-- dropped what a client set would hand that client back a playlist claiming
|
|
21
|
+
-- not to be public — the client's own setting, denied to it.
|
|
22
|
+
public INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
-- When anything about the playlist last moved: its name, its comment, or a
|
|
25
|
+
-- song. The protocol shows both timestamps to a client, and a client that
|
|
26
|
+
-- syncs by them needs this one to change on an edit — a `changed` that only
|
|
27
|
+
-- moved when songs were added would tell such a client its stale copy of the
|
|
28
|
+
-- name was current.
|
|
29
|
+
changed_at TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- A song's place in a playlist.
|
|
33
|
+
--
|
|
34
|
+
-- `position` is the order the client is shown, and every write lays it out from
|
|
35
|
+
-- zero with no holes — because the protocol addresses entries by their index,
|
|
36
|
+
-- and `songIndexToRemove` counts on the same density. A song taken out by the
|
|
37
|
+
-- cascade below does leave its number unused until the next write, and nothing
|
|
38
|
+
-- minds: the number is read as an order (the rows are sorted by it), never as
|
|
39
|
+
-- the position a client means.
|
|
40
|
+
--
|
|
41
|
+
-- The same song may sit at two positions. The protocol says nothing against it,
|
|
42
|
+
-- a listener may well want it, and a schema that forbade it would be inventing
|
|
43
|
+
-- a rule the client cannot see.
|
|
44
|
+
--
|
|
45
|
+
-- `ON DELETE CASCADE` on the track is the whole of what "a playlist survives a
|
|
46
|
+
-- rescan" costs. A track still on disk keeps its id — the scanner upserts on
|
|
47
|
+
-- `(album_id, ordinal)` rather than re-inserting — so its entries stay exactly
|
|
48
|
+
-- where they were. A track whose file is gone is dropped by the sweep, and its
|
|
49
|
+
-- entries go with it instead of pointing at a row that is no longer there.
|
|
50
|
+
--
|
|
51
|
+
-- The cascade on the playlist is the obvious half: a deleted playlist holds
|
|
52
|
+
-- nothing. It is also what makes `deletePlaylist` one statement.
|
|
53
|
+
CREATE TABLE playlist_track (
|
|
54
|
+
playlist_id INTEGER NOT NULL REFERENCES playlist(id) ON DELETE CASCADE,
|
|
55
|
+
position INTEGER NOT NULL,
|
|
56
|
+
track_id INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE,
|
|
57
|
+
PRIMARY KEY (playlist_id, position)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- The sweep deletes tracks, and SQLite has to find this table's rows to cascade
|
|
61
|
+
-- into them; without this it scans every entry of every playlist per deleted
|
|
62
|
+
-- track.
|
|
63
|
+
CREATE INDEX playlist_track_track ON playlist_track (track_id);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- A playlist id is handed out once and never handed out again.
|
|
2
|
+
--
|
|
3
|
+
-- `playlist.id` is an INTEGER PRIMARY KEY, which in SQLite is the rowid: the
|
|
4
|
+
-- next row takes the highest number ever used plus one, so a row deleted and a
|
|
5
|
+
-- row created afterwards are given the *same* number — the highest ever used
|
|
6
|
+
-- went down when the first was deleted. Measured: create, delete, create, and
|
|
7
|
+
-- the second playlist is `pl:1` as well.
|
|
8
|
+
--
|
|
9
|
+
-- That is invisible to a client holding nothing and wrong for one holding a
|
|
10
|
+
-- playlist. An id this server handed out names a different playlist after the
|
|
11
|
+
-- original is deleted, and nothing in the answer says so — a player with the
|
|
12
|
+
-- old one open shows the new one, a bookmark opens somebody else's list.
|
|
13
|
+
--
|
|
14
|
+
-- `AUTOINCREMENT` is SQLite's own answer to exactly this, and it needs the
|
|
15
|
+
-- table rebuilt, which this migration deliberately is not: the table is already
|
|
16
|
+
-- deployed and may already hold a listener's playlists. A counter in a table of
|
|
17
|
+
-- its own says the same thing — the number is handed out, written down, and
|
|
18
|
+
-- never read back out of the rows.
|
|
19
|
+
CREATE TABLE playlist_sequence (
|
|
20
|
+
only_row INTEGER PRIMARY KEY CHECK (only_row = 1),
|
|
21
|
+
next INTEGER NOT NULL
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
-- Seeded past whatever is already there, so an id already handed out is not
|
|
25
|
+
-- handed out again by this either.
|
|
26
|
+
INSERT INTO playlist_sequence (only_row, next)
|
|
27
|
+
VALUES (1, (SELECT COALESCE(MAX(id), 0) + 1 FROM playlist));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- A playlist that a file asked for.
|
|
2
|
+
--
|
|
3
|
+
-- A `.m3u` sitting in the collection is a list somebody made, and one whose
|
|
4
|
+
-- songs come from different folders is a list this server should offer rather
|
|
5
|
+
-- than a folder it should browse. Importing it means writing a row here — so
|
|
6
|
+
-- the row has to say which file it came from, or the next scan would import the
|
|
7
|
+
-- same list again under a second name, and the one after that under a third.
|
|
8
|
+
--
|
|
9
|
+
-- `ON DELETE CASCADE` is the other half: the file is the list, and a list whose
|
|
10
|
+
-- file is gone is a list nobody asked for. The sweep deletes files; this makes
|
|
11
|
+
-- the imported playlist go with its own.
|
|
12
|
+
--
|
|
13
|
+
-- Rows written by the listener have no file and keep this NULL — the column
|
|
14
|
+
-- says where a playlist *came from*, not what it is, and the two kinds are told
|
|
15
|
+
-- apart by whether anything is there.
|
|
16
|
+
ALTER TABLE playlist ADD COLUMN source_file_id INTEGER REFERENCES file(id) ON DELETE CASCADE;
|
|
17
|
+
|
|
18
|
+
-- The import asks this question once per file, on every scan that re-reads one.
|
|
19
|
+
CREATE INDEX playlist_source_file ON playlist (source_file_id);
|