Taylor Fox Dahlin
commited on
Revamp of unit tests (#775)
Browse files* Updated test suite to use new more robust mock generator.
* Fixed flake8 issues.
* Added type annotations and return to mock generating function.
* Added exception test to get_ytplayer_js.
* Added test for uniqueify helper function.
* Updated README to reference YouTube Rewind 2019 instead of gangnam style.
* Added cached HTML for various URLs associated with the youtube rewind 2019 video, in order to prevent a unit test from running in to 429 responses.
* Removed url parameter that was breaking get_video_info response.
* Fixed unit test to match change to extract implementation.
* Added additional pattern for fetching js url.
* Changed fixture generation to be based on strictly HTML, rather than partial serialization of YouTube object.
- README.md +82 -71
- pytube/__main__.py +11 -26
- pytube/extract.py +1 -1
- pytube/helpers.py +43 -0
- tests/conftest.py +22 -15
- tests/generate_fixture.py +0 -27
- tests/mocks/yt-video-2lAe1cqCOXo-html.json.gz +0 -0
- tests/mocks/yt-video-9bZkp7q19f0.json.gz +0 -0
- tests/mocks/yt-video-QRS8MkLhQmM-html.json.gz +0 -0
- tests/mocks/yt-video-QRS8MkLhQmM.json.gz +0 -0
- tests/mocks/yt-video-WXxV9g7lsFE-html.json.gz +0 -0
- tests/mocks/yt-video-WXxV9g7lsFE.json.gz +0 -0
- tests/mocks/yt-video-irauhITDrsE-html.json.gz +0 -0
- tests/mocks/yt-video-irauhITDrsE.json.gz +0 -0
- tests/test_cli.py +4 -3
- tests/test_extract.py +13 -7
- tests/test_helpers.py +77 -0
- tests/test_query.py +8 -8
- tests/test_streams.py +32 -29
README.md
CHANGED
@@ -32,8 +32,8 @@ Finally *pytube* also includes a command-line utility, allowing you to quickly d
|
|
32 |
### Behold, a perfect balance of simplicity versus flexibility:
|
33 |
|
34 |
```python
|
35 |
-
>>> YouTube('https://youtu.be/
|
36 |
-
>>> yt = YouTube('http://youtube.com/watch?v=
|
37 |
>>> yt.streams
|
38 |
... .filter(progressive=True, file_extension='mp4')
|
39 |
... .order_by('resolution')
|
@@ -67,37 +67,39 @@ Let's begin with showing how easy it is to download a video with pytube:
|
|
67 |
|
68 |
```python
|
69 |
>>> from pytube import YouTube
|
70 |
-
>>> YouTube('
|
71 |
```
|
72 |
This example will download the highest quality progressive download stream available.
|
73 |
|
74 |
Next, let's explore how we would view what video streams are available:
|
75 |
|
76 |
```python
|
77 |
-
>>> yt = YouTube('
|
78 |
-
>>> yt.streams
|
79 |
-
[<Stream: itag="
|
80 |
-
<Stream: itag="
|
81 |
-
<Stream: itag="
|
82 |
-
<Stream: itag="
|
83 |
-
<Stream: itag="
|
84 |
-
<Stream: itag="
|
85 |
-
<Stream: itag="
|
86 |
-
<Stream: itag="
|
87 |
-
<Stream: itag="
|
88 |
-
<Stream: itag="
|
89 |
-
<Stream: itag="
|
90 |
-
<Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">,
|
91 |
-
<Stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9">,
|
92 |
-
<Stream: itag="
|
93 |
-
<Stream: itag="
|
94 |
-
<Stream: itag="
|
95 |
-
<Stream: itag="
|
96 |
-
<Stream: itag="
|
97 |
-
<Stream: itag="
|
98 |
-
<Stream: itag="
|
99 |
-
<Stream: itag="
|
100 |
-
<Stream: itag="
|
|
|
|
|
101 |
```
|
102 |
You may notice that some streams listed have both a video codec and audio codec, while others have just video or just audio, this is a result of YouTube supporting a streaming technique called Dynamic Adaptive Streaming over HTTP (DASH).
|
103 |
|
@@ -108,35 +110,37 @@ The legacy streams that contain the audio and video in a single file (referred t
|
|
108 |
To only view these progressive download streams:
|
109 |
|
110 |
```python
|
111 |
-
>>> yt.streams.filter(progressive=True)
|
112 |
-
[<Stream: itag="
|
113 |
-
<Stream: itag="
|
114 |
-
<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">,
|
115 |
-
<Stream: itag="36" mime_type="video/3gpp" res="240p" fps="30fps" vcodec="mp4v.20.3" acodec="mp4a.40.2">,
|
116 |
-
<Stream: itag="17" mime_type="video/3gpp" res="144p" fps="30fps" vcodec="mp4v.20.3" acodec="mp4a.40.2">]
|
117 |
```
|
118 |
|
119 |
Conversely, if you only want to see the DASH streams (also referred to as "adaptive") you can do:
|
120 |
|
121 |
```python
|
122 |
-
>>> yt.streams.filter(adaptive=True)
|
123 |
-
[<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">,
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
|
|
|
|
|
|
|
|
|
|
140 |
```
|
141 |
|
142 |
You can also download a complete Youtube playlist:
|
@@ -156,27 +160,32 @@ Pytube allows you to filter on every property available (see the documentation f
|
|
156 |
To list the audio only streams:
|
157 |
|
158 |
```python
|
159 |
-
>>> yt.streams.filter(only_audio=True)
|
160 |
-
[<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">,
|
161 |
-
<Stream: itag="
|
162 |
-
<Stream: itag="
|
163 |
-
<Stream: itag="
|
164 |
-
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus">]
|
165 |
```
|
166 |
|
167 |
To list only ``mp4`` streams:
|
168 |
|
169 |
```python
|
170 |
>>> yt.streams.filter(subtype='mp4').all()
|
171 |
-
[<Stream: itag="
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
```
|
181 |
|
182 |
Multiple filters can also be specified:
|
@@ -185,20 +194,22 @@ Multiple filters can also be specified:
|
|
185 |
>>> yt.streams.filter(subtype='mp4', progressive=True).all()
|
186 |
>>> # this can also be expressed as:
|
187 |
>>> yt.streams.filter(subtype='mp4').filter(progressive=True).all()
|
188 |
-
[<Stream: itag="
|
189 |
-
<Stream: itag="
|
190 |
```
|
191 |
You also have an interface to select streams by their itag, without needing to filter:
|
192 |
|
193 |
```python
|
194 |
>>> yt.streams.get_by_itag(22)
|
195 |
-
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">
|
196 |
```
|
197 |
|
198 |
If you need to optimize for a specific feature, such as the "highest resolution" or "lowest average bitrate":
|
199 |
|
200 |
```python
|
201 |
>>> yt.streams.filter(progressive=True).order_by('resolution').desc().all()
|
|
|
|
|
202 |
```
|
203 |
Note that ``order_by`` cannot be used if your attribute is undefined in any of the Stream instances, so be sure to apply a filter to remove those before calling it.
|
204 |
|
@@ -227,12 +238,12 @@ pytube also ships with a tiny cli interface for downloading and probing videos.
|
|
227 |
Let's start with downloading:
|
228 |
|
229 |
```bash
|
230 |
-
$ pytube http://youtube.com/watch?v=
|
231 |
```
|
232 |
To view available streams:
|
233 |
|
234 |
```bash
|
235 |
-
$ pytube http://youtube.com/watch?v=
|
236 |
```
|
237 |
|
238 |
Finally, if you're filing a bug report, the cli contains a switch called ``--build-playback-report``, which bundles up the state, allowing others to easily replay your issue.
|
|
|
32 |
### Behold, a perfect balance of simplicity versus flexibility:
|
33 |
|
34 |
```python
|
35 |
+
>>> YouTube('https://youtu.be/2lAe1cqCOXo').streams.first().download()
|
36 |
+
>>> yt = YouTube('http://youtube.com/watch?v=2lAe1cqCOXo')
|
37 |
>>> yt.streams
|
38 |
... .filter(progressive=True, file_extension='mp4')
|
39 |
... .order_by('resolution')
|
|
|
67 |
|
68 |
```python
|
69 |
>>> from pytube import YouTube
|
70 |
+
>>> YouTube('https://youtube.com/watch?v=2lAe1cqCOXo').streams.first().download()
|
71 |
```
|
72 |
This example will download the highest quality progressive download stream available.
|
73 |
|
74 |
Next, let's explore how we would view what video streams are available:
|
75 |
|
76 |
```python
|
77 |
+
>>> yt = YouTube('https://youtube.com/watch?v=2lAe1cqCOXo')
|
78 |
+
>>> yt.streams
|
79 |
+
[<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">,
|
80 |
+
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">,
|
81 |
+
<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028" progressive="False" type="video">,
|
82 |
+
<Stream: itag="248" mime_type="video/webm" res="1080p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
83 |
+
<Stream: itag="399" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.08M.08" progressive="False" type="video">,
|
84 |
+
<Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f" progressive="False" type="video">,
|
85 |
+
<Stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
86 |
+
<Stream: itag="398" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.05M.08" progressive="False" type="video">,
|
87 |
+
<Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e" progressive="False" type="video">,
|
88 |
+
<Stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
89 |
+
<Stream: itag="397" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.04M.08" progressive="False" type="video">,
|
90 |
+
<Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e" progressive="False" type="video">,
|
91 |
+
<Stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
92 |
+
<Stream: itag="396" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.01M.08" progressive="False" type="video">,
|
93 |
+
<Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015" progressive="False" type="video">,
|
94 |
+
<Stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
95 |
+
<Stream: itag="395" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.00M.08" progressive="False" type="video">,
|
96 |
+
<Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c" progressive="False" type="video">,
|
97 |
+
<Stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
98 |
+
<Stream: itag="394" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.00M.08" progressive="False" type="video">,
|
99 |
+
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">,
|
100 |
+
<Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus" progressive="False" type="audio">,
|
101 |
+
<Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus" progressive="False" type="audio">,
|
102 |
+
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus" progressive="False" type="audio">]
|
103 |
```
|
104 |
You may notice that some streams listed have both a video codec and audio codec, while others have just video or just audio, this is a result of YouTube supporting a streaming technique called Dynamic Adaptive Streaming over HTTP (DASH).
|
105 |
|
|
|
110 |
To only view these progressive download streams:
|
111 |
|
112 |
```python
|
113 |
+
>>> yt.streams.filter(progressive=True)
|
114 |
+
[<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">,
|
115 |
+
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">]
|
|
|
|
|
|
|
116 |
```
|
117 |
|
118 |
Conversely, if you only want to see the DASH streams (also referred to as "adaptive") you can do:
|
119 |
|
120 |
```python
|
121 |
+
>>> yt.streams.filter(adaptive=True)
|
122 |
+
[<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028" progressive="False" type="video">,
|
123 |
+
<Stream: itag="248" mime_type="video/webm" res="1080p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
124 |
+
<Stream: itag="399" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.08M.08" progressive="False" type="video">,
|
125 |
+
<Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f" progressive="False" type="video">,
|
126 |
+
<Stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
127 |
+
<Stream: itag="398" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.05M.08" progressive="False" type="video">,
|
128 |
+
<Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e" progressive="False" type="video">,
|
129 |
+
<Stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
130 |
+
<Stream: itag="397" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.04M.08" progressive="False" type="video">,
|
131 |
+
<Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e" progressive="False" type="video">,
|
132 |
+
<Stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
133 |
+
<Stream: itag="396" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.01M.08" progressive="False" type="video">,
|
134 |
+
<Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015" progressive="False" type="video">,
|
135 |
+
<Stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
136 |
+
<Stream: itag="395" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.00M.08" progressive="False" type="video">,
|
137 |
+
<Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c" progressive="False" type="video">,
|
138 |
+
<Stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9" progressive="False" type="video">,
|
139 |
+
<Stream: itag="394" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.00M.08" progressive="False" type="video">,
|
140 |
+
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">,
|
141 |
+
<Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus" progressive="False" type="audio">,
|
142 |
+
<Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus" progressive="False" type="audio">,
|
143 |
+
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus" progressive="False" type="audio">]
|
144 |
```
|
145 |
|
146 |
You can also download a complete Youtube playlist:
|
|
|
160 |
To list the audio only streams:
|
161 |
|
162 |
```python
|
163 |
+
>>> yt.streams.filter(only_audio=True)
|
164 |
+
[<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">,
|
165 |
+
<Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus" progressive="False" type="audio">,
|
166 |
+
<Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus" progressive="False" type="audio">,
|
167 |
+
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus" progressive="False" type="audio">]
|
|
|
168 |
```
|
169 |
|
170 |
To list only ``mp4`` streams:
|
171 |
|
172 |
```python
|
173 |
>>> yt.streams.filter(subtype='mp4').all()
|
174 |
+
[<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">,
|
175 |
+
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">,
|
176 |
+
<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028" progressive="False" type="video">,
|
177 |
+
<Stream: itag="399" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.08M.08" progressive="False" type="video">,
|
178 |
+
<Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f" progressive="False" type="video">,
|
179 |
+
<Stream: itag="398" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.05M.08" progressive="False" type="video">,
|
180 |
+
<Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e" progressive="False" type="video">,
|
181 |
+
<Stream: itag="397" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.04M.08" progressive="False" type="video">,
|
182 |
+
<Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e" progressive="False" type="video">,
|
183 |
+
<Stream: itag="396" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.01M.08" progressive="False" type="video">,
|
184 |
+
<Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015" progressive="False" type="video">,
|
185 |
+
<Stream: itag="395" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.00M.08" progressive="False" type="video">,
|
186 |
+
<Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c" progressive="False" type="video">,
|
187 |
+
<Stream: itag="394" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.00M.08" progressive="False" type="video">,
|
188 |
+
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">]
|
189 |
```
|
190 |
|
191 |
Multiple filters can also be specified:
|
|
|
194 |
>>> yt.streams.filter(subtype='mp4', progressive=True).all()
|
195 |
>>> # this can also be expressed as:
|
196 |
>>> yt.streams.filter(subtype='mp4').filter(progressive=True).all()
|
197 |
+
[<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">,
|
198 |
+
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">]
|
199 |
```
|
200 |
You also have an interface to select streams by their itag, without needing to filter:
|
201 |
|
202 |
```python
|
203 |
>>> yt.streams.get_by_itag(22)
|
204 |
+
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">
|
205 |
```
|
206 |
|
207 |
If you need to optimize for a specific feature, such as the "highest resolution" or "lowest average bitrate":
|
208 |
|
209 |
```python
|
210 |
>>> yt.streams.filter(progressive=True).order_by('resolution').desc().all()
|
211 |
+
[<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">,
|
212 |
+
<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">]
|
213 |
```
|
214 |
Note that ``order_by`` cannot be used if your attribute is undefined in any of the Stream instances, so be sure to apply a filter to remove those before calling it.
|
215 |
|
|
|
238 |
Let's start with downloading:
|
239 |
|
240 |
```bash
|
241 |
+
$ pytube http://youtube.com/watch?v=2lAe1cqCOXo --itag=22
|
242 |
```
|
243 |
To view available streams:
|
244 |
|
245 |
```bash
|
246 |
+
$ pytube http://youtube.com/watch?v=2lAe1cqCOXo --list
|
247 |
```
|
248 |
|
249 |
Finally, if you're filing a bug report, the cli contains a switch called ``--build-playback-report``, which bundles up the state, allowing others to easily replay your issue.
|
pytube/__main__.py
CHANGED
@@ -9,7 +9,6 @@ smaller peripheral modules and functions.
|
|
9 |
"""
|
10 |
import json
|
11 |
import logging
|
12 |
-
from html import unescape
|
13 |
from typing import Dict
|
14 |
from typing import List
|
15 |
from typing import Optional
|
@@ -113,27 +112,17 @@ class YouTube:
|
|
113 |
:rtype: None
|
114 |
|
115 |
"""
|
116 |
-
logger.info("init started")
|
117 |
-
|
118 |
self.vid_info = dict(parse_qsl(self.vid_info_raw))
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
i_start = self.watch_html.lower().index("<title>") + len(
|
130 |
-
"<title>"
|
131 |
-
)
|
132 |
-
i_end = self.watch_html.lower().index("</title>")
|
133 |
-
title = self.watch_html[i_start:i_end].strip()
|
134 |
-
index = title.lower().rfind(" - youtube")
|
135 |
-
title = title[:index] if index > 0 else title
|
136 |
-
self.player_config_args["title"] = unescape(title)
|
137 |
|
138 |
# https://github.com/nficano/pytube/issues/165
|
139 |
stream_maps = ["url_encoded_fmt_stream_map"]
|
@@ -165,8 +154,6 @@ class YouTube:
|
|
165 |
self.stream_monostate.title = self.title
|
166 |
self.stream_monostate.duration = self.length
|
167 |
|
168 |
-
logger.info("init finished successfully")
|
169 |
-
|
170 |
def prefetch(self) -> None:
|
171 |
"""Eagerly download all necessary data.
|
172 |
|
@@ -280,9 +267,7 @@ class YouTube:
|
|
280 |
:rtype: str
|
281 |
|
282 |
"""
|
283 |
-
return self.
|
284 |
-
self.player_response.get("videoDetails", {}).get("title")
|
285 |
-
)
|
286 |
|
287 |
@property
|
288 |
def description(self) -> str:
|
|
|
9 |
"""
|
10 |
import json
|
11 |
import logging
|
|
|
12 |
from typing import Dict
|
13 |
from typing import List
|
14 |
from typing import Optional
|
|
|
112 |
:rtype: None
|
113 |
|
114 |
"""
|
|
|
|
|
115 |
self.vid_info = dict(parse_qsl(self.vid_info_raw))
|
116 |
+
self.player_config_args = self.vid_info
|
117 |
+
self.player_response = json.loads(self.vid_info['player_response'])
|
118 |
+
|
119 |
+
# On pre-signed videos, we need to use get_ytplayer_config to fix
|
120 |
+
# the player_response item
|
121 |
+
if 'streamingData' not in self.player_config_args['player_response']:
|
122 |
+
config_response = get_ytplayer_config(
|
123 |
+
self.watch_html
|
124 |
+
)['args']['player_response']
|
125 |
+
self.player_config_args['player_response'] = config_response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
|
127 |
# https://github.com/nficano/pytube/issues/165
|
128 |
stream_maps = ["url_encoded_fmt_stream_map"]
|
|
|
154 |
self.stream_monostate.title = self.title
|
155 |
self.stream_monostate.duration = self.length
|
156 |
|
|
|
|
|
157 |
def prefetch(self) -> None:
|
158 |
"""Eagerly download all necessary data.
|
159 |
|
|
|
267 |
:rtype: str
|
268 |
|
269 |
"""
|
270 |
+
return self.player_response['videoDetails']['title']
|
|
|
|
|
271 |
|
272 |
@property
|
273 |
def description(self) -> str:
|
pytube/extract.py
CHANGED
@@ -71,7 +71,6 @@ def video_info_url(video_id: str, watch_url: str) -> str:
|
|
71 |
params = OrderedDict(
|
72 |
[
|
73 |
("video_id", video_id),
|
74 |
-
("el", "$el"),
|
75 |
("ps", "default"),
|
76 |
("eurl", quote(watch_url)),
|
77 |
("hl", "en_US"),
|
@@ -160,6 +159,7 @@ def get_ytplayer_js(html: str) -> Any:
|
|
160 |
"""
|
161 |
js_url_patterns = [
|
162 |
r"\"jsUrl\":\"([^\"]*)\"",
|
|
|
163 |
]
|
164 |
for pattern in js_url_patterns:
|
165 |
regex = re.compile(pattern)
|
|
|
71 |
params = OrderedDict(
|
72 |
[
|
73 |
("video_id", video_id),
|
|
|
74 |
("ps", "default"),
|
75 |
("eurl", quote(watch_url)),
|
76 |
("hl", "en_US"),
|
|
|
159 |
"""
|
160 |
js_url_patterns = [
|
161 |
r"\"jsUrl\":\"([^\"]*)\"",
|
162 |
+
r"\"js\":\"([^\"]*base\.js)\""
|
163 |
]
|
164 |
for pattern in js_url_patterns:
|
165 |
regex = re.compile(pattern)
|
pytube/helpers.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""Various helper functions implemented by pytube."""
|
3 |
import functools
|
|
|
|
|
4 |
import logging
|
5 |
import os
|
6 |
import re
|
@@ -174,3 +176,44 @@ def uniqueify(duped_list: List) -> List:
|
|
174 |
seen[item] = True
|
175 |
result.append(item)
|
176 |
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""Various helper functions implemented by pytube."""
|
3 |
import functools
|
4 |
+
import gzip
|
5 |
+
import json
|
6 |
import logging
|
7 |
import os
|
8 |
import re
|
|
|
176 |
seen[item] = True
|
177 |
result.append(item)
|
178 |
return result
|
179 |
+
|
180 |
+
|
181 |
+
def create_mock_html_json(vid_id) -> Dict[str, Any]:
|
182 |
+
"""Generate a json.gz file with sample html responses.
|
183 |
+
|
184 |
+
:param str vid_id
|
185 |
+
YouTube video id
|
186 |
+
|
187 |
+
:return dict data
|
188 |
+
Dict used to generate the json.gz file
|
189 |
+
"""
|
190 |
+
from pytube import YouTube
|
191 |
+
gzip_filename = 'yt-video-%s-html.json.gz' % vid_id
|
192 |
+
|
193 |
+
# Get the pytube directory in order to navigate to /tests/mocks
|
194 |
+
pytube_dir_path = os.path.abspath(
|
195 |
+
os.path.join(
|
196 |
+
os.path.dirname(__file__),
|
197 |
+
os.path.pardir
|
198 |
+
)
|
199 |
+
)
|
200 |
+
pytube_mocks_path = os.path.join(pytube_dir_path, 'tests', 'mocks')
|
201 |
+
gzip_filepath = os.path.join(pytube_mocks_path, gzip_filename)
|
202 |
+
|
203 |
+
yt = YouTube(
|
204 |
+
'https://www.youtube.com/watch?v=%s' % vid_id,
|
205 |
+
defer_prefetch_init=True
|
206 |
+
)
|
207 |
+
yt.prefetch()
|
208 |
+
html_data = {
|
209 |
+
'url': yt.watch_url,
|
210 |
+
'js': yt.js,
|
211 |
+
'embed_html': yt.embed_html,
|
212 |
+
'watch_html': yt.watch_html,
|
213 |
+
'vid_info_raw': yt.vid_info_raw
|
214 |
+
}
|
215 |
+
|
216 |
+
with gzip.open(gzip_filepath, 'wb') as f:
|
217 |
+
f.write(json.dumps(html_data).encode('utf-8'))
|
218 |
+
|
219 |
+
return html_data
|
tests/conftest.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3 |
import gzip
|
4 |
import json
|
5 |
import os
|
|
|
6 |
|
7 |
import pytest
|
8 |
|
@@ -19,35 +20,41 @@ def load_playback_file(filename):
|
|
19 |
return json.loads(content)
|
20 |
|
21 |
|
22 |
-
|
|
|
23 |
"""Load a gzip json playback file and create YouTube instance."""
|
24 |
pb = load_playback_file(filename)
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
|
33 |
@pytest.fixture
|
34 |
def cipher_signature():
|
35 |
-
"""Youtube instance initialized with video id
|
36 |
-
filename = "yt-video-
|
37 |
return load_and_init_from_playback_file(filename)
|
38 |
|
39 |
|
40 |
@pytest.fixture
|
41 |
def presigned_video():
|
42 |
"""Youtube instance initialized with video id QRS8MkLhQmM."""
|
43 |
-
filename = "yt-video-QRS8MkLhQmM.json.gz"
|
44 |
return load_and_init_from_playback_file(filename)
|
45 |
|
46 |
|
47 |
@pytest.fixture
|
48 |
def age_restricted():
|
49 |
-
"""Youtube instance initialized with video id
|
50 |
-
filename = "yt-video-irauhITDrsE.json.gz"
|
51 |
return load_playback_file(filename)
|
52 |
|
53 |
|
@@ -83,8 +90,8 @@ def stream_dict():
|
|
83 |
file_path = os.path.join(
|
84 |
os.path.dirname(os.path.realpath(__file__)),
|
85 |
"mocks",
|
86 |
-
"yt-video-WXxV9g7lsFE.json.gz",
|
87 |
)
|
88 |
with gzip.open(file_path, "rb") as f:
|
89 |
-
content = f.read().decode("utf-8")
|
90 |
-
return
|
|
|
3 |
import gzip
|
4 |
import json
|
5 |
import os
|
6 |
+
from unittest import mock
|
7 |
|
8 |
import pytest
|
9 |
|
|
|
20 |
return json.loads(content)
|
21 |
|
22 |
|
23 |
+
@mock.patch('pytube.request.urlopen')
|
24 |
+
def load_and_init_from_playback_file(filename, mock_urlopen):
|
25 |
"""Load a gzip json playback file and create YouTube instance."""
|
26 |
pb = load_playback_file(filename)
|
27 |
+
|
28 |
+
# Mock the responses to YouTube
|
29 |
+
mock_url_open_object = mock.Mock()
|
30 |
+
mock_url_open_object.read.side_effect = [
|
31 |
+
pb['watch_html'].encode('utf-8'),
|
32 |
+
pb['vid_info_raw'].encode('utf-8'),
|
33 |
+
pb['js'].encode('utf-8')
|
34 |
+
]
|
35 |
+
mock_urlopen.return_value = mock_url_open_object
|
36 |
+
|
37 |
+
return YouTube(pb["url"])
|
38 |
|
39 |
|
40 |
@pytest.fixture
|
41 |
def cipher_signature():
|
42 |
+
"""Youtube instance initialized with video id 2lAe1cqCOXo."""
|
43 |
+
filename = "yt-video-2lAe1cqCOXo-html.json.gz"
|
44 |
return load_and_init_from_playback_file(filename)
|
45 |
|
46 |
|
47 |
@pytest.fixture
|
48 |
def presigned_video():
|
49 |
"""Youtube instance initialized with video id QRS8MkLhQmM."""
|
50 |
+
filename = "yt-video-QRS8MkLhQmM-html.json.gz"
|
51 |
return load_and_init_from_playback_file(filename)
|
52 |
|
53 |
|
54 |
@pytest.fixture
|
55 |
def age_restricted():
|
56 |
+
"""Youtube instance initialized with video id irauhITDrsE."""
|
57 |
+
filename = "yt-video-irauhITDrsE-html.json.gz"
|
58 |
return load_playback_file(filename)
|
59 |
|
60 |
|
|
|
90 |
file_path = os.path.join(
|
91 |
os.path.dirname(os.path.realpath(__file__)),
|
92 |
"mocks",
|
93 |
+
"yt-video-WXxV9g7lsFE-html.json.gz",
|
94 |
)
|
95 |
with gzip.open(file_path, "rb") as f:
|
96 |
+
content = json.loads(f.read().decode("utf-8"))
|
97 |
+
return content['watch_html']
|
tests/generate_fixture.py
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
# flake8: noqa: E402
|
3 |
-
import json
|
4 |
-
import sys
|
5 |
-
from os import path
|
6 |
-
|
7 |
-
from pytube import YouTube
|
8 |
-
|
9 |
-
currentdir = path.dirname(path.realpath(__file__))
|
10 |
-
parentdir = path.dirname(currentdir)
|
11 |
-
sys.path.append(parentdir)
|
12 |
-
|
13 |
-
|
14 |
-
yt = YouTube(sys.argv[1], defer_prefetch_init=True)
|
15 |
-
yt.prefetch()
|
16 |
-
output = {
|
17 |
-
"url": sys.argv[1],
|
18 |
-
"watch_html": yt.watch_html,
|
19 |
-
"video_info": yt.vid_info,
|
20 |
-
"js": yt.js,
|
21 |
-
"embed_html": yt.embed_html,
|
22 |
-
}
|
23 |
-
|
24 |
-
outpath = path.join(currentdir, "mocks", "yt-video-" + yt.video_id + ".json")
|
25 |
-
print("Writing to: " + outpath)
|
26 |
-
with open(outpath, "w") as f:
|
27 |
-
json.dump(output, f)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/mocks/yt-video-2lAe1cqCOXo-html.json.gz
ADDED
Binary file (609 kB). View file
|
|
tests/mocks/yt-video-9bZkp7q19f0.json.gz
DELETED
Binary file (646 kB)
|
|
tests/mocks/yt-video-QRS8MkLhQmM-html.json.gz
ADDED
Binary file (593 kB). View file
|
|
tests/mocks/yt-video-QRS8MkLhQmM.json.gz
DELETED
Binary file (472 kB)
|
|
tests/mocks/yt-video-WXxV9g7lsFE-html.json.gz
ADDED
Binary file (655 kB). View file
|
|
tests/mocks/yt-video-WXxV9g7lsFE.json.gz
DELETED
Binary file (55.7 kB)
|
|
tests/mocks/yt-video-irauhITDrsE-html.json.gz
ADDED
Binary file (550 kB). View file
|
|
tests/mocks/yt-video-irauhITDrsE.json.gz
DELETED
Binary file (38.5 kB)
|
|
tests/test_cli.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
import argparse
|
|
|
3 |
from unittest import mock
|
4 |
from unittest.mock import MagicMock
|
5 |
from unittest.mock import patch
|
@@ -472,12 +473,12 @@ def test_ffmpeg_downloader(unique_name, download, run, unlink):
|
|
472 |
[
|
473 |
"ffmpeg",
|
474 |
"-i",
|
475 |
-
"target
|
476 |
"-i",
|
477 |
-
"target
|
478 |
"-codec",
|
479 |
"copy",
|
480 |
-
"target
|
481 |
]
|
482 |
)
|
483 |
unlink.assert_called()
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
import argparse
|
3 |
+
import os
|
4 |
from unittest import mock
|
5 |
from unittest.mock import MagicMock
|
6 |
from unittest.mock import patch
|
|
|
473 |
[
|
474 |
"ffmpeg",
|
475 |
"-i",
|
476 |
+
os.path.join("target", "video_name.video_subtype"),
|
477 |
"-i",
|
478 |
+
os.path.join("target", "audio_name.audio_subtype"),
|
479 |
"-codec",
|
480 |
"copy",
|
481 |
+
os.path.join("target", "safe_title.video_subtype"),
|
482 |
]
|
483 |
)
|
484 |
unlink.assert_called()
|
tests/test_extract.py
CHANGED
@@ -7,9 +7,9 @@ from pytube.exceptions import RegexMatchError
|
|
7 |
|
8 |
|
9 |
def test_extract_video_id():
|
10 |
-
url = "https://www.youtube.com/watch?v=
|
11 |
video_id = extract.video_id(url)
|
12 |
-
assert video_id == "
|
13 |
|
14 |
|
15 |
def test_info_url(age_restricted):
|
@@ -29,16 +29,16 @@ def test_info_url_age_restricted(cipher_signature):
|
|
29 |
watch_url=cipher_signature.watch_url,
|
30 |
)
|
31 |
expected = (
|
32 |
-
"https://youtube.com/get_video_info?video_id=
|
33 |
"&ps=default&eurl=https%253A%2F%2Fyoutube.com%2Fwatch%253Fv%"
|
34 |
-
"
|
35 |
)
|
36 |
assert video_info_url == expected
|
37 |
|
38 |
|
39 |
def test_js_url(cipher_signature):
|
40 |
expected = (
|
41 |
-
"https://youtube.com/s/player/
|
42 |
)
|
43 |
result = extract.js_url(cipher_signature.watch_html)
|
44 |
assert expected == result
|
@@ -70,6 +70,12 @@ def test_get_ytplayer_config_with_no_match_should_error():
|
|
70 |
extract.get_ytplayer_config("")
|
71 |
|
72 |
|
|
|
|
|
|
|
|
|
|
|
73 |
def test_signature_cipher_does_not_error(stream_dict):
|
74 |
-
extract.
|
75 |
-
|
|
|
|
7 |
|
8 |
|
9 |
def test_extract_video_id():
|
10 |
+
url = "https://www.youtube.com/watch?v=2lAe1cqCOXo"
|
11 |
video_id = extract.video_id(url)
|
12 |
+
assert video_id == "2lAe1cqCOXo"
|
13 |
|
14 |
|
15 |
def test_info_url(age_restricted):
|
|
|
29 |
watch_url=cipher_signature.watch_url,
|
30 |
)
|
31 |
expected = (
|
32 |
+
"https://youtube.com/get_video_info?video_id=2lAe1cqCOXo"
|
33 |
"&ps=default&eurl=https%253A%2F%2Fyoutube.com%2Fwatch%253Fv%"
|
34 |
+
"253D2lAe1cqCOXo&hl=en_US"
|
35 |
)
|
36 |
assert video_info_url == expected
|
37 |
|
38 |
|
39 |
def test_js_url(cipher_signature):
|
40 |
expected = (
|
41 |
+
"https://youtube.com/s/player/9b65e980/player_ias.vflset/en_US/base.js"
|
42 |
)
|
43 |
result = extract.js_url(cipher_signature.watch_html)
|
44 |
assert expected == result
|
|
|
70 |
extract.get_ytplayer_config("")
|
71 |
|
72 |
|
73 |
+
def test_get_ytplayer_js_with_no_match_should_error():
|
74 |
+
with pytest.raises(RegexMatchError):
|
75 |
+
extract.get_ytplayer_js("")
|
76 |
+
|
77 |
+
|
78 |
def test_signature_cipher_does_not_error(stream_dict):
|
79 |
+
config_args = extract.get_ytplayer_config(stream_dict)['args']
|
80 |
+
extract.apply_descrambler(config_args, "url_encoded_fmt_stream_map")
|
81 |
+
assert "s" in config_args["url_encoded_fmt_stream_map"][0].keys()
|
tests/test_helpers.py
CHANGED
@@ -1,4 +1,7 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
2 |
import os
|
3 |
from unittest import mock
|
4 |
|
@@ -7,9 +10,11 @@ import pytest
|
|
7 |
from pytube import helpers
|
8 |
from pytube.exceptions import RegexMatchError
|
9 |
from pytube.helpers import cache
|
|
|
10 |
from pytube.helpers import deprecated
|
11 |
from pytube.helpers import setup_logger
|
12 |
from pytube.helpers import target_directory
|
|
|
13 |
|
14 |
|
15 |
def test_regex_search_no_match():
|
@@ -90,3 +95,75 @@ def test_setup_logger(logging):
|
|
90 |
logging.getLogger.assert_called_with("pytube")
|
91 |
logger.addHandler.assert_called()
|
92 |
logger.setLevel.assert_called_with(20)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
+
import gzip
|
3 |
+
import io
|
4 |
+
import json
|
5 |
import os
|
6 |
from unittest import mock
|
7 |
|
|
|
10 |
from pytube import helpers
|
11 |
from pytube.exceptions import RegexMatchError
|
12 |
from pytube.helpers import cache
|
13 |
+
from pytube.helpers import create_mock_html_json
|
14 |
from pytube.helpers import deprecated
|
15 |
from pytube.helpers import setup_logger
|
16 |
from pytube.helpers import target_directory
|
17 |
+
from pytube.helpers import uniqueify
|
18 |
|
19 |
|
20 |
def test_regex_search_no_match():
|
|
|
95 |
logging.getLogger.assert_called_with("pytube")
|
96 |
logger.addHandler.assert_called()
|
97 |
logger.setLevel.assert_called_with(20)
|
98 |
+
|
99 |
+
|
100 |
+
@mock.patch('builtins.open', new_callable=mock.mock_open)
|
101 |
+
@mock.patch('pytube.request.urlopen')
|
102 |
+
def test_create_mock_html_json(mock_url_open, mock_open):
|
103 |
+
video_id = '2lAe1cqCOXo'
|
104 |
+
gzip_html_filename = 'yt-video-%s-html.json.gz' % video_id
|
105 |
+
|
106 |
+
# Get the pytube directory in order to navigate to /tests/mocks
|
107 |
+
pytube_dir_path = os.path.abspath(
|
108 |
+
os.path.join(
|
109 |
+
os.path.dirname(__file__),
|
110 |
+
os.path.pardir
|
111 |
+
)
|
112 |
+
)
|
113 |
+
pytube_mocks_path = os.path.join(pytube_dir_path, 'tests', 'mocks')
|
114 |
+
gzip_html_filepath = os.path.join(pytube_mocks_path, gzip_html_filename)
|
115 |
+
|
116 |
+
# Mock the responses to YouTube
|
117 |
+
mock_url_open_object = mock.Mock()
|
118 |
+
|
119 |
+
# Order is:
|
120 |
+
# 1. watch_html -- must have js match
|
121 |
+
# 2. vid_info_raw
|
122 |
+
# 3. js
|
123 |
+
mock_url_open_object.read.side_effect = [
|
124 |
+
b'"jsUrl":"base.js"',
|
125 |
+
b'vid_info_raw',
|
126 |
+
b'js_result',
|
127 |
+
]
|
128 |
+
mock_url_open.return_value = mock_url_open_object
|
129 |
+
|
130 |
+
# Generate a json with sample html json
|
131 |
+
result_data = create_mock_html_json(video_id)
|
132 |
+
|
133 |
+
# Assert that a write was only made once
|
134 |
+
mock_open.assert_called_once_with(gzip_html_filepath, 'wb')
|
135 |
+
|
136 |
+
# The result data should look like this:
|
137 |
+
gzip_file = io.BytesIO()
|
138 |
+
with gzip.GzipFile(
|
139 |
+
filename=gzip_html_filename,
|
140 |
+
fileobj=gzip_file,
|
141 |
+
mode='wb'
|
142 |
+
) as f:
|
143 |
+
f.write(json.dumps(result_data).encode('utf-8'))
|
144 |
+
gzip_data = gzip_file.getvalue()
|
145 |
+
|
146 |
+
file_handle = mock_open.return_value.__enter__.return_value
|
147 |
+
|
148 |
+
# For some reason, write gets called multiple times, so we have to
|
149 |
+
# concatenate all the write calls to get the full data before we compare
|
150 |
+
# it to the BytesIO object value.
|
151 |
+
full_content = b''
|
152 |
+
for call in file_handle.write.call_args_list:
|
153 |
+
args, kwargs = call
|
154 |
+
full_content += b''.join(args)
|
155 |
+
|
156 |
+
# The file header includes time metadata, so *occasionally* a single
|
157 |
+
# byte will be off at the very beginning. In theory, this difference
|
158 |
+
# should only affect bytes 5-8 (or [4:8] because of zero-indexing),
|
159 |
+
# but I've excluded the 10-byte metadata header altogether from the
|
160 |
+
# check, just to be safe.
|
161 |
+
# Source: https://en.wikipedia.org/wiki/Gzip#File_format
|
162 |
+
assert gzip_data[10:] == full_content[10:]
|
163 |
+
|
164 |
+
|
165 |
+
def test_uniqueify():
|
166 |
+
non_unique_list = [1, 2, 3, 3, 4, 5]
|
167 |
+
expected = [1, 2, 3, 4, 5]
|
168 |
+
result = uniqueify(non_unique_list)
|
169 |
+
assert result == expected
|
tests/test_query.py
CHANGED
@@ -6,9 +6,9 @@ import pytest
|
|
6 |
@pytest.mark.parametrize(
|
7 |
("test_input", "expected"),
|
8 |
[
|
9 |
-
({"progressive": True}, [18]),
|
10 |
-
({"resolution": "720p"}, [136, 247]),
|
11 |
-
({"res": "720p"}, [136, 247]),
|
12 |
({"fps": 30, "resolution": "480p"}, [135, 244]),
|
13 |
({"mime_type": "audio/mp4"}, [140]),
|
14 |
({"type": "audio"}, [140, 249, 250, 251]),
|
@@ -115,7 +115,7 @@ def test_order_by_non_numerical_ascending(cipher_signature):
|
|
115 |
|
116 |
def test_order_by_with_none_values(cipher_signature):
|
117 |
abrs = [s.abr for s in cipher_signature.streams.order_by("abr").asc()]
|
118 |
-
assert abrs == ["50kbps", "70kbps", "96kbps", "128kbps", "160kbps"]
|
119 |
|
120 |
|
121 |
def test_get_by_itag(cipher_signature):
|
@@ -138,13 +138,13 @@ def test_get_lowest_resolution(cipher_signature):
|
|
138 |
|
139 |
|
140 |
def test_get_highest_resolution(cipher_signature):
|
141 |
-
assert cipher_signature.streams.get_highest_resolution().itag ==
|
142 |
|
143 |
|
144 |
def test_filter_is_dash(cipher_signature):
|
145 |
streams = cipher_signature.streams.filter(is_dash=False)
|
146 |
itags = [s.itag for s in streams]
|
147 |
-
assert itags == [18, 399, 398, 397, 396, 395, 394]
|
148 |
|
149 |
|
150 |
def test_get_audio_only(cipher_signature):
|
@@ -156,13 +156,13 @@ def test_get_audio_only_with_subtype(cipher_signature):
|
|
156 |
|
157 |
|
158 |
def test_sequence(cipher_signature):
|
159 |
-
assert len(cipher_signature.streams) ==
|
160 |
assert cipher_signature.streams[0] is not None
|
161 |
|
162 |
|
163 |
def test_otf(cipher_signature):
|
164 |
non_otf = cipher_signature.streams.otf()
|
165 |
-
assert len(non_otf) ==
|
166 |
|
167 |
otf = cipher_signature.streams.otf(True)
|
168 |
assert len(otf) == 0
|
|
|
6 |
@pytest.mark.parametrize(
|
7 |
("test_input", "expected"),
|
8 |
[
|
9 |
+
({"progressive": True}, [18, 22]),
|
10 |
+
({"resolution": "720p"}, [22, 136, 247]),
|
11 |
+
({"res": "720p"}, [22, 136, 247]),
|
12 |
({"fps": 30, "resolution": "480p"}, [135, 244]),
|
13 |
({"mime_type": "audio/mp4"}, [140]),
|
14 |
({"type": "audio"}, [140, 249, 250, 251]),
|
|
|
115 |
|
116 |
def test_order_by_with_none_values(cipher_signature):
|
117 |
abrs = [s.abr for s in cipher_signature.streams.order_by("abr").asc()]
|
118 |
+
assert abrs == ["50kbps", "70kbps", "96kbps", "128kbps", "160kbps", "192kbps"]
|
119 |
|
120 |
|
121 |
def test_get_by_itag(cipher_signature):
|
|
|
138 |
|
139 |
|
140 |
def test_get_highest_resolution(cipher_signature):
|
141 |
+
assert cipher_signature.streams.get_highest_resolution().itag == 22
|
142 |
|
143 |
|
144 |
def test_filter_is_dash(cipher_signature):
|
145 |
streams = cipher_signature.streams.filter(is_dash=False)
|
146 |
itags = [s.itag for s in streams]
|
147 |
+
assert itags == [18, 22, 399, 398, 397, 396, 395, 394]
|
148 |
|
149 |
|
150 |
def test_get_audio_only(cipher_signature):
|
|
|
156 |
|
157 |
|
158 |
def test_sequence(cipher_signature):
|
159 |
+
assert len(cipher_signature.streams) == 24
|
160 |
assert cipher_signature.streams[0] is not None
|
161 |
|
162 |
|
163 |
def test_otf(cipher_signature):
|
164 |
non_otf = cipher_signature.streams.otf()
|
165 |
+
assert len(non_otf) == 24
|
166 |
|
167 |
otf = cipher_signature.streams.otf(True)
|
168 |
assert len(otf) == 0
|
tests/test_streams.py
CHANGED
@@ -40,30 +40,25 @@ def test_filesize(cipher_signature):
|
|
40 |
def test_filesize_approx(cipher_signature):
|
41 |
stream = cipher_signature.streams[0]
|
42 |
|
43 |
-
assert stream.filesize_approx ==
|
44 |
stream.bitrate = None
|
45 |
assert stream.filesize_approx == 6796391
|
46 |
|
47 |
|
48 |
def test_default_filename(cipher_signature):
|
49 |
-
expected = "
|
50 |
stream = cipher_signature.streams[0]
|
51 |
assert stream.default_filename == expected
|
52 |
|
53 |
|
54 |
def test_title(cipher_signature):
|
55 |
expected = "title"
|
56 |
-
cipher_signature.player_config_args["title"] = expected
|
57 |
-
assert cipher_signature.title == expected
|
58 |
-
|
59 |
-
expected = "title2"
|
60 |
-
del cipher_signature.player_config_args["title"]
|
61 |
cipher_signature.player_response = {"videoDetails": {"title": expected}}
|
62 |
assert cipher_signature.title == expected
|
63 |
|
64 |
|
65 |
def test_expiration(cipher_signature):
|
66 |
-
assert cipher_signature.streams[0].expiration == datetime(2020, 10,
|
67 |
|
68 |
|
69 |
def test_caption_tracks(presigned_video):
|
@@ -76,34 +71,42 @@ def test_captions(presigned_video):
|
|
76 |
|
77 |
def test_description(cipher_signature):
|
78 |
expected = (
|
79 |
-
"
|
80 |
-
"
|
81 |
-
"
|
82 |
-
"
|
83 |
-
"
|
84 |
-
"
|
85 |
-
"
|
86 |
-
"
|
87 |
-
"
|
88 |
-
"
|
89 |
-
"
|
90 |
-
"
|
91 |
-
"
|
92 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
)
|
94 |
assert cipher_signature.description == expected
|
95 |
|
96 |
|
97 |
def test_rating(cipher_signature):
|
98 |
-
assert cipher_signature.rating ==
|
99 |
|
100 |
|
101 |
def test_length(cipher_signature):
|
102 |
-
assert cipher_signature.length ==
|
103 |
|
104 |
|
105 |
def test_views(cipher_signature):
|
106 |
-
assert cipher_signature.views
|
107 |
|
108 |
|
109 |
@mock.patch(
|
@@ -133,7 +136,7 @@ def test_download_with_prefix(cipher_signature):
|
|
133 |
file_path = stream.download(filename_prefix="prefix")
|
134 |
assert file_path == os.path.join(
|
135 |
"/target",
|
136 |
-
"
|
137 |
)
|
138 |
|
139 |
|
@@ -171,7 +174,7 @@ def test_download_with_existing(cipher_signature):
|
|
171 |
file_path = stream.download()
|
172 |
assert file_path == os.path.join(
|
173 |
"/target",
|
174 |
-
"
|
175 |
)
|
176 |
assert not request.stream.called
|
177 |
|
@@ -192,7 +195,7 @@ def test_download_with_existing_no_skip(cipher_signature):
|
|
192 |
file_path = stream.download(skip_existing=False)
|
193 |
assert file_path == os.path.join(
|
194 |
"/target",
|
195 |
-
"
|
196 |
)
|
197 |
assert request.stream.called
|
198 |
|
@@ -264,7 +267,7 @@ def test_thumbnail_when_in_details(cipher_signature):
|
|
264 |
|
265 |
|
266 |
def test_thumbnail_when_not_in_details(cipher_signature):
|
267 |
-
expected = "https://img.youtube.com/vi/
|
268 |
cipher_signature.player_response = {}
|
269 |
assert cipher_signature.thumbnail_url == expected
|
270 |
|
|
|
40 |
def test_filesize_approx(cipher_signature):
|
41 |
stream = cipher_signature.streams[0]
|
42 |
|
43 |
+
assert stream.filesize_approx == 28309811
|
44 |
stream.bitrate = None
|
45 |
assert stream.filesize_approx == 6796391
|
46 |
|
47 |
|
48 |
def test_default_filename(cipher_signature):
|
49 |
+
expected = "YouTube Rewind 2019 For the Record YouTubeRewind.mp4"
|
50 |
stream = cipher_signature.streams[0]
|
51 |
assert stream.default_filename == expected
|
52 |
|
53 |
|
54 |
def test_title(cipher_signature):
|
55 |
expected = "title"
|
|
|
|
|
|
|
|
|
|
|
56 |
cipher_signature.player_response = {"videoDetails": {"title": expected}}
|
57 |
assert cipher_signature.title == expected
|
58 |
|
59 |
|
60 |
def test_expiration(cipher_signature):
|
61 |
+
assert cipher_signature.streams[0].expiration == datetime(2020, 10, 30, 5, 39, 41)
|
62 |
|
63 |
|
64 |
def test_caption_tracks(presigned_video):
|
|
|
71 |
|
72 |
def test_description(cipher_signature):
|
73 |
expected = (
|
74 |
+
"In 2018, we made something you didn’t like. "
|
75 |
+
"For Rewind 2019, let’s see what you DID like.\n\n"
|
76 |
+
"Celebrating the creators, music and moments "
|
77 |
+
"that mattered most to you in 2019. \n\n"
|
78 |
+
"To learn how the top lists in Rewind were generated: "
|
79 |
+
"https://rewind.youtube/about\n\n"
|
80 |
+
"Top lists featured the following channels:\n\n"
|
81 |
+
"@1MILLION Dance Studio \n@A4 \n@Anaysa \n"
|
82 |
+
"@Andymation \n@Ariana Grande \n@Awez Darbar \n"
|
83 |
+
"@AzzyLand \n@Billie Eilish \n@Black Gryph0n \n"
|
84 |
+
"@BLACKPINK \n@ChapkisDanceUSA \n@Daddy Yankee \n"
|
85 |
+
"@David Dobrik \n@Dude Perfect \n@Felipe Neto \n"
|
86 |
+
"@Fischer's-フィッシャーズ- \n@Galen Hooks \n@ibighit \n"
|
87 |
+
"@James Charles \n@jeffreestar \n@Jelly \n@Kylie Jenner \n"
|
88 |
+
"@LazarBeam \n@Lil Dicky \n@Lil Nas X \n@LOUD \n@LOUD Babi \n"
|
89 |
+
"@LOUD Coringa \n@Magnet World \n@MrBeast \n"
|
90 |
+
"@Nilson Izaias Papinho Oficial \n@Noah Schnapp\n"
|
91 |
+
"@백종원의 요리비책 Paik's Cuisine \n@Pencilmation \n@PewDiePie \n"
|
92 |
+
"@SethEverman \n@shane \n@Shawn Mendes \n@Team Naach \n"
|
93 |
+
"@whinderssonnunes \n@워크맨-Workman \n@하루한끼 one meal a day \n\n"
|
94 |
+
"To see the full list of featured channels in Rewind 2019, "
|
95 |
+
"visit: https://rewind.youtube/about"
|
96 |
)
|
97 |
assert cipher_signature.description == expected
|
98 |
|
99 |
|
100 |
def test_rating(cipher_signature):
|
101 |
+
assert cipher_signature.rating == 2.0860765
|
102 |
|
103 |
|
104 |
def test_length(cipher_signature):
|
105 |
+
assert cipher_signature.length == 337
|
106 |
|
107 |
|
108 |
def test_views(cipher_signature):
|
109 |
+
assert cipher_signature.views >= 108531745
|
110 |
|
111 |
|
112 |
@mock.patch(
|
|
|
136 |
file_path = stream.download(filename_prefix="prefix")
|
137 |
assert file_path == os.path.join(
|
138 |
"/target",
|
139 |
+
"prefixYouTube Rewind 2019 For the Record YouTubeRewind.mp4"
|
140 |
)
|
141 |
|
142 |
|
|
|
174 |
file_path = stream.download()
|
175 |
assert file_path == os.path.join(
|
176 |
"/target",
|
177 |
+
"YouTube Rewind 2019 For the Record YouTubeRewind.mp4"
|
178 |
)
|
179 |
assert not request.stream.called
|
180 |
|
|
|
195 |
file_path = stream.download(skip_existing=False)
|
196 |
assert file_path == os.path.join(
|
197 |
"/target",
|
198 |
+
"YouTube Rewind 2019 For the Record YouTubeRewind.mp4"
|
199 |
)
|
200 |
assert request.stream.called
|
201 |
|
|
|
267 |
|
268 |
|
269 |
def test_thumbnail_when_not_in_details(cipher_signature):
|
270 |
+
expected = "https://img.youtube.com/vi/2lAe1cqCOXo/maxresdefault.jpg"
|
271 |
cipher_signature.player_response = {}
|
272 |
assert cipher_signature.thumbnail_url == expected
|
273 |
|