# 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.