feat: add K-TV Channel JSON format documentation for import and export

This commit is contained in:
2026-03-11 21:38:10 +01:00
parent 37167fc19c
commit d1122656f3

View File

@@ -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 19901999 |
| `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=<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.01.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.