Taylor Fox Dahlin commited on
Commit
d68bda8
·
unverified ·
1 Parent(s): 1d9ce0c

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 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/9bZkp7q19f0').streams.first().download()
36
- >>> yt = YouTube('http://youtube.com/watch?v=9bZkp7q19f0')
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('http://youtube.com/watch?v=9bZkp7q19f0').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('http://youtube.com/watch?v=9bZkp7q19f0')
78
- >>> yt.streams.all()
79
- [<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">,
80
- <Stream: itag="43" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp8.0" acodec="vorbis">,
81
- <Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">,
82
- <Stream: itag="36" mime_type="video/3gpp" res="240p" fps="30fps" vcodec="mp4v.20.3" acodec="mp4a.40.2">,
83
- <Stream: itag="17" mime_type="video/3gpp" res="144p" fps="30fps" vcodec="mp4v.20.3" acodec="mp4a.40.2">,
84
- <Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">,
85
- <Stream: itag="248" mime_type="video/webm" res="1080p" fps="30fps" vcodec="vp9">,
86
- <Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">,
87
- <Stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9">,
88
- <Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">,
89
- <Stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9">,
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="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">,
93
- <Stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9">,
94
- <Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">,
95
- <Stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9">,
96
- <Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">,
97
- <Stream: itag="171" mime_type="audio/webm" abr="128kbps" acodec="vorbis">,
98
- <Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus">,
99
- <Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus">,
100
- <Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus">]
 
 
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).all()
112
- [<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">,
113
- <Stream: itag="43" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp8.0" acodec="vorbis">,
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).all()
123
- [<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">,
124
- <Stream: itag="248" mime_type="video/webm" res="1080p" fps="30fps" vcodec="vp9">,
125
- <Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">,
126
- <Stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9">,
127
- <Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">,
128
- <Stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9">,
129
- <Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">,
130
- <Stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9">,
131
- <Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">,
132
- <Stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9">,
133
- <Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">,
134
- <Stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9">,
135
- <Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">,
136
- <Stream: itag="171" mime_type="audio/webm" abr="128kbps" acodec="vorbis">,
137
- <Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus">,
138
- <Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus">,
139
- <Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus">]
 
 
 
 
 
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).all()
160
- [<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">,
161
- <Stream: itag="171" mime_type="audio/webm" abr="128kbps" acodec="vorbis">,
162
- <Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus">,
163
- <Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus">,
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="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">,
172
- <Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">,
173
- <Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">,
174
- <Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">,
175
- <Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">,
176
- <Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">,
177
- <Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">,
178
- <Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">,
179
- <Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">]
 
 
 
 
 
 
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="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">,
189
- <Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">]
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=9bZkp7q19f0 --itag=22
231
  ```
232
  To view available streams:
233
 
234
  ```bash
235
- $ pytube http://youtube.com/watch?v=9bZkp7q19f0 --list
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
- if self.age_restricted:
120
- self.player_config_args = self.vid_info
121
- else:
122
- assert self.watch_html is not None
123
- self.player_config_args = get_ytplayer_config(self.watch_html)[
124
- "args"
125
- ]
126
-
127
- # Fix for KeyError: 'title' issue #434
128
- if "title" not in self.player_config_args: # type: ignore
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.player_config_args.get("title") or (
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
- def load_and_init_from_playback_file(filename):
 
23
  """Load a gzip json playback file and create YouTube instance."""
24
  pb = load_playback_file(filename)
25
- yt = YouTube(pb["url"], defer_prefetch_init=True)
26
- yt.watch_html = pb["watch_html"]
27
- yt.js = pb["js"]
28
- yt.vid_info = pb["video_info"]
29
- yt.descramble()
30
- return yt
 
 
 
 
 
31
 
32
 
33
  @pytest.fixture
34
  def cipher_signature():
35
- """Youtube instance initialized with video id 9bZkp7q19f0."""
36
- filename = "yt-video-9bZkp7q19f0.json.gz"
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 zRbsm3e2ltw."""
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 json.loads(content)
 
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/video_name.video_subtype",
476
  "-i",
477
- "target/audio_name.audio_subtype",
478
  "-codec",
479
  "copy",
480
- "target/safe_title.video_subtype",
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=9bZkp7q19f0"
11
  video_id = extract.video_id(url)
12
- assert video_id == "9bZkp7q19f0"
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=9bZkp7q19f0&el=%24el"
33
  "&ps=default&eurl=https%253A%2F%2Fyoutube.com%2Fwatch%253Fv%"
34
- "253D9bZkp7q19f0&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/4a1799bd/player_ias.vflset/en_US/base.js"
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.apply_descrambler(stream_dict, "url_encoded_fmt_stream_map")
75
- assert "s" in stream_dict["url_encoded_fmt_stream_map"][0].keys()
 
 
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 == 18
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) == 23
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) == 23
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 == 22350604
44
  stream.bitrate = None
45
  assert stream.filesize_approx == 6796391
46
 
47
 
48
  def test_default_filename(cipher_signature):
49
- expected = "PSY - GANGNAM STYLE(강남스타일) MV.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_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, 24, 11, 7, 41)
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
- "PSY - ‘I LUV IT’ M/V @ https://youtu.be/Xvjnoagk6GU\n"
80
- "PSY - ‘New FaceM/V @https://youtu.be/OwJPPaEyqhI\n\n"
81
- "PSY - 8TH ALBUM '4X2=8' on iTunes @\n"
82
- "https://smarturl.it/PSY_8thAlbum\n\n"
83
- "PSY - GANGNAM STYLE(강남스타일) on iTunes @ "
84
- "http://smarturl.it/PsyGangnam\n\n"
85
- "#PSY #싸이 #GANGNAMSTYLE #강남스타일\n\n"
86
- "More about PSY@\nhttp://www.youtube.com/officialpsy\n"
87
- "http://www.facebook.com/officialpsy\n"
88
- "http://twitter.com/psy_oppa\n"
89
- "https://www.instagram.com/42psy42\n"
90
- "http://iTunes.com/PSY\n"
91
- "http://sptfy.com/PSY\n"
92
- "http://weibo.com/psyoppa"
 
 
 
 
 
 
 
 
93
  )
94
  assert cipher_signature.description == expected
95
 
96
 
97
  def test_rating(cipher_signature):
98
- assert cipher_signature.rating == 4.5375643
99
 
100
 
101
  def test_length(cipher_signature):
102
- assert cipher_signature.length == 252
103
 
104
 
105
  def test_views(cipher_signature):
106
- assert cipher_signature.views == 3830838693
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
- "prefixPSY - GANGNAM STYLE(강남스타일) MV.mp4"
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
- "PSY - GANGNAM STYLE(강남스타일) MV.mp4"
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
- "PSY - GANGNAM STYLE(강남스타일) MV.mp4"
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/9bZkp7q19f0/maxresdefault.jpg"
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, lets 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