# Shifted Seasons Handling This file defines the behavioral contract for mapping source season and episode numbering to target season and episode numbering through stored shifted-season rules. Primary sources: - `requirements/project.md` - `requirements/architecture.md` - actual tool code in `src/ffx/` Secondary source: - `SCRATCHPAD.md`, used only to clarify current hardening gaps and not as the primary contract source. ## Scope - Persisting shifted-season rules in SQLite. - Treating shifted-season rules as show-level data rather than pattern-level data. - Matching source season and episode numbers against one stored rule. - Applying additive season and episode offsets to produce target numbering. - Using shifted target numbering during `convert` for TMDB episode lookup and generated season and episode filename tokens. - Managing shifted-season rules from the Textual show-editing workflow. ## Out Of Scope - General filename parsing rules for detecting season and episode values. - Standalone `rename` command behavior, which currently uses explicit rename inputs rather than stored shifted-season rules. - Stream or track mapping behavior unrelated to season and episode numbering. ## Terms - `shifted-season rule`: one persisted row that belongs to one show and defines how one source-numbering range maps into target numbering. - `source numbering`: the season and episode values detected from the current source file or supplied as source-side conversion inputs before shifting. - `target numbering`: the season and episode values after one matching shifted-season rule has been applied. - `original season`: the source-domain season number a shifted-season rule is eligible to match. - `episode range`: the optional source-domain episode interval covered by one shifted-season rule. - `open bound`: an unbounded start or end of the episode range. Current storage uses `-1` as the internal sentinel for an open bound. - `sibling shifted-season rules`: all shifted-season rules stored for the same show. ## Rules - `SHIFTED_SEASONS_HANDLING-0001`: The domain model shall treat shifted-season rules as children of a show. Shifted-season rules shall not belong to patterns. - `SHIFTED_SEASONS_HANDLING-0002`: Each persisted shifted-season rule shall belong to exactly one show. - `SHIFTED_SEASONS_HANDLING-0003`: A shifted-season rule shall carry these fields: `original_season`, `first_episode`, `last_episode`, `season_offset`, and `episode_offset`. - `SHIFTED_SEASONS_HANDLING-0004`: `season_offset` and `episode_offset` shall be additive signed integers applied to matched source numbering to produce target numbering. - `SHIFTED_SEASONS_HANDLING-0005`: A shifted-season rule shall match a source tuple only when: - the source season equals `original_season`, - the source episode is greater than or equal to `first_episode` when the lower bound is closed, - the source episode is less than or equal to `last_episode` when the upper bound is closed. - `SHIFTED_SEASONS_HANDLING-0006`: An open lower or upper episode bound shall represent an unbounded side of the covered source episode range. - `SHIFTED_SEASONS_HANDLING-0007`: If one shifted-season rule matches, target numbering shall be: - `target season = source season + season_offset` - `target episode = source episode + episode_offset` - `SHIFTED_SEASONS_HANDLING-0008`: If no shifted-season rule matches, source numbering shall pass through unchanged. - `SHIFTED_SEASONS_HANDLING-0009`: Shifted-season handling shall operate in a source-to-target numbering model. Stored rules map detected source numbering to the target numbering used by conversion-facing metadata and output naming. - `SHIFTED_SEASONS_HANDLING-0010`: Pattern matching may identify the owning show, but shifted-season rule selection shall depend on the show and source numbering, not on which pattern matched. - `SHIFTED_SEASONS_HANDLING-0011`: For one show and one `original_season`, shifted-season rules shall not overlap in their effective episode coverage. At most one rule may apply to any one source season and episode tuple. - `SHIFTED_SEASONS_HANDLING-0012`: If a shifted-season rule uses two closed episode bounds, `last_episode` shall be greater than or equal to `first_episode`. - `SHIFTED_SEASONS_HANDLING-0013`: Shifted-season rule evaluation shall be deterministic. Released behavior shall not depend on arbitrary database row order when more than one stored rule could match. - `SHIFTED_SEASONS_HANDLING-0014`: During `convert`, when show, season, and episode values are available and stored shifting is active, the shifted target numbering shall drive: - TMDB episode lookup - season and episode filename tokens such as `S01E02` - generated episode basenames that include season and episode numbering - `SHIFTED_SEASONS_HANDLING-0015`: When conversion is supplied explicit target-domain season or episode values for TMDB naming, the system shall not apply stored shifting on top of those already-targeted values. - `SHIFTED_SEASONS_HANDLING-0016`: Operator-facing show editing shall expose list, add, edit, and delete flows for shifted-season rules as part of the show-management workflow. - `SHIFTED_SEASONS_HANDLING-0017`: User-facing shifted-season editing should present open episode bounds as a natural empty-state input rather than forcing operators to type the internal sentinel directly. ## Acceptance - A show can exist with zero or more shifted-season rules. - A shifted-season rule is stored against one show, not against one pattern. - A source tuple matching one stored rule yields exactly one shifted target season and episode tuple derived by additive offsets. - A source tuple matching no stored rule retains its original season and episode values. - Two shifted-season rules for the same show and original season cannot both be valid if they cover overlapping episode ranges. - A rule with closed bounds such as `first_episode=1` and `last_episode=10` rejects an inverted interval such as `20..10`. - A show with several patterns still uses one shared shifted-season rule set, because shifted-season ownership is show-scoped. - During `convert`, shifted numbering is what TMDB episode lookup and generated season and episode tokens see when stored shifting is active. - The TUI show-management flow can display and maintain shifted-season rules for the current show. ## Current Code Fit - `src/ffx/model/shifted_season.py` defines the persisted `ShiftedSeason` entity with `show_id`, `original_season`, episode bounds, and additive offsets. - `src/ffx/model/show.py` implements the one-to-many `Show -> ShiftedSeason` relationship, which already aligns with show-level ownership. - `src/ffx/shifted_season_controller.py` implements create, update, lookup, delete, sibling retrieval, and the runtime `shiftSeason(...)` mapping step. - `src/ffx/show_details_screen.py`, `src/ffx/shifted_season_details_screen.py`, and `src/ffx/shifted_season_delete_screen.py` provide the current Textual CRUD flow for managing show-scoped shifted-season rules. - `src/ffx/cli.py` applies `shiftSeason(...)` during `convert` before TMDB episode lookup and before output season and episode suffix generation. - The current `convert` implementation disables stored shifting whenever its TMDB override bucket is present, including cases such as `--show` without an explicit target season or episode override, so current behavior is broader than the minimum bypass contract stated above. - The current code does not fully satisfy the intended validation contract yet: overlap rejection and update-time range validation are not hardened sufficiently, and deterministic selection depends too much on invalid overlap state not being present. ## Risks - The current CLI groups `--show`, `--season`, and `--episode` under one override bucket used for TMDB-related behavior. The exact source-domain versus target-domain semantics of each override should stay documented clearly so stored shifting is neither skipped nor double-applied unexpectedly. - Current modern automated test coverage for shifted-season behavior is light, so validation and convert-time numbering behavior are not yet strongly locked down by focused tests. - Existing databases created before stricter validation may already contain invalid overlapping or inverted shifted-season rules, so migration and repair paths should continue to treat explicit validation failures as recoverable operator signals.