ytlounge
Client for the YouTube Lounge API: the protocol behind the "Play on TV" button. Pair with a screen once, then play videos on it and manage its queue from Python. Synchronous, one dependency (httpx), no device code.
from ytlounge import Lounge
with Lounge(name="my-remote") as lounge:
screen = lounge.pair("123 456 789") # code from Settings › Link with TV code
lounge.play(screen, ["dQw4w9WgXcQ"]) # play now
lounge.add(screen, ["jNQXAC9IVRw"]) # append to the queue
lounge.play_next(screen, "aqz-KE-bpKQ") # jump the queue, behind what plays
lounge.queue(screen) # what is queued right now
lounge.remove(screen, "jNQXAC9IVRw") # drop one video
lounge.clear(screen) # empty the queue
Screen is a small frozen dataclass; keep screen_id and you can always
get a fresh token with lounge.refresh(screen). Tokens live about two
weeks, Screen.is_expired() tells you when.
Install
pip install ytlounge
Python 3.11 or newer.
What it does and does not do
- Pair with a TV code, refresh a token from a screen id, open a session, play a list of videos (with a start index and start time), append videos to the queue, insert one behind the current video, remove one, empty the queue, and read what the screen has queued.
ytlounge.video.parse_videoturns an id or any of the usual YouTube URL forms into aVideowith an optional start time.- It does not find TVs, launch apps or store anything. That is the job of a tool built on top, such as yttv, which adds Cast, Apple TV and DIAL backends, a cache and a command line.
The Lounge API is not documented by Google and can change at any time. Everything below was verified on the wire in September 2026 and is kept as assertions in the tests; when something breaks, that is where to look.
The protocol, as verified
| Field | Lives | Comes from |
|---|---|---|
screen_id |
for good, survives a cold start of the TV | one pairing with a TV code |
lounge_token |
about 13 days | get_lounge_token_batch from the screen id |
SID, gsessionid |
one session | a bind at the start of every command |
- Pair:
POST /api/lounge/pairing/get_screenwithpairing_code=. Theexpirationhere is a string, on the token endpoint a number. - Refresh:
POST /api/lounge/pairing/get_lounge_token_batchwithscreen_ids=. A refresh does not revoke earlier tokens. - Session:
POST /api/lounge/bc/bind?CVER=1&RID=1&VER=8&app=youtube-desktop&device=REMOTE_CONTROL&id=remote&loungeIdToken=…&name=…with an empty body. The endpoint insists on aContent-Lengthheader even then (httpx always sends one). The reply is framed: a decimal length on its own line, then that many characters of a JSON array of numbered messages;["c", SID, …]and["S", gsessionid]are in the first frame, followed by the screen's status and current queue. - Play:
POST bind?CVER=1&RID=2&SID=…&VER=8&gsessionid=…&loungeIdToken=…, form bodycount=1&req0__sc=setPlaylist&req0_currentIndex=0&req0_currentTime=0&req0_videoId=…&req0_videoIds=a,b,c. - Add: same query,
req0__sc=addVideo&req0_videoId=…, one request per video with a random 2–5 s pause before each. The pause is not cosmetic: without it consecutive adds race on the screen and the queue comes out incomplete or reordered. - Play next, remove, clear: same query,
req0__sc=insertVideo&req0_videoId=…,req0__sc=removeVideo&req0_videoId=…andreq0__sc=clearPlaylistwith no fields of its own.insertVideoputs the video behind the one currently playing, not at the end: a queue of[zoo, bunny]with zoo playing became[zoo, rick, bunny]. All three answer with the same ack as play,8\n[0,-1,0]. - Reading the queue: no request of its own. The bind reply already carries
a
playlistModifiedevent whosevideoIdsis the queue as one comma-separated string, soSession.queueis filled while the session is opened. It is a snapshot of that moment and does not update afterwards: the commands answer with a bare ack and carry no new queue, so a fresh bind is the way to see the result of one. - Sessions do not go stale quickly. casttube rebinds before every queue
action, saying a session drifts out of step after about 30 seconds. Not
reproduced here: a session left idle for 35 seconds still executed an insert
that took effect on the screen. Opening one session per command, as the
Loungemethods do, sidesteps the question anyway.
Development
pixi run test # fixture tests
YTLOUNGE_SCREEN_ID=… pixi run test-device # against the real API, no TV changes
pixi run check # build and validate the artifacts
Origins
The Lounge protocol was reverse-engineered independently by several people. This client contains none of their code, but learned the protocol from Marco Lucidi's ytcast, whose requests were the reference for verifying this implementation on the wire, and — for the names of the three queue actions — from reading casttube. Protocol knowledge, no code: nothing here is a translation of either. Through ytcast this client also stands on the sources it 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
The YouTube URL forms in the tests come from this gist.
Metadata
Release files for ytlounge 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ytlounge-0.2.0.tar.gz | 19.5 kB | With provenance | Details for ytlounge-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ytlounge-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for ytlounge-0.2.0-py3-none-any.whl |
Total release size: 32.6 kB
Release files / ytlounge-0.2.0.tar.gz
| Download URL | ytlounge-0.2.0.tar.gz |
|---|---|
| Size | 19.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ffb31dd3a50cbf7ef55b24d2d5907254b0fb1534665e3592db35d38f2c0b95ad
|
|
BLAKE2b-256 checksum How to use checksums |
af6f4ccbf6fd31ee4148f7980aec486ef864b58e845321dd6333d42345e132ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / ytlounge-0.2.0-py3-none-any.whl
| Download URL | ytlounge-0.2.0-py3-none-any.whl |
|---|---|
| Size | 13.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e0a3825456a4d047f91f1def3dc45c5eb418b9ad514d773d13d9c09c0104c453
|
|
BLAKE2b-256 checksum How to use checksums |
30391d6e15f3ac713205efd2f75e3860cd54e26c4de82acaf9603a9506678bf4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logSecurity for ytlounge 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ytlounge-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.