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