Files
ersatztv/docs/domain-model.md
T
timothyandClaude Fable 5 50ae0a7f3b
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m56s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m52s
docs: onboarding/convention docs part 1 + handoff past #180/#182/#183 (parity endgame)
Adds docs/README.md (index), api-conventions.md, spa-conventions.md, e2e-local.md +
scripts/e2e-local.sh, blazor-route-parity.md (#91 phase-b tracker), domain-model.md,
decisions.md. Rule (CLAUDE.md): read these at session start instead of re-recon; update
in the same PR that changes a convention/route/decision. Part 2 = #185.

Handoff: #180/#141/#158/#161 closed (PRs #181/#182/#183); #145 playback-only; #91
readiness plan posted; next prompt = #185 + quick wins + #155/#151/#152/#153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:03:01 +02:00

113 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Domain model — glossary + concept map
Purpose: what the app IS — the entity chain and vocabulary an agent needs before touching
scheduling, playout, or IPTV code. **Update this doc in the same PR that changes any fact below.**
## The pipeline in one paragraph
Media libraries (Local/Plex/Jellyfin/Emby) are scanned into media items, which are grouped into
collections/playlists/etc. Those groupings are arranged onto a **playout** by one of several
scheduling engines (classic schedule, block/template calendar, sequential YAML, scripted,
external-JSON) — a playout belongs to exactly one **channel**. Channels are exposed to IPTV clients
(Jellyfin, Dispatcharr) as an M3U playlist + XMLTV guide, and streamed on demand via FFmpeg, all
under the `/iptv/*` routes (`ErsatzTV/Controllers/IptvController.cs`).
## Entity chain sketch
```
Channel (1) ──< Playout (0..N per channel; ChannelPlayoutSource distinguishes Generated vs Mirror)
├─ ScheduleKind: None/Classic/Block/Sequential/Scripted/ExternalJson
│ (PlayoutScheduleKind, ErsatzTV.Core/Domain/PlayoutScheduleKind.cs)
├─ Classic: ProgramSchedule ──< ProgramScheduleItem (TPT: One/Multiple/Flood/Duration)
│ └─< ProgramScheduleAlternate (day/date-conditional alt schedule)
├─ Block: PlayoutTemplate (day/date-conditional, like ProgramScheduleAlternate)
│ ──> Template ──< TemplateItem (time-of-day) ──> Block
│ Block ──< BlockItem (ordered collection items)
│ PlayoutTemplate also optionally points at a DecoTemplate
│ Playout can also point directly at a default Deco (DecoId)
├─ Sequential: Playout.ScheduleFile (YAML, validated via the SPA's Schedule
│ Validator / TroubleshootController)
├─ Scripted: ScriptedScheduleController-backed (not yet detailed here)
└─ ExternalJson: Playout.ScheduleFile (JSON)
Playout.Items = List<PlayoutItem> (the built, materialized schedule)
Playout.PlayoutHistory = rotation/rerun state (per Block for block playouts)
```
`Deco`/`DecoTemplate` are orthogonal to the schedule kind (mostly used with Block playouts but
`Playout.DecoId` and `Playout.Templates` (`PlayoutTemplate.DecoTemplateId`) are independent FKs on
`Playout`, not nested inside `ProgramSchedule`).
## Glossary
| Term | Meaning | Key entity | Edited at (SPA) |
|---|---|---|---|
| **Classic playout** | Schedule = ordered `ProgramScheduleItem` rows on a `ProgramSchedule`, played in sequence/loop. Alternate schedules let day-of-week/day-of-month/month/date-range conditions pick a different `ProgramScheduleAlternate` by `Index` order, **first match wins**; the row with the broadest/no conditions, placed **last**, acts as the catch-all default. | `ProgramSchedule`, `ProgramScheduleItem`, `ProgramScheduleAlternate` | `/app/schedules` |
| **Block playout** | A calendar of `Template`s assigned to times of day (`TemplateItem.StartTime`) and to a playout via `PlayoutTemplate` (day/date-conditional, same first-match/catch-all-last pattern as alternate schedules). Each `Template` is a day-grid of `Block`s; each `Block` is an ordered list of `BlockItem`s (collection/media/search references) with a `Minutes` duration and a `BlockStopScheduling` rule (`AfterDurationEnd` vs `BeforeDurationEnd`). | `Template`, `TemplateItem`, `Block`, `BlockItem`, `PlayoutTemplate` | `/app/templates`, `/app/blocks`, `/app/playouts/{id}/templates` |
| **Sequential playout** | Driven by a YAML file (`Playout.ScheduleFile`); validated via the Schedule Validator screen. | `Playout.ScheduleFile` | `/app/troubleshooting/yaml` (validate only; file itself is server-side) |
| **Scripted playout** | `PlayoutScheduleKind.Scripted`; backed by `ScriptedScheduleController`. | — | — |
| **External-JSON playout** | `PlayoutScheduleKind.ExternalJson = 20`; JSON-driven, same shape idea as Sequential but JSON instead of YAML. | `Playout.ScheduleFile` | — |
| **Deco** | Per-playout "decoration": one of 4 independently-modal sections — watermark, graphics elements, default filler, dead-air fallback — plus break content. Each mode section is `Inherit`/`Disable`/`Override`/`Merge` (`DecoMode`). Can attach directly to a Block playout via `Playout.DecoId`. | `Deco`, `DecoGroup`, `DecoBreakContent` | `/app/decos` |
| **DecoTemplate** | Time-of-day (`DecoTemplateItem.StartTime`/`EndTime`) calendar of `Deco`s, assigned to a playout via `PlayoutTemplate.DecoTemplateId` (same row as the Block-template assignment — one `PlayoutTemplate` entry carries both a `Template` and an optional `DecoTemplate`). | `DecoTemplate`, `DecoTemplateItem`, `DecoTemplateGroup` | `/app/deco-templates` |
| **Default deco vs deco templates** | `Playout.DecoId` = one static deco for the whole playout; `PlayoutTemplate.DecoTemplateId` = a time-varying deco schedule. Both are optional and independent. | `Playout`, `PlayoutTemplate` | `/app/playouts/{id}/templates` |
| **FillerPreset** | A reusable filler definition: `FillerKind` (PreRoll/MidRoll/PostRoll/Tail/Fallback; also `GuideMode=99`, `DecoDefault=100`) × `FillerMode` (None/Duration/Count/Pad/RandomCount) over a collection/media-item/multi-collection/smart-collection/playlist source, with an optional `Expression` (NCalc). Referenced from `ProgramScheduleItem` (Pre/Mid/Post/Tail/FallbackFillerId) and `Channel.FallbackFillerId`. | `FillerPreset`, `FillerKind`, `FillerMode` | `/app/filler-presets` |
| **Watermark** | `ChannelWatermark` image overlay; attached at channel, schedule-item, block-item, deco, or playout-item level with position/size/opacity. | `ChannelWatermark`, `DecoWatermark`, `BlockItemWatermark`, `ProgramScheduleItemWatermark` | `/app/watermarks` |
| **Collection** | Manual list of media items (`CollectionItem`). | `Collection` | `/app/collections` |
| **SmartCollection** | Saved search — a `Query` string, no static item list. | `SmartCollection` | `/app/collections` |
| **MultiCollection** | Combines multiple `Collection`s and/or `SmartCollection`s (with grouping via `MultiCollectionItem`/`MultiCollectionSmartItem`). | `MultiCollection` | `/app/collections` |
| **RerunCollection** | One source (collection/media item/multi/smart) with separate `FirstRunPlaybackOrder` vs `RerunPlaybackOrder`. | `RerunCollection` | `/app/collections` (blocked on API #152 per parity tracker) |
| **Playlist** / **PlaylistGroup** | Ordered `PlaylistItem`s; `IsSystem` flag marks built-in/non-deletable playlists and groups. | `Playlist`, `PlaylistGroup`, `PlaylistItem` | `/app/collections` |
| **Media kinds** | `CollectionType` enum distinguishes container kinds (Collection/TelevisionShow/TelevisionSeason/Artist/MultiCollection/SmartCollection/Playlist/RerunFirstRun/RerunRerun/SearchQuery) from leaf media kinds (Movie/Episode/MusicVideo/OtherVideo/Song/Image/RemoteStream) plus synthetic `FakeCollection`/`FakePlaylistItem`. Concrete media entities: `Movie`, `Show`/`Season`/`Episode`, `Artist`/`MusicVideo`/`Song`, `OtherVideo`, `Image`, `RemoteStream` (`ErsatzTV.Core/Domain/MediaItem/`). | `MediaItem` subclasses | `/app/media?kind=...` |
| **Library / LibraryPath / LibraryFolder** | `Library` (abstract; Local/Plex/Jellyfin/Emby subclasses) owns one or more `LibraryPath`s (scan roots); each path has a `LibraryFolder` tree used for browsing and image-folder duration metadata. | `Library`, `LibraryPath`, `LibraryFolder` | `/app/libraries` |
| **Media source kind** | `MediaSourceKind`: Local/Plex/Jellyfin/Emby — the origin server type for a `Library`. | `MediaSourceKind` | `/app/libraries` |
| **MediaItemState** | Health flag on a media item: Normal/FileNotFound/Unavailable/RemoteOnly. Drives the Trash screen. | `MediaItemState` | `/app/trash` |
| **PlayoutItem** | One materialized, built entry in a playout's timeline (the actual thing that will play at a given time). | `PlayoutItem` | (generated, not directly edited) |
| **PlayoutHistory** | Rotation/rerun bookkeeping per block (`BlockId`) + collection `Key`/`ChildKey`, used by block-playout schedulers to avoid repeats; inspectable via Troubleshooting. | `PlayoutHistory` | `/app/troubleshooting/blocks` |
| **Channel concepts** | `Number` (validated by `Channel.NumberValidator` regex), `Group`, `PlayoutSource` (Generated/Mirror; Mirror channels relay another channel via `MirrorSourceChannelId`+`PlayoutOffset`), `PlayoutMode` (Continuous/OnDemand), `TranscodeMode` (OnDemand only, today), `IdleBehavior` (StopOnDisconnect/KeepRunning), `StreamingMode` (TransportStream/HttpLiveStreamingDirect/HttpLiveStreamingSegmenter/TransportStreamHybrid). | `Channel` | `/app/channels`, `/app/edit-channel/{id}`, `/app/new-channel` |
| **Guide / EPG (XMLTV)** | Per-channel programme guide generated from playout items; channels with `ShowInEpg=false` are excluded. | `GetChannelGuideHandler` | `/app/guide` (viewer); settings at `/app/settings/xmltv` |
| **M3U** | The channel lineup playlist Jellyfin/Dispatcharr consume. | `ChannelPlaylist.ToM3U()` | — |
## Where things are edited (SPA routes)
Primary nav: `/app` (dashboard), `/app/channels`, `/app/new-channel`, `/app/guide`,
`/app/schedules`, `/app/blocks`, `/app/templates`, `/app/decos`, `/app/deco-templates`,
`/app/playouts` (+ sub-paths `/app/playouts/{id}/alternate-schedules`,
`/app/playouts/{id}/templates`).
Media nav: `/app/media` (generic kind-filtered browse, `?kind=movies|shows|artists|music-videos|
other-videos|remote-streams|images`), `/app/search`, `/app/trash`, `/app/collections` (manual +
smart + multi + playlist; rerun-collection editing still gated on API #152),
`/app/filler-presets`, `/app/libraries`, `/app/trakt-lists`.
System nav: `/app/settings` (sub-tabs: streaming/system/logging/playout/scanner/general/xmltv —
all mapped 1:1 from legacy `/settings/*` Blazor routes), `/app/logs`, `/app/troubleshooting` (+
`/app/troubleshooting/blocks` block-playout history, `/app/troubleshooting/yaml` sequential-schedule
validator; playback troubleshooting still gated on API #145), `/app/ffmpeg-profiles`,
`/app/watermarks`.
`/app/edit-channel/{id}` (edit) is reached from the channels table, not the primary sidebar nav.
Per-item media detail pages and the image-folder browser (`MediaDetailScreen`'s
`MovieDetailScreen`/`ShowDetailScreen`/`SeasonDetailScreen`/`ArtistDetailScreen`,
`ImageBrowserScreen`) landed via #141 (PR #183) at `/app/media/{movies|shows|seasons|artists}/{id}`
and `/app/media/images/browser`. Not yet in the SPA: multi/rerun-collection + playlist-variant
management (API gaps #151/#152/#153/#155). See `docs/blazor-route-parity.md` for the full
route-by-route tracker.
## Key handler / file locations
- **Classic scheduling engine**: `ErsatzTV.Core/Scheduling/PlayoutBuilder.cs`,
`PlayoutModeSchedulerBase.cs` (+ `One`/`Multiple`/`Flood`/`Duration` variants),
`AlternateScheduleSelector.cs` (first-match-wins alternate/template selection).
- **Block scheduling engine**: `ErsatzTV.Core/Scheduling/PlayoutModeBlock.cs`.
- **Build entry point**: `ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs`.
- **M3U generation**: `ErsatzTV.Core/Iptv/ChannelPlaylist.cs``ToM3U()`.
- **XMLTV generation**: `ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs`.
- **Streaming / IPTV routes**: `ErsatzTV/Controllers/IptvController.cs` (`/iptv/channels.m3u`,
`/iptv/xmltv.xml`, `/iptv/channel/{number}.ts`, `/iptv/session/{number}/hls.m3u8`, HDHR routes,
logos).