7.1 KiB
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
{
"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:
blockscan also be nested underschedule_config.blocks— both layouts are accepted.
Programming blocks
Each entry in blocks describes a time slot that repeats every 24 hours.
{
"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 |
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.
{
"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.
{
"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=<ID>).
Recycle policy
Controls how aggressively items are repeated across schedule generations.
{
"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:
{
"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.