yttv
Play YouTube videos on a TV from the command line or from Python: Apple TV, Cast TVs (Chromecast, Samsung Tizen) and DIAL devices (Fire TV, WebOS).
yttv https://youtu.be/dQw4w9WgXcQ # play on the last used screen
yttv -a ID1 ID2 # append to its queue, one call for all
yttv -d bedroom ID # pick a screen by name or address
yttv -l # list known screens
Install
pip install 'yttv[all]' # every backend
pip install 'yttv[cast]' # Cast TVs only
pip install 'yttv[appletv]' # Apple TV only
pip install yttv # DIAL and screens paired by code; only httpx
Or, in a pixi project: pixi add --pypi 'yttv[all]'.
Setting up a screen
Every TV needs one of these once. yttv remembers the result under
~/.cache/yttv/devices.json and picks the last used screen by default.
| TV | Once | Then |
|---|---|---|
| Apple TV | yttv --appletv 192.168.1.5 and type the PIN it shows |
yttv URL opens the app and plays |
| Chromecast, Samsung Tizen, other Cast TVs | yttv --cast 192.168.1.6 |
yttv URL; the first cast makes a Samsung ask you to accept Cast terms, once |
| Fire TV, WebOS, other DIAL devices | yttv -s finds them |
yttv URL wakes the TV and starts the app |
| Anything with a YouTube app | yttv --pair 123456789 with the code from Settings › Link with TV code |
yttv URL, with the app already open |
Videos are ids or URLs in any of the usual forms (watch?v=, youtu.be,
shorts, live, embed, ...). A t= parameter becomes the start
position. Several videos are always sent together in one call: sending
them one after another scrambles the TV's queue.
From Python
import yttv
yttv.devices() # known screens, from the cache, no network
yttv.cast(["dQw4w9WgXcQ"]) # play now on the last used screen
yttv.cast([url1, url2], queue=True) # append to its queue
yttv.cast([url], device="bedroom") # pick a screen
yttv.pair("123 456 789") # link a screen by TV code
yttv.add_device("cast", "192.168.1.6", cast_uuid="...")
yttv.discover() # DIAL search
Every failure is a subclass of yttv.YttvError with a message meant for
people, so a caller can show it as is. cast() takes up to timeout
seconds (default 90) for waking the TV and starting the app; that is how
long a sleeping Fire TV needs.
Screens paired with ytcast are
taken over from ~/.cache/ytcast/ytcast.json on the first run.
How it works
Three ways to reach a TV, one protocol to drive it:
| Device | Finds and starts the app via | Plays via |
|---|---|---|
| Apple TV | pyatv, Companion protocol | Lounge |
| Cast TVs | pychromecast, the YouTube receiver's mdx channel |
Lounge |
| DIAL devices | SSDP and the DIAL REST interface, built in | Lounge |
The Lounge API is the unofficial protocol behind the "Play on TV" button in the phone app: pair with a screen, play a video, append to the queue. Each backend's only job is to bring the YouTube app up and obtain the screen's id; from there everything goes through ytlounge, a separate package by the same author that knows nothing about devices, caches or files.
The Lounge API is not documented and can change at any time. If it does, expect this to break the same way for every tool built on it.
Python versions
Python 3.11 and newer, 3.14 included — core and all three backends.
The backends are extras because each pulls a sizeable dependency tree, not
because of any version limit: install yttv[appletv], yttv[cast] or
yttv[all] for the ones you need. Asking for a backend you did not install
tells you which extra is missing.
When the search finds nothing
The DIAL search is a multicast packet; every device answers with a unicast reply from its own address. A stateful firewall on your machine (ufw, firewalld) does not connect that reply to the packet you sent and drops it silently. Discovery then reports "no device found" although the packets are on the wire.
yttv --doctor
sends the searches, counts the replies and, when nothing comes back, prints
the rule to check and the ufw line that fixes it. Cast TVs and Apple TV
never answer DIAL searches; that is expected, use --cast and --appletv
for those. --doctor --host <ip> probes a device directly and sidesteps
multicast.
Origins
The Lounge protocol was reverse-engineered independently by several people; nothing here is derived from their code, but their write-ups made the protocol knowable. yttv and ytlounge learned it from Marco Lucidi's ytcast (Go), whose behaviour served as the reference for verifying requests on the wire, and, through it, from the sources ytcast itself credits:
- https://0x41.cf/automation/2021/03/02/google-assistant-youtube-smart-tvs.html
- https://github.com/thedroidgeek/youtube-cast-automation-api
- https://github.com/mutantmonkey/youtube-remote
- https://bugs.xdavidhu.me/google/2021/04/05/i-built-a-tv-that-plays-all-of-your-private-youtube-videos
- https://github.com/aykevl/plaincast
- https://github.com/ur1katz/casttube
The list of YouTube URL forms used in the tests comes from this gist.
Development
pixi run test # unit and fixture tests, Python 3.13 with all backends
pixi run -e core314 test # the same on Python 3.14 without any backend extra
pixi run test-device # needs a real TV on the network
pixi run check # build wheel and sdist, validate the metadata
The pixi environments take ytlounge from a checkout in ../ytlounge, so
both can be changed together; a released yttv gets it from PyPI.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file yttv-0.1.1.tar.gz.
File metadata
- Download URL: yttv-0.1.1.tar.gz
- Upload date:
- Size: 42.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a24152a07a6cee21c512d4b03b022f71f5144431930f686406116dc4c5d5fcd
|
|
| MD5 |
ca6549c7011a44d9553c271c80380432
|
|
| BLAKE2b-256 |
5ec5fc5c2ed1efd44d559e99cad55e7e28223c9553ae8224a4dcd2d2f76b94ca
|
Provenance
The following attestation bundles were made for yttv-0.1.1.tar.gz:
Publisher:
release.yml on desvaters/yttv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
yttv-0.1.1.tar.gz -
Subject digest:
6a24152a07a6cee21c512d4b03b022f71f5144431930f686406116dc4c5d5fcd - Sigstore transparency entry: 2818076323
- Sigstore integration time:
-
Permalink:
desvaters/yttv@af7ad26ecb1923775161e77efb190571b7a3126d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/desvaters
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@af7ad26ecb1923775161e77efb190571b7a3126d -
Trigger Event:
release
-
Statement type:
File details
Details for the file yttv-0.1.1-py3-none-any.whl.
File metadata
- Download URL: yttv-0.1.1-py3-none-any.whl
- Upload date:
- Size: 28.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2724dd16351ba4bf1a1f68b4dc137175898b73ab4ac88dfa76df9e6e9e70e914
|
|
| MD5 |
15969109e0e4a0931f6682fbce50de48
|
|
| BLAKE2b-256 |
607de9f93cf2109196e0d16a748c79181d1a5d665dc38c3a036a6975a7c98c5d
|
Provenance
The following attestation bundles were made for yttv-0.1.1-py3-none-any.whl:
Publisher:
release.yml on desvaters/yttv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
yttv-0.1.1-py3-none-any.whl -
Subject digest:
2724dd16351ba4bf1a1f68b4dc137175898b73ab4ac88dfa76df9e6e9e70e914 - Sigstore transparency entry: 2818076365
- Sigstore integration time:
-
Permalink:
desvaters/yttv@af7ad26ecb1923775161e77efb190571b7a3126d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/desvaters
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@af7ad26ecb1923775161e77efb190571b7a3126d -
Trigger Event:
release
-
Statement type: