The TV guide that runs on your hardware.
Point it at your M3U playlists, Xtream panels, Stalker portals and HDHomeRun tuners. Get one colourful, set-top-box-style guide — logos, release years, a cinematic TMDB panel — served from a single container on your home lab. No account on someone else's server.
01 the guide
A grid you'd actually leave open.
Virtualised both ways, so thousands of channels scroll at 60fps. Cells are coloured by genre, the now-line glows down the current minute, and a long film keeps its title pinned as you scroll past it. On a phone it folds into a single-channel agenda.
- Live now-line — with a per-channel offset you nudge from a programme, to line up an East/West feed pair.
- Full keyboard nav — arrows, PgUp/Dn, Home jumps to now.
- Installable PWA — offline shell, and an in-tab prompt when a new version lands.
- Drag to reorder sources — the guide lists channels in that order.
- Search the whole line-up — every channel, two weeks out, in guide order.
- Watchlist reminders — flag one airing or a whole title; an email lands ~15 min before it starts.
- Push notifications — reminders, source-health alerts and opt-in activity pings to Gotify, ntfy, Discord, Telegram, Pushover… via Apprise, alongside email.
02 sources
Everything you already watch, in one lineup.
The same source types the tvdinner CLI supports — brought in behind an SSRF
guard, refreshed on a schedule by the background worker, credentials encrypted at rest.
And back out again: turn on export feeds and tvtimes serves the merged line-up as one M3U playlist + XMLTV guide — times already zone-corrected per channel — behind a rotatable token. Drop the two URLs into Jellyfin, Plex, Emby, TiviMate or Threadfin.
Or just press Play on a programme and that one channel opens straight in a
player — tvdinner on desktop, the system app chooser on Android, or
Download .m3u for anything else — no export feed needed. On a
Fire TV Stick it just works in the built-in Silk browser: Play hands the channel
over to MX Player Pro, nothing sideloaded.
03 enrichment
Click a film. Get the whole story.
Add your own TMDB key and a movie-category programme opens into a cinematic panel: textless backdrop, title logo, ★ rating, genres, director, top cast and synopsis — matched carefully so a wrong result never shows the wrong art.
- Channel logos — from the playlist, else the iptv-org database, else the SiliconDust guide for HDHomeRun.
- Global cache — enrichment is public data, cached 30 days, negatives cached but failures never.
- Release year & runtime — on the cell and in the panel.
04 yours to run
One image. One compose up.
A single multi-arch image carries the API, the worker and the web app; it serves the SPA from the same origin, so there's no CORS and the session cookie is first-party. Postgres, Redis and the generated secrets live in named volumes.
# grab the compose file and a starter .env, then: docker compose up -d # open the URL you set as TVTIMES_PUBLIC_ORIGIN — default :8888
Passkeys first
WebAuthn primary, Argon2id + HIBP password fallback, TOTP, 60-day rotating sessions with replay detection.
Secrets on first run
The signing key and the at-rest encryption key are generated into a volume and never regenerated.
Migrations on start
alembic upgrade head runs before the API serves; docker compose pull is the whole upgrade.
amd64 & arm64
Runs on a NUC or a Pi. Published to Docker Hub and mirrored to GHCR on every v* tag.
05 under the hood
Boring where it counts.
- Backend
- FastAPI · SQLAlchemy 2.0 async · Alembic · arq worker · structlog with credential redaction
- Data
- Postgres 16 · Redis 7 · streaming XMLTV parse · conditional GET · rolling −1d…+14d window
- Frontend
- React 18 · Vite · TypeScript · TanStack Query · react-virtual · typed client from the OpenAPI schema
- Security
- SSRF guard on every user URL · Fernet at rest · EdDSA access JWT · double-submit CSRF · strict CSP
- Tests
- ruff + mypy --strict + eslint + tsc in CI, plus a full image build on every PR