feat(docs): enhance documentation with Docker deployment and local files provider sections

This commit is contained in:
2026-03-14 04:10:57 +01:00
parent cf92cc49c2
commit f7f4d92376
3 changed files with 355 additions and 4 deletions

View File

@@ -121,13 +121,17 @@ const TOC = [
{ id: "requirements", label: "Requirements" },
{ id: "backend-setup", label: "Backend setup" },
{ id: "frontend-setup", label: "Frontend setup" },
{ id: "docker", label: "Docker deployment" },
{ id: "jellyfin", label: "Connecting Jellyfin" },
{ id: "local-files", label: "Local files" },
{ 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: "iptv", label: "IPTV export" },
{ id: "channel-password", label: "Channel passwords" },
{ id: "tv-page", label: "Watching TV" },
{ id: "troubleshooting", label: "Troubleshooting" },
];
@@ -359,6 +363,77 @@ npm run dev`}</Pre>
</Note>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="docker">
<H2>Docker deployment</H2>
<P>
The recommended way to run K-TV in production is with Docker Compose.
The repository ships a <Code>compose.yml</Code> that runs the backend
and frontend as separate containers, and an optional{" "}
<Code>compose.traefik.yml</Code> overlay for HTTPS via Traefik.
</P>
<H3>Minimal compose.yml</H3>
<Pre>{`services:
backend:
image: registry.example.com/k-tv-backend:latest
environment:
HOST: 0.0.0.0
DATABASE_URL: sqlite:/app/data/k-tv.db?mode=rwc
CORS_ALLOWED_ORIGINS: https://tv.example.com
JWT_SECRET: <openssl rand -hex 32>
COOKIE_SECRET: <64+ char random string>
SECURE_COOKIE: "true"
PRODUCTION: "true"
JELLYFIN_BASE_URL: http://jellyfin:8096
JELLYFIN_API_KEY: <key>
JELLYFIN_USER_ID: <user-id>
volumes:
- ./data:/app/data
frontend:
image: registry.example.com/k-tv-frontend:latest
environment:
API_URL: http://backend:3000/api/v1
ports:
- "3001:3000"`}</Pre>
<H3>Build-time vs runtime env vars</H3>
<P>
<Code>NEXT_PUBLIC_API_URL</Code> is embedded into the frontend bundle
at build time. It must be passed as a{" "}
<Code>--build-arg</Code> when building the image:
</P>
<Pre>{`docker build \\
--build-arg NEXT_PUBLIC_API_URL=https://tv-api.example.com/api/v1 \\
-t registry.example.com/k-tv-frontend:latest .`}</Pre>
<P>
If you use the provided <Code>compose.yml</Code>, set{" "}
<Code>NEXT_PUBLIC_API_URL</Code> under <Code>build.args</Code> so it
is picked up automatically on every build.
</P>
<P>
<Code>API_URL</Code> (server-side only used by Next.js API routes)
is set at runtime via the container environment and can reference the
backend by its internal Docker hostname:{" "}
<Code>http://backend:3000/api/v1</Code>. It is never baked into the
image.
</P>
<H3>HTTPS with Traefik</H3>
<P>
Merge <Code>compose.traefik.yml</Code> over the base file to add
Traefik labels for automatic TLS certificates and routing:
</P>
<Pre>{`docker compose -f compose.yml -f compose.traefik.yml up -d`}</Pre>
<Note>
Set <Code>SECURE_COOKIE=true</Code> and{" "}
<Code>PRODUCTION=true</Code> whenever the backend is behind HTTPS.
The default cookie secret is publicly known always replace it
before going live.
</Note>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="jellyfin">
<H2>Connecting Jellyfin</H2>
@@ -419,6 +494,66 @@ npm run dev`}</Pre>
</P>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="local-files">
<H2>Local files provider</H2>
<P>
In addition to Jellyfin, K-TV can serve content directly from a
local directory. This is useful when you want to schedule video files
without running a separate media server.
</P>
<H3>Enabling local files</H3>
<P>
Build the backend with the <Code>local-files</Code> Cargo feature
and set the <Code>LOCAL_FILES_DIR</Code> environment variable to the
root of your video library:
</P>
<Pre>{`cargo run --features local-files
# .env
LOCAL_FILES_DIR=/media/videos`}</Pre>
<P>
On startup the backend indexes all video files under{" "}
<Code>LOCAL_FILES_DIR</Code>. Duration is detected via{" "}
<strong className="text-zinc-300">ffprobe</strong> (must be
installed and on <Code>PATH</Code>). Tags are derived from ancestor
directory names; the top-level subdirectory acts as the collection
ID.
</P>
<H3>Rescanning</H3>
<P>
When you add or remove files, trigger a rescan from the Dashboard
(the <strong className="text-zinc-300">Rescan library</strong>{" "}
button appears when the local files provider is active) or call the
API directly:
</P>
<Pre>{`POST /api/v1/files/rescan
Authorization: Bearer <token>
# Response
{ "items_found": 142 }`}</Pre>
<H3>Streaming</H3>
<P>
Local file streams are served by{" "}
<Code>GET /api/v1/files/stream/:id</Code>. This endpoint is{" "}
<strong className="text-zinc-300">public</strong> (no auth required)
and supports <Code>Range</Code> headers for seeking. The frontend
player uses the native <Code>&lt;video&gt;</Code> element for local
files instead of hls.js.
</P>
<H3>Filter support</H3>
<Note>
When the local files provider is active, the series picker and genre
filter are hidden in the block editor those fields are only
supported by Jellyfin. Tags, decade, duration limits, and collection
filters work normally.
</Note>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="first-channel">
<H2>Your first channel</H2>
@@ -743,6 +878,75 @@ Output only valid JSON matching this structure:
</Note>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="iptv">
<H2>IPTV export</H2>
<P>
K-TV can export your channels as a standard IPTV playlist so you can
watch in any IPTV client TiviMate, VLC, Infuse, Jellyfin, and
others.
</P>
<H3>Getting the URLs</H3>
<P>
Open the Dashboard and click the antenna icon on any channel card to
open the IPTV Export dialog. It shows two URLs:
</P>
<Table
head={["URL", "Format", "Purpose"]}
rows={[
[
<Code key="m3u">/iptv/playlist.m3u?token=</Code>,
"M3U",
"Channel list — paste this into your IPTV client as the playlist source.",
],
[
<Code key="xml">/iptv/epg.xml?token=</Code>,
"XMLTV",
"Electronic program guide — paste this as the EPG / guide data source.",
],
]}
/>
<H3>Adding to an IPTV client</H3>
<P>
Copy the M3U URL and add it as a new playlist in your client. If the
client supports XMLTV, also add the EPG URL so programme titles and
descriptions appear in the guide.
</P>
<Warn>
Both URLs contain your session JWT as a query parameter. Anyone with
the URL can access your channels treat it like a password and do
not share it publicly. Rotating your session (logging out and back
in) invalidates the old URLs.
</Warn>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="channel-password">
<H2>Channel passwords</H2>
<P>
Individual channels can be protected with an optional password. When
set, TV viewers are prompted to enter the password before the stream
plays. Channels without a password are always public.
</P>
<H3>Setting a password</H3>
<P>
Enter a password in the <strong className="text-zinc-300">Password</strong>{" "}
field when creating a channel or editing it in the Dashboard. Leave
the field blank to remove an existing password.
</P>
<Warn>
Channel passwords are not end-to-end encrypted. They prevent casual
access someone who can intercept network traffic or extract the
JWT from an IPTV URL can still reach the stream. Do not use channel
passwords as the sole protection for sensitive content.
</Warn>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="tv-page">
<H2>Watching TV</H2>

View File

@@ -15,7 +15,7 @@ export default function MainLayout({ children }: { children: ReactNode }) {
<header className="sticky top-0 z-50 border-b border-zinc-800 bg-zinc-950/80 backdrop-blur">
<nav className="mx-auto flex h-14 max-w-7xl items-center justify-between px-6">
<Link
href="/tv"
href="/"
className="text-sm font-semibold tracking-widest text-zinc-100 uppercase"
>
K-TV

View File

@@ -1,5 +1,152 @@
import { redirect } from "next/navigation";
import Link from "next/link";
export default function Home() {
redirect("/tv");
export default function LandingPage() {
return (
<div className="min-h-screen bg-zinc-950 text-zinc-100">
{/* Header */}
<header className="flex items-center justify-between px-6 py-4 border-b border-zinc-800">
<span className="text-lg font-bold tracking-tight text-zinc-100">
K-TV
</span>
<nav className="flex items-center gap-6 text-sm text-zinc-400">
<Link href="/tv" className="hover:text-zinc-100 transition-colors">
TV
</Link>
<Link href="/docs" className="hover:text-zinc-100 transition-colors">
Docs
</Link>
<Link
href="/login"
className="rounded-md border border-zinc-700 px-3 py-1.5 text-zinc-300 hover:border-zinc-500 hover:text-zinc-100 transition-colors"
>
Login
</Link>
</nav>
</header>
{/* Hero */}
<section className="mx-auto max-w-4xl px-6 py-24 text-center">
<h1 className="mb-4 text-5xl font-bold tracking-tight text-zinc-100 leading-tight">
Your media library,
<br />
broadcast as linear TV
</h1>
<p className="mb-10 text-lg text-zinc-400 max-w-2xl mx-auto leading-relaxed">
K-TV turns your self-hosted media collection into algorithmic TV
channels. Define programming blocks, set filters, and let the
scheduler fill them. Viewers tune in mid-show no seeking, just TV.
</p>
<div className="flex items-center justify-center gap-4">
<Link
href="/tv"
className="rounded-lg bg-zinc-100 px-6 py-2.5 text-sm font-semibold text-zinc-900 hover:bg-white transition-colors"
>
Watch TV
</Link>
<Link
href="/docs"
className="rounded-lg border border-zinc-700 px-6 py-2.5 text-sm font-semibold text-zinc-300 hover:border-zinc-500 hover:text-zinc-100 transition-colors"
>
Read the docs
</Link>
</div>
</section>
{/* Feature cards */}
<section className="mx-auto max-w-5xl px-6 pb-20">
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
<h3 className="mb-2 font-semibold text-zinc-100">
Linear scheduling
</h3>
<p className="text-sm leading-relaxed text-zinc-400">
Draw time blocks on a 24-hour timeline. Each block has its own
filters, fill strategy, and recycle policy. Schedules are
generated on demand and valid for 48 hours.
</p>
</div>
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
<h3 className="mb-2 font-semibold text-zinc-100">
Jellyfin &amp; local files
</h3>
<p className="text-sm leading-relaxed text-zinc-400">
Connect your Jellyfin server or point K-TV at a local directory.
Filter by genre, decade, tags, series, or collection. All
providers share the same scheduling engine.
</p>
</div>
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
<h3 className="mb-2 font-semibold text-zinc-100">
EPG &amp; live program guide
</h3>
<p className="text-sm leading-relaxed text-zinc-400">
A full electronic program guide shows what is on now and what is
coming up. Keyboard shortcuts, channel numbers, subtitles, and an
&quot;Up next&quot; banner are all built in.
</p>
</div>
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
<h3 className="mb-2 font-semibold text-zinc-100">
Import, export &amp; IPTV
</h3>
<p className="text-sm leading-relaxed text-zinc-400">
Channels are plain JSON paste one from an LLM, share it with a
friend, or import it across instances. Export an M3U playlist and
XMLTV EPG to watch in TiviMate, VLC, or Infuse.
</p>
</div>
</div>
</section>
{/* Quick-start */}
<section className="mx-auto max-w-3xl px-6 pb-24">
<h2 className="mb-4 text-xl font-semibold text-zinc-100">
Quick start
</h2>
<pre className="overflow-x-auto rounded-lg border border-zinc-800 bg-zinc-900 p-5 font-mono text-[13px] leading-relaxed text-zinc-300">{`# Docker (recommended)
git clone <repo-url> k-tv && cd k-tv
cp .env.example .env # fill in JWT_SECRET, COOKIE_SECRET, JELLYFIN_*
docker compose up -d
# Or run from source
cd k-tv-backend && cargo run
cd k-tv-frontend && npm install && npm run dev`}</pre>
<p className="mt-3 text-sm text-zinc-500">
See the{" "}
<Link
href="/docs"
className="text-zinc-400 underline underline-offset-2 hover:text-zinc-200"
>
docs
</Link>{" "}
for full environment variable reference and Docker deployment
instructions.
</p>
</section>
{/* Footer */}
<footer className="border-t border-zinc-800 px-6 py-6">
<div className="mx-auto flex max-w-5xl items-center justify-between text-sm text-zinc-500">
<span>K-TV</span>
<nav className="flex gap-5">
<Link href="/tv" className="hover:text-zinc-300 transition-colors">
TV
</Link>
<Link
href="/docs"
className="hover:text-zinc-300 transition-colors"
>
Docs
</Link>
<Link
href="/dashboard"
className="hover:text-zinc-300 transition-colors"
>
Dashboard
</Link>
</nav>
</div>
</footer>
</div>
);
}