Skip to main content

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_video turns an id or any of the usual YouTube URL forms into a Video with 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_screen with pairing_code=. The expiration here is a string, on the token endpoint a number.
  • Refresh: POST /api/lounge/pairing/get_lounge_token_batch with screen_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 a Content-Length header 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 body count=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=… and req0__sc=clearPlaylist with no fields of its own. insertVideo puts 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 playlistModified event whose videoIds is the queue as one comma-separated string, so Session.queue is 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 Lounge methods 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:

The YouTube URL forms in the tests come from this gist.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ytlounge-0.2.0.tar.gz (19.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ytlounge-0.2.0-py3-none-any.whl (13.1 kB view details)

Uploaded Python 3

File details

Details for the file ytlounge-0.2.0.tar.gz.

File metadata

  • Download URL: ytlounge-0.2.0.tar.gz
  • Upload date:
  • Size: 19.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ytlounge-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ffb31dd3a50cbf7ef55b24d2d5907254b0fb1534665e3592db35d38f2c0b95ad
MD5 c3922c67bf3d62562e2b2dc8630c0433
BLAKE2b-256 af6f4ccbf6fd31ee4148f7980aec486ef864b58e845321dd6333d42345e132ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for ytlounge-0.2.0.tar.gz:

Publisher: release.yml on desvaters/ytlounge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ytlounge-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: ytlounge-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ytlounge-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e0a3825456a4d047f91f1def3dc45c5eb418b9ad514d773d13d9c09c0104c453
MD5 bf77698b633af2bc3a9e007d190c06c4
BLAKE2b-256 30391d6e15f3ac713205efd2f75e3860cd54e26c4de82acaf9603a9506678bf4

See more details on using hashes here.

Provenance

The following attestation bundles were made for ytlounge-0.2.0-py3-none-any.whl:

Publisher: release.yml on desvaters/ytlounge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page