# Channel Architecture ## Channel Entity Defined in `ErsatzTV.Core/Domain/Channel.cs`. Key fields: - **Identity**: `Number` (e.g., "1", "2.1"), `Name`, `UniqueId` (GUID for M3U/XMLTV) - **Encoding**: `FFmpegProfileId` — video/audio codec, bitrate, resolution, hardware acceleration - **Streaming**: `StreamingMode` (TransportStream, HLS Direct, HLS Segmenter, TS Hybrid) - **Behavior**: `PlayoutMode` (Continuous vs OnDemand), `IdleBehavior` (StopOnDisconnect vs KeepRunning) - **Visual**: `WatermarkId`, `FallbackFillerId`, artwork (logos) - **Mirroring**: `PlayoutSource` (Generated vs Mirror) — a mirror channel copies another with optional time offset - **Display**: `Group`, `Categories`, `ShowInEpg`, `IsEnabled` - **Audio/Subtitle defaults**: preferred language codes, subtitle mode ## Content Sources Channels get content through a **Playout** → **ProgramSchedule** → **ProgramScheduleItem** chain. ### Collection Types | Type | Description | |------|-------------| | `Collection` | Manual grouping of media items with custom playback order | | `MultiCollection` | Aggregate of collections + smart collections | | `SmartCollection` | Query-based (Lucene.Net) dynamic filtering | | `Playlist` | Ordered items with per-item config (count, fillers, playback order) | | `TelevisionShow` / `TelevisionSeason` | Structured TV hierarchy | | `Movie`, `Episode`, `MusicVideo`, `OtherVideo`, `Song`, `Image` | Individual media items | | `RerunCollection` | Wraps any collection with separate first-run/rerun playback orders | | `SearchQuery` | Dynamic results from a search | | `RemoteStream` | External stream URLs | ### Media Sources Media items are imported from configured libraries (Jellyfin, Plex, Emby, or local filesystem). Each source type has its own entity variants (e.g., `JellyfinMovie`, `PlexEpisode`). ## Scheduling ### Schedule Kinds - **Classic**: Traditional ProgramSchedule with items — the most common - **Block**: Template-based block scheduling - **Sequential**: Strict sequential ordering - **Scripted**: External script-driven playout - **ExternalJson**: Playout defined by external JSON file ### Schedule Item Types Each `ProgramScheduleItem` is one of four concrete types: 1. **One** — Play exactly 1 item per cycle 2. **Multiple** — Play N items (fixed count, collection size, or playlist item size) 3. **Duration** — Fill a time window (with tail mode: none, offline, slate, or filler) 4. **Flood** — Play items continuously until the next fixed-start item Items can have `StartType` of Fixed (anchored to clock time) or Dynamic (follows previous item). ### Playback Orders `Chronological`, `Random`, `Shuffle`, `ShuffleInOrder`, `MultiEpisodeShuffle`, `SeasonEpisode`, `RandomRotation`, `Marathon` (group by show/season/artist/album/director). ### Filler System `FillerPreset` defines content to fill gaps. Each schedule item can have: - **PreRoll** — before main content - **MidRoll** — during (chapter breaks) - **PostRoll** — after main content - **Tail** — pad remaining time in a duration block - **Fallback** — channel-level default when nothing else available Filler modes: Duration, Count, Pad (to nearest minute), RandomCount. ### Alternate Schedules `ProgramScheduleAlternate` overrides the main schedule for specific days of week, days of month, months of year, or date ranges. Useful for seasonal programming or weekend variations. The date predicate itself is `IAlternateScheduleItem` (`ErsatzTV.Core/Domain/Scheduling/`), implemented by `ProgramScheduleAlternate` (classic playouts) and `PlayoutTemplate` (block playouts). Both are evaluated by `AlternateScheduleSelector.GetScheduleForDate`: rows are tested in `Index` order, **first match wins**, and the broadest/unconditional row placed **last** acts as the catch-all default. `DaysOfWeek`, `DaysOfMonth` and `MonthsOfYear` are ANDed with the date range, so a row only matches when *every* condition holds. ### Recipe: seasonal / holiday programming Date-conditional scheduling is fully supported — there is no need to hand-build it seasonally. The mechanism is named "Alternate Schedules" (classic) / "Playout Templates" (block), which is why it isn't obvious if you go looking for "seasonal". **Classic playout — a December holiday channel:** 1. Build the seasonal schedule at `/app/schedules` (clone your normal one, swap in the holiday content). 2. Go to `/app/playouts/{id}/alternate-schedules`. 3. Add a row pointing at the seasonal schedule; tick **Limit to date range**; set start `12/1`, end `12/31`. 4. **Leave the start/end year fields empty** — see the gotcha below; this is what makes it recur. 5. Add (or keep) a row for your normal schedule with no conditions, ordered **last** — the catch-all for the other eleven months. **Block playout — same idea, plus holiday branding:** 1. Build the seasonal `Template` at `/app/templates` (a day-grid of `Block`s). 2. Go to `/app/playouts/{id}/templates`, add a date-limited `PlayoutTemplate` row for it (same date-range fields, same empty-years rule, same catch-all-last ordering). 3. Optionally set that row's **DecoTemplate** for seasonal watermarks/filler — one `PlayoutTemplate` row carries both a `Template` and an optional `DecoTemplate`, so branding is date-gated along with content. **Gotchas** (all enforced in `AlternateScheduleSelector.cs:32-40`): - **Empty years = repeats every year.** Years default to the *queried date's* year, so `12/1 → 12/31` with blank years fires every December, forever. Set explicit years only for a genuine one-off window. - **It's both years or neither.** The explicit-year branch requires `StartYear` **and** `EndYear` to be set. Filling in only one silently falls back to the yearly-repeat behaviour. - **Wrap-around ranges work — but only with blank years.** `11/1 → 2/1` correctly spans the new year. Setting explicit years disables wrap detection (`reverse = false`), turning the range into a plain start→end window. - **Invalid dates are clamped, not rejected.** A start of `2/31` rolls to the 1st of the next month; an end of `2/31` reduces to the last day of February (leap-year aware). - **Decos have no dates of their own.** `DecoTemplateItem.StartTime`/`EndTime` are *time-of-day*. Date-gate a deco via the `PlayoutTemplate` row that carries its `DecoTemplateId`; a playout's default `Playout.DecoId` is deliberately never date-gated (it's the fallback). Not supported: *soft* prioritization ("prefer collection X in December" without swapping the schedule). Selection is binary first-match-wins — see `decisions.md` 2026-07-17 (#73) and the weighting work in #70. ## Playout Pipeline ``` Channel └── Playout ├── ProgramSchedule │ └── ProgramScheduleItems (One|Multiple|Duration|Flood) │ └── Content source (Collection, Playlist, SmartCollection, etc.) ├── PlayoutItems (generated — the actual timeline) │ └── MediaItem + start/finish times + filler kind + watermarks ├── PlayoutGaps (time periods with no content) └── ProgramScheduleAlternates (day/date overrides) ``` The scheduling engine (`ErsatzTV.Core/Scheduling/`) resolves schedule items into concrete `PlayoutItem` entries with precise start/finish times. Each `PlayoutItem` references a specific `MediaItem` and includes trim points (`InPoint`/`OutPoint`), filler classification, and per-item audio/subtitle overrides. ## Watermarks `ChannelWatermark` supports modes: Permanent, Intermittent, OpacityExpression. Image sources: custom upload, channel logo, or built-in resource. Positioned with percentage-based margins and z-index. Note: `ChannelLogoGenerator.GenerateChannelLogoUrl()` hardcodes `localhost` for watermark logo fetching — see issue #1 for details.