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

12 KiB
Raw Blame History

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 Templates 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 Blocks; each Block is an ordered list of BlockItems (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 Decos, 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 Collections and/or SmartCollections (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 PlaylistItems; 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 LibraryPaths (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.csToM3U().
  • 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).