feat: add K-TV Channel JSON format documentation for import and export
This commit is contained in:
235
k-tv-frontend/public/channel-format.md
Normal file
235
k-tv-frontend/public/channel-format.md
Normal 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 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=<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.
|
||||||
Reference in New Issue
Block a user