---
title: Channel Scheduler
menu_group: Linear TV
menu_order: 30
tab: Linear TV
tab_order: 30
requires_linear_tv: true
summary: Schedule playlists onto channels with a three-panel workspace, channel-local times, precise resize, weekly recurrence, guide generation, and fallback programming.
---

# Channel Scheduler

Open **Channels → Channel Schedule** (`mediablaster-channel-schedule`).

The scheduler uses a modern three-panel workspace:

- **Playlists** (left) — search, filter by content type, drag or click to create a draft block
- **Calendar** (center) — Week or Day view with drag, resize, configurable snapping, and overnight segments
- **Block Details** (right) — persistent inspector for editing; no jQuery UI modal overlay

## Prerequisites

1. At least one published **Channel** with timezone and optional fallback playlist.
2. At least one published **Playlist** with valid items.
3. Select a channel from the toolbar dropdown before scheduling.

## Toolbar

| Control | Behavior |
|---------|----------|
| **Channel** | Loads schedule blocks; calendar/inspector use that channel’s IANA timezone |
| **Previous / Today / Next** | Navigates by 7 days in Week view, 1 day in Day view |
| **Date label** | Shows the visible week range or selected day (channel-local) |
| **Snap** | 15 minutes (default), 5 minutes, or 1 minute — stored in browser `localStorage` key `mediablaster.scheduler.snapSeconds` |
| **Day / Week** | Switches calendar view (Week is default) |
| **Generate Guide** | Runs the schedule compiler (`POST .../compile`) |
| **Save Block** | Saves the open Block Details form (new draft or existing block) |
| **Save menu → Duplicate This Week to Next Week** | Preview then confirm copy of one-time blocks only |

The toolbar shows **Times shown in {channel timezone}**. Canonical storage remains UTC in `mediablaster_channel_schedule`. Snap changes affect drag/resize only — they do not round existing saved blocks.

## Scheduling playlists

| Action | Result |
|--------|--------|
| **Click a playlist** | Opens a **draft** in Block Details (noon channel-local on the visible day; exact playlist duration when known); nothing is saved until **Save Block** |
| **Drag a playlist** onto the grid | Draft with start snapped to the drop position using the Snap setting |
| **Click a schedule block** | Selects it and opens Block Details (all overnight segments share the same selection) |
| **Drag a block** | Moves the whole block (exact duration preserved); optimistic PATCH with **Saved · Undo** |
| **Drag the top or bottom edge** | Live resize (top changes start with end fixed; bottom changes end with start fixed); tooltip, endpoint guide, magnets, previous/next collision clamping; optimistic PATCH with Undo |

There is **no modal editor** for Block Details. Unsaved inspector changes use a scheduler confirm dialog (Save / Discard / Cancel) and `beforeunload` while dirty.

### Block Details fields

- **Playlist** — required; dropdown of published playlists
- **Start** / **End** — `datetime-local` fields with seconds (`step=1`), shown in channel-local time
- **Duration** — hours, minutes, and seconds (minimum one minute) with presets (**15m**, **30m**, **1h**, **2h**)
- **Duration actions** — Use playlist length · End at next block · Fill until midnight · Add/Remove 15 minutes
- **Repeat** — **Does not repeat** or **Weekly**
- **Recurrence until (local date)** — required for weekly saves
- **Loop playlist until block ends** — repeat playlist items until the block ends (not the channel fallback)
- **Enabled** — disabled blocks are ignored by the compiler and shown with a **Disabled** badge
- **Notes** — optional; stored in `settings.notes` (no database migration)

Opening an existing block does **not** round its start, end, or duration. **Use playlist length** applies the exact playlist duration in seconds (independent of Snap).

### Duration intelligence

| Situation | Message |
|-----------|---------|
| Block equals playlist length | Matches playlist length |
| Block shorter | Cuts off {duration} of programming |
| Block longer, Loop off, no fallback | Leaves {duration} without scheduled playlist content |
| Block longer, Loop off, fallback configured | Fallback programming fills the remaining {duration} |
| Loop on | Playlist repeats to fill this block |
| Variable / live playlist (no fixed duration) | This playlist has no fixed duration… (1 hour editable default; Use playlist length hidden) |

### Resize interaction

- Invisible top and bottom edge hit areas (~16px), centred on the block border — no internal grip bar in the card content
- Hovering an edge (not the whole block) shows a short centred line; active resize uses the primary colour on that edge
- Top edge changes **start** (end fixed); bottom edge changes **end** (start fixed)
- Live updates: block `top` + height, time label, floating tooltip near the active edge, inspector Start/End/Duration, duration message
- Magnetic targets: playlist length, previous/next enabled block, hour/half-hour, midnight / day boundary
- Client clamps against the previous enabled end (top) and next enabled start (bottom); disabled blocks ignored
- Weekly blocks keep recurrence: top-edge edits shift the anchor start; bottom-edge edits shift the anchor end
- Screen-reader live region announces at completion (not every pointer move)
- Respects `prefers-reduced-motion`

### Overnight (cross-midnight) blocks

A block such as 11:00 PM–2:00 AM renders as **linked segments** on consecutive days (same block ID). The segment that contains the true start gets the top handle; the segment that contains the true end gets the bottom handle; intermediate clips have none. Dragging either segment moves the whole block. Ending exactly at midnight does not create an empty next-day segment. Collision checks use absolute UTC start/end, not segment geometry. After a resize stops, segments rebuild so overnight geometry stays in sync.

### Optimistic save and Undo

Successful drag/resize updates local state immediately, PATCHes the server, merges the canonical `block` from the response, and shows **Saved · Undo** (~9 seconds). Failures restore the previous block and show the server error (including structured conflict data when present). Full calendar reload runs only when reconciliation is impossible. Per-block write tokens ignore stale responses.

## Channel timezone and DST-safe weekly recurrence

- Display and edit in the channel’s IANA timezone (never a fixed UTC offset).
- Storage remains UTC (`start_at_utc` / `end_at_utc`).
- Weekly expansion advances in the block/channel timezone so local wall-clock time (for example 8:00 PM Edmonton) is preserved across DST.
- On weekly save, `settings.recurrence_local` stores `{ weekday, time, timezone }` for compatibility (no schema migration).
- Legacy weekly rows without that metadata derive wall clock from the UTC anchor in the block/channel timezone on first expand/edit after this update (UTC instants of future occurrences may shift by ±1 hour to keep local time).
- Spring DST gap: invalid local times are rejected.
- Fall DST ambiguity: the **earlier** offset (still on DST) is used.

## Overlap validation

Enabled blocks cannot overlap on the same channel. Validation expands weekly occurrences (capped at 520 weeks / `recurrence_until_utc`) in the channel timezone. Exact adjacency (`end == next start`) is allowed. Disabled blocks do not conflict. HTTP **409** responses include structured `data.conflict` (`block_id`, `title`, `start_at_utc`, `end_at_utc`) when possible. Duplicate-week and create/update share this logic.

## Playlist panel

- **Search** filters playlist titles on the client
- **Filter chips** — All, Movies, Episodes, Series, Live — use actual playlist item types
- Playlist colors match calendar blocks
- **+ New Playlist** links to `post-new.php?post_type=playlists`

## Generate Guide

Click **Generate Guide** after changing schedule blocks. This runs the schedule compiler for the selected channel, writes airings to `{prefix}mediablaster_channel_airings`, and clears cached EPG transients.

## Duplicate week

Use **Save Block → Duplicate This Week to Next Week**:

1. Preview request without `confirm`
2. Confirm dialog with block count
3. Same request with `"confirm": true`
4. On success, navigates to the destination week

Only **one-time** blocks are copied; weekly recurring blocks are skipped.

## Schedule blocks (server rules)

- Times stored as **UTC**; UI converts using the channel timezone.
- Weekly recurrence stores one row; the grid expands occurrences for the visible range.
- Overlapping enabled occurrences return HTTP 409 with conflict details.
- Weekly recurrence requires `recurrence_until_utc`.
- Create/update responses include the hydrated `block` object (plus `id` / `success` for compatibility).
- PATCH/drag/resize payloads must include existing `settings` so arbitrary keys (including `notes` and `recurrence_local`) are preserved; the server merges settings on update.

## Fallback programming

Configure **Fallback Playlist** on the Channel edit screen (channel-level, not per block). Gaps between explicit blocks can be filled automatically; enable **Loop Fallback Playlist** on the channel to repeat until the next block.

## Assets

| File | Role |
|------|------|
| `admin/js/mediablaster-channel-scheduler-time.js` | Pure time/duration/collision helpers (`MediaBlasterSchedulerTime`) |
| `admin/js/mediablaster-channel-scheduler.js` | Scheduler UI behavior (localized as `mediablasterScheduler`) |
| `admin/css/mediablaster-channel-scheduler.css` | Scheduler UI only — scoped under `#mediablaster-channel-scheduler` |
| `admin/css/mediablaster-linear-admin.css` | Playlist Builder styles (not scheduler) |

Scheduler CSS must include `#mediablaster-channel-scheduler [hidden] { display: none !important; }` so loading/empty overlays that use `display: flex` do not override the HTML `[hidden]` attribute.

## Troubleshooting

| Symptom | Check |
|---------|-------|
| Stuck on **Loading…** with no console errors | Hard-refresh; confirm `[hidden]` CSS rule above |
| Times look “wrong” vs UTC docs | UI is channel-local; storage remains UTC |
| Save Block disabled | Open a draft or block and change a field |
| Resize snaps back | Client should clamp first; if 409, read conflict title/time in the toast |
| Overnight block missing on day 2 | Confirm end is after midnight; exact midnight end has no day-2 segment |
| Weekly time shifts after DST | Expected for legacy UTC-only anchors until re-saved; new saves store `settings.recurrence_local` |
| Variable playlist forced to 1 hour | Editable default only — duration intelligence should say no fixed duration |
| Notes lost after drag/resize | PATCH must send `settings`; server merges existing settings |
| Duplicate week fails | Destination overlap (409) |
| No sidebar / schedule menu | Enable Live Channels on General Settings, save, reload wp-admin |
