tvctl
A Rust CLI that lets agents (or people) play videos and music in windows on this computer and control them from the command line, like a TV remote for your screen. (It does not control physical TVs.) Every command prints JSON to stdout; errors go to stderr with exit code 1.
Services are plugins (tvctl plugins lists them). Currently: YouTube,
Spotify (web player) and Amazon Prime Video (amazon.com, US).
macOS only for now.
Install
ChatGPT / Codex plugin and agent skill
skills/tvctl/SKILL.md teaches an agent to install or update tvctl
(cargo install tvctl at the start of every session) and to use it.
scripts/package-skill.sh builds:
dist/tvctl-plugin.zip: an Agent Plugins 1.0 package (plugin.jsonat the root,skills/tvctl/SKILL.md) for ChatGPT and Codex. To install it, give the zip to ChatGPT and ask it to install the plugin. To publish it, upload it on the Plugins page of the OpenAI Platform dashboard.dist/tvctl-skill.zip: the bare skill, for "Upload a skill" in ChatGPT or Claude, or copyskills/tvctlinto any agent's skills directory.
tvctl opens windows on your Mac, so the agent must be able to run commands on that Mac (for example the ChatGPT desktop app, Codex CLI, Claude Code, pi); cloud sandboxes cannot show anything on your screen.
Usage
# => {"id": "1", "service": "youtube", "backend": "webview", "player": {...}, "window": {...}}
| |
|
The background server starts on demand (open, auth) and stops a few seconds
after the last player closes (TVCTL_IDLE_SECS, default 5), quitting
tvctl's Chrome too. tvctl server up keeps it running without players
(avoids restarting Chrome between uses); tvctl server down stops it and
closes all players; tvctl server status shows its state.
--size is the video area; layouts without --size fill their screen region.
tvctl --help describes the workflow, the JSON output and error format, and
examples; tvctl <command> --help describes each argument.
Backends
- webview: a native window with a WebView (tao + wry). No dependencies. Used for services with public embed APIs (YouTube logged out).
- chrome: an app window of Google Chrome, driven through the Chrome DevTools
Protocol, using a dedicated profile at
~/Library/Application Support/tvctl/chrome-profile. Used where a login or DRM is needed. Chrome refuses DevTools control of your everyday profile, so tvctl has its own; sign in to each service once withtvctl auth login. Always-on-top is not available in this backend.
A plugin lists its backends in order of preference. If a plugin declares
prefer_backend_when_logged_in and you are logged in, that backend is used
(YouTube: Chrome, so Premium accounts get no ads). open --backend overrides.
auth login opens the login page in a plain Chrome (not controlled, because
Google refuses sign-in in automated browsers) on tvctl's profile. Sign in, then
quit that Chrome with Cmd+Q; tvctl then checks the login cookies.
Services
| Plugin | Backends | Login | Supported actions |
|---|---|---|---|
youtube |
webview (logged out), chrome (logged in) | optional (Premium: no ads) | all; audio only in chrome |
spotify |
chrome | required | play, pause, toggle, next, previous, seek, volume, mute |
primevideo |
chrome | required (Amazon account with Prime Video, US) | all except previous |
Spotify links: tracks, albums, playlists, artists and episodes on
open.spotify.com. A track link plays that track; other pages start from the top.
Spotify opens in its large cover-art view (shown inside the window, so window
layouts keep working). The plugin drives the web player's UI (no public API), so Spotify UI
changes can break it; the E2E tests detect that.
Prime Video links: amazon.com/gp/video/detail/<id>, amazon.com/dp/<ASIN> and
primevideo.com/detail/<id> (all opened on amazon.com, where the US account is
signed in). A series page plays the episode its Play button would (usually the
next one to watch); a title resumes where it was left unless --start is given.
next starts the next episode (next_episode in status). Speeds are the
player's fixed rates (0.75 to 2); qualities are its data-usage settings good,
better, best. Ads are not skipped: while one plays, ad_showing is true,
ad_remaining gives the seconds left, seeking/subtitles/audio/speed are
refused, and --start/--subtitles of open are applied when it ends
(pending). Amazon limits simultaneous streams per account; a window that
cannot play reports the player's message (e.g. "Maximum Streams Playing") as an
error. The plugin uses the web player's internal stores (no public API), so
Prime Video changes can break it; the E2E tests detect that.
Plugins
Each service is a directory plugins/<name>/:
plugin.toml:name,description,domains(subdomains match too),backendsactions: supported player actions; optional[unsupported_actions]lists actions a backend lacks (e.g.webview = ["audio"]). The server refuses other actions with an error listing the supported ones, andtvctl pluginsshows both, so agents can check before calling.- optional
[auth]:login_url,cookie_url,logged_in_cookies,prefer_backend_when_logged_in,login_mode(plainby default, orcontrolled)
webview.html: the page for the WebView backend.frame.js(optional): injected into that WebView's subframes.chrome.js: the adapter for the Chrome backend. It is loaded into every document of the player window, so it can restore its page state after reloads.
Adapters define window.__tvctl.run(action), which returns a status object.
Actions: open {url, start, paused, subtitles}, status, play, pause,
toggle, next, previous, seek {seconds, relative}, subtitles {lang}, audio {lang},
volume {level, muted}, speed {rate}, quality {level}. Unsupported actions
throw. In the Chrome backend, open may return {"navigate": url} to be loaded
at another URL first.
Built-in plugins are compiled into the binary. Plugins in $TVCTL_PLUGIN_DIR
or ~/.config/tvctl/plugins/<name>/ are loaded when the server starts and
override built-ins with the same name.
Environment
TVCTL_SOCKET: server socket (default$TMPDIR/tvctl.sock; the log is next to it astvctl.log)TVCTL_DATA_DIR: data directory (Chrome profile, login state)TVCTL_PLUGIN_DIR: external plugin directoryTVCTL_CHROME: Chrome executableTVCTL_IDLE_SECS: idle time before an on-demand server stops (default 5)
For plugin development, the hidden command tvctl __eval <ID> <JS> evaluates
JavaScript in a Chrome-backend player page and prints the (awaited) result.
Tests
TVCTL_E2E_PROFILE="/Library/Application Support/tvctl" \
TVCTL_E2E_PROFILE="/Library/Application Support/tvctl" \
The end-to-end tests open windows. e2e_fake uses a fake plugin and a local site.
e2e_youtube uses a fresh logged-out profile, so ads may play and the tests
wait for them. To use your logged-in profile instead, set
TVCTL_E2E_PROFILE="$HOME/Library/Application Support/tvctl".