15 KiB
PCD Entities
Source: src/lib/server/pcd/entities/
This doc covers the five PCD entity types, their fields, and their CRUD patterns. For the underlying infrastructure -- layers, writer pipeline, value guards, and conflict resolution -- see pcd.md.
Table of Contents
- Common Patterns
- Custom Formats
- Quality Profiles
- Regular Expressions
- Delay Profiles
- Media Management
- Summary
Common Patterns
Every entity mutation flows through writeOperation() (see
pcd.md: Writer). A few patterns repeat across all
entities:
- Value guards -- UPDATE/DELETE ops include WHERE clauses checking old
field values. If the upstream PCD changed a field, the guard fails
(
rowcount = 0) and the op is flagged as conflicted. - Stable keys -- each op carries a
stableKey({ key, value }) that identifies the target entity by its primary lookup column (usually name). - Group IDs -- when a single user action produces multiple ops (rename
- cascade, delete + dependents), they share a
groupIdUUID.
- cascade, delete + dependents), they share a
- Cascading -- some mutations produce "generated" ops that update dependent entities (e.g. renaming a CF updates QP scoring refs).
- Case-insensitive names -- all entity names are unique case-insensitively. Create/rename checks enforce this.
Custom Formats
Source: entities/customFormats/
Tables: custom_formats, custom_format_tags, custom_format_conditions,
9 condition type tables, custom_format_tests
Custom formats define match logic via conditions and optional test cases. They're the building blocks of quality profile scoring.
Fields: name, description, include_in_rename, tags
Create inserts the base row with name and empty description, then separate ops for description, include_in_rename, and tags if non-default.
Update splits into per-field ops (see pcd.md: Op Splitting):
| Field | Guard | Notes |
|---|---|---|
| description | old description value | NULL normalized to empty string |
| include_in_rename | old flag value | |
| tags | none (idempotent) | delete + re-insert pattern |
| name (rename) | none | cascades to QP scoring refs |
When a CF is renamed, the writer generates dependent ops that update every
quality_profile_custom_formats row referencing the old name. These
dependent ops are grouped with the rename op and marked generated: true.
Delete first writes explicit ops to remove the CF from all QP scoring (one per profile), then deletes the CF itself. Foreign key cascades clean up conditions, tests, and tags.
Conditions
Source: entities/customFormats/conditions/
Tables: custom_format_conditions + one type table per condition type
Each condition has a type field and stores its data in exactly one
type-specific table, joined by (custom_format_name, condition_name):
| Type | Type table | Key data |
|---|---|---|
| release_title | condition_patterns | regex reference |
| release_group | condition_patterns | regex reference |
| edition | condition_patterns | regex reference |
| language | condition_languages | language + except flag |
| source | condition_sources | source enum |
| resolution | condition_resolutions | resolution enum |
| quality_modifier | condition_quality_modifiers | modifier enum |
| release_type | condition_release_types | type enum |
| indexer_flag | condition_indexer_flags | flag string |
| size | condition_sizes | min/max bytes |
| year | condition_years | min/max year |
Condition updates produce per-condition ops: deletes for removed
conditions, inserts for new ones, and delete+insert pairs for changed ones
(the type-specific table row is replaced). All ops use
skipRecompile: true except the last, which triggers a single recompile.
Pattern-type conditions track their regex dependency via dependsOn in
metadata.
Tests
Source: entities/customFormats/tests/
Tables: custom_format_tests
Test cases have a composite key of (custom_format_name, title, type).
Fields: title, type (movie/series), should_match, description. Updates use
value guards on all fields. No op splitting.
The CF testing page evaluates test cases against the format's conditions
using the parser service. Each test title is parsed for
metadata (source, resolution, languages, etc.), then pattern-based
conditions are matched against the .NET regex engine. The evaluator
(customFormats/evaluator.ts) combines parsed metadata with pattern match
results to produce per-condition match outcomes. See
parser.md: Custom Format Testing for
the full flow.
Quality Profiles
Source: entities/qualityProfiles/
Tables: quality_profiles, quality_profile_tags,
quality_profile_languages, quality_profile_qualities, quality_groups,
quality_group_members, quality_profile_custom_formats
Quality profiles tie everything together: an ordered list of acceptable qualities, custom format scores that influence selection, language preferences, and upgrade thresholds.
Fields: name, description, tags, language, upgrades_allowed, minimum_custom_format_score, upgrade_until_score, upgrade_score_increment
Create inserts the profile with defaults (upgrades_allowed = 1,
scores at 0, increment at 1), then seeds all available qualities as
enabled entries, inserts tags, groups, and a language if selected.
General update splits per-field: description, language (delete + re-insert), tags (delete + re-insert), name (rename).
Qualities
Source: entities/qualityProfiles/qualities/
Tables: quality_profile_qualities, quality_groups, quality_group_members
The quality list is an ordered set of individual qualities and/or quality
groups. Each entry has a position, enabled flag, and an optional
upgrade_until marker (at most one per profile, enforced by a partial
unique index).
Updates use a batched strategy: all row-level changes (removes, adds,
updates to position/enabled/upgrade_until) are collected into a single
writeOperation call. Group membership changes (adding/removing members)
are included in the same batch. The op carries an ordered_items
desired state with from/to arrays for full-list conflict detection.
Groups are profile-specific -- two profiles can have groups with the same name without conflict.
Scoring
Source: entities/qualityProfiles/scoring/
Tables: quality_profiles, quality_profile_custom_formats
Each profile assigns scores to custom formats, optionally per arr_type
(all, radarr, sonarr). When a score set to all is modified for a
specific arr_type, the all row is expanded into per-type rows so the
change only affects the targeted type.
Score operations are per-CF: insert (new score), update (changed score,
guarded on old value), or delete (score removed). Profile-level settings
(upgrades_allowed, minimum_custom_format_score,
upgrade_until_score, upgrade_score_increment) are updated in separate
ops with their own guards.
Entity Testing
Source: entities/qualityProfiles/entityTests/
Tables: test_entities, test_releases
Entity testing validates quality profile scoring against real examples. A
test entity is a movie or series identified by (type, tmdb_id). Each
entity has test releases with fields: title, type, size_bytes, languages
(JSON array), indexers (JSON array), and flags (JSON array).
Create entities uses bulk insert, skipping duplicates by composite key. Delete entity removes releases first (value-guarded), then the entity. Create releases supports single or bulk insert; bulk skips duplicate titles per entity. Update/delete releases guard on all fields (title, size, languages, indexers, flags).
Ops can target base or user layers depending on whether the database has base write access.
Test entities are created from three sources: TMDB search (adds entities), manual release entry (title + metadata), and import from Arr (pulls recent releases from configured instances).
The evaluation API (/api/v1/entity-testing/evaluate) uses the
parser service to batch-parse all release titles, extract
patterns from every custom format's conditions, batch-match patterns
against parsed metadata, and evaluate each CF per release. The UI then
computes profile scores using allCfScores() from scoring/read.ts. See
parser.md: Entity Testing for the evaluation
pipeline.
Regular Expressions
Source: entities/regularExpressions/
Tables: regular_expressions, regular_expression_tags
Regex patterns are first-class entities referenced by custom format
conditions (via condition_patterns). They're shared -- one regex can be
used by many conditions across many CFs.
Fields: name, pattern, description, regex101_id, tags
Update writes a single op for the regex fields (all changed fields in one query, each guarded). Tags use the standard delete + re-insert pattern.
Rename triggers generated ops that update every condition_patterns
row referencing the old name, grouped with the rename op.
Delete first writes generated ops to remove all dependent condition
references, then deletes tag links, then deletes the regex with guards on
name and pattern. All grouped under a single groupId.
Delay Profiles
Source: entities/delayProfiles/
Tables: delay_profiles
Delay profiles control release delay rules and minimum CF score gates.
Fields: name, preferred_protocol, usenet_delay, torrent_delay, bypass_if_highest_quality, bypass_if_above_custom_format_score, minimum_custom_format_score
The schema enforces protocol constraints via CHECK clauses:
only_torrent--usenet_delaymust be NULLonly_usenet--torrent_delaymust be NULL- bypass disabled --
minimum_custom_format_scoremust be NULL
Create and delete use single atomic ops. Delete is name-only guarded
(matches every other entity's delete contract). Update splits into per-field
ops with value guards on each changed field. Pure rename writes one name
op with previousName; if the same submit changes other fields, all
emitted ops share a groupId.
Some field pairs stay atomic to satisfy schema constraints:
- protocol changes group with delay NULLing when moving into or out of
only_torrent/only_usenet - bypass-score changes group with
minimum_custom_format_scorewhen the score must become NULL or non-NULL
Media Management
Source: entities/mediaManagement/
Media management covers three sub-entity types, each with Radarr and Sonarr variants. All three are editable on linked databases through user-layer ops.
Naming
Tables: radarr_naming, sonarr_naming
Format strings for file and folder naming. Radarr has movie format + folder format. Sonarr adds standard/daily/anime episode formats, series folder format, season folder format, and multi-episode style. Both share rename toggle, illegal character replacement, and colon replacement settings.
Create uses one insert op. Delete is name-only guarded, so upstream changes
to format strings or other fields do not block a delete. Update splits into
per-field ops with value guards on each changed field; Sonarr's int-coded
colon_replacement_format and multi_episode_style use the int form in the
SET and guard while desired_state records the string enum. Pure rename
writes one name op with previousName; if the same submit changes other
fields, all emitted ops share a groupId.
Media Settings
Tables: radarr_media_settings, sonarr_media_settings
Global settings for propers/repacks handling and media info. Both variants have the same fields: name, propers_repacks, and enable_media_info.
Create uses one insert op. Delete is name-only guarded, so upstream changes
to propers_repacks or enable_media_info do not block a delete. Update splits
into per-field ops with value guards on each changed field. Pure rename writes
one name op with previousName; if the same submit changes scalar fields,
all emitted ops share a groupId.
Quality Definitions
Tables: radarr_quality_definitions, sonarr_quality_definitions
Size limits (min, max, preferred) per quality tier. Tiers are fixed by the
qualities foreign key, so configs only modify existing tiers; tiers are
not added or removed by the user.
Create writes one op containing one INSERT per tier. Delete is name-only
guarded as a single statement, so upstream tier-level changes do not block
a delete. Update splits into one op per changed tier (covering
min_size/max_size/preferred_size together as a full-row UPDATE with
value guards), plus a separate single-statement op for a config rename
(UPDATE ... SET name = ? WHERE name = ?). When one submit produces
multiple ops, they share a groupId. Per-tier ops carry a new
metadata.qualityName field identifying the tier; override and conflict
handling key off this alongside stableKey to reconstruct intent against
the current cache.
Summary
| Entity | Op splitting | Cascading on rename | Cascading on delete |
|---|---|---|---|
| Custom format | per-field, per-condition | QP scoring refs | explicit QP score removal + FK |
| Quality profile | per-field, per-score | -- | explicit sub-entity removal + FK |
| Regular expression | main + dependents | condition_patterns refs | dependent conditions + FK |
| Delay profile | per-field, constrained pairs | -- | FK cascade |
| Media naming | per-field | -- | -- |
| Media settings | per-field | -- | -- |
| Quality definitions | per-tier (full-row) | -- | -- |