feat(docs): enhance documentation with Docker deployment and local files provider sections
This commit is contained in:
@@ -121,13 +121,17 @@ const TOC = [
|
|||||||
{ id: "requirements", label: "Requirements" },
|
{ id: "requirements", label: "Requirements" },
|
||||||
{ id: "backend-setup", label: "Backend setup" },
|
{ id: "backend-setup", label: "Backend setup" },
|
||||||
{ id: "frontend-setup", label: "Frontend setup" },
|
{ id: "frontend-setup", label: "Frontend setup" },
|
||||||
|
{ id: "docker", label: "Docker deployment" },
|
||||||
{ id: "jellyfin", label: "Connecting Jellyfin" },
|
{ id: "jellyfin", label: "Connecting Jellyfin" },
|
||||||
|
{ id: "local-files", label: "Local files" },
|
||||||
{ id: "first-channel", label: "Your first channel" },
|
{ id: "first-channel", label: "Your first channel" },
|
||||||
{ id: "blocks", label: "Programming blocks" },
|
{ id: "blocks", label: "Programming blocks" },
|
||||||
{ id: "filters", label: "Filters reference" },
|
{ id: "filters", label: "Filters reference" },
|
||||||
{ id: "strategies", label: "Fill strategies" },
|
{ id: "strategies", label: "Fill strategies" },
|
||||||
{ id: "recycle-policy", label: "Recycle policy" },
|
{ id: "recycle-policy", label: "Recycle policy" },
|
||||||
{ id: "import-export", label: "Import & export" },
|
{ id: "import-export", label: "Import & export" },
|
||||||
|
{ id: "iptv", label: "IPTV export" },
|
||||||
|
{ id: "channel-password", label: "Channel passwords" },
|
||||||
{ id: "tv-page", label: "Watching TV" },
|
{ id: "tv-page", label: "Watching TV" },
|
||||||
{ id: "troubleshooting", label: "Troubleshooting" },
|
{ id: "troubleshooting", label: "Troubleshooting" },
|
||||||
];
|
];
|
||||||
@@ -359,6 +363,77 @@ npm run dev`}</Pre>
|
|||||||
</Note>
|
</Note>
|
||||||
</Section>
|
</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">
|
<Section id="jellyfin">
|
||||||
<H2>Connecting Jellyfin</H2>
|
<H2>Connecting Jellyfin</H2>
|
||||||
@@ -419,6 +494,66 @@ npm run dev`}</Pre>
|
|||||||
</P>
|
</P>
|
||||||
</Section>
|
</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><video></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">
|
<Section id="first-channel">
|
||||||
<H2>Your first channel</H2>
|
<H2>Your first channel</H2>
|
||||||
@@ -743,6 +878,75 @@ Output only valid JSON matching this structure:
|
|||||||
</Note>
|
</Note>
|
||||||
</Section>
|
</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">
|
<Section id="tv-page">
|
||||||
<H2>Watching TV</H2>
|
<H2>Watching TV</H2>
|
||||||
|
|||||||
@@ -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">
|
<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">
|
<nav className="mx-auto flex h-14 max-w-7xl items-center justify-between px-6">
|
||||||
<Link
|
<Link
|
||||||
href="/tv"
|
href="/"
|
||||||
className="text-sm font-semibold tracking-widest text-zinc-100 uppercase"
|
className="text-sm font-semibold tracking-widest text-zinc-100 uppercase"
|
||||||
>
|
>
|
||||||
K-TV
|
K-TV
|
||||||
|
|||||||
@@ -1,5 +1,152 @@
|
|||||||
import { redirect } from "next/navigation";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Home() {
|
export default function LandingPage() {
|
||||||
redirect("/tv");
|
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 & 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 & 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
|
||||||
|
"Up next" 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 & 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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user