diff --git a/k-tv-frontend/public/channel-format.md b/k-tv-frontend/public/channel-format.md new file mode 100644 index 0000000..679879a --- /dev/null +++ b/k-tv-frontend/public/channel-format.md @@ -0,0 +1,235 @@ +# K-TV Channel JSON Format + +This document describes the JSON format used to import and export channels in K-TV. +You can write this by hand, share it with the community, or ask an LLM to generate one for you. + +--- + +## Top-level structure + +```json +{ + "name": "90s Sitcom Network", + "description": "Nothing but classic sitcoms, all day long.", + "timezone": "America/New_York", + "blocks": [ ... ], + "recycle_policy": { ... } +} +``` + +| Field | Type | Required | Description | +|---|---|---|---| +| `name` | string | yes | Channel display name | +| `description` | string | no | Short description shown in the dashboard | +| `timezone` | string | no | IANA timezone (default: `"UTC"`) | +| `blocks` | array | no | Programming blocks (default: empty) | +| `recycle_policy` | object | no | Cooldown and repeat rules (default: sensible values) | + +> **Tip:** `blocks` can also be nested under `schedule_config.blocks` — both layouts are accepted. + +--- + +## Programming blocks + +Each entry in `blocks` describes a time slot that repeats every 24 hours. + +```json +{ + "name": "Evening Movies", + "start_time": "20:00", + "duration_mins": 180, + "content": { ... } +} +``` + +| Field | Type | Required | Description | +|---|---|---|---| +| `id` | string (UUID) | no | Stable identifier — auto-generated if omitted | +| `name` | string | no | Block label shown in the timeline (default: `"Unnamed block"`) | +| `start_time` | string | yes | Wall-clock start time in the channel's timezone. Accepts `HH:MM` or `HH:MM:SS` | +| `duration_mins` | integer ≥ 1 | yes | How many minutes this block occupies | +| `content` | object | yes | What to play — see [Content types](#content-types) | + +Blocks are anchored to the **channel's timezone**. A block starting at `20:00` always begins at 8 PM local time, regardless of DST changes. + +Gaps between blocks are valid — the channel shows a no-signal screen during gaps. + +--- + +## Content types + +### Algorithmic (recommended) + +The scheduler picks items automatically from your Jellyfin library based on filters and a fill strategy. + +```json +{ + "type": "algorithmic", + "filter": { + "content_type": "movie", + "genres": ["Comedy", "Sci-Fi"], + "decade": 1990, + "tags": ["family-friendly"], + "min_duration_secs": 3600, + "max_duration_secs": 7200, + "collections": ["abc123def456"] + }, + "strategy": "random" +} +``` + +#### `filter` fields + +All filter fields are optional. Omit any field (or set it to `null`) to match everything. + +| Field | Type | Description | +|---|---|---| +| `content_type` | `"movie"` \| `"episode"` \| `"short"` \| `null` | Restrict to a specific media type. `null` or omitted = any type | +| `genres` | string[] | Only include items that match **all** listed genres (case-sensitive, must match Jellyfin exactly). Empty array = no restriction | +| `decade` | integer \| `null` | Filter by production decade. `1990` matches years 1990–1999 | +| `tags` | string[] | Only include items with **all** listed tags. Empty array = no restriction | +| `min_duration_secs` | integer \| `null` | Minimum item duration in seconds (e.g. `1800` = 30 min) | +| `max_duration_secs` | integer \| `null` | Maximum item duration in seconds (e.g. `5400` = 90 min) | +| `collections` | string[] | Jellyfin library/folder IDs to restrict the pool. Find these in Jellyfin's URL when browsing a library. Empty array = all libraries | + +#### `strategy` values + +| Value | Behaviour | +|---|---| +| `"random"` | Shuffle the matching pool and fill the block. Each schedule generation produces a different order | +| `"sequential"` | Play items in the order Jellyfin returns them (good for series watched in order) | +| `"best_fit"` | Greedy bin-packing: pick the longest item that still fits in the remaining time, minimising dead air | + +### Manual + +Plays a fixed list of Jellyfin item IDs in order, ignoring filters entirely. + +```json +{ + "type": "manual", + "items": [ + "f5c78aa34dd3ae087fe73ca7fee84c11", + "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" + ] +} +``` + +| Field | Type | Description | +|---|---|---| +| `items` | string[] | Jellyfin item IDs played in the listed order | + +Find item IDs in Jellyfin's URL when viewing a media item (`/web/index.html#!/details?id=`). + +--- + +## Recycle policy + +Controls how aggressively items are repeated across schedule generations. + +```json +{ + "cooldown_days": 7, + "cooldown_generations": 3, + "min_available_ratio": 0.15 +} +``` + +| Field | Type | Default | Description | +|---|---|---|---| +| `cooldown_days` | integer ≥ 0 \| `null` | `null` | Don't replay an item until at least N days have passed since it last aired | +| `cooldown_generations` | integer ≥ 0 \| `null` | `null` | Don't replay an item until at least N schedule generations have passed | +| `min_available_ratio` | float 0.0–1.0 | `0.1` | Safety valve: even if cooldowns are active, always keep at least this fraction of the pool selectable. Prevents the scheduler from running out of content on small libraries | + +--- + +## Full example + +A two-block channel with morning cartoons and primetime movies: + +```json +{ + "name": "Family TV", + "description": "Cartoons in the morning, movies at night.", + "timezone": "Europe/London", + "blocks": [ + { + "name": "Morning Cartoons", + "start_time": "08:00", + "duration_mins": 180, + "content": { + "type": "algorithmic", + "filter": { + "content_type": "episode", + "genres": ["Animation", "Comedy"], + "max_duration_secs": 1800 + }, + "strategy": "random" + } + }, + { + "name": "Primetime Movies", + "start_time": "20:00", + "duration_mins": 240, + "content": { + "type": "algorithmic", + "filter": { + "content_type": "movie", + "min_duration_secs": 4800 + }, + "strategy": "best_fit" + } + } + ], + "recycle_policy": { + "cooldown_days": 14, + "min_available_ratio": 0.2 + } +} +``` + +--- + +## Generating with an LLM + +You can ask any LLM to create a channel config. Paste this prompt and fill in your preference: + +``` +Generate a K-TV channel JSON for a channel called "[your channel name]". +The channel should [describe your theme, e.g. "play 90s action movies in the evening and crime dramas late at night"]. +Use timezone "[your timezone, e.g. America/Chicago]". +Use algorithmic blocks with appropriate genres, content types, and strategies. +Output only valid JSON matching this structure: + +{ + "name": string, + "description": string, + "timezone": string, + "blocks": [ + { + "name": string, + "start_time": "HH:MM", + "duration_mins": number, + "content": { + "type": "algorithmic", + "filter": { + "content_type": "movie" | "episode" | "short" | null, + "genres": string[], + "decade": number | null, + "tags": string[], + "min_duration_secs": number | null, + "max_duration_secs": number | null, + "collections": [] + }, + "strategy": "random" | "sequential" | "best_fit" + } + } + ], + "recycle_policy": { + "cooldown_days": number | null, + "cooldown_generations": number | null, + "min_available_ratio": number + } +} +``` + +> **Note:** Genre names must exactly match what Jellyfin uses in your library. Check your Jellyfin library filters to see available genres and tags.