diff --git a/k-tv-frontend/app/(main)/docs/page.tsx b/k-tv-frontend/app/(main)/docs/page.tsx index da7380a..5dde28c 100644 --- a/k-tv-frontend/app/(main)/docs/page.tsx +++ b/k-tv-frontend/app/(main)/docs/page.tsx @@ -1,8 +1,883 @@ -export default function DocsPage() { +import type { ReactNode } from "react"; + +// --------------------------------------------------------------------------- +// Primitive components +// --------------------------------------------------------------------------- + +function Section({ id, children }: { id: string; children: ReactNode }) { return ( -
-

Docs

-

API reference and usage documentation go here.

+
+ {children} +
+ ); +} + +function H2({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function H3({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function P({ children }: { children: ReactNode }) { + return

{children}

; +} + +function Code({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function Pre({ children }: { children: ReactNode }) { + return ( +
+      {children}
+    
+ ); +} + +function Note({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function Warn({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function Ul({ children }: { children: ReactNode }) { + return ( + + ); +} + +function Li({ children }: { children: ReactNode }) { + return
  • {children}
  • ; +} + +function Table({ + head, + rows, +}: { + head: string[]; + rows: (string | ReactNode)[][]; +}) { + return ( +
    + + + + {head.map((h) => ( + + ))} + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
    + {h} +
    + {cell} +
    +
    + ); +} + +// --------------------------------------------------------------------------- +// Table of contents +// --------------------------------------------------------------------------- + +const TOC = [ + { id: "overview", label: "Overview" }, + { id: "requirements", label: "Requirements" }, + { id: "backend-setup", label: "Backend setup" }, + { id: "frontend-setup", label: "Frontend setup" }, + { id: "jellyfin", label: "Connecting Jellyfin" }, + { id: "first-channel", label: "Your first channel" }, + { id: "blocks", label: "Programming blocks" }, + { id: "filters", label: "Filters reference" }, + { id: "strategies", label: "Fill strategies" }, + { id: "recycle-policy", label: "Recycle policy" }, + { id: "import-export", label: "Import & export" }, + { id: "tv-page", label: "Watching TV" }, + { id: "troubleshooting", label: "Troubleshooting" }, +]; + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + +export default function DocsPage() { + return ( +
    + {/* Sidebar TOC */} + + + {/* Main content */} +
    + {/* ---------------------------------------------------------------- */} +
    +

    Overview

    +

    + K-TV turns your self-hosted media library into broadcast-style linear + TV channels. You define programming blocks — time slots with filters + and fill strategies — and the scheduler automatically picks content + from your{" "} + + Jellyfin + {" "} + library to fill them. Viewers open the TV page and watch a live + stream with no seeking — just like real TV. +

    +

    + The project has two parts: a{" "} + backend (Rust / Axum) + that manages channels, generates schedules, and proxies streams from + Jellyfin, and a{" "} + frontend (Next.js) that + provides the TV viewer and the channel management dashboard. +

    +
    + + {/* ---------------------------------------------------------------- */} +
    +

    Requirements

    + Rust, "1.77+", "Install via rustup"], + [Node.js, "20+", "Frontend only"], + [Jellyfin, "10.8+", "Your media server"], + [ + SQLite or PostgreSQL, + "any", + "SQLite is the default — no extra setup needed", + ], + ]} + /> + + SQLite is the default and requires no additional database setup. + PostgreSQL support is available by rebuilding the backend with the{" "} + postgres Cargo feature. + + + + {/* ---------------------------------------------------------------- */} +
    +

    Backend setup

    +

    + Clone the repository and start the server. All configuration is read + from environment variables or a .env file in the + working directory. +

    +
    {`git clone  k-tv-backend
    +cd k-tv-backend
    +cargo run`}
    +

    + The server starts on http://127.0.0.1:3000 by default. + Database migrations run automatically on startup. +

    + +

    Environment variables

    +
    HOST, + 127.0.0.1, + "Bind address. Use 0.0.0.0 in containers.", + ], + [ + PORT, + 3000, + "HTTP port.", + ], + [ + DATABASE_URL, + sqlite:data.db?mode=rwc, + "SQLite file path or postgres:// connection string.", + ], + [ + CORS_ALLOWED_ORIGINS, + http://localhost:5173, + "Comma-separated list of allowed frontend origins.", + ], + [ + JELLYFIN_BASE_URL, + "—", + "Jellyfin server URL, e.g. http://192.168.1.10:8096", + ], + [ + JELLYFIN_API_KEY, + "—", + "Jellyfin API key (see Connecting Jellyfin).", + ], + [ + JELLYFIN_USER_ID, + "—", + "Jellyfin user ID used for library browsing.", + ], + [ + JWT_SECRET, + "—", + "Secret used to sign login tokens. Generate with: openssl rand -hex 32", + ], + [ + JWT_EXPIRY_HOURS, + 24, + "How long a login token stays valid.", + ], + [ + COOKIE_SECRET, + "dev default", + "Must be at least 64 bytes in production.", + ], + [ + SECURE_COOKIE, + false, + "Set to true when serving over HTTPS.", + ], + [ + DB_MAX_CONNECTIONS, + 5, + "Connection pool maximum.", + ], + [ + DB_MIN_CONNECTIONS, + 1, + "Connections kept alive in the pool.", + ], + [ + PRODUCTION, + false, + "Set to true or 1 to enable production mode.", + ], + ]} + /> + +

    Minimal production .env

    +
    {`HOST=0.0.0.0
    +PORT=3000
    +DATABASE_URL=sqlite:/app/data/k-tv.db?mode=rwc
    +CORS_ALLOWED_ORIGINS=https://your-frontend-domain.com
    +JWT_SECRET=
    +COOKIE_SECRET=<64+ character random string>
    +SECURE_COOKIE=true
    +PRODUCTION=true
    +JELLYFIN_BASE_URL=http://jellyfin:8096
    +JELLYFIN_API_KEY=
    +JELLYFIN_USER_ID=`}
    + + + Always set a strong JWT_SECRET in production. The + default COOKIE_SECRET is publicly known and must be + replaced before going live. + + + + {/* ---------------------------------------------------------------- */} +
    +

    Frontend setup

    +
    {`cd k-tv-frontend
    +cp .env.local.example .env.local
    +# edit .env.local
    +npm install
    +npm run dev`}
    + +

    Environment variables

    +
    NEXT_PUBLIC_API_URL, + http://localhost:3000/api/v1, + "Backend API base URL — sent to the browser.", + ], + [ + API_URL, + "Falls back to NEXT_PUBLIC_API_URL", + "Server-side API URL used by Next.js API routes. Set this if the frontend container reaches the backend via a private hostname.", + ], + ]} + /> + + + The TV page and channel list are fully public — no login required to + watch. An account is only needed to create or manage channels from + the Dashboard. + + + + {/* ---------------------------------------------------------------- */} +
    +

    Connecting Jellyfin

    +

    + K-TV fetches content metadata and HLS stream URLs from Jellyfin. You + need three things: the server URL, an API key, and the user ID K-TV + will browse as. +

    + +

    1. API key

    +

    + In Jellyfin go to{" "} + + Dashboard → API Keys + {" "} + and create a new key. Give it a name like K-TV. Copy the + value into JELLYFIN_API_KEY. +

    + +

    2. User ID

    +

    + Go to{" "} + Dashboard → Users, click + the user K-TV should browse as (usually your admin account), and + copy the user ID from the browser URL: +

    +
    {`/web/index.html#!/useredit?userId=`}
    +

    + Paste it into JELLYFIN_USER_ID. +

    + +

    3. Library IDs (optional)

    +

    + Library IDs are used in the collections filter field to + restrict a block to a specific Jellyfin library or folder. Browse to + a library in Jellyfin and copy the parentId query + parameter from the URL. Leave collections empty to + search across all libraries. +

    + +

    Stream format

    +

    + K-TV requests adaptive HLS streams from Jellyfin. Jellyfin + transcodes to H.264 / AAC on the fly (or serves a direct stream if + the file is already compatible). The frontend player handles + bitrate adaptation and seeks to the correct broadcast position + automatically so viewers join mid-show at the right point. +

    + +

    Subtitles

    +

    + External subtitle files (SRT, ASS) attached to a Jellyfin item are + automatically converted to WebVTT and embedded in the HLS manifest. + A CC button appears in + the TV player when tracks are available. Image-based subtitles + (PGS/VOBSUB from Blu-ray sources) require burn-in transcoding and + are not currently supported. +

    +
    + + {/* ---------------------------------------------------------------- */} +
    +

    Your first channel

    +

    + Log in and open the{" "} + Dashboard. Click{" "} + New channel and fill in: +

    +
    +

    + After creating the channel, open the edit sheet (pencil icon). Add + programming blocks in the list or draw them directly on the 24-hour + timeline. Once the schedule looks right, click{" "} + Generate schedule on the + channel card. K-TV queries Jellyfin, fills each block with matching + content, and starts broadcasting immediately. +

    + + Schedules are valid for 48 hours. K-TV does not regenerate them + automatically — return to the Dashboard and click{" "} + Generate whenever you + want a fresh lineup. + + + + {/* ---------------------------------------------------------------- */} +
    +

    Programming blocks

    +

    + A programming block is a repeating daily time slot. Every day the + block starts at its start_time (in the channel + timezone) and runs for duration_mins minutes. The + scheduler fills it with as many items as will fit. +

    + +

    Timeline editor

    +
      +
    • + Draw a block — click + and drag on an empty area of the 24-hour timeline. +
    • +
    • + Move a block — drag the + block body left or right. Snaps to 15-minute increments. +
    • +
    • + Resize a block — drag + its right edge. +
    • +
    • + Select a block — click + it on the timeline to scroll its detail editor into view below. +
    • +
    +

    + Gaps between blocks are fine — the TV player shows a no-signal + screen during those times. You do not need to fill every minute of + the day. +

    + +

    Content types

    +
    algorithmic, + "The scheduler picks items from your Jellyfin library based on filters you define. Recommended for most blocks.", + ], + [ + manual, + "Plays a fixed, ordered list of Jellyfin item IDs. Useful for a specific playlist or sequential episode run.", + ], + ]} + /> + + + {/* ---------------------------------------------------------------- */} +
    +

    Filters reference

    +

    + Filters apply to algorithmic blocks. All fields are + optional — omit or leave blank to match everything. Multiple values + in an array field must all match (AND logic). +

    +
    content_type, + <> + movie |{" "} + episode |{" "} + short + , + "Restrict to one media type. Leave empty for any type. Short films are stored as movies in Jellyfin.", + ], + [ + genres, + "string[]", + "Only include items matching all listed genres. Names are case-sensitive and must match Jellyfin exactly.", + ], + [ + decade, + "integer", + "Filter by production decade. 1990 matches 1990–1999.", + ], + [ + tags, + "string[]", + "Only include items that have all listed tags.", + ], + [ + min_duration_secs, + "integer", + "Minimum item duration in seconds. 1800 = 30 min, 3600 = 1 hour.", + ], + [ + max_duration_secs, + "integer", + "Maximum item duration in seconds.", + ], + [ + collections, + "string[]", + "Jellyfin library / folder IDs. Find the ID in the Jellyfin URL when browsing a library. Leave empty to search all libraries.", + ], + ]} + /> + + Genre and tag names come from Jellyfin metadata. If a filter returns + no results, check the exact spelling in the Jellyfin library browser + filter panel. + + + + {/* ---------------------------------------------------------------- */} +
    +

    Fill strategies

    +

    + The fill strategy controls how items are ordered and selected from + the filtered pool. +

    +
    random, + "Shuffles the pool and fills the block in random order. Each schedule generation produces a different lineup.", + "Movie channels, variety blocks — anything where you want variety.", + ], + [ + sequential, + "Items are played in the order Jellyfin returns them (typically name or episode number).", + "Series watched in order, e.g. a block dedicated to one show.", + ], + [ + best_fit, + "Greedy bin-packing: repeatedly picks the longest item that still fits in the remaining time, minimising dead air at the end.", + "Blocks where you want the slot filled as tightly as possible.", + ], + ]} + /> + + + {/* ---------------------------------------------------------------- */} +
    +

    Recycle policy

    +

    + The recycle policy controls how soon the same item can reappear + across schedule generations, preventing a small library from cycling + the same content every day. +

    +
    cooldown_days, + "null (disabled)", + "An item won't be scheduled again until at least this many days have passed since it last aired.", + ], + [ + cooldown_generations, + "null (disabled)", + "An item won't be scheduled again until at least this many schedule generations have passed.", + ], + [ + min_available_ratio, + "0.1", + "Safety valve. Even with cooldowns active, always keep at least this fraction of the pool available. A value of 0.1 means 10% of items are always eligible, preventing the scheduler from running dry on small libraries.", + ], + ]} + /> +

    + Both cooldowns can be combined — an item must satisfy both before + becoming eligible. min_available_ratio overrides + cooldowns when too many items are excluded. +

    + + + {/* ---------------------------------------------------------------- */} +
    +

    Import & export

    +

    + Channels can be exported as JSON and shared or reimported. This + makes it easy to build configurations with an LLM and paste them + directly into K-TV. +

    + +

    Exporting

    +

    + Click the download icon on any channel card in the Dashboard. A{" "} + .json file is saved containing the channel name, + timezone, all programming blocks, and the recycle policy. +

    + +

    Importing

    +

    + Click Import channel at + the top of the Dashboard. You can paste JSON text into the text area + or drag and drop a .json file. A live preview shows the + parsed channel name, timezone, and block list before you confirm. +

    +

    + The importer is lenient: block IDs are generated automatically if + missing, and start_time accepts both{" "} + HH:MM and HH:MM:SS. +

    + +

    JSON format

    +
    {`{
    +  "name": "90s Sitcom Network",
    +  "description": "Nothing but classic sitcoms.",
    +  "timezone": "America/New_York",
    +  "blocks": [
    +    {
    +      "name": "Morning Sitcoms",
    +      "start_time": "09:00",
    +      "duration_mins": 180,
    +      "content": {
    +        "type": "algorithmic",
    +        "filter": {
    +          "content_type": "episode",
    +          "genres": ["Comedy"],
    +          "decade": 1990,
    +          "tags": [],
    +          "min_duration_secs": null,
    +          "max_duration_secs": 1800,
    +          "collections": []
    +        },
    +        "strategy": "random"
    +      }
    +    }
    +  ],
    +  "recycle_policy": {
    +    "cooldown_days": 7,
    +    "cooldown_generations": null,
    +    "min_available_ratio": 0.15
    +  }
    +}`}
    + +

    Generating channels with an LLM

    +

    + Paste this prompt into any LLM and fill in your preferences: +

    +
    {`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
    +  }
    +}`}
    + + Genre and tag names must exactly match what Jellyfin uses in your + library. After importing, verify filter fields against your Jellyfin + library before generating a schedule. + +
    + + {/* ---------------------------------------------------------------- */} +
    +

    Watching TV

    +

    + Open /tv to start watching. No login required. The + player tunes to the first channel and syncs to the current broadcast + position automatically — you join mid-show, just like real TV. +

    + +

    Keyboard shortcuts

    +
    + +

    Overlays

    +

    + Move your mouse or press any key to reveal the on-screen overlays. + They fade after a few seconds of inactivity. +

    +
      +
    • + Bottom-left — channel + info: what is playing, episode details, description, genre tags, + and a progress bar with start/end times. +
    • +
    • + Bottom-right — channel + controls (previous / next). +
    • +
    • + Top-right — Guide + toggle and CC button (when subtitles are available). +
    • +
    + +

    Program guide

    +

    + Press G or click the + Guide button to open the upcoming schedule for the current channel. + Colour-coded blocks show each slot; the current item is highlighted. +

    + +

    Subtitles (CC)

    +

    + When the playing item has subtitle tracks in its HLS stream, a{" "} + CC button appears in the + top-right corner. Click it to pick a language track or turn + subtitles off. The button is highlighted when subtitles are active. +

    + +

    Up next banner

    +

    + When the current item is more than 80% complete, an "Up next" banner + appears at the bottom showing the next item's title and start time. +

    + +

    Autoplay after page refresh

    +

    + Browsers block video autoplay on page refresh until the user + interacts with the page. Move your mouse or press any key after + refreshing and playback resumes immediately. +

    + + + {/* ---------------------------------------------------------------- */} +
    +

    Troubleshooting

    + +

    Schedule generation fails

    +

    + Check that JELLYFIN_BASE_URL,{" "} + JELLYFIN_API_KEY, and JELLYFIN_USER_ID are + all set. The backend logs a warning on startup when any are missing. + Confirm the Jellyfin server is reachable from the machine running + the backend. +

    + +

    Video won't play / stream error

    +

    + Click Retry on the error + screen. If it keeps failing, check that Jellyfin is online and the + API key has not been revoked. For transcoding errors, check the + Jellyfin dashboard for active sessions and codec errors in its logs. +

    + +

    Block fills with no items

    +

    + Your filter is too strict or Jellyfin returned nothing matching. + Try: +

    +
      +
    • Removing one filter at a time to find the culprit.
    • +
    • + Verifying genre/tag names match Jellyfin exactly — they are + case-sensitive. +
    • +
    • + Clearing collections to search all libraries. +
    • +
    • + Lowering min_available_ratio if the recycle cooldown + is excluding too many items. +
    • +
    + +

    Channel shows no signal

    +

    + No signal means there is no scheduled slot at the current time. + Either no schedule has been generated yet (click Generate on the + Dashboard), or the current time falls in a gap between blocks. Add a + block covering the current time and regenerate. +

    + +

    CORS errors in the browser

    +

    + Make sure CORS_ALLOWED_ORIGINS contains the exact + origin of the frontend — scheme, hostname, and port, no trailing + slash. Example: https://ktv.example.com. Wildcards are + not supported. +

    + +

    Subtitles not showing

    +

    + The CC button only appears when Jellyfin includes subtitle tracks in + the HLS manifest. Verify the media item has external subtitle files + (SRT/ASS) associated in Jellyfin. Image-based subtitles (PGS/VOBSUB + from Blu-ray sources) are not supported by the HLS path. +

    +
    + ); } diff --git a/k-tv-frontend/app/(main)/layout.tsx b/k-tv-frontend/app/(main)/layout.tsx index 8047e75..b0ebae0 100644 --- a/k-tv-frontend/app/(main)/layout.tsx +++ b/k-tv-frontend/app/(main)/layout.tsx @@ -5,6 +5,7 @@ import { NavAuth } from "./components/nav-auth"; const NAV_LINKS = [ { href: "/tv", label: "TV" }, { href: "/dashboard", label: "Dashboard" }, + { href: "/docs", label: "Docs" }, ]; export default function MainLayout({ children }: { children: ReactNode }) {