A command-line IPTV player. Plays streams from an M3U/M3U8 playlist (or
a direct stream URL) using mpv, with a TiviMate-style on-screen EPG
overlay and a full program guide sourced from XMLTV data — auto-discovered
from the playlist, or an explicit URL — including timezone-aware
scheduling and configurable clock-correction shifts for feeds that
report incorrect times.
Primarily developed for and packaged on Linux (.deb/.rpm, see
below); also packaged for Windows as a self-contained installer (see
Windows installer) that bundles its own mpv so
nothing else needs installing first.
This README covers install and the full command/options reference. For a deeper walkthrough of each source (M3U, Xtream Codes, Stalker Portal, HDHomeRun, Plex, local files, YouTube) and feature (program guide, recording & scheduling, casting, TMDB ratings, backup, and more), see the wiki.
The full program guide — channels down the left, a timeline across the top, and a live "now" marker:
A cinematic "now playing" hero for a movie you're watching, live or on demand — TMDB backdrop art (or Plex's own), shown full-bleed and translucent over the picture, with a TMDB title logo top-right, rating, director, synopsis, and what's on next:
The on-screen EPG banner — shown on channel switch or with i for
anything that isn't a TMDB-backdrop-matched movie (see above) — current
programme, poster art, stream quality badges, and a favorited channel's
heart marker:
Full programme details for the guide's selected show, poster art included:
Filtering the guide by name or group:
tvdinner bookmarks — an interactive picker for saved playlists:
Every option, from tvdinner --help:
- Linux (developed against Ubuntu 26.04+) or Windows
mpvon Linux (the Windows installer bundles its own; only needed separately if running from source)- Python 3.10+ (not needed at all with the Windows installer, which bundles its own)
macOS is not supported. There's no package, and nothing runs against it in CI. It may well start from source -- it's Python and libmpv -- but two things are known not to work, and neither is a small fix:
tvdinner:andtvtimes:links can't be registered. macOS only lets an application bundle claim a URL scheme, viaCFBundleURLTypesin itsInfo.plist. There's no.appto put that in, so tvtimes' Play and Open in tvdinner buttons have nothing to hand the link to. Unlike Linux and Windows this can't be fixed with a command or an installer flag -- it needs macOS packaging, and realistically notarisation on top, or every user meets Gatekeeper.- Config goes to
~/.config/tvdinner/, the Linux location, rather than~/Library/Application Support/.
If you want it supported, say so on issue #3 -- it's open precisely to gauge whether the packaging work is worth doing.
sudo add-apt-repository ppa:issinoho/tvdinner
sudo apt install tvdinner
Upgrades then arrive through apt along with everything else on the
machine. Built for 24.04 (noble) and 26.04 (resolute); tvdinner is
architecture-independent, so one build serves every architecture.
22.04 (jammy) isn't covered and won't be: its python3-mpv,
python3-pil and python3-requests all sit below what tvdinner needs,
and its python3-mpv 0.5.2 can't even load against a current libmpv.
For Debian, or an Ubuntu series the PPA doesn't cover, either install
tvdinner_<version>_all.deb from the latest
release, or build
the .deb locally:
sudo apt install debhelper dh-python python3-all python3-setuptools pybuild-plugin-pyproject fakeroot lintian
dpkg-buildpackage -us -uc -b
sudo apt install ../tvdinner_<version>_all.deb
This pulls in mpv, python3-mpv, python3-pil, python3-requests,
and fonts-dejavu-core as dependencies, installs the tvdinner(1) man
page, and installs a desktop entry + icon so tvdinner shows up as an
opener for .m3u/.m3u8 files (see Desktop
integration below). desktop-file-utils
is a Recommends -- it just keeps the "Open With" menu's cache current.
Build on the target distribution (or in a mock/chroot matching it),
not on Debian/Ubuntu -- the spec relies on that distro's own
python3-rpm-macros package to resolve %{python3_sitelib} and
%py3_build/%py3_install correctly for its Python version:
sudo dnf install rpm-build python3-devel python3-setuptools python3-pip
git archive --format=tar.gz --prefix=tvdinner-1.0.0/ HEAD -o ~/rpmbuild/SOURCES/tvdinner-1.0.0.tar.gz
rpmbuild -bb rpm/tvdinner.spec
sudo dnf install ~/rpmbuild/RPMS/noarch/tvdinner-1.0.0-1.*.noarch.rpm
This pulls in mpv, python3-pillow, python3-requests,
dejavu-sans-fonts, and hicolor-icon-theme as dependencies, and
installs a desktop entry + icon (see Desktop
integration below). python-mpv
(tvdinner's Python binding to mpv) has no Fedora/RHEL RPM equivalent, so
it's deliberately left off the spec's Requires -- install it
separately first, e.g. pip install --user python-mpv.
A source RPM (rpmbuild -bs rpm/tvdinner.spec) can be built from
anywhere, including Debian/Ubuntu, since it doesn't execute %build/
%install -- only turning it into an installable binary RPM needs a
real RPM-based host.
python3 -m venv .venv
.venv/bin/pip install .
mpv itself must still be installed separately via your package manager
(e.g. sudo apt install mpv).
The .deb and .rpm install /usr/share/applications/tvdinner.desktop
(plus an icon), which registers tvdinner as an opener for .m3u /
.m3u8 files and for tvdinner: links -- so it appears in a file
manager's Open With menu, in a browser's "what should I do with this
file" prompt, and a tvdinner:<url> link hands the URL straight to it
(no download). The entry is Terminal=true: tvdinner is a
keyboard-driven TUI, so the desktop launches it inside your terminal
emulator, with mpv's own video window alongside.
It does not make itself the default handler (an .m3u is just as
often a local music playlist), so the first double-click still shows an
application picker. To make tvdinner the default and skip that dialog:
tvdinner default-handler
That's a wrapper around xdg-mime default tvdinner.desktop <the M3U MIME types + x-scheme-handler/tvdinner> — it writes your own
~/.config/mimeapps.list (no root, nothing system-wide) and verifies
the result. Undo it from a file manager's Open With dialog, or by
editing that file. The equivalent by hand:
xdg-mime default tvdinner.desktop audio/x-mpegurl audio/mpegurl application/x-mpegurl application/vnd.apple.mpegurl x-scheme-handler/tvdinner
xdg-mime query default audio/x-mpegurl # confirm: tvdinner.desktop
Running from source (no package)? tvdinner default-handler also drops
a ~/.local/share/applications/tvdinner.desktop for you when it can't
find an installed one. To do it by hand:
install -Dm644 data/tvdinner.desktop ~/.local/share/applications/tvdinner.desktop
install -Dm644 data/tvdinner.svg ~/.local/share/icons/hicolor/scalable/apps/tvdinner.svg
update-desktop-database ~/.local/share/applications
A plain https://…/playlist.m3u link is still the browser's call --
it typically downloads the file and then opens it through this entry;
there's no handler hook for https itself. A tvdinner: link
(tvdinner:https://…/playlist.m3u) does have a hook: after
default-handler, the browser hands it straight to tvdinner (one
remembered "Open tvdinner?" prompt), no file saved. tvtimes' "Play"
button emits one of these on desktop.
A tvtimes: link (tvtimes://host?token=…, or tvtimess:// for
https) is hooked the same way, and hands over a whole
tvtimes account -- its entire merged line-up and guide,
rather than the one channel a tvdinner: Play link carries. tvtimes'
Settings → Export feeds panel has an "Open in tvdinner" button that
emits one.
Download tvdinner-setup-<version>.exe from the
latest release
and run it. It bundles a pre-built mpv (see
windows/THIRD_PARTY_NOTICES.txt for
its license) and everything else tvdinner needs -- there's no separate
Python or mpv install step. From 1.43.0 the installer and the
tvdinner.exe it installs are both code-signed with a
Certum certificate (see
CODE_SIGNING_POLICY.md); the executable is what
Windows checks every time you launch tvdinner, not just once at install.
Earlier releases are unsigned, and Windows SmartScreen shows an
"unrecognized app" warning on first run for those -- click "More info"
then "Run anyway" to proceed. SmartScreen may still warn on a newly
signed release until the certificate builds reputation. An optional
install step adds tvdinner to your PATH so you can run tvdinner from
any Command Prompt (open a new one after installing for this to take
effect).
The per-channel EPG shift file (--epg-shifts) defaults to
%APPDATA%\tvdinner\epg_shifts.json on Windows, rather than the
~/.config/... path used on Linux (similarly for --favorites and
--bookmarks-file).
For development, or if you'd rather not use the installer:
- Install Python 3.10+ from python.org (or
the Microsoft Store), and
mpv-- e.g. via Chocolatey (choco install mpv) or a libmpv build withmpv-2.dllplaced somewhere onPATH. pip install .from a checkout of this repository (a PyPI release isn't published yet).- Run
tvdinnerfrom the same shell/venv.
tvdinner [OPTIONS] URL
tvdinner (same as `tvdinner bookmarks`)
tvdinner bookmarks [--bookmarks-file PATH]
tvdinner bookmarks list [--json] [--bookmarks-file PATH]
tvdinner bookmarks add --name NAME --url URL [--epg URL] [--channel C] [--tmdb-api-token TOKEN] [--replace] [--json] [--bookmarks-file PATH]
tvdinner bookmarks edit NAME|INDEX [--name NAME] [--url URL] [--epg URL | --clear-epg] [--channel C | --clear-channel] [--tmdb-api-token TOKEN | --clear-tmdb-api-token] [--json] [--bookmarks-file PATH]
tvdinner bookmarks remove NAME|INDEX [--json] [--bookmarks-file PATH]
tvdinner backup [PATH] [--epg-shifts PATH] [--favorites PATH] [--bookmarks-file PATH] [--tmdb-token-file PATH] [--gdrive [--gdrive-filename NAME] [--gdrive-token-file PATH]]
tvdinner restore [PATH] [--epg-shifts PATH] [--favorites PATH] [--bookmarks-file PATH] [--tmdb-token-file PATH] [-y] [--gdrive [--gdrive-filename NAME] [--gdrive-token-file PATH]]
tvdinner gdrive-login [--client-id ID] [--client-secret SECRET] [--gdrive-token-file PATH] [--no-browser]
tvdinner gdrive-logout [--gdrive-token-file PATH]
tvdinner stats [--bookmarks-file PATH] [--history-file PATH]
tvdinner store-tmdb TOKEN [--tmdb-token-file PATH]
tvdinner clear-tmdb [--tmdb-token-file PATH]
tvdinner default-handler
tvdinner hard-reset [--epg-shifts PATH] [--favorites PATH] [--bookmarks-file PATH] [--tmdb-token-file PATH] [--schedule-file PATH] [--playback-positions-file PATH] [--history-file PATH] [-y]
URL may be an M3U/M3U8 playlist (http(s) or a local file path), an
Xtream Codes login (xtream://username:password@host:port),
a Stalker Portal login
(stalker://host:port/portal/path?mac=AA:BB:CC:DD:EE:FF), an
HDHomeRun tuner (hdhomerun://host[:port]), a
Plex Media Server login
(plex://host:port?X-Plex-Token=...), a tvtimes account
(tvtimes://host[:port]?token=...), a direct video/audio stream URL, a
local video file (e.g. a movie) to play directly -- see Local
files below -- or a YouTube video URL -- see
YouTube below. If it resolves to a channel list, playback
starts on the channel given by --channel, or the first channel otherwise
— use the program guide (see Keybindings below) to switch channels without
restarting. A Plex URL is different: there's no channel list, just a
library browser (see Plex Media Server below, or
the wiki page
for the full walkthrough).
tvdinner bookmarks opens an interactive terminal table of saved
playlists instead -- as does running tvdinner with no arguments at
all, rather than argparse's usual "the following arguments are
required" error, since picking from what's already saved is the
natural thing to want with nothing else typed: a adds one
(description, URL -- anything the URL argument above accepts,
optional EPG URL, optional default channel e.g.
CNN, optional TMDB API token, optional tvtimes device
name), e edits the selected
one, d deletes it (with confirmation), K/J moves the selected row
up/down the list (saved immediately, same as add/edit/delete), SPACE
toggles that row's "EPG Refresh" checkbox (unchecked by default, and
not remembered between sessions), t toggles its "tvtimes" checkbox,
and ENTER launches tvdinner with it, exactly as if its
URL/--epg/--channel/--tmdb-api-token had been typed directly --
adding --refresh-epg-cache too if the checkbox was checked.
The tvtimes column only applies to a tvtimes source. It
starts checked on any tvtimes:///tvtimess:// row -- a bookmark you've
deliberately paired is one you generally want fully paired -- and is
greyed out on every other row, where t does nothing. Launching a
checked row adds --record-watchlist, --report-watch-state and
--sync-favourites in one go, the whole pairing described under
Everything at once,
plus --device-name if the bookmark has one saved. That label is only
ever the one you typed -- never guessed from the hostname, which would
put a machine name into your account's watch history that you never asked
to send. Press t to opt a single launch back out; like "EPG Refresh",
the state isn't remembered between sessions.
Saved to ~/.config/tvdinner/bookmarks.json by default
(%APPDATA%\tvdinner\bookmarks.json on Windows). A saved TMDB token is
never shown in the table (nor is it a column any more) -- only the
add/edit form shows it.
tvdinner bookmarks list / add / edit / remove manage that same
file non-interactively, for scripting or for another tool to
register a source (add a row whose --url is a merged M3U and --epg
its XMLTV, and that provider is one ENTER away in the picker). edit
and remove take either an exact bookmark name or its 1-based position
from list; edit leaves unnamed fields alone, and the --clear-*
flags (--clear-epg, --clear-channel, --clear-tmdb-api-token,
--clear-device-name) unset an optional one. Plain list masks any login credentials in
a bookmark's URL and hides its token (as the picker does); list --json
emits the raw bookmarks.json array — real URLs and tokens — for a
caller to consume, and add / edit / remove take --json to print
the affected row instead of a status line. add refuses a name that's
already taken unless --replace, which overwrites that row in place.
tvdinner backup writes the EPG shifts, favorites, bookmarks, and
stored default TMDB token (see below) files into a single compressed
archive for offline storage or moving to another machine (default
filename: tvdinner-backup-<timestamp>.zip in the current directory;
the EPG cache and log file are deliberately left out, since they're
disposable, not configuration). tvdinner restore extracts a backup
archive back onto disk, overwriting the current files — it prompts for
confirmation unless -y/--yes is given. Add --gdrive to either
command to use Google Drive instead of/alongside a local file --
tvdinner backup --gdrive still writes the local archive too, then
uploads it; tvdinner restore --gdrive downloads it instead of taking
a local PATH (omit PATH in that case). See Google Drive
backup below for one-time setup.
tvdinner stats prints a table of on-disk cache usage: one row per
bookmarked feed's EPG cache, for whichever bookmarks have a
deterministically knowable EPG source without fetching anything -- an
explicit saved EPG URL, or an Xtream login's own xmltv.php export --
plus the caches every feed shares regardless of source (TMDB
ratings/metadata, channel logos/poster art,
iptv-org's online logo database, and
the log/watch history files). A bookmark relying on
M3U auto-discovery (x-tvg-url, which needs an actual playlist fetch
to resolve) or with no EPG at all
(Stalker, HDHomeRun without a DVR subscription, Plex) is listed as
unknown rather than guessed; its cache still counts toward the "other"
total. Nothing here is fetched over the network -- it only reads
whatever's already on disk.
It also reports watching activity from that same watch history log: total watch time this week, this month, and all-time, broken down by live channel/VOD/recording, plus the most-watched live channels this month and all-time (skipped if nothing in the log is a live channel -- a Plex-only or VOD-only history has no "top channels" to show).
tvdinner hard-reset deletes every file and directory tvdinner itself
writes -- bookmarks, favorites, EPG shifts, a stored default TMDB
token, scheduled recordings, playback positions, watch history,
update-check state, the EPG/TMDB/image caches, and the log file --
reverting it to exactly the state a fresh install would be in. It
prompts for confirmation (listing every path first) unless -y/--yes
is given, same as
tvdinner restore. It never touches --record-dir -- a recording
is real media you made, not disposable app state, so resetting
tvdinner has no business deleting it.
tvdinner backup --gdrive/tvdinner restore --gdrive store/fetch the
backup archive in Google Drive instead of (or in addition to, for
backup) a local file, using an app-created file only -- tvdinner never
sees the rest of a Drive account's contents.
tvdinner gdrive-login
opens a browser for Google's sign-in/consent screen (using tvdinner's
own bundled OAuth client -- see below -- so there's no Google Cloud
Console setup needed), then stores the resulting credentials at
~/.config/tvdinner/gdrive_token.json
(%APPDATA%\tvdinner\gdrive_token.json on Windows): a refresh token
plus the client ID/secret, never the account password. Since the app
isn't Google-verified, the consent screen shows an "unverified app"
warning first -- click "Advanced" then "Go to tvdinner (unsafe)" to
proceed; this is normal for a small open-source tool and doesn't mean
anything is actually wrong (see below for why). tvdinner always prints
the sign-in URL as a fallback alongside trying to open it automatically;
add --no-browser to skip that automatic open attempt (useful if it'd
pick the wrong browser, or fail noisily on a machine with none
installed) and just print the URL. Either way, the flow itself needs a
browser's redirect to land back on 127.0.0.1 on this same machine, so
on a fully headless/SSH-only box you'll also need to forward the local
port the URL redirects to (e.g. ssh -L <port>:localhost:<port> user@host, using the port from the printed URL's redirect_uri)
before opening the link elsewhere.
From then on:
tvdinner backup --gdrive # writes the local archive, then uploads it
tvdinner restore --gdrive # downloads it and restores, prompting first
Both default to a Drive file named tvdinner-backup.zip
(--gdrive-filename NAME to use a different one -- e.g. one per
machine); backing up again updates that same file rather than creating
a duplicate. tvdinner gdrive-logout removes the stored credentials
locally (it doesn't revoke Google's own record of the grant -- see
myaccount.google.com/permissions
to do that).
If you'd rather not share tvdinner's bundled OAuth client's request
quota, bring your own: in Google Cloud
Console, create a project, enable
the Google Drive API for it (APIs & Services → Library), create an
OAuth client of type Desktop app under Credentials (or the newer
Google Auth Platform → Clients), then
tvdinner gdrive-login --client-id ID --client-secret SECRET
(only needed the first time, or after gdrive-logout -- a later
gdrive-login reuses whichever client is already stored if omitted).
Why a bundled client secret is fine here: for an OAuth "Desktop app" client, the secret isn't actually confidential -- the app can't keep it hidden from whoever's running it, so RFC 8252 (OAuth for Native Apps) and Google's own docs both treat it as a public identifier rather than something to protect. The real security boundary is PKCE plus each user's own consent-screen approval, same as with any other installed- app OAuth client.
| Option | Description |
|---|---|
-c, --channel CHANNEL |
Channel name (or 1-based index) to play; defaults to the first channel in the playlist. |
--list |
List channels in the playlist and exit without playing. |
--epg URL |
XMLTV EPG URL or local file, overriding any EPG source discovered in the M3U playlist. |
--tz NAME |
IANA timezone for displaying EPG times, e.g. Europe/London (default: system local timezone). |
--time-shift SHIFT |
Correct EPG feed clock errors, e.g. +1h, -30m, or minutes as a plain integer. Applies to any channel without its own override in --epg-shifts. |
--epg-shifts PATH |
JSON file mapping a channel's display name (as shown by --list) to a per-channel EPG time-shift override, for feeds where different channels are off by different amounts (default: ~/.config/tvdinner/epg_shifts.json on Linux, %APPDATA%\tvdinner\epg_shifts.json on Windows). See below. |
--favorites PATH |
JSON file storing favorited channels per playlist (see the h keybinding below), keyed by the playlist URL/path so different feeds don't share one favorites list (default: ~/.config/tvdinner/favorites.json on Linux, %APPDATA%\tvdinner\favorites.json on Windows). |
--record-dir PATH |
Directory to save r-key recordings into (see Keybindings below); default: ~/Videos/tvdinner on Linux, %USERPROFILE%\Videos\tvdinner on Windows. |
--vod-group GROUP |
An M3U group-title (exact match) to pull out of the guide/channel list and into the VOD movie browser (see the m keybinding below) instead -- repeatable to name several groups. Only affects plain M3U/local playlists; Xtream and Stalker panels expose VOD as a separate API and are always browsed this way when present. Has no effect by default, so existing M3U playlists behave exactly as before unless you opt a group in. |
--schedule-file PATH |
JSON file storing EPG-scheduled recordings (see the s guide keybinding below), default: ~/.config/tvdinner/schedule.json on Linux, %APPDATA%\tvdinner\schedule.json on Windows. tvdinner must still be running when a scheduled recording's time arrives -- there's no background service. |
--record-watchlist |
tvtimes source only: poll that account's watchlist every 15 minutes and schedule a recording for each upcoming airing anyone on it flagged -- set a reminder in the tvtimes web app (from your phone, say) and this box records it. Entries it added are withdrawn again when they leave the watchlist; recordings you scheduled by hand here are never touched. See below. |
--report-watch-state |
tvtimes source only: report what you watch back to that account every 15 minutes, so its web guide can dim and tick watched programmes. Only live-channel watches from that source are sent (never a local file, YouTube or Plex), as plain start/stop intervals rather than programme references. Off by default -- this is the one thing the export token can write. See below. |
--device-name NAME |
Label this box in the watch state reported by --report-watch-state (e.g. living room), so a household with more than one player can tell them apart. |
--sync-favourites |
tvtimes source only: star the channels anyone on that account has favourited there, once at startup. Additive and one-way -- it never removes a favourite you set here, so un-starring in tvtimes leaves this box's star in place. See below. |
--live-buffer-minutes MINUTES |
How long the p keybinding can pause a live channel before it resumes automatically (default: 10). |
--disable-full-screen |
Start in a normal window instead of full screen (the default). |
--glsl-shader PATH |
A custom GLSL shader file (e.g. an Anime4K or FSRCNNX shader) to apply on top of mpv's own built-in high-quality scalers (hardware decoding and mpv's gpu-hq scaling profile are both always on). Repeatable to layer several, applied in the order given. Off by default: custom shaders can be significantly heavier on the GPU than the built-in scalers alone. |
--interpolation |
Smooth motion by interpolating between frames (mpv's interpolation plus video-sync=display-resample). Off by default: only actually helps when the display's refresh rate is a clean multiple of the video's frame rate, adds GPU cost, and changes how mpv times playback against audio. |
--audio-passthrough |
Send the encoded audio bitstream (AC3/DTS/E-AC3/TrueHD) straight to an AVR/soundbar over S/PDIF or HDMI instead of decoding it here. Only takes effect when the output device actually supports the format; mpv falls back to normal decoding otherwise, same as leaving this off. |
--audio-downmix-boost |
Raise the center/surround channels' volume when downmixing surround audio to stereo, so dialogue and surround effects don't end up quiet relative to the front L/R channels the way a naive downmix leaves them (mpv's own audio-normalize-downmix). |
--loudness-normalization |
Even out volume across (and between) titles via ffmpeg's loudnorm filter. Off by default: adds a small amount of processing, and some listeners prefer a title's original dynamic range. |
--no-chapter-skip |
Keep UP/DOWN as mpv's default 60-second seek, even when playing a Plex VOD item with real chapter markers (on by default -- see the UP/DOWN keybinding above). |
--no-skip-markers |
Don't show the "Skip Intro"/"Skip Credits" prompt (on by default -- see the j/ENTER keybinding above). |
--no-autoplay-next-episode |
Don't offer the next episode of a Plex TV show when one finishes (on by default -- see the "Up Next" keybinding above). |
--autoplay-countdown-seconds SECONDS |
How long the "Up Next" prompt waits before playing the next episode on its own (default: 10). |
--playback-positions-file PATH |
JSON file remembering where you left off in each recording (see the w recordings browser) or VOD item, so reopening one resumes instead of starting over (default: ~/.config/tvdinner/playback_positions.json on Linux, %APPDATA%\tvdinner\playback_positions.json on Windows). A recording's entry is dropped once the file itself is deleted; a VOD entry -- there being no file to check -- is instead dropped after 90 days of nobody resuming or updating it. |
--history-file PATH |
JSONL file logging what's watched (channel/VOD/recording), when, and for how long -- browse it with the x keybinding (default: ~/.config/tvdinner/history.jsonl on Linux, %APPDATA%\tvdinner\history.jsonl on Windows). See below. |
--no-history |
Don't record watch history. |
--no-plex-activity |
Plex source only: don't report playback to the Plex server -- on by default, this is what makes tvdinner playback show up in Plex's own dashboard and third-party tools like Tautulli, and lets Plex update its own watched/resume status for the item. Reading Plex's own watched/resume status is unaffected either way. |
--no-plex-theme-music |
Plex source only: don't play a show's theme-music preview while browsing its library page -- on by default, matching the official Plex clients. Starts after a short pause on a show, fades out on navigating away or picking something to actually watch. |
--epg-cache-hours HOURS |
How long a downloaded EPG is reused from disk before re-fetching (default: 24). |
--no-epg-cache |
Always re-download the EPG instead of using a cached copy, and don't write one either. |
--refresh-epg-cache |
Force a fresh EPG download for this run, ignoring any existing cached copy no matter its age, then refresh the on-disk cache with it (unlike --no-epg-cache, later runs still benefit from the cache). |
--no-online-logos |
Don't fall back to iptv-org's community channel/logo database for channels with no logo of their own or in their EPG (common for bare M3U playlists) -- on by default, sharing --epg-cache-hours/--no-epg-cache/--refresh-epg-cache's caching. |
--tmdb-api-token TOKEN |
TMDB v4 read-access Bearer token -- enables a gold star rating (e.g. ★ 7.6) plus the required TMDB attribution mark on movie programmes in the guide grid and details popup; the details popup also shows the director, falling back to TMDB only when the EPG feed doesn't already tag one itself (see below). Movies only, matched by programme category. Ratings are fetched in the background and cached on disk for 30 days. Off by default; overrides any token saved via tvdinner store-tmdb. For a local video file, this instead enables the i overlay's poster/synopsis/rating/director. See below. |
--tmdb-token-file PATH |
Where tvdinner store-tmdb/tvdinner clear-tmdb read/write the default TMDB token (default: ~/.config/tvdinner/tmdb_token.json on Linux, %APPDATA%\tvdinner\tmdb_token.json on Windows). |
--no-tmdb-cache |
Always query TMDB instead of using a cached rating/metadata/artwork, and don't write one either -- same escape hatch as --no-epg-cache, for clearing a bad cached entry (e.g. a mismatched title) without waiting out the 30-day cache. |
--refresh-tmdb-cache |
Force a fresh TMDB lookup for whatever's fetched this run, ignoring any existing cached entry no matter its age, then refresh the on-disk cache with it (unlike --no-tmdb-cache, later runs still benefit from the cache). |
--title TITLE |
Local video file playback only: override the guessed movie title used for the --tmdb-api-token lookup. |
--year YEAR |
Local video file playback only: override the guessed release year used for the --tmdb-api-token lookup. |
--no-update-check |
Don't check GitHub Releases for a newer tvdinner version at startup -- on by default, at most once every 24 hours, cached in a small local file so most launches don't touch the network at all. See below. |
--log-file PATH |
Where to log startup/shutdown, user actions, and warnings/errors (default: ~/.cache/tvdinner/tvdinner.log on Linux, %LOCALAPPDATA%\tvdinner\tvdinner.log on Windows). Capped at 5MB with one rotated backup (tvdinner.log.1), so it never grows without bound. |
--no-log |
Disable file logging entirely. |
# List the channels in a playlist
tvdinner https://example.com/playlist.m3u --list
# Play a channel directly by name
tvdinner playlist.m3u --channel "BBC One"
# Play a direct stream URL
tvdinner https://example.com/stream.m3u8
# Log into an Xtream Codes panel directly
tvdinner 'xtream://myuser:mypass@panel.example.com:8080'
# Log into a Stalker Portal directly
tvdinner 'stalker://panel.example.com:8080/c/?mac=AA:BB:CC:DD:EE:FF'
# Tune an HDHomeRun network tuner directly
tvdinner 'hdhomerun://192.168.1.50'
# Browse and play from a Plex Media Server
tvdinner 'plex://192.168.0.218:32400?X-Plex-Token=abcdef123456'
# Play your whole tvtimes line-up, guide included
tvdinner 'tvtimess://tv.example.com?token=abcdef123456'
# Play a local movie file, with TMDB metadata for the 'i' overlay
tvdinner ~/Videos/'His Girl Friday (1940).webm' --tmdb-api-token TOKEN
Instead of an M3U URL, URL can be an Xtream Codes panel login:
xtream://username:password@host:port
Use xtreams:// instead of xtream:// if the panel is served over https.
tvdinner logs in, fetches the live channel list (mapping each panel
category to a channel group, same as an M3U group-title), and points EPG
loading at the panel's own XMLTV export (xmltv.php) — everything else
(the guide, favorites, EPG shifts, recording, scheduling, bookmarks) works
exactly as it does for an M3U playlist. Live stream URLs default to a .ts
container; add ?output=m3u8 if your panel needs that instead:
xtream://myuser:mypass@panel.example.com:8080?output=m3u8
If the panel exposes a VOD library or a TV-series library, those are
fetched alongside the live channels and browsed with the m (movies) and
l (series: categories → shows → seasons → episodes) keybindings — see the
keybindings table. The series tree is walked lazily, one level per drill-in,
so a large catalogue doesn't slow startup.
Note that, like an M3U URL that happens to embed credentials in its query
string, an xtream:// URL's username and password are stored as plain text
wherever the source URL itself is stored — bookmarks.json, favorites.json
(keyed by feed), and inside a tvdinner backup archive. They're never
written to the log file, which always shows a redacted user:***@host
form instead.
URL can also be a Stalker Portal (also known as Ministra, or "Stalker
Middleware" -- the protocol MAG25x/26x set-top boxes speak) login:
stalker://host:port/portal/path?mac=AA:BB:CC:DD:EE:FF
Use stalkers:// instead of stalker:// if the portal is served over
https. The path is whatever your provider gave you (e.g. /c/ or
/stalker_portal/c/, copied from a MAG box's settings screen) --
portal.php is appended automatically if it isn't already there.
Optional &serial=, &device_id=, and &stb_type= (default MAG250)
query params can be added for portals picky about device identification.
tvdinner logs in with the given MAC (there's no separate username/password
step), fetches the channel list, and resolves each channel's actual
playable stream URL up front via the portal's create_link call (each
channel's raw cmd field isn't directly playable). There is currently no
EPG/program-guide support for Stalker sources -- channels behave like any
other EPG-less playlist. Because Stalker Portal has no official spec and
many vendor forks behave slightly differently, some providers may need a
different stb_type or an adjusted portal path to work.
Like the Xtream Codes case above, a stalker:// URL's MAC address is
stored as plain text wherever the source URL itself is stored
(bookmarks.json, favorites.json, backup archives); it's shown redacted
(all but the first two octets masked) in the log file.
URL can also point directly at an HDHomeRun
network tuner on your LAN:
hdhomerun://host[:port]
e.g. hdhomerun://192.168.1.50. There's no login step -- HDHomeRun
devices have no authentication at all -- and no auto-discovery either:
give tvdinner the device's IP or hostname directly (found via your
router, or SiliconDust's own discovery tools). tvdinner fetches the
device's channel lineup and uses each channel's stream URL as-is.
If the device reports a paid HDHomeRun DVR guide subscription, tvdinner also fetches program guide data automatically from SiliconDust's XMLTV API -- no further configuration needed. Without a subscription, the fetch simply fails and channels behave like any other EPG-less playlist (the same graceful "EPG data not available" you'd see for any inaccessible guide source).
URL can also point at a Plex Media Server:
plex://host:port?X-Plex-Token=...
Use plexs:// instead of plex:// if the server is served over https. A
Plex source has no live channels or EPG at all -- it's a library browser
instead. On connecting, tvdinner lists the server's movie and TV-show
libraries as a TUI overlay -- plus a synthetic "On Deck" row
first, pulled from Plex's own server-wide on-deck feed (movies you left
partway through, and the next unwatched episode of any show you're
partway through), so picking up where you left off doesn't need
navigating into a specific library first. In On Deck an episode is shown
under its season poster rather than an episode screengrab, so it
reads as "the show you're mid-way through" at a glance; everywhere else
an episode keeps its own still. Each row shows its poster/cover art plus
year, content rating, Plex's own audience score, and (for a movie or
episode) resolution (e.g. "1080p", "4K", "SD"), all fetched from
Plex itself with no extra lookups: arrows/PGUP/PGDWN to move, ENTER to
drill in (library → show → season → episode) or play a movie/episode,
ESC/LEFT to go back a level or close the browser, and l to reopen it
later -- right back where you left off, not the library root, even
after starting playback. Press / at any point to search the whole
server via Plex's own search API, not just whatever's currently on
screen, or y to filter by release year instead -- every movie, show,
and individual episode (matched by its own air date, not the show's
premiere year) across every library released that year -- movies from
every movie library treated as one alphabetical-by-film-name list, TV
content from every TV library treated as one alphabetical-by-show
(then numeric by season/episode) list, movies always ahead of TV.
h favorites the selected movie or show (never a
season or episode -- nothing finer-grained than that), saving to
--favorites immediately alongside any guide channel favorites, shown
with the same heart marker as the guide; v shrinks the current listing
down to just favorited movies/shows. Playback is always
direct-play (the file's own container/codecs, streamed straight from
Plex) -- tvdinner never asks Plex to transcode, so a file mpv can't
decode on its own won't play here even if it would in Plex's own apps.
Once something's playing, i shows a poster/synopsis/rating/director/
progress overlay pulled from Plex's own metadata, and resuming/
reconnecting on a dropped connection works the same as any other
on-demand source (see --playback-positions-file below). The first
time you play something tvdinner has no local resume position for yet,
it falls back to Plex's own reported progress instead -- so picking up
a movie or episode you left partway through in Plex's own apps resumes
from there too, not just from where tvdinner itself last left off. The
same i overlay also shows a technical-details line/block --
container, video/audio bitrate, and every audio/subtitle track the
file has, not just whichever one happens to be selected.
When the source file has real embedded chapters (e.g. a Blu-ray/DVD
rip), they show as tick marks on the progress bar, and UP/DOWN
preview the next/previous chapter instead of doing a plain seek -- a
small panel shows its thumbnail (Plex's own, when it generated one, or
a frame grabbed on the fly otherwise) and title without seeking yet;
keep pressing UP/DOWN to move further through the chapter list,
ENTER to jump there immediately, ESC to cancel, or just stop
pressing keys for a couple of seconds to jump there automatically (see
the keybindings table below, and --no-chapter-skip). When Plex's own
intro/credits
detection has run on the item's library (a Plex Pass feature -- most
libraries don't have this enabled, so don't be surprised if it never
shows up), a small "Skip Intro"/"Skip Credits" prompt appears while
playback is inside one of those windows, confirmed with j or ENTER
(see --no-skip-markers). And when a TV episode plays through to a
real end, an "Up Next" prompt offers the next episode with a
cancellable countdown (ESC to cancel; see --no-autoplay-next-episode
and --autoplay-countdown-seconds).
Playback is also reported to Plex's server as a real session -- the same
timeline API Plex Web/mobile apps use -- so what's playing via tvdinner
shows up in Plex's own dashboard and in third-party tools like Tautulli,
and Plex's own watched status/resume position for the item gets updated
too. On by default; pass --no-plex-activity to turn it off (reading
Plex's own watched/resume status above is unaffected either way).
Browsing a show also plays a short loop of its theme music, the same
ambience the official Plex clients add to a show's library page --
starts after a brief pause on a show (so it doesn't fire while quickly
scrolling past it) and fades out on navigating away or picking
something to actually watch, via a second, fully separate audio-only
mpv instance so it never interferes with anything actually playing. On
by default; pass --no-plex-theme-music to turn it off.
Finding your token: play anything in Plex Web, open your browser's dev
tools → Network tab, and look for X-Plex-Token=... in any request's
query string (or see
Plex's own instructions).
Like the Xtream Codes/Stalker Portal cases above, a plex:// URL's token
is stored as plain text wherever the source URL itself is stored
(bookmarks.json, backup archives); it's shown redacted (first four
characters kept, the rest masked) in the log file.
tvtimes is the companion self-hosted TV-guide web app: it aggregates several IPTV/tuner sources into one line-up and one clock-shift-corrected XMLTV guide, and publishes both behind a single rotatable token. Point tvdinner at it and you get that whole merged line-up, with its guide, as one source:
tvtimes://host[:port]?token=...
Use tvtimess:// instead of tvtimes:// if the server is served over
https (the usual case behind a reverse proxy). In tvtimes, turn the feeds
on under Settings → Export feeds and use its Open in tvdinner
button, which hands the whole URL over ready-made.
This is plain sugar, not a new protocol: the URL expands to that server's
two export feeds --
<host>/api/exports/playlist.m3u?token=... and
<host>/api/exports/epg.xml?token=... -- so everything downstream (the
program guide, favorites, recording, scheduling, bookmarks, the EPG
cache) behaves exactly as it does for any other M3U + XMLTV pair. A base
path is kept if tvtimes sits under a sub-path on your proxy, e.g.
tvtimess://example.com/tv?token=....
The EPG URL is derived from the host you typed rather than the
url-tvg= header inside the playlist: tvtimes builds that header from
its own configured public origin, which needn't be the address this
machine reaches it on. An explicit --epg still wins over both.
Add --record-watchlist and tvdinner polls that account's watchlist
every 15 minutes, scheduling a recording for each upcoming airing anyone
on it flagged:
tvdinner 'tvtimess://tv.example.com?token=...' --record-watchlist
So you press Remind me (or Watch this title) in the tvtimes web app — from your phone, on the bus — and the box at home records it. The watchlist is per user but the export token is per account, so a shared household account records whatever anyone on it flagged, de-duplicated per broadcast.
Entries tvdinner creates this way are tagged in schedule.json and
reconciled on every poll: un-watchlist something in tvtimes and its
recording disappears too. Recordings you scheduled by hand from the
guide are never touched — and if one already covers an airing the
watchlist also wants, no duplicate is added. Times come from the feed
already clock-shift corrected, so they line up with the guide.
The usual caveat applies: tvdinner has no background service, so it must still be running when the recording's time arrives.
--report-watch-state sends what you watch on a tvtimes source back to
that account every 15 minutes, so its web guide dims and ticks the
programmes you've already seen:
tvdinner 'tvtimess://tv.example.com?token=...' --report-watch-state \
--device-name 'living room'
Only live-channel watches from this tvtimes source are sent -- never a local file, a YouTube video, a Plex episode, or a channel from a different playlist. What goes over the wire is plain start/stop intervals, not "programme X was watched": tvtimes works out which programmes those cover by overlapping them against its own guide, so a guide refresh or a corrected clock-shift changes the answer without anything having to be re-reported.
The last week of history is resent on every tick rather than tracked as
"already sent". tvtimes de-duplicates, so a restart or a spell offline
catches up by itself with no local bookkeeping to fall out of step.
When a guide already carries its own clock corrections, tvdinner stops
applying yours to it -- a tvtimes export shifts times as it writes them,
so an --epg-shifts entry for the same channel would apply the
correction twice and leave the guide a whole shift in the past. The
stored shift isn't deleted, because it's keyed by channel name and you
still want it when watching that channel direct from its provider; it's
just not used for the corrected guide. [/] says so rather than
silently doing nothing. Detected from the feed's own
generator-info-name, so it covers a tvtimes:// source and a one-off
Play hand-off alike.
--device-name labels the box, so a household with more than one player
can tell them apart -- save one on a bookmark (or pass it here) and
tvtimes records it against every interval that report carries. It's
truncated to 120 characters to match what tvtimes stores; an over-long
one would otherwise be rejected along with the whole batch of events.
This uses the same export token as everything else, which is the only thing that token can write -- see tvtimes' own docs for what that means.
--sync-favourites stars the channels anyone on the tvtimes account has
favourited there, so a star set in the web app shows up in tvdinner's
guide:
tvdinner 'tvtimess://tv.example.com?token=...' --sync-favourites
It runs once at startup and is additive and one-way: it never removes
a favourite you set here, so un-starring in tvtimes leaves this box's
star in place. That's deliberate -- favorites.json records only channel
names, with no note of where each came from, so a two-way reconcile
couldn't tell "removed upstream" from "added locally", and silently
deleting your own favourite is the worse failure. Un-star it here with
h if you want it gone.
On Windows the installer registers the tvdinner:, tvtimes: and
tvtimess: URL schemes for you, which is what makes tvtimes' Play and
Open in tvdinner buttons work. Installers before 1.41 didn't -- those
links did nothing at all. Re-run the installer to fix an existing install.
tvdinner default-handler is Linux-only and doesn't cover this.
T (shift-t) opens the tvtimes web app for whatever's on the current
channel -- the reverse of tvtimes' own Play button. It's a search
URL (/search?q=<title>) rather than a link to the exact guide cell:
finding the thing by name is what you actually want from this end, and it
works without tvtimes' virtualised grid needing scroll-to-cell support.
With no EPG data for the channel it just opens tvtimes itself.
Every one of these rides the same export token, and the feeds behind them are ordinary HTTP -- documented as OpenAPI at tvtimes Export API if you want to build something else against the same account.
Like the Xtream Codes/Stalker/Plex cases above, a tvtimes:// URL's
token is stored as plain text wherever the source URL itself is stored
(bookmarks.json, backup archives); it's shown redacted in the log file.
URL can also be a local video file, played directly -- no
playlist/EPG/channel/Plex library involved, just mpv pointed at a file
on disk:
tvdinner ~/Videos/'His Girl Friday (1940).webm'
It's told apart from a local M3U playlist by content, not extension (the
first few KB are sniffed for #EXTM3U), so a genuine playlist file still
loads as one as always. Since a local video file carries no provider
metadata of its own, tvdinner guesses its movie identity from the
filename -- a 19xx/20xx year anywhere in it, in parens/brackets/
dashes/dots (e.g. Title (Year).ext, Title.Year.1080p.BluRay.x264- GROUP.mkv, or a yt-dlp download's Year - Title - Cast - Tagline [videoID].ext, all naming conventions real tools produce), plus
whatever text sits on the more informative side of it -- and, if
--tmdb-api-token is given, looks that guess up on
TMDB in the background, trying a couple
of candidate search strings the same way a YouTube title
does (see below) since a filename can chain the same kind of cast/
tagline noise onto the real title, so i shows the same poster/
synopsis/rating/progress overlay as a Plex or Xtream/Stalker VOD item
(see the i keybinding below) -- without a token, i still shows the
guessed title and playback progress, just without the TMDB-sourced
fields. --title/--year (see Options above) override a bad guess
without renaming the file. Resuming
(--playback-positions-file) and r-key recording (--record-dir) both
work the same as anywhere else. A local video file's path also works as
a bookmark's URL, complete with its own saved TMDB token --
handy for a small, frequently-rewatched local collection.
URL can also be a plain YouTube video URL (youtube.com/watch?v=...,
youtu.be/..., or youtube.com/shorts/...) -- mpv already plays these
directly via its ytdl_hook script, which shells out to a separate
yt-dlp (or youtube-dl) binary on
PATH to resolve the actual stream (sudo apt install yt-dlp, sudo dnf install yt-dlp, or pip install yt-dlp -- not bundled by tvdinner or by
mpv itself, on any platform including the Windows installer), so this is
really about getting the i overlay working for them too:
tvdinner https://www.youtube.com/watch?v=wEx-z1TYPKU
Unlike a local file, a YouTube video's title/uploader/thumbnail are
fetched for free from YouTube's own public oEmbed endpoint (no API key,
no --tmdb-api-token needed) as soon as playback starts, in the
background -- i shows them once that lands (mpv's own window title,
set by its yt-dlp hook, is untouched either way). If --tmdb-api-token
is given, tvdinner additionally tries a TMDB lookup using that title;
--title/--year override it outright, otherwise the title itself is
tried as a couple of candidate search strings in turn -- its first
-/| segment (many archive-channel and official-studio titles
chain cast names/taglines/genre tags onto the real movie name this way,
e.g. "1940 - His Girl Friday - Cary Grant and Rosalind Russell - ..."
splits to just "His Girl Friday", and "McLintock! | FULL MOVIE | John
Wayne, Maureen O'Hara | Western Rancher Cowboy Comedy" splits to just
"McLintock!"), then the whole remainder unsplit as a broader fallback
for a movie whose real title happens to contain one of those
separators -- whether or not the title carries a year at all. A
successful TMDB match replaces the oEmbed poster/description with TMDB's
poster/synopsis/rating/director (falling back to YouTube's own thumbnail
if TMDB has no poster). Resuming
(--playback-positions-file) works the same as any other VOD source. A
YouTube URL also works as a bookmark's URL.
Press k at any point to cast whatever's currently playing (a live
channel, a VOD item, or a Plex movie/episode) to a Chromecast device on
your LAN: arrows to move, ENTER to connect, ESC to close. tvdinner
tells the device to fetch and play the same stream URL it's itself
playing -- it never proxies or transcodes the stream, so a codec/container
the device's receiver can't decode natively (raw MPEG-TS, the common
shape for a live IPTV channel URL, has only limited support on real
Chromecast hardware) may simply fail to cast even though it plays fine
locally. Local playback pauses for the duration of the cast (freeing up
local decode/bandwidth) and resumes automatically from the same position
once you disconnect -- reopen the picker with k while casting and a red
"Disconnect" entry appears above the device list.
Chromecast support is an optional extra, not installed by default.
tvdinner[chromecast] isn't a package name you can pip install
directly (there's no PyPI release yet -- see Install above), so install
it from a checkout the same way as the base install, just with the
extra added:
python3 -m venv .venv && .venv/bin/pip install ".[chromecast]"
This needs Python 3.11+ (pychromecast's own requirement -- tvdinner
itself still supports 3.10). On Debian/Ubuntu, apt install python3-pychromecast python3-zeroconf works instead; on Fedora/RHEL/
openSUSE there's no distro package at all, so use
sudo pip install --prefix=/usr pychromecast (see the RPM spec's own
notes on why --prefix=/usr specifically, next to the same advice for
python-mpv). Without it installed, k shows a message saying so instead
of a device list -- every other feature works unaffected. Discovery uses
mDNS (UDP multicast); Windows may prompt for a firewall permission the
first time k is pressed.
tvdinner checks GitHub Releases for a newer version at startup, at most
once every 24 hours (cached locally, so most launches don't touch the
network at all). If a newer release is found, a card appears over the
video: y opens the release page in your browser so you can download
and install it your platform's normal way -- there's no silent
self-update on any platform (the three packages have too little in
common: a Windows install can safely self-upgrade in place, but
.deb/.rpm need root and have no hosted repo). n or ESC dismisses
the card instead. Either way, that specific version won't be shown again -- a
genuinely newer release still notifies normally. Disable checking
entirely with --no-update-check.
Some feeds have different channels running off different clock
corrections (e.g. an East/West regional pair). --epg-shifts points to
a JSON file mapping each channel's display name to a shift string:
{"BBC One": "+1h", "TCM US West": "-3h"}Channels are keyed by display name rather than tvg_id, since
real-world playlists commonly have several distinct channels sharing
one tvg_id for EPG mapping. A missing file is not an error; malformed
entries are reported as warnings on startup and skipped. Shifts can also
be adjusted live from the program guide with the [ / ] keys (see
below), which write straight back to this file.
--tmdb-api-token adds a gold star rating (e.g. ★ 7.6) to movie
programmes in the guide grid and details popup, sourced from
TMDB and matched by title/year against
the programme's category. Get a free token from TMDB: create an
account, then under
Settings -> API request an
API key (any use case description is fine) and copy the "API Read
Access Token" (the long JWT-looking string, not the shorter "API Key")
-- that's the value --tmdb-api-token wants. Ratings are fetched in
background threads (never blocking guide rendering) and cached on disk
for 30 days, since a vote average barely moves day to day. Off by
default; the TMDB attribution mark shown alongside every rating is
required by TMDB's API terms. If a cached entry ever looks wrong (a
mismatched title, say), --no-tmdb-cache/--refresh-tmdb-cache clear
it without waiting out the 30 days -- see the table above.
The i overlay (both the compact current/next-programme banner and
the guide's full details popup, plus the VOD info overlay for a local
file or YouTube video) also shows the movie's director, when
available. Some EPG feeds already tag this themselves (XMLTV's
<credits><director>) -- that's used directly, for free, with no
token required. Only when a feed doesn't provide one does a
--tmdb-api-token fall back to a TMDB lookup, and unlike rating,
that fallback isn't bulk-fetched for every movie visible in the guide
grid -- only for the one programme currently shown in an i overlay,
so a fresh view sometimes shows no director yet that way; reopening
it picks it up once fetched.
Retyping --tmdb-api-token on every invocation gets old fast, so
there are two ways to save one instead, checked in this order:
- Per bookmark (see
tvdinner bookmarksabove) -- like the Xtream/Stalker/Plex credentials above, it's stored as plain text inbookmarks.json, but unlike those it's never even partially shown in the log file (fully masked, not just redacted). Launching that bookmark applies its token the same as typing--tmdb-api-tokendirectly would. - A global default, via
tvdinner store-tmdb TOKEN-- applies to every invocation that doesn't otherwise specify one (directly or via a bookmark), stored as plain text in~/.config/tvdinner/tmdb_token.jsonby default (%APPDATA%\tvdinner\tmdb_token.jsonon Windows; override with--tmdb-token-file).tvdinner clear-tmdbremoves it.
An explicit --tmdb-api-token (typed directly, or carried by a
launched bookmark) always overrides the global default.
tvdinner logs what you watch -- live channel, VOD item, or recording,
with when and for how long -- to ~/.config/tvdinner/history.jsonl
(%APPDATA%\tvdinner\history.jsonl on Windows; override with
--history-file). Press x during playback to browse it: newest
first, grouped by day, with a thumbnail (a VOD's poster, a channel's
logo, or -- for a recording -- an actual frame grabbed from the video
itself the first time it's shown, then cached), duration, and -- for a
movie with --tmdb-api-token or Plex metadata available -- year,
rating, and director too. It's a read-only viewer for now, not a
launcher (see below).
One JSON object per line, oldest first:
{"kind": "channel", "title": "BBC One", "url": "https://.../bbc1.m3u8", "playlist_source": "https://.../playlist.m3u", "started_at": "2026-08-15T20:00:00+00:00", "ended_at": "2026-08-15T20:41:12+00:00", "duration_seconds": 2472.0, "image_url": "https://.../bbc1-logo.png", "year": null, "rating": null, "rating_is_tmdb": false, "director": null}kind is channel, vod, or recording; playlist_source is the
playlist/login/server it came from (null for a local file, YouTube
video, or bare direct-stream URL, none of which have one). image_url
is a VOD item's poster, a channel's own logo, or (for a recording) a
tvdinner-recording-thumb://<path> marker resolved to an actual frame
captured from that recording's own file when the history browser needs
it (null only when no image is available at all); year/rating/
rating_is_tmdb/director are only ever populated for a vod entry,
and only when the source actually supplied them. A watch under 5
seconds isn't recorded
at all, so flipping past a channel while browsing the guide doesn't
clutter the log. Reconnecting after a dropped stream doesn't start a
new entry -- it's still the same watch, just interrupted.
Disable entirely with --no-history; tvdinner hard-reset deletes it
along with everything else tvdinner stores. Not included in tvdinner backup/restore -- like playback positions and the schedule, it's
accumulated data, not configuration to carry to a new machine.
In addition to mpv's own default key bindings. Wherever ENTER is
listed below, the numpad's KP_ENTER works identically -- every guide/
browser/prompt that binds one binds the other alongside it.
| Key | Action |
|---|---|
i |
Show the current/next programme info overlay (with video/audio quality badges: resolution, codecs, fps, HDR, channel layout, and a movie's director when available -- see TMDB ratings); while the program guide is open, shows full details for the selected guide programme instead. While watching back a recording, shows its own label, recorded date, and playback progress instead of live EPG info. While playing a VOD/Plex/local file/YouTube video, shows its poster, synopsis, rating, director, and playback progress instead (Plex populates all of that; a local file or YouTube video gets it from a background lookup -- YouTube's own oEmbed always, TMDB additionally if --tmdb-api-token was given; other VOD sources show whatever fields they have). Also shows a technical-details line/block -- container, video/audio bitrate, and every audio/subtitle track the file has (not just whichever one is currently selected), for any source, not just Plex. While the Plex library browser is open and a movie or episode is selected, shows that item's own details ("DETAILS", no progress bar) instead of whatever's currently playing -- a show/season/library row has no single file to show details for, so i falls back to the normal playback-info behavior there. Pressing i again while this overlay is already showing opens the item's TMDB page in the default browser instead of re-showing the same info, when a TMDB match is known -- a movie (from any source that resolves one) or a Plex show/episode (read straight from Plex's own metadata, no --tmdb-api-token needed); otherwise it shows "No TMDB page available". |
g |
Toggle the full program guide. |
MENU (tap) |
Show the programme info overlay -- same as i (the button most IR/BLE air-mouse remotes send for their MENU key). In a Plex session (no guide to hold for) this is MENU's only behavior, tap or hold alike. |
MENU (hold, 0.5s+) |
Toggle the full program guide -- same as g. |
b |
Switch to the last watched channel (like a TV remote's "last channel" button) -- repeated presses toggle back and forth between the two, since every switch (guide or b itself) remembers whatever was playing right before it. No-op if nothing's been switched away from yet this session. |
LEFT / RIGHT |
Page the program guide's timeline back/forward by 30 minutes (guide only). While the keyboard-shortcuts help (?) is open, switches its tabs instead. Otherwise, these seek the video as usual. |
UP / DOWN |
Move the program guide's channel selection cursor (guide only). Otherwise, for a Plex item with real chapter markers, previews the next/previous chapter instead (UP forward, DOWN back -- matching mpv's own sense for these keys) -- a small panel shows its thumbnail and title without seeking yet; falls back to mpv's default 60-second seek for anything without chapters, or if --no-chapter-skip is given. |
ENTER / ESC (while a chapter preview is showing) |
ENTER jumps to the previewed chapter immediately; ESC cancels with no seek. Left alone for a couple of seconds, it jumps there automatically. |
PGUP / PGDWN |
Move the program guide's channel selection cursor a full page at a time (guide only). |
ENTER |
While the guide, a browser, or a text-entry prompt is open: whatever that view's own ENTER does (switch to the selected channel and close the guide, confirm a filter/search query, play the selected recording/VOD item, connect to the selected Chromecast/Plex item, etc.). Otherwise, pauses/resumes -- see p above. |
[ / ] |
Nudge the selected guide channel's EPG shift back/forward by 1 minute, saving the change to --epg-shifts immediately (guide only). |
f |
Open a text-entry dialog to filter the program guide's channel list by name or group (as shown by --list, case-insensitive substring match against either); ENTER applies it, ESC cancels (guide only). |
c |
Clear any active guide filter and show every channel again (guide only). |
h |
Toggle the selected guide channel as a favorite (or the currently-playing one if the guide isn't open), saving to --favorites immediately; favorited channels show a heart next to their name in the guide. In a Plex session, toggles the selected movie or show in the library browser instead -- favorites are movie/show level only, never a season or episode. |
v |
Toggle showing only favorited channels in the guide, or favorited movies/shows in the Plex library browser. |
ESC / GO_BACK |
Close the programme details popup, or cancel an in-progress guide filter query (and, throughout the rest of the app, whatever else ESC currently closes/cancels -- a browser, an overlay, a text-entry prompt). GO_BACK is the key name mpv reports for a remote's dedicated back button; it's a permanent alias for ESC, always doing exactly whatever ESC currently would -- except in a Plex session with nothing open (just watching), where plain ESC has no meaning of its own and would otherwise fall through to mpv's own default binding (cycle fullscreen/window mode); there, GO_BACK instead acts like BS, stopping the current item and dropping back into the library browser. |
z |
Cycle the video's display aspect ratio (Auto, 4:3, 16:9, 2.35:1, 1:1). |
e |
Cycle a sleep timer: Off → 15 → 30 → 60 → 90 minutes → Off. Pauses playback when it fires (same as pressing p), one-shot, and stays running across a channel/VOD switch -- it's tied to the session, not whatever happens to be playing. |
[ / ] / { / } |
mpv's own defaults, not overridden here: adjust playback speed by ±10% / halve / double (outside the guide, where [/] mean something else -- see above). |
Ctrl++ / Ctrl+- |
mpv's own default: adjust audio sync (delay/advance the audio relative to the video). |
Alt++ / Alt+- / Alt + arrows |
mpv's own defaults: zoom the video in/out and pan it around once zoomed; Alt+BS resets both. |
BS |
Stop playback and quit tvdinner cleanly -- the closest equivalent this always-something-loaded, single-window app has to a remote's dedicated STOP/DEL button (confirmed live: at least one real remote's "DEL" button reports as BS). In a Plex session, BS instead stops the current item and drops back into the library browser exactly where you left off, rather than quitting -- there's always a browser to fall back into there, so "stop" means "stop this and pick something else." Shadowed by the guide filter/Plex search/Plex year text-entry prompts' own BS "delete last character" while one of those is open, and restored once it closes. |
p / PLAY / PAUSE / PLAYPAUSE / ENTER |
Pause/resume live TV (PLAY/PAUSE/PLAYPAUSE are the key names mpv reports for a remote's dedicated play/pause button; ENTER is the OK/center button most IR/BLE air-mouse remotes send, doubling as play/pause here -- shadowed by the guide's/every browser's own ENTER binding while one of those is open, and restored once it closes). While paused, the stream keeps buffering in the background (up to --live-buffer-minutes, default 10) so resuming (manually or automatically once the limit's reached) continues from where you paused rather than jumping back to live -- use mpv's normal seek keys (LEFT/RIGHT, etc.) to rewind/fast-forward within that window. Recorded/played-back files just pause normally, with no time limit. Pausing also shows the same info overlay i/MENU would (the EPG banner for a live channel, or the poster/synopsis/progress card for a recording/VOD/Plex item), so it's clear what's paused -- it auto-hides itself after a few seconds like always, leaving just the paused frame, or disappears immediately on resume if it's still up. |
r |
Toggle recording the current stream to disk as a raw copy (no re-encoding), saved under --record-dir as <channel>_<timestamp>.ts. |
o |
Toggle picture-in-picture: shrinks the window to a small, always-on-top, borderless corner window (bottom-right, ~25% size) so you can keep watching while using other apps; press again to restore. Closes any open guide/browser overlay first. Relies on the window manager honoring mpv's placement request -- confirmed working on GNOME/Mutter, but some Wayland compositors may only shrink/keep-on-top without actually relocating the window. |
t |
Toggle subtitles on/off, if the current stream has a subtitle track (e.g. many UK DVB broadcasts carry one). Reports "No subtitles available" if it doesn't. To pick a different subtitle track (e.g. a different language), use mpv's own default j/J keys to cycle through them -- except while the Skip Intro/Credits prompt below is showing, where j (and ENTER) confirm it instead, restored back to subtitle-cycling the moment the prompt closes. |
j / ENTER |
While a Plex VOD item's intro or credits marker window is showing a "Skip Intro"/"Skip Credits" prompt (bottom-right corner): confirms it, seeking straight to the end of that window. ENTER works from an IR/BLE air-mouse remote's OK button too (its own base "pause" meaning is only shadowed while the prompt is up, restored the instant it closes); j is an unadvertised keyboard-only alias. Never automatic -- the prompt just sits there until confirmed or the window passes. Requires the library's intro/credits detection (a Plex Pass feature) to have actually run; pass --no-skip-markers to turn the prompt off entirely. |
ESC (Up Next) |
While the "Up Next" countdown is showing after a Plex TV episode plays through to a real end: cancels it, leaving playback exactly where it is. Left alone, the next episode plays automatically once the countdown reaches zero -- see --autoplay-countdown-seconds/--no-autoplay-next-episode below. |
s |
While programme details are shown (guide only): schedule that programme to record automatically, switching channels and starting/stopping the recording at its start/stop time even if you're watching something else -- press again to cancel. Saved to --schedule-file; only fires while tvdinner is running. A scheduled programme shows a small red "R" badge in the guide. |
m |
Browse VOD movies pulled out of the playlist via --vod-group (plain M3U/local playlists only -- an Xtream or Stalker panel's own VOD API populates this automatically instead), grouped by group-title, alphabetical by title within each group -- UP/DOWN/PGUP/PGDWN to move the selection, ENTER to play it (resuming where you left off, if you didn't finish it last time -- see --playback-positions-file), any letter or digit to jump to the next title starting with it (press again to cycle to the next match), ESC to close. No-op with a "No VOD movies found" message if nothing qualifies. This is movies only -- an Xtream panel's TV series are browsed separately with l. |
w |
Browse past recordings from --record-dir, grouped by date -- UP/DOWN/PGUP/PGDWN to move the selection, ENTER to play it back (resuming where you left off, if you didn't finish it last time -- see --playback-positions-file), d twice to permanently delete the selected one (the first press just arms the confirmation), ESC to close. |
u |
Browse upcoming scheduled recordings (see the s guide keybinding above), soonest first, marking whichever one is currently recording -- UP/DOWN/PGUP/PGDWN to move the selection, ENTER to cancel the selected one, ESC to close. Since only one recording can happen at a time, an overlapping schedule that never got a turn shows up here (and as an on-screen notification) under "Missed", with the reason why. |
l |
(Re)open a browsable media library, for the two source types that have one. Plex: the server's libraries -- UP/DOWN/PGUP/PGDWN to move the selection, ENTER to drill into a library/show/season or play a movie/episode, ESC/LEFT to go back a level (or close it, from the top level). While showing a listing of movies or shows specifically (not the top-level library list, and not seasons/episodes), any letter or digit other than g/h/v/l/y jumps to the next title starting with it (press again to cycle to the next match) -- those five keep their own meanings below even at that level, so grid/list view, favoriting, closing, and the year filter are always one keypress away; reaching a title starting with one of those five still just takes an extra arrow press or two, or / search. Xtream: the panel's TV series (categories → shows → seasons → episodes), fetched lazily one level at a time, and separate from VOD movies (m) -- UP/DOWN/PGUP/PGDWN to move, ENTER to drill in or play the selected episode (resuming where you left off, if you didn't finish it last time -- see --playback-positions-file), ESC/LEFT to go back a level or close from the top. No-op with a "No TV series found" message if the panel exposes none. |
ENTER (hold) |
While the Plex library browser is open, on a movie, show, or episode: opens a small item menu -- "Play from Start" (bypasses any resume position; not shown for a show, which has no single file of its own), "Mark as Watched", "Mark as Unwatched". UP/DOWN moves, ENTER activates, ESC/LEFT/GO_BACK cancels without changing anything. Has no effect on a library/season row. A normal (short) ENTER tap is unaffected -- it still plays/drills in exactly as before. |
g |
While the Plex library browser is open: switch between Grid view (the default -- large poster tiles, columns at a time) and List view (a scrolling row per item), keeping whatever's currently selected in view. The chosen view persists as you browse -- drilling into a show/season, searching, filtering by year, etc. all stay in whichever view you last picked, until you press g again. In Grid view, LEFT/RIGHT move across columns instead of LEFT going back a level -- use ESC/GO_BACK for that there instead; List view is unaffected. |
/ |
While the Plex library browser is open: search the whole server via Plex's own search API -- ENTER runs the search and shows results as a new browsable list, ESC/LEFT cancels. |
y |
While the Plex library browser is open: filter by release year (digits only) -- ENTER shows every movie/show/episode across every library released that year (an episode by its own air date, not its show's premiere year), as a new browsable list: every movie library treated as one alphabetical-by-film-name list, every TV library treated as one alphabetical-by-show (then numeric by season/episode) list, movies always ahead of TV; ESC/LEFT cancels. |
k |
Open the Chromecast device picker for whatever's currently playing -- UP/DOWN/PGUP/PGDWN to move, ENTER to connect, ESC to close. While already casting, reopening shows a red "Disconnect" entry above the device list. Requires the optional pychromecast extra -- see Casting below. |
x |
Browse watch history -- every channel/VOD item/recording actually watched, newest first, grouped by day, with a thumbnail (a VOD's poster, a channel's logo, or a frame grabbed from a recording's own video), duration, and (for movies) year/rating/director. UP/DOWN/PGUP/PGDWN to scroll, ENTER/ESC to close -- a read-only viewer, not a launcher. |
a |
Toggle an about card: logo, app name, version, and a one-line summary -- press again or ESC to close. |
T |
On a tvtimes source, open the tvtimes web app for whatever's on this channel -- the reverse of tvtimes' own Play button. Uses its search page (/search?q=<title>) rather than linking to the exact guide cell, so it needs no deep-link support at the far end and finding the thing by name is what you want from here anyway. Falls back to opening tvtimes itself when there's no EPG data for the channel; reports "Not a tvtimes source" otherwise. |
y / n |
Only shown on the update-available card (appears automatically, at most once every 24 hours, when a newer release exists): y opens the release page in your browser, n (or ESC) dismisses it. Either way that version won't be shown again. |
? |
Toggle a keyboard-shortcuts cheat sheet, grouped into tabs (Guide, Playback, VOD & Chapters, Recording & History, Plex) -- LEFT/RIGHT switches tabs while it's open, press ? again or ESC to close. |
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
MIT — see LICENSE.






