From b360dfbcaebacccbfe073918eafe1b54278431a7 Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Wed, 31 Dec 2025 00:02:50 +1030 Subject: [PATCH] feat(docs): add database schema and manifest specification --- docs/0.schema.sql | 389 +++++++++++++++++++++++++++++++++++++++++++ docs/1.languages.sql | 65 ++++++++ docs/2.qualities.sql | 61 +++++++ docs/PCD SPEC.md | 102 ++++++++++++ docs/manifest.md | 63 +++++++ 5 files changed, 680 insertions(+) create mode 100644 docs/0.schema.sql create mode 100644 docs/1.languages.sql create mode 100644 docs/2.qualities.sql create mode 100644 docs/PCD SPEC.md create mode 100644 docs/manifest.md diff --git a/docs/0.schema.sql b/docs/0.schema.sql new file mode 100644 index 0000000..2e30bf7 --- /dev/null +++ b/docs/0.schema.sql @@ -0,0 +1,389 @@ +-- ============================================================================ +-- PCD SCHEMA v1 +-- ============================================================================ + +-- ============================================================================ +-- CORE ENTITY TABLES (Independent - No Foreign Key Dependencies) +-- ============================================================================ +-- These tables form the foundation and can be populated in any order + +-- Tags are reusable labels that can be applied to multiple entity types +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(50) UNIQUE NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Languages used for profile configuration and custom format conditions +CREATE TABLE languages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(30) UNIQUE NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Regular expressions used in custom format pattern conditions +CREATE TABLE regular_expressions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) UNIQUE NOT NULL, + pattern TEXT NOT NULL, + regex101_id VARCHAR(50), -- Optional link to regex101.com for testing + description TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Individual quality definitions (e.g., "1080p Bluray", "2160p REMUX") +CREATE TABLE qualities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) UNIQUE NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Maps Profilarr canonical qualities to arr-specific API names +-- Absence of a row means the quality doesn't exist for that arr +CREATE TABLE quality_api_mappings ( + quality_id INTEGER NOT NULL, + arr_type VARCHAR(20) NOT NULL, -- 'radarr', 'sonarr' + api_name VARCHAR(100) NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (quality_id, arr_type), + FOREIGN KEY (quality_id) REFERENCES qualities(id) ON DELETE CASCADE +); + +-- Custom formats define patterns and conditions for media matching +CREATE TABLE custom_formats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- DEPENDENT ENTITY TABLES (Depend on Core Entities) +-- ============================================================================ + +-- Quality profiles define complete media acquisition strategies +CREATE TABLE quality_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + upgrades_allowed INTEGER NOT NULL DEFAULT 1, + minimum_custom_format_score INTEGER NOT NULL DEFAULT 0, + upgrade_until_score INTEGER NOT NULL DEFAULT 0, + upgrade_score_increment INTEGER NOT NULL DEFAULT 1 CHECK (upgrade_score_increment > 0), + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Quality groups combine multiple qualities treated as equivalent +-- Each group is specific to a quality profile (profiles do not share groups) +CREATE TABLE quality_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + quality_profile_id INTEGER NOT NULL, + name VARCHAR(100) NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(quality_profile_id, name), + FOREIGN KEY (quality_profile_id) REFERENCES quality_profiles(id) ON DELETE CASCADE +); + +-- Conditions define the matching logic for custom formats +-- Each condition has a type and corresponding data in a type-specific table +CREATE TABLE custom_format_conditions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + custom_format_id INTEGER NOT NULL, + name VARCHAR(100) NOT NULL, + type VARCHAR(50) NOT NULL, + arr_type VARCHAR(20) NOT NULL, -- 'radarr', 'sonarr', 'all' + negate INTEGER NOT NULL DEFAULT 0, + required INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (custom_format_id) REFERENCES custom_formats(id) ON DELETE CASCADE +); + +-- ============================================================================ +-- JUNCTION TABLES (Many-to-Many Relationships) +-- ============================================================================ + +-- Link regular expressions to tags +CREATE TABLE regular_expression_tags ( + regular_expression_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (regular_expression_id, tag_id), + FOREIGN KEY (regular_expression_id) REFERENCES regular_expressions(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- Link custom formats to tags +CREATE TABLE custom_format_tags ( + custom_format_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (custom_format_id, tag_id), + FOREIGN KEY (custom_format_id) REFERENCES custom_formats(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- Link quality profiles to tags +CREATE TABLE quality_profile_tags ( + quality_profile_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (quality_profile_id, tag_id), + FOREIGN KEY (quality_profile_id) REFERENCES quality_profiles(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- Link quality profiles to languages with type modifiers +-- Type can be: 'must', 'only', 'not', or 'simple' (default language preference) +CREATE TABLE quality_profile_languages ( + quality_profile_id INTEGER NOT NULL, + language_id INTEGER NOT NULL, + type VARCHAR(20) NOT NULL DEFAULT 'simple', -- 'must', 'only', 'not', 'simple' + PRIMARY KEY (quality_profile_id, language_id), + FOREIGN KEY (quality_profile_id) REFERENCES quality_profiles(id) ON DELETE CASCADE, + FOREIGN KEY (language_id) REFERENCES languages(id) ON DELETE CASCADE +); + +-- Define which qualities belong to which quality groups +-- All qualities in a group are treated as equivalent +CREATE TABLE quality_group_members ( + quality_group_id INTEGER NOT NULL, + quality_id INTEGER NOT NULL, + PRIMARY KEY (quality_group_id, quality_id), + FOREIGN KEY (quality_group_id) REFERENCES quality_groups(id) ON DELETE CASCADE, + FOREIGN KEY (quality_id) REFERENCES qualities(id) ON DELETE CASCADE +); + +-- Define the quality list for a profile (ordered by position) +-- Each item references either a single quality OR a quality group (never both) +-- Every quality must be represented (either directly or in a group) +-- The enabled flag controls whether the quality/group is active +CREATE TABLE quality_profile_qualities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + quality_profile_id INTEGER NOT NULL, + quality_id INTEGER, -- References a single quality + quality_group_id INTEGER, -- OR references a quality group + position INTEGER NOT NULL, -- Display order in the profile + enabled INTEGER NOT NULL DEFAULT 1, -- Whether this quality/group is enabled + upgrade_until INTEGER NOT NULL DEFAULT 0, -- Stop upgrading at this quality + CHECK ((quality_id IS NOT NULL AND quality_group_id IS NULL) OR (quality_id IS NULL AND quality_group_id IS NOT NULL)), + FOREIGN KEY (quality_profile_id) REFERENCES quality_profiles(id) ON DELETE CASCADE, + FOREIGN KEY (quality_id) REFERENCES qualities(id) ON DELETE CASCADE, + FOREIGN KEY (quality_group_id) REFERENCES quality_groups(id) ON DELETE CASCADE +); + +-- Assign custom formats to quality profiles with scoring +-- Scores determine upgrade priority and filtering behavior +CREATE TABLE quality_profile_custom_formats ( + quality_profile_id INTEGER NOT NULL, + custom_format_id INTEGER NOT NULL, + arr_type VARCHAR(20) NOT NULL, -- 'radarr', 'sonarr', 'all', + score INTEGER NOT NULL, + PRIMARY KEY (quality_profile_id, custom_format_id, arr_type), + FOREIGN KEY (quality_profile_id) REFERENCES quality_profiles(id) ON DELETE CASCADE, + FOREIGN KEY (custom_format_id) REFERENCES custom_formats(id) ON DELETE CASCADE +); + +-- ============================================================================ +-- CUSTOM FORMAT CONDITION TYPE TABLES +-- ============================================================================ +-- Each condition type has a dedicated table storing type-specific data +-- A condition_id should only appear in ONE of these tables, matching its type + +-- Pattern-based conditions (release_title, release_group, edition) +-- Each pattern condition references exactly one regular expression +CREATE TABLE condition_patterns ( + custom_format_condition_id INTEGER PRIMARY KEY, + regular_expression_id INTEGER NOT NULL, + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE, + FOREIGN KEY (regular_expression_id) REFERENCES regular_expressions(id) ON DELETE CASCADE +); + +-- Language-based conditions +CREATE TABLE condition_languages ( + custom_format_condition_id INTEGER PRIMARY KEY, + language_id INTEGER NOT NULL, + except_language INTEGER NOT NULL DEFAULT 0, -- Match everything EXCEPT this language + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE, + FOREIGN KEY (language_id) REFERENCES languages(id) ON DELETE CASCADE +); + +-- Indexer flag conditions (e.g., "Scene", "Freeleech") +CREATE TABLE condition_indexer_flags ( + custom_format_condition_id INTEGER PRIMARY KEY, + flag VARCHAR(100) NOT NULL, + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE +); + +-- Source conditions (e.g., "Bluray", "Web", "DVD") +CREATE TABLE condition_sources ( + custom_format_condition_id INTEGER PRIMARY KEY, + source VARCHAR(100) NOT NULL, + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE +); + +-- Resolution conditions (e.g., "1080p", "2160p") +CREATE TABLE condition_resolutions ( + custom_format_condition_id INTEGER PRIMARY KEY, + resolution VARCHAR(100) NOT NULL, + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE +); + +-- Quality modifier conditions (e.g., "REMUX", "WEBDL") +CREATE TABLE condition_quality_modifiers ( + custom_format_condition_id INTEGER PRIMARY KEY, + quality_modifier VARCHAR(100) NOT NULL, + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE +); + +-- Size-based conditions with min/max bounds in bytes +CREATE TABLE condition_sizes ( + custom_format_condition_id INTEGER PRIMARY KEY, + min_bytes INTEGER, -- Null means no minimum + max_bytes INTEGER, -- Null means no maximum + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE +); + +-- Release type conditions (e.g., "Movie", "Episode") +CREATE TABLE condition_release_types ( + custom_format_condition_id INTEGER PRIMARY KEY, + release_type VARCHAR(100) NOT NULL, + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE +); + +-- Year-based conditions with min/max bounds +CREATE TABLE condition_years ( + custom_format_condition_id INTEGER PRIMARY KEY, + min_year INTEGER, -- Null means no minimum + max_year INTEGER, -- Null means no maximum + FOREIGN KEY (custom_format_condition_id) REFERENCES custom_format_conditions(id) ON DELETE CASCADE +); + +-- ============================================================================ +-- MEDIA MANAGEMENT TABLES +-- ============================================================================ + +-- Radarr quality size definitions +CREATE TABLE radarr_quality_definitions ( + quality_id INTEGER PRIMARY KEY, + min_size INTEGER NOT NULL DEFAULT 0, + max_size INTEGER NOT NULL, + preferred_size INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (quality_id) REFERENCES qualities(id) ON DELETE CASCADE +); + +-- Sonarr quality size definitions +CREATE TABLE sonarr_quality_definitions ( + quality_id INTEGER PRIMARY KEY, + min_size INTEGER NOT NULL DEFAULT 0, + max_size INTEGER NOT NULL, + preferred_size INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (quality_id) REFERENCES qualities(id) ON DELETE CASCADE +); + +-- Radarr naming configuration +CREATE TABLE radarr_naming ( + id INTEGER PRIMARY KEY CHECK (id = 1), + rename INTEGER NOT NULL DEFAULT 1, + movie_format TEXT NOT NULL, + movie_folder_format TEXT NOT NULL, + replace_illegal_characters INTEGER NOT NULL DEFAULT 0, + colon_replacement_format VARCHAR(20) NOT NULL DEFAULT 'smart', + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Sonarr naming configuration +CREATE TABLE sonarr_naming ( + id INTEGER PRIMARY KEY CHECK (id = 1), + rename INTEGER NOT NULL DEFAULT 1, + standard_episode_format TEXT NOT NULL, + daily_episode_format TEXT NOT NULL, + anime_episode_format TEXT NOT NULL, + series_folder_format TEXT NOT NULL, + season_folder_format TEXT NOT NULL, + replace_illegal_characters INTEGER NOT NULL DEFAULT 0, + colon_replacement_format INTEGER NOT NULL DEFAULT 4, + custom_colon_replacement_format TEXT, + multi_episode_style INTEGER NOT NULL DEFAULT 5, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Radarr general media settings +CREATE TABLE radarr_media_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + propers_repacks VARCHAR(50) NOT NULL DEFAULT 'doNotPrefer', + enable_media_info INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Sonarr general media settings +CREATE TABLE sonarr_media_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + propers_repacks VARCHAR(50) NOT NULL DEFAULT 'doNotPrefer', + enable_media_info INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- DELAY PROFILES +-- ============================================================================ + +-- Delay profiles control download timing preferences +CREATE TABLE delay_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) UNIQUE NOT NULL, + preferred_protocol VARCHAR(20) NOT NULL CHECK ( + preferred_protocol IN ('prefer_usenet', 'prefer_torrent', 'only_usenet', 'only_torrent') + ), + usenet_delay INTEGER, -- minutes, NULL if only_torrent + torrent_delay INTEGER, -- minutes, NULL if only_usenet + bypass_if_highest_quality INTEGER NOT NULL DEFAULT 0, + bypass_if_above_custom_format_score INTEGER NOT NULL DEFAULT 0, + minimum_custom_format_score INTEGER, -- Required when bypass_if_above_custom_format_score = 1 + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- Enforce usenet_delay is NULL only when only_torrent + CHECK ( + (preferred_protocol = 'only_torrent' AND usenet_delay IS NULL) OR + (preferred_protocol != 'only_torrent' AND usenet_delay IS NOT NULL) + ), + -- Enforce torrent_delay is NULL only when only_usenet + CHECK ( + (preferred_protocol = 'only_usenet' AND torrent_delay IS NULL) OR + (preferred_protocol != 'only_usenet' AND torrent_delay IS NOT NULL) + ), + -- Enforce minimum_custom_format_score required when bypass enabled + CHECK ( + (bypass_if_above_custom_format_score = 0 AND minimum_custom_format_score IS NULL) OR + (bypass_if_above_custom_format_score = 1 AND minimum_custom_format_score IS NOT NULL) + ) +); + +-- Link delay profiles to tags (at least 1 required - enforced at application level) +CREATE TABLE delay_profile_tags ( + delay_profile_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (delay_profile_id, tag_id), + FOREIGN KEY (delay_profile_id) REFERENCES delay_profiles(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- ============================================================================ +-- INDEXES AND CONSTRAINTS +-- ============================================================================ + +-- Ensure only one quality item per profile can be marked as upgrade_until +CREATE UNIQUE INDEX idx_one_upgrade_until_per_profile +ON quality_profile_qualities(quality_profile_id) +WHERE upgrade_until = 1; diff --git a/docs/1.languages.sql b/docs/1.languages.sql new file mode 100644 index 0000000..f262d82 --- /dev/null +++ b/docs/1.languages.sql @@ -0,0 +1,65 @@ +-- Languages +INSERT INTO languages (name) VALUES +('Unknown'), +('English'), +('French'), +('Spanish'), +('German'), +('Italian'), +('Danish'), +('Dutch'), +('Japanese'), +('Icelandic'), +('Chinese'), +('Russian'), +('Polish'), +('Vietnamese'), +('Swedish'), +('Norwegian'), +('Finnish'), +('Turkish'), +('Portuguese'), +('Flemish'), +('Greek'), +('Korean'), +('Hungarian'), +('Hebrew'), +('Lithuanian'), +('Czech'), +('Hindi'), +('Romanian'), +('Thai'), +('Bulgarian'), +('Portuguese (Brazil)'), +('Arabic'), +('Ukrainian'), +('Persian'), +('Bengali'), +('Slovak'), +('Latvian'), +('Spanish (Latino)'), +('Catalan'), +('Croatian'), +('Serbian'), +('Bosnian'), +('Estonian'), +('Tamil'), +('Indonesian'), +('Telugu'), +('Macedonian'), +('Slovenian'), +('Malayalam'), +('Kannada'), +('Albanian'), +('Afrikaans'), +('Marathi'), +('Tagalog'), +('Urdu'), +('Romansh'), +('Mongolian'), +('Georgian'), +('Azerbaijani'), +('Uzbek'), +('Malay'), +('Any'), +('Original'); \ No newline at end of file diff --git a/docs/2.qualities.sql b/docs/2.qualities.sql new file mode 100644 index 0000000..e96ac95 --- /dev/null +++ b/docs/2.qualities.sql @@ -0,0 +1,61 @@ +-- Qualities +INSERT INTO qualities (name) VALUES +('Unknown'), +('WORKPRINT'), +('CAM'), +('TELESYNC'), +('TELECINE'), +('DVDSCR'), +('REGIONAL'), +('SDTV'), +('DVD'), +('DVD-R'), +('HDTV-480p'), +('HDTV-720p'), +('HDTV-1080p'), +('HDTV-2160p'), +('WEBDL-480p'), +('WEBDL-720p'), +('WEBDL-1080p'), +('WEBDL-2160p'), +('WEBRip-480p'), +('WEBRip-720p'), +('WEBRip-1080p'), +('WEBRip-2160p'), +('Bluray-480p'), +('Bluray-576p'), +('Bluray-720p'), +('Bluray-1080p'), +('Bluray-2160p'), +('Remux-1080p'), +('Remux-2160p'), +('BR-DISK'), +('Raw-HD'); + +-- Radarr mappings (30 qualities) +INSERT INTO quality_api_mappings (quality_id, arr_type, api_name) +SELECT id, 'radarr', name FROM qualities WHERE name IN ( + 'Unknown', 'WORKPRINT', 'CAM', 'TELESYNC', 'TELECINE', 'DVDSCR', 'REGIONAL', + 'SDTV', 'DVD', 'DVD-R', 'HDTV-720p', 'HDTV-1080p', 'HDTV-2160p', + 'WEBDL-480p', 'WEBDL-720p', 'WEBDL-1080p', 'WEBDL-2160p', + 'WEBRip-480p', 'WEBRip-720p', 'WEBRip-1080p', 'WEBRip-2160p', + 'Bluray-480p', 'Bluray-576p', 'Bluray-720p', 'Bluray-1080p', 'Bluray-2160p', + 'Remux-1080p', 'Remux-2160p', 'BR-DISK', 'Raw-HD' +); + +-- Sonarr mappings (20 exact matches + 2 remuxes with different names) +INSERT INTO quality_api_mappings (quality_id, arr_type, api_name) +SELECT id, 'sonarr', name FROM qualities WHERE name IN ( + 'Unknown', 'SDTV', 'DVD', 'HDTV-720p', 'HDTV-1080p', 'HDTV-2160p', + 'WEBDL-480p', 'WEBDL-720p', 'WEBDL-1080p', 'WEBDL-2160p', + 'WEBRip-480p', 'WEBRip-720p', 'WEBRip-1080p', 'WEBRip-2160p', + 'Bluray-480p', 'Bluray-576p', 'Bluray-720p', 'Bluray-1080p', 'Bluray-2160p', + 'Raw-HD' +); + +-- Sonarr remux mappings (different names) +INSERT INTO quality_api_mappings (quality_id, arr_type, api_name) +SELECT id, 'sonarr', 'Bluray-1080p Remux' FROM qualities WHERE name = 'Remux-1080p'; + +INSERT INTO quality_api_mappings (quality_id, arr_type, api_name) +SELECT id, 'sonarr', 'Bluray-2160p Remux' FROM qualities WHERE name = 'Remux-2160p'; diff --git a/docs/PCD SPEC.md b/docs/PCD SPEC.md new file mode 100644 index 0000000..cac67b3 --- /dev/null +++ b/docs/PCD SPEC.md @@ -0,0 +1,102 @@ +# Profile Compliant Databases (PCDs) + +## 1. Purpose + +PCDs describe a database as a sequence of SQL operations, not as final data. The +stored artifact is **how to build the state**, not **the state** itself. We +describe this as _operational_, instead of the traditional _stateful_. + +## 2. Operational SQL (OSQL) + +PCDs use SQL in an append-only, ordered way. Call this **Operational SQL +(OSQL)**. + +1. **Append-only**: once an operation exists, it is never edited or deleted. +2. **Ordered**: operations run in a defined order; later operations can override + the effects of earlier ones. +3. **Replayable**: anyone can rebuild the database by replaying operations in + order. +4. **Relational**: operations target real tables/columns/rows, so constraints + (FKs) still apply. + +This gives "Mutable Immutability": history is immutable; results are mutable +because new ops (operations) can be added. + +## 3. Change-Driven Development (CDD) + +CDD is the workflow for producing operations. + +1. Start from a change: "profile `1080p Quality HDR` should give `Dolby Atmos` a + higher score". +2. Express it as a single SQL operation: + +```sql +UPDATE quality_profile_custom_formats +SET score = 1200 +WHERE profile_id = qp('1080p Quality HDR') +AND custom_format_id = cf('Dolby Atmos') +AND score = 400; -- expected previous value +``` + +3. Append it to the appropriate layer (see Layers below) +4. Recompose. + +The expected-value guard (`AND score = 400`) is what makes conflicts explicit. + +## 4. Layers + +PCDs run in layers. Every layer is append-only, but later layers can override +the effect of earlier ones. + +1. **Schema**\ + Core DDL for the PCD. Created and maintained by Profilarr. Creates tables, + FKs, indexes. **No data.** + +2. **Dependencies**\ + Reserved for future use. Will allow PCDs to compose with other PCDs. + +3. **Base**\ + The actual shipped database content (profiles, quality lists, format + definitions) for this PCD/version. + +4. **Tweaks**\ + Optional, append-only operations that adjust behaviour (allow DV, allow CAMS, + disable group Z). + +5. **User Ops**\ + User changes created for a specific instantiation of a database. Heavy value + guards to detect conflicts and alert users when upstream changes. + +## 5. Repository Layout + +A PCD repository has a manifest, an operations folder, and an optional tweaks +folder. + +```text +my-pcd/ +├── pcd.json +├── ops/ +│ ├── 1.create-1080p-Efficient.sql +└── tweaks/ + ├── allow-DV-no-fallback.sql + └── ban-megusta.sql +``` + +In the case of the schema, it's the same layout, with only the DDL in `ops/` and +no tweaks: + +```text +schema-pcd/ +├── pcd.json +└── ops/ + └── 0.schema.sql +``` + +## 6. Dependencies (Post-2.0) + +**Dependencies are not part of 2.0.** At current scale (~10 in use databases), +forking solves shared-code needs without the complexity of dependency +resolution, version conflicts, and circular dependency detection. The layer +system supports adding dependencies in 2.1+ without breaking existing PCDs. +We'll build dependency support when clear duplication patterns emerge and +forking proves insufficient. diff --git a/docs/manifest.md b/docs/manifest.md new file mode 100644 index 0000000..eef1f21 --- /dev/null +++ b/docs/manifest.md @@ -0,0 +1,63 @@ +# Manifest Specification + +Every Profilarr Compliant Database must include a `pcd.json` manifest file in +its root directory. This file defines the database's identity, compatibility, +and dependencies. + +## Required Fields + +| Field | Description | +| --------------------------- | --------------------------------------------------------------------------------------------- | +| `name` | Unique identifier for the database (lowercase, hyphens preferred) | +| `version` | Semantic version of the database (MAJOR.MINOR.PATCH) | +| `description` | Short summary of what the database provides | +| `dependencies` | Object mapping dependency names to semver ranges. All PCDs must depend on `schema` at minimum | +| `profilarr.minimum_version` | Minimum Profilarr version required to use this database | + +## Optional Fields + +| Field | Description | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `arr_types` | Array of supported arr applications (`["radarr"]`, `["sonarr"]`, or `["radarr", "sonarr"]`). If omitted, assumes all are supported | +| `authors` | Array of contributor objects with name and optional email | +| `license` | SPDX license identifier | +| `repository` | Git repository URL | +| `dependencies` | Can include other PCDs in addition to the schema, enabling layered databases | +| `tags` | Array of descriptive keywords for discovery | +| `links` | External resource URLs (homepage, documentation, issues) | + +## Example + +```json +{ + "name": "db", + "version": "2.1.35", + "description": "Seraphys' OCD Playground", + "arr_types": ["radarr", "sonarr", "whisparr"], + + "dependencies": { + "schema": "^1.1.0" + }, + + "authors": [ + { + "name": "Dictionarry Team", + "email": "team@dictionarry.dev" + } + ], + + "license": "MIT", + "repository": "https://github.com/dictionarry-hub/database", + + "tags": ["4k", "hdr", "remux", "quality", "archival"], + + "links": { + "homepage": "https://dictionarry.dev", + "issues": "https://github.com/dictionarry-hub/db/issues" + }, + + "profilarr": { + "minimum_version": "2.0.0" + } +} +```