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 and
Spotify (web player).
macOS only for now.
Install
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 |
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.
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" \
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".