161 lines
7.3 KiB
SQL
161 lines
7.3 KiB
SQL
-- Movie side of the domain model in DESIGN.md §4. Series, seasons and
|
|
-- episodes are issue #34 and are deliberately absent here.
|
|
--
|
|
-- Every table is STRICT: SQLite's default type affinity would happily store
|
|
-- a string in an INTEGER column, and this schema is the last place that
|
|
-- should be forgiving.
|
|
|
|
CREATE TABLE policies (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
-- §5.2. Which audio track a release must carry, expressed against the
|
|
-- title's original language rather than a fixed list.
|
|
required_audio TEXT NOT NULL CHECK (json_valid(required_audio)),
|
|
-- §5.2. Languages that never satisfy required_audio unless they are the
|
|
-- title's own original language. Subtitles are not filtered.
|
|
dub_blacklist TEXT NOT NULL CHECK (json_valid(dub_blacklist)),
|
|
-- §5.3. Dolby Vision profiles, knowable only from ffprobe.
|
|
hdr_rules TEXT NOT NULL CHECK (json_valid(hdr_rules)),
|
|
-- §5.5. Per resolution: floor, target and the penalty above target.
|
|
size_bands TEXT NOT NULL CHECK (json_valid(size_bands)),
|
|
resolution_pref TEXT NOT NULL CHECK (json_valid(resolution_pref)),
|
|
-- §5.5. Small tiebreaker, not the dominant scoring term.
|
|
source_weights TEXT NOT NULL CHECK (json_valid(source_weights)),
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT;
|
|
|
|
-- §5.1. One policy per root, two roots per media kind.
|
|
CREATE TABLE roots (
|
|
id INTEGER PRIMARY KEY,
|
|
kind TEXT NOT NULL CHECK (kind IN ('movie', 'tv')),
|
|
audience TEXT NOT NULL CHECK (audience IN ('main', 'kids')),
|
|
path TEXT NOT NULL UNIQUE,
|
|
policy_id INTEGER NOT NULL REFERENCES policies (id),
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
UNIQUE (kind, audience)
|
|
) STRICT;
|
|
|
|
-- §4.1. `wanted` is the only intent. `state` is a cache of what reconcile
|
|
-- last observed, and arr-core (#7) owns the canonical enum this mirrors.
|
|
CREATE TABLE movies (
|
|
id INTEGER PRIMARY KEY,
|
|
tmdb_id INTEGER NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
year INTEGER,
|
|
-- §5.2. BCP-47 from TMDB. Nullable until the first metadata refresh.
|
|
original_language TEXT,
|
|
root_id INTEGER NOT NULL REFERENCES roots (id),
|
|
wanted INTEGER NOT NULL DEFAULT 1 CHECK (wanted IN (0, 1)),
|
|
-- §5.1. Per-title relaxations and tightenings of the root policy.
|
|
overrides TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(overrides)),
|
|
state TEXT NOT NULL DEFAULT 'missing'
|
|
CHECK (state IN ('missing', 'grabbed', 'imported')),
|
|
-- §6.3. Stops targeted search while leaving RSS matching on.
|
|
blocked INTEGER NOT NULL DEFAULT 0 CHECK (blocked IN (0, 1)),
|
|
search_attempts INTEGER NOT NULL DEFAULT 0,
|
|
last_searched_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT;
|
|
|
|
-- §8. The reconcile loop's work list is "wanted, not blocked, least recently
|
|
-- searched first", so the partial index carries the sort column.
|
|
CREATE INDEX movies_pending_search
|
|
ON movies (last_searched_at)
|
|
WHERE wanted = 1 AND blocked = 0;
|
|
|
|
CREATE INDEX movies_state ON movies (state);
|
|
CREATE INDEX movies_root ON movies (root_id);
|
|
|
|
-- §4. Polymorphic owner so episodes (#34) reuse the table unchanged.
|
|
CREATE TABLE media_files (
|
|
id INTEGER PRIMARY KEY,
|
|
owner_kind TEXT NOT NULL CHECK (owner_kind IN ('movie', 'episode')),
|
|
owner_id INTEGER NOT NULL,
|
|
path TEXT NOT NULL UNIQUE,
|
|
size INTEGER NOT NULL,
|
|
-- §5.6. What ffprobe found: resolution, source, hdr, audio and sub tracks.
|
|
probed TEXT CHECK (probed IS NULL OR json_valid(probed)),
|
|
-- §5.7. Which rule was relaxed to allow this import, if any.
|
|
waiver TEXT CHECK (waiver IS NULL OR json_valid(waiver)),
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT;
|
|
|
|
CREATE INDEX media_files_owner ON media_files (owner_kind, owner_id);
|
|
|
|
-- §8. A file with no probe result yet is a pending import.
|
|
CREATE INDEX media_files_unprobed ON media_files (id) WHERE probed IS NULL;
|
|
|
|
CREATE TABLE releases (
|
|
id INTEGER PRIMARY KEY,
|
|
-- Prowlarr's indexer id. Not a foreign key: indexers live in Prowlarr.
|
|
indexer_id INTEGER NOT NULL,
|
|
guid TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
seeders INTEGER,
|
|
publish_date TEXT,
|
|
download_url TEXT NOT NULL,
|
|
-- §5.6. What the release name claims, before anything is downloaded.
|
|
parsed TEXT NOT NULL CHECK (json_valid(parsed)),
|
|
score REAL,
|
|
-- §9.3. Three buckets. `rejected_rule` names the rule that killed it so
|
|
-- an over-strict filter is visible without reading release names.
|
|
verdict TEXT CHECK (verdict IN ('eligible', 'waived', 'rejected')),
|
|
rejected_rule TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
UNIQUE (indexer_id, guid),
|
|
CHECK ((verdict = 'rejected') = (rejected_rule IS NOT NULL))
|
|
) STRICT;
|
|
|
|
CREATE INDEX releases_verdict ON releases (verdict, score);
|
|
|
|
-- §7.3. The torrent and the library entry are separate state machines; this
|
|
-- row is the torrent's. Download progress stays in memory.
|
|
CREATE TABLE grabs (
|
|
id INTEGER PRIMARY KEY,
|
|
release_id INTEGER NOT NULL REFERENCES releases (id),
|
|
target_kind TEXT NOT NULL CHECK (target_kind IN ('movie', 'episode')),
|
|
target_id INTEGER NOT NULL,
|
|
infohash TEXT NOT NULL UNIQUE,
|
|
state TEXT NOT NULL DEFAULT 'sent'
|
|
CHECK (state IN ('sent', 'downloaded', 'imported', 'failed')),
|
|
grabbed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
imported_at TEXT
|
|
) STRICT;
|
|
|
|
CREATE INDEX grabs_state ON grabs (state);
|
|
CREATE INDEX grabs_target ON grabs (target_kind, target_id);
|
|
|
|
-- §6.3. Keyed on infohash and on normalised name, because the same release
|
|
-- reappears under a different infohash.
|
|
CREATE TABLE blacklist (
|
|
id INTEGER PRIMARY KEY,
|
|
infohash TEXT UNIQUE,
|
|
normalised_name TEXT NOT NULL,
|
|
reason TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT;
|
|
|
|
CREATE INDEX blacklist_name ON blacklist (normalised_name);
|
|
|
|
-- §4.3. A tag on titles, driving filtering and notification routing. Never a path.
|
|
CREATE TABLE owners (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
ntfy_topic TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT;
|
|
|
|
CREATE TABLE title_owners (
|
|
title_kind TEXT NOT NULL CHECK (title_kind IN ('movie', 'series')),
|
|
title_id INTEGER NOT NULL,
|
|
owner_id INTEGER NOT NULL REFERENCES owners (id) ON DELETE CASCADE,
|
|
PRIMARY KEY (title_kind, title_id, owner_id)
|
|
) STRICT;
|
|
|
|
CREATE INDEX title_owners_owner ON title_owners (owner_id);
|