Files
k-tv/k-tv-frontend/public/channel-format.md

7.1 KiB
Raw Blame History

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

{
  "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

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

{
  "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.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:

{
  "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.