yttv 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yttv-0.1.0/.gitignore +10 -0
- yttv-0.1.0/LICENSE +21 -0
- yttv-0.1.0/PKG-INFO +167 -0
- yttv-0.1.0/README.md +136 -0
- yttv-0.1.0/pixi.toml +64 -0
- yttv-0.1.0/pyproject.toml +62 -0
- yttv-0.1.0/src/yttv/__init__.py +45 -0
- yttv-0.1.0/src/yttv/api.py +323 -0
- yttv-0.1.0/src/yttv/backends/__init__.py +66 -0
- yttv-0.1.0/src/yttv/backends/appletv.py +150 -0
- yttv-0.1.0/src/yttv/backends/cast.py +160 -0
- yttv-0.1.0/src/yttv/backends/dial.py +243 -0
- yttv-0.1.0/src/yttv/cache.py +264 -0
- yttv-0.1.0/src/yttv/cli.py +249 -0
- yttv-0.1.0/src/yttv/doctor.py +104 -0
- yttv-0.1.0/src/yttv/ssdp.py +143 -0
- yttv-0.1.0/tests/__init__.py +0 -0
- yttv-0.1.0/tests/yttv/__init__.py +0 -0
- yttv-0.1.0/tests/yttv/fixtures/bind_command.txt +2 -0
- yttv-0.1.0/tests/yttv/fixtures/bind_session.txt +8 -0
- yttv-0.1.0/tests/yttv/fixtures/dial_app_running.xml +11 -0
- yttv-0.1.0/tests/yttv/fixtures/dial_app_stopped.xml +6 -0
- yttv-0.1.0/tests/yttv/fixtures/dial_description.xml +11 -0
- yttv-0.1.0/tests/yttv/fixtures/ssdp_dial_response.txt +9 -0
- yttv-0.1.0/tests/yttv/fixtures/ytcast_cache.json +39 -0
- yttv-0.1.0/tests/yttv/test_api.py +371 -0
- yttv-0.1.0/tests/yttv/test_appletv.py +218 -0
- yttv-0.1.0/tests/yttv/test_backends.py +47 -0
- yttv-0.1.0/tests/yttv/test_cache.py +208 -0
- yttv-0.1.0/tests/yttv/test_cast.py +149 -0
- yttv-0.1.0/tests/yttv/test_cli.py +281 -0
- yttv-0.1.0/tests/yttv/test_dial.py +283 -0
- yttv-0.1.0/tests/yttv/test_doctor.py +95 -0
- yttv-0.1.0/tests/yttv/test_ssdp.py +99 -0
yttv-0.1.0/.gitignore
ADDED
yttv-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 desvaters
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
yttv-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: yttv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Play YouTube videos on a TV: Cast, Apple TV and DIAL devices, all driven through the YouTube Lounge API
|
|
5
|
+
Project-URL: Homepage, https://github.com/desvaters/yttv
|
|
6
|
+
Project-URL: Issues, https://github.com/desvaters/yttv/issues
|
|
7
|
+
Author-email: desvaters <pypi@desvate.rs>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: apple-tv,cast,chromecast,dial,lounge,tv,youtube
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Multimedia :: Video
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: ytlounge<0.2,>=0.1
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: pyatv>=0.18; (python_version < '3.14') and extra == 'all'
|
|
25
|
+
Requires-Dist: pychromecast>=14; extra == 'all'
|
|
26
|
+
Provides-Extra: appletv
|
|
27
|
+
Requires-Dist: pyatv>=0.18; (python_version < '3.14') and extra == 'appletv'
|
|
28
|
+
Provides-Extra: cast
|
|
29
|
+
Requires-Dist: pychromecast>=14; extra == 'cast'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# yttv
|
|
33
|
+
|
|
34
|
+
Play YouTube videos on a TV from the command line or from Python: Apple TV,
|
|
35
|
+
Cast TVs (Chromecast, Samsung Tizen) and DIAL devices (Fire TV, WebOS).
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
yttv https://youtu.be/dQw4w9WgXcQ # play on the last used screen
|
|
39
|
+
yttv -a ID1 ID2 # append to its queue, one call for all
|
|
40
|
+
yttv -d bedroom ID # pick a screen by name or address
|
|
41
|
+
yttv -l # list known screens
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install 'yttv[all]' # every backend
|
|
48
|
+
pip install 'yttv[cast]' # Cast TVs only
|
|
49
|
+
pip install 'yttv[appletv]' # Apple TV only (needs Python < 3.14, see below)
|
|
50
|
+
pip install yttv # DIAL and screens paired by code; only httpx
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or, in a [pixi](https://pixi.sh) project: `pixi add --pypi 'yttv[all]'`.
|
|
54
|
+
|
|
55
|
+
## Setting up a screen
|
|
56
|
+
|
|
57
|
+
Every TV needs one of these once. yttv remembers the result under
|
|
58
|
+
`~/.cache/yttv/devices.json` and picks the last used screen by default.
|
|
59
|
+
|
|
60
|
+
| TV | Once | Then |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Apple TV | `yttv --appletv 192.168.1.5` and type the PIN it shows | `yttv URL` opens the app and plays |
|
|
63
|
+
| 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 |
|
|
64
|
+
| Fire TV, WebOS, other DIAL devices | `yttv -s` finds them | `yttv URL` wakes the TV and starts the app |
|
|
65
|
+
| Anything with a YouTube app | `yttv --pair 123456789` with the code from *Settings › Link with TV code* | `yttv URL`, with the app already open |
|
|
66
|
+
|
|
67
|
+
Videos are ids or URLs in any of the usual forms (`watch?v=`, `youtu.be`,
|
|
68
|
+
`shorts`, `live`, `embed`, ...). A `t=` parameter becomes the start
|
|
69
|
+
position. Several videos are always sent together in one call: sending
|
|
70
|
+
them one after another scrambles the TV's queue.
|
|
71
|
+
|
|
72
|
+
## From Python
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import yttv
|
|
76
|
+
|
|
77
|
+
yttv.devices() # known screens, from the cache, no network
|
|
78
|
+
yttv.cast(["dQw4w9WgXcQ"]) # play now on the last used screen
|
|
79
|
+
yttv.cast([url1, url2], queue=True) # append to its queue
|
|
80
|
+
yttv.cast([url], device="bedroom") # pick a screen
|
|
81
|
+
yttv.pair("123 456 789") # link a screen by TV code
|
|
82
|
+
yttv.add_device("cast", "192.168.1.6", cast_uuid="...")
|
|
83
|
+
yttv.discover() # DIAL search
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Every failure is a subclass of `yttv.YttvError` with a message meant for
|
|
87
|
+
people, so a caller can show it as is. `cast()` takes up to `timeout`
|
|
88
|
+
seconds (default 90) for waking the TV and starting the app; that is how
|
|
89
|
+
long a sleeping Fire TV needs.
|
|
90
|
+
|
|
91
|
+
Screens paired with [ytcast](https://github.com/MarcoLucidi01/ytcast) are
|
|
92
|
+
taken over from `~/.cache/ytcast/ytcast.json` on the first run.
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
Three ways to reach a TV, one protocol to drive it:
|
|
97
|
+
|
|
98
|
+
| Device | Finds and starts the app via | Plays via |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| Apple TV | [pyatv](https://github.com/postlund/pyatv), Companion protocol | Lounge |
|
|
101
|
+
| Cast TVs | [pychromecast](https://github.com/home-assistant-libs/pychromecast), the YouTube receiver's `mdx` channel | Lounge |
|
|
102
|
+
| DIAL devices | SSDP and the DIAL REST interface, built in | Lounge |
|
|
103
|
+
|
|
104
|
+
The Lounge API is the unofficial protocol behind the "Play on TV" button in
|
|
105
|
+
the phone app: pair with a screen, play a video, append to the queue. Each
|
|
106
|
+
backend's only job is to bring the YouTube app up and obtain the screen's
|
|
107
|
+
id; from there everything goes through
|
|
108
|
+
[ytlounge](https://github.com/desvaters/ytlounge), a separate package by
|
|
109
|
+
the same author that knows nothing about devices, caches or files.
|
|
110
|
+
|
|
111
|
+
The Lounge API is not documented and can change at any time. If it does,
|
|
112
|
+
expect this to break the same way for every tool built on it.
|
|
113
|
+
|
|
114
|
+
### Python versions
|
|
115
|
+
|
|
116
|
+
The core runs on Python 3.11 and newer, 3.14 included. The Apple TV backend
|
|
117
|
+
depends on pyatv, which does not run on 3.14 yet; its extra is skipped
|
|
118
|
+
there and `yttv` says so when an Apple TV is used.
|
|
119
|
+
|
|
120
|
+
## When the search finds nothing
|
|
121
|
+
|
|
122
|
+
The DIAL search is a multicast packet; every device answers with a unicast
|
|
123
|
+
reply from its own address. A stateful firewall on your machine (ufw,
|
|
124
|
+
firewalld) does not connect that reply to the packet you sent and drops it
|
|
125
|
+
silently. Discovery then reports "no device found" although the packets are
|
|
126
|
+
on the wire.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
yttv --doctor
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
sends the searches, counts the replies and, when nothing comes back, prints
|
|
133
|
+
the rule to check and the ufw line that fixes it. Cast TVs and Apple TV
|
|
134
|
+
never answer DIAL searches; that is expected, use `--cast` and `--appletv`
|
|
135
|
+
for those. `--doctor --host <ip>` probes a device directly and sidesteps
|
|
136
|
+
multicast.
|
|
137
|
+
|
|
138
|
+
## Origins
|
|
139
|
+
|
|
140
|
+
The Lounge protocol was reverse-engineered independently by several
|
|
141
|
+
people; nothing here is derived from their code, but their write-ups made
|
|
142
|
+
the protocol knowable. yttv and ytlounge learned it from Marco Lucidi's
|
|
143
|
+
[ytcast](https://github.com/MarcoLucidi01/ytcast) (Go), whose behaviour
|
|
144
|
+
served as the reference for verifying requests on the wire, and, through
|
|
145
|
+
it, from the sources ytcast itself credits:
|
|
146
|
+
|
|
147
|
+
- https://0x41.cf/automation/2021/03/02/google-assistant-youtube-smart-tvs.html
|
|
148
|
+
- https://github.com/thedroidgeek/youtube-cast-automation-api
|
|
149
|
+
- https://github.com/mutantmonkey/youtube-remote
|
|
150
|
+
- https://bugs.xdavidhu.me/google/2021/04/05/i-built-a-tv-that-plays-all-of-your-private-youtube-videos
|
|
151
|
+
- https://github.com/aykevl/plaincast
|
|
152
|
+
- https://github.com/ur1katz/casttube
|
|
153
|
+
|
|
154
|
+
The list of YouTube URL forms used in the tests comes from
|
|
155
|
+
[this gist](https://gist.github.com/rodrigoborgesdeoliveira/987683cfbfcc8d800192da1e73adc486).
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pixi run test # unit and fixture tests, Python 3.13 with all backends
|
|
161
|
+
pixi run -e core314 test # the same on Python 3.14 without any backend extra
|
|
162
|
+
pixi run test-device # needs a real TV on the network
|
|
163
|
+
pixi run check # build wheel and sdist, validate the metadata
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The pixi environments take `ytlounge` from a checkout in `../ytlounge`, so
|
|
167
|
+
both can be changed together; a released `yttv` gets it from PyPI.
|
yttv-0.1.0/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# yttv
|
|
2
|
+
|
|
3
|
+
Play YouTube videos on a TV from the command line or from Python: Apple TV,
|
|
4
|
+
Cast TVs (Chromecast, Samsung Tizen) and DIAL devices (Fire TV, WebOS).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
yttv https://youtu.be/dQw4w9WgXcQ # play on the last used screen
|
|
8
|
+
yttv -a ID1 ID2 # append to its queue, one call for all
|
|
9
|
+
yttv -d bedroom ID # pick a screen by name or address
|
|
10
|
+
yttv -l # list known screens
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install 'yttv[all]' # every backend
|
|
17
|
+
pip install 'yttv[cast]' # Cast TVs only
|
|
18
|
+
pip install 'yttv[appletv]' # Apple TV only (needs Python < 3.14, see below)
|
|
19
|
+
pip install yttv # DIAL and screens paired by code; only httpx
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or, in a [pixi](https://pixi.sh) project: `pixi add --pypi 'yttv[all]'`.
|
|
23
|
+
|
|
24
|
+
## Setting up a screen
|
|
25
|
+
|
|
26
|
+
Every TV needs one of these once. yttv remembers the result under
|
|
27
|
+
`~/.cache/yttv/devices.json` and picks the last used screen by default.
|
|
28
|
+
|
|
29
|
+
| TV | Once | Then |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Apple TV | `yttv --appletv 192.168.1.5` and type the PIN it shows | `yttv URL` opens the app and plays |
|
|
32
|
+
| 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 |
|
|
33
|
+
| Fire TV, WebOS, other DIAL devices | `yttv -s` finds them | `yttv URL` wakes the TV and starts the app |
|
|
34
|
+
| Anything with a YouTube app | `yttv --pair 123456789` with the code from *Settings › Link with TV code* | `yttv URL`, with the app already open |
|
|
35
|
+
|
|
36
|
+
Videos are ids or URLs in any of the usual forms (`watch?v=`, `youtu.be`,
|
|
37
|
+
`shorts`, `live`, `embed`, ...). A `t=` parameter becomes the start
|
|
38
|
+
position. Several videos are always sent together in one call: sending
|
|
39
|
+
them one after another scrambles the TV's queue.
|
|
40
|
+
|
|
41
|
+
## From Python
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import yttv
|
|
45
|
+
|
|
46
|
+
yttv.devices() # known screens, from the cache, no network
|
|
47
|
+
yttv.cast(["dQw4w9WgXcQ"]) # play now on the last used screen
|
|
48
|
+
yttv.cast([url1, url2], queue=True) # append to its queue
|
|
49
|
+
yttv.cast([url], device="bedroom") # pick a screen
|
|
50
|
+
yttv.pair("123 456 789") # link a screen by TV code
|
|
51
|
+
yttv.add_device("cast", "192.168.1.6", cast_uuid="...")
|
|
52
|
+
yttv.discover() # DIAL search
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Every failure is a subclass of `yttv.YttvError` with a message meant for
|
|
56
|
+
people, so a caller can show it as is. `cast()` takes up to `timeout`
|
|
57
|
+
seconds (default 90) for waking the TV and starting the app; that is how
|
|
58
|
+
long a sleeping Fire TV needs.
|
|
59
|
+
|
|
60
|
+
Screens paired with [ytcast](https://github.com/MarcoLucidi01/ytcast) are
|
|
61
|
+
taken over from `~/.cache/ytcast/ytcast.json` on the first run.
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
Three ways to reach a TV, one protocol to drive it:
|
|
66
|
+
|
|
67
|
+
| Device | Finds and starts the app via | Plays via |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| Apple TV | [pyatv](https://github.com/postlund/pyatv), Companion protocol | Lounge |
|
|
70
|
+
| Cast TVs | [pychromecast](https://github.com/home-assistant-libs/pychromecast), the YouTube receiver's `mdx` channel | Lounge |
|
|
71
|
+
| DIAL devices | SSDP and the DIAL REST interface, built in | Lounge |
|
|
72
|
+
|
|
73
|
+
The Lounge API is the unofficial protocol behind the "Play on TV" button in
|
|
74
|
+
the phone app: pair with a screen, play a video, append to the queue. Each
|
|
75
|
+
backend's only job is to bring the YouTube app up and obtain the screen's
|
|
76
|
+
id; from there everything goes through
|
|
77
|
+
[ytlounge](https://github.com/desvaters/ytlounge), a separate package by
|
|
78
|
+
the same author that knows nothing about devices, caches or files.
|
|
79
|
+
|
|
80
|
+
The Lounge API is not documented and can change at any time. If it does,
|
|
81
|
+
expect this to break the same way for every tool built on it.
|
|
82
|
+
|
|
83
|
+
### Python versions
|
|
84
|
+
|
|
85
|
+
The core runs on Python 3.11 and newer, 3.14 included. The Apple TV backend
|
|
86
|
+
depends on pyatv, which does not run on 3.14 yet; its extra is skipped
|
|
87
|
+
there and `yttv` says so when an Apple TV is used.
|
|
88
|
+
|
|
89
|
+
## When the search finds nothing
|
|
90
|
+
|
|
91
|
+
The DIAL search is a multicast packet; every device answers with a unicast
|
|
92
|
+
reply from its own address. A stateful firewall on your machine (ufw,
|
|
93
|
+
firewalld) does not connect that reply to the packet you sent and drops it
|
|
94
|
+
silently. Discovery then reports "no device found" although the packets are
|
|
95
|
+
on the wire.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
yttv --doctor
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
sends the searches, counts the replies and, when nothing comes back, prints
|
|
102
|
+
the rule to check and the ufw line that fixes it. Cast TVs and Apple TV
|
|
103
|
+
never answer DIAL searches; that is expected, use `--cast` and `--appletv`
|
|
104
|
+
for those. `--doctor --host <ip>` probes a device directly and sidesteps
|
|
105
|
+
multicast.
|
|
106
|
+
|
|
107
|
+
## Origins
|
|
108
|
+
|
|
109
|
+
The Lounge protocol was reverse-engineered independently by several
|
|
110
|
+
people; nothing here is derived from their code, but their write-ups made
|
|
111
|
+
the protocol knowable. yttv and ytlounge learned it from Marco Lucidi's
|
|
112
|
+
[ytcast](https://github.com/MarcoLucidi01/ytcast) (Go), whose behaviour
|
|
113
|
+
served as the reference for verifying requests on the wire, and, through
|
|
114
|
+
it, from the sources ytcast itself credits:
|
|
115
|
+
|
|
116
|
+
- https://0x41.cf/automation/2021/03/02/google-assistant-youtube-smart-tvs.html
|
|
117
|
+
- https://github.com/thedroidgeek/youtube-cast-automation-api
|
|
118
|
+
- https://github.com/mutantmonkey/youtube-remote
|
|
119
|
+
- https://bugs.xdavidhu.me/google/2021/04/05/i-built-a-tv-that-plays-all-of-your-private-youtube-videos
|
|
120
|
+
- https://github.com/aykevl/plaincast
|
|
121
|
+
- https://github.com/ur1katz/casttube
|
|
122
|
+
|
|
123
|
+
The list of YouTube URL forms used in the tests comes from
|
|
124
|
+
[this gist](https://gist.github.com/rodrigoborgesdeoliveira/987683cfbfcc8d800192da1e73adc486).
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pixi run test # unit and fixture tests, Python 3.13 with all backends
|
|
130
|
+
pixi run -e core314 test # the same on Python 3.14 without any backend extra
|
|
131
|
+
pixi run test-device # needs a real TV on the network
|
|
132
|
+
pixi run check # build wheel and sdist, validate the metadata
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The pixi environments take `ytlounge` from a checkout in `../ytlounge`, so
|
|
136
|
+
both can be changed together; a released `yttv` gets it from PyPI.
|
yttv-0.1.0/pixi.toml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
name = "yttv"
|
|
3
|
+
channels = ["conda-forge"]
|
|
4
|
+
platforms = ["linux-64"]
|
|
5
|
+
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
# Shared by every environment: the core's runtime dependency, the test
|
|
8
|
+
# runner and the tasks. The Python version and the project install live in
|
|
9
|
+
# the features below, because they differ between environments.
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
[dependencies]
|
|
12
|
+
# The only runtime dependency of the core. Without it: ModuleNotFoundError
|
|
13
|
+
# on `import ytlounge`.
|
|
14
|
+
httpx = ">=0.27"
|
|
15
|
+
# Test runner. Without it `pixi run test` fails with "pytest: command not found".
|
|
16
|
+
pytest = ">=8"
|
|
17
|
+
# Builds wheel and sdist for `pixi run build`. Without it: "No module named build".
|
|
18
|
+
python-build = ">=1.2"
|
|
19
|
+
# `pixi run check` validates the built metadata the way PyPI will; also the
|
|
20
|
+
# eventual upload. Without it: "twine: command not found".
|
|
21
|
+
twine = ">=6"
|
|
22
|
+
|
|
23
|
+
[tasks]
|
|
24
|
+
# Unit and fixture tests only. Device tests are opt-in via `pixi run test-device`
|
|
25
|
+
# because they need a TV on the network and would fail in any other setup.
|
|
26
|
+
test = "pytest -m 'not device'"
|
|
27
|
+
test-device = "pytest -m device"
|
|
28
|
+
test-all = "pytest"
|
|
29
|
+
# Release: build both artifacts into dist/, then let twine validate them.
|
|
30
|
+
build = "python -m build"
|
|
31
|
+
check = { cmd = "twine check dist/*", depends-on = ["build"] }
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# backends: Python 3.13 with every backend extra. 3.13 and not 3.14 because
|
|
35
|
+
# pyatv (Apple TV backend) breaks on 3.14, see NOTES.md §5. With 3.14 the
|
|
36
|
+
# appletv extra would silently resolve to nothing (its marker excludes it)
|
|
37
|
+
# and every Apple TV test would error with ModuleNotFoundError: pyatv.
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
[feature.backends.dependencies]
|
|
40
|
+
python = "3.13.*"
|
|
41
|
+
|
|
42
|
+
[feature.backends.pypi-dependencies]
|
|
43
|
+
# The project itself, editable, with all backend extras. Editable so that
|
|
44
|
+
# changes under src/ are visible without reinstalling; without this line
|
|
45
|
+
# `import yttv` fails inside the environment.
|
|
46
|
+
yttv = { path = ".", editable = true, extras = ["all"] }
|
|
47
|
+
# The protocol core from its own checkout next door, editable, so both can
|
|
48
|
+
# be developed together. Released installs get it from PyPI instead.
|
|
49
|
+
ytlounge = { path = "../ytlounge", editable = true }
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# py314: the newest Python without any backend extra. Proves that ytlounge
|
|
53
|
+
# and the yttv core do not depend on anything that caps the Python version.
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
[feature.py314.dependencies]
|
|
56
|
+
python = "3.14.*"
|
|
57
|
+
|
|
58
|
+
[feature.py314.pypi-dependencies]
|
|
59
|
+
yttv = { path = ".", editable = true }
|
|
60
|
+
ytlounge = { path = "../ytlounge", editable = true }
|
|
61
|
+
|
|
62
|
+
[environments]
|
|
63
|
+
default = { features = ["backends"] }
|
|
64
|
+
core314 = { features = ["py314"] }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yttv"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Play YouTube videos on a TV: Cast, Apple TV and DIAL devices, all driven through the YouTube Lounge API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [{ name = "desvaters", email = "pypi@desvate.rs" }]
|
|
14
|
+
keywords = ["youtube", "tv", "cast", "chromecast", "apple-tv", "dial", "lounge"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Multimedia :: Video",
|
|
25
|
+
]
|
|
26
|
+
# ytlounge is the protocol core (same author, separate distribution) and
|
|
27
|
+
# brings httpx, the only other runtime dependency, with it.
|
|
28
|
+
dependencies = ["ytlounge>=0.1,<0.2", "httpx>=0.27"]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
# Cast TVs (e.g. Samsung Tizen): discovery via mDNS and app launch through
|
|
32
|
+
# pychromecast. Only the launch and screenId retrieval come from here; play and
|
|
33
|
+
# queue always go through ytlounge.
|
|
34
|
+
cast = ["pychromecast>=14"]
|
|
35
|
+
# Apple TV: app launch through the Companion protocol. pyatv does not run on
|
|
36
|
+
# Python 3.14 (asyncio.get_event_loop() raises), which is why this is an extra
|
|
37
|
+
# and not a hard dependency: it must not cap the Python version of the core.
|
|
38
|
+
appletv = ["pyatv>=0.18; python_version < '3.14'"]
|
|
39
|
+
all = ["yttv[cast,appletv]"]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
yttv = "yttv.cli:main"
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/desvaters/yttv"
|
|
46
|
+
Issues = "https://github.com/desvaters/yttv/issues"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/yttv"]
|
|
50
|
+
|
|
51
|
+
# The sdist is what people read; keep the reference implementation and the
|
|
52
|
+
# private working notes out of it (both are git-excluded locally, which
|
|
53
|
+
# hatch does not see).
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
exclude = ["/upstream", "/NOTES.md", "/HANDOFF.md", "/.pixi", "/pixi.lock", "/.claude"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = "-ra"
|
|
60
|
+
markers = [
|
|
61
|
+
"device: needs a real TV on the network (deselected by default in pixi tasks)",
|
|
62
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""yttv: play YouTube videos on a TV.
|
|
2
|
+
|
|
3
|
+
Device backends (Cast, Apple TV, DIAL) bring the YouTube app to the front and
|
|
4
|
+
obtain the screen's ``screen_id``; the ``ytlounge`` package does the actual
|
|
5
|
+
playing.
|
|
6
|
+
|
|
7
|
+
The public API is three functions::
|
|
8
|
+
|
|
9
|
+
import yttv
|
|
10
|
+
yttv.devices() # paired screens, from the cache
|
|
11
|
+
yttv.cast(["dQw4w9WgXcQ"]) # play now on the last used screen
|
|
12
|
+
yttv.cast([url1, url2], queue=True) # append to the queue
|
|
13
|
+
yttv.pair("123 456 789") # link a screen by TV code
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .api import (
|
|
17
|
+
CastError,
|
|
18
|
+
InvalidVideoError,
|
|
19
|
+
NoDeviceError,
|
|
20
|
+
PairError,
|
|
21
|
+
YttvError,
|
|
22
|
+
add_device,
|
|
23
|
+
attach,
|
|
24
|
+
cast,
|
|
25
|
+
devices,
|
|
26
|
+
discover,
|
|
27
|
+
pair,
|
|
28
|
+
)
|
|
29
|
+
from .cache import Cache, Device
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Cache",
|
|
33
|
+
"CastError",
|
|
34
|
+
"Device",
|
|
35
|
+
"InvalidVideoError",
|
|
36
|
+
"NoDeviceError",
|
|
37
|
+
"PairError",
|
|
38
|
+
"YttvError",
|
|
39
|
+
"add_device",
|
|
40
|
+
"attach",
|
|
41
|
+
"cast",
|
|
42
|
+
"devices",
|
|
43
|
+
"discover",
|
|
44
|
+
"pair",
|
|
45
|
+
]
|