more coverage
Browse files- docs/index.rst +1 -1
- pytube/cipher.py +11 -10
- pytube/cli.py +1 -1
- pytube/extract.py +12 -5
- pytube/query.py +62 -23
- pytube/streams.py +3 -2
- tests/requirements.txt +1 -0
- tests/test_streams.py +40 -0
docs/index.rst
CHANGED
@@ -36,7 +36,7 @@ Release v\ |version|. (:ref:`Installation <install>`)
|
|
36 |
>>> YouTube('http://youtube.com/watch?v=9bZkp7q19f0').streams.first().download()
|
37 |
>>> yt = YouTube('http://youtube.com/watch?v=9bZkp7q19f0')
|
38 |
>>> yt.streams
|
39 |
-
... .filter(progressive=True,
|
40 |
... .order_by('resolution')
|
41 |
... .desc()
|
42 |
... .first()
|
|
|
36 |
>>> YouTube('http://youtube.com/watch?v=9bZkp7q19f0').streams.first().download()
|
37 |
>>> yt = YouTube('http://youtube.com/watch?v=9bZkp7q19f0')
|
38 |
>>> yt.streams
|
39 |
+
... .filter(progressive=True, file_extension='mp4')
|
40 |
... .order_by('resolution')
|
41 |
... .desc()
|
42 |
... .first()
|
pytube/cipher.py
CHANGED
@@ -51,7 +51,7 @@ def get_transform_plan(js):
|
|
51 |
:param str js:
|
52 |
The contents of the base.js asset file.
|
53 |
|
54 |
-
Sample Output
|
55 |
|
56 |
.. code-block:: python
|
57 |
|
@@ -85,7 +85,7 @@ def get_transform_object(js, var):
|
|
85 |
The obfuscated variable name that stores an object with all functions
|
86 |
that descrambles the signature.
|
87 |
|
88 |
-
Sample Output
|
89 |
|
90 |
.. code-block:: python
|
91 |
|
@@ -138,7 +138,7 @@ def reverse(arr, b):
|
|
138 |
This method takes an unused ``b`` variable as their transform functions
|
139 |
universally sent two arguments.
|
140 |
|
141 |
-
Example usage
|
142 |
~~~~~~~~~~~~~~
|
143 |
>>> reverse([1, 2, 3, 4])
|
144 |
[4, 3, 2, 1]
|
@@ -155,7 +155,7 @@ def splice(arr, b):
|
|
155 |
|
156 |
function(a, b) { a.splice(0, b) }.
|
157 |
|
158 |
-
Example usage
|
159 |
~~~~~~~~~~~~~~
|
160 |
>>> splice([1, 2, 3, 4], 2)
|
161 |
[1, 2]
|
@@ -172,7 +172,7 @@ def swap(arr, b):
|
|
172 |
|
173 |
function(a, b) { var c=a[0];a[0]=a[b%a.length];a[b]=c }.
|
174 |
|
175 |
-
Example usage
|
176 |
~~~~~~~~~~~~~~
|
177 |
>>> swap([1, 2, 3, 4], 2)
|
178 |
[3, 2, 1, 4]
|
@@ -209,7 +209,7 @@ def map_functions(js_func):
|
|
209 |
def parse_function(js_func):
|
210 |
"""Parse the Javascript transform function.
|
211 |
|
212 |
-
Break a JavaScript transform function down into a two element tuple
|
213 |
containing the function name and some integer-based argument.
|
214 |
|
215 |
Sample input:
|
@@ -228,21 +228,22 @@ def parse_function(js_func):
|
|
228 |
The JavaScript version of the transform function.
|
229 |
|
230 |
"""
|
231 |
-
pattern = r'\w+\.(\w+)\(\w,(\d+)\)'
|
232 |
logger.debug('parsing transform function')
|
233 |
-
return regex_search(
|
234 |
|
235 |
|
236 |
def get_signature(js, ciphered_signature):
|
237 |
"""Decipher the signature.
|
238 |
|
239 |
-
Taking the ciphered signature, applies the transform functions
|
240 |
-
returns the decrypted version.
|
241 |
|
242 |
:param str js:
|
243 |
The contents of the base.js asset file.
|
244 |
:param str ciphered_signature:
|
245 |
The ciphered signature sent in the ``player_config``.
|
|
|
|
|
|
|
246 |
|
247 |
"""
|
248 |
tplan = get_transform_plan(js)
|
|
|
51 |
:param str js:
|
52 |
The contents of the base.js asset file.
|
53 |
|
54 |
+
**Sample Output**:
|
55 |
|
56 |
.. code-block:: python
|
57 |
|
|
|
85 |
The obfuscated variable name that stores an object with all functions
|
86 |
that descrambles the signature.
|
87 |
|
88 |
+
**Sample Output**:
|
89 |
|
90 |
.. code-block:: python
|
91 |
|
|
|
138 |
This method takes an unused ``b`` variable as their transform functions
|
139 |
universally sent two arguments.
|
140 |
|
141 |
+
**Example usage**:
|
142 |
~~~~~~~~~~~~~~
|
143 |
>>> reverse([1, 2, 3, 4])
|
144 |
[4, 3, 2, 1]
|
|
|
155 |
|
156 |
function(a, b) { a.splice(0, b) }.
|
157 |
|
158 |
+
**Example usage**:
|
159 |
~~~~~~~~~~~~~~
|
160 |
>>> splice([1, 2, 3, 4], 2)
|
161 |
[1, 2]
|
|
|
172 |
|
173 |
function(a, b) { var c=a[0];a[0]=a[b%a.length];a[b]=c }.
|
174 |
|
175 |
+
**Example usage**:
|
176 |
~~~~~~~~~~~~~~
|
177 |
>>> swap([1, 2, 3, 4], 2)
|
178 |
[3, 2, 1, 4]
|
|
|
209 |
def parse_function(js_func):
|
210 |
"""Parse the Javascript transform function.
|
211 |
|
212 |
+
Break a JavaScript transform function down into a two element ``tuple``
|
213 |
containing the function name and some integer-based argument.
|
214 |
|
215 |
Sample input:
|
|
|
228 |
The JavaScript version of the transform function.
|
229 |
|
230 |
"""
|
|
|
231 |
logger.debug('parsing transform function')
|
232 |
+
return regex_search(r'\w+\.(\w+)\(\w,(\d+)\)', js_func, groups=True)
|
233 |
|
234 |
|
235 |
def get_signature(js, ciphered_signature):
|
236 |
"""Decipher the signature.
|
237 |
|
238 |
+
Taking the ciphered signature, applies the transform functions.
|
|
|
239 |
|
240 |
:param str js:
|
241 |
The contents of the base.js asset file.
|
242 |
:param str ciphered_signature:
|
243 |
The ciphered signature sent in the ``player_config``.
|
244 |
+
:rtype: str
|
245 |
+
:returns:
|
246 |
+
Decrypted signature required to download the media content.
|
247 |
|
248 |
"""
|
249 |
tplan = get_transform_plan(js)
|
pytube/cli.py
CHANGED
@@ -139,7 +139,7 @@ def on_progress(stream, file_handle, bytes_remaining):
|
|
139 |
:param file_handle:
|
140 |
The file handle where the media is being written to.
|
141 |
:type file_handle:
|
142 |
-
:class:`
|
143 |
:param int bytes_remaining:
|
144 |
How many bytes have been downloaded.
|
145 |
|
|
|
139 |
:param file_handle:
|
140 |
The file handle where the media is being written to.
|
141 |
:type file_handle:
|
142 |
+
:py:class:`io.BufferedWriter`
|
143 |
:param int bytes_remaining:
|
144 |
How many bytes have been downloaded.
|
145 |
|
pytube/extract.py
CHANGED
@@ -75,12 +75,17 @@ def js_url(watch_html):
|
|
75 |
def mime_type_codec(mime_type_codec):
|
76 |
"""Parse the type data.
|
77 |
|
78 |
-
Breaks up the data in the ``type`` key of the manifest, which contains
|
79 |
-
type and codecs serialized together
|
80 |
-
|
81 |
|
82 |
:param str mime_type_codec:
|
83 |
-
String containing mime type and codecs
|
|
|
|
|
|
|
|
|
|
|
84 |
|
85 |
"""
|
86 |
pattern = r'(\w+\/\w+)\;\scodecs=\"([a-zA-Z-0-9.,\s]*)\"'
|
@@ -97,7 +102,9 @@ def get_ytplayer_config(watch_html):
|
|
97 |
|
98 |
:param str watch_html:
|
99 |
The html contents of the watch page.
|
100 |
-
|
|
|
|
|
101 |
"""
|
102 |
pattern = r';ytplayer\.config\s*=\s*({.*?});'
|
103 |
yt_player_config = regex_search(pattern, watch_html, group=1)
|
|
|
75 |
def mime_type_codec(mime_type_codec):
|
76 |
"""Parse the type data.
|
77 |
|
78 |
+
Breaks up the data in the ``type`` key of the manifest, which contains the
|
79 |
+
mime type and codecs serialized together, and splits them into separate
|
80 |
+
elements.
|
81 |
|
82 |
:param str mime_type_codec:
|
83 |
+
String containing mime type and codecs, for example:
|
84 |
+
'audio/webm; codecs="opus"'
|
85 |
+
:rtype: tuple
|
86 |
+
:returns:
|
87 |
+
The mime type and a list of codecs, for example:
|
88 |
+
('audio/webm', ['opus'])
|
89 |
|
90 |
"""
|
91 |
pattern = r'(\w+\/\w+)\;\scodecs=\"([a-zA-Z-0-9.,\s]*)\"'
|
|
|
102 |
|
103 |
:param str watch_html:
|
104 |
The html contents of the watch page.
|
105 |
+
:rtype: str
|
106 |
+
:returns:
|
107 |
+
Substring of the html containing the serialized manifest data.
|
108 |
"""
|
109 |
pattern = r';ytplayer\.config\s*=\s*({.*?});'
|
110 |
yt_player_config = regex_search(pattern, watch_html, group=1)
|
pytube/query.py
CHANGED
@@ -21,43 +21,82 @@ class StreamQuery:
|
|
21 |
):
|
22 |
"""Apply the given filtering criterion.
|
23 |
|
24 |
-
:param
|
25 |
-
(optional) The frames per second
|
26 |
-
:
|
|
|
|
|
|
|
27 |
(optional) Alias to ``res``.
|
28 |
-
:
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
(optional) Two-part identifier for file formats and format contents
|
32 |
-
composed of a "type", a "subtype"
|
33 |
-
:
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
(optional) Alias to ``sub_type``.
|
39 |
-
:
|
|
|
|
|
|
|
40 |
(optional) Average bitrate (ABR) refers to the average amount of
|
41 |
-
data transferred per unit of time (e.g.: 64kbps, 192kbps)
|
42 |
-
:
|
|
|
|
|
|
|
43 |
(optional) Alias to ``abr``.
|
44 |
-
:
|
45 |
-
|
46 |
-
|
47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
:param bool progressive:
|
49 |
Excludes adaptive streams (one file contains both audio and video
|
50 |
tracks).
|
|
|
51 |
:param bool adaptive:
|
52 |
Excludes progressive streams (audio and video are on separate
|
53 |
tracks).
|
|
|
54 |
:param bool only_audio:
|
55 |
Excludes streams with video tracks.
|
|
|
56 |
:param bool only_video:
|
57 |
Excludes streams with audio tracks.
|
58 |
-
|
|
|
59 |
(optional) Interface for defining complex filters without
|
60 |
subclassing.
|
|
|
|
|
61 |
|
62 |
"""
|
63 |
filters = []
|
@@ -135,7 +174,7 @@ class StreamQuery:
|
|
135 |
return self
|
136 |
|
137 |
def get_by_itag(self, itag):
|
138 |
-
"""Get a :class:`Stream <Stream>` for an itag, or None if not found.
|
139 |
|
140 |
:param str itag:
|
141 |
YouTube format identifier code.
|
@@ -149,7 +188,7 @@ class StreamQuery:
|
|
149 |
def first(self):
|
150 |
"""Get the first element in the results.
|
151 |
|
152 |
-
Return the first result of this query or None if the result doesn't
|
153 |
contain any streams.
|
154 |
|
155 |
"""
|
@@ -161,7 +200,7 @@ class StreamQuery:
|
|
161 |
def last(self):
|
162 |
"""Get the last element in the results.
|
163 |
|
164 |
-
Return the last result of this query or None if the result doesn't
|
165 |
contain any streams.
|
166 |
|
167 |
"""
|
|
|
21 |
):
|
22 |
"""Apply the given filtering criterion.
|
23 |
|
24 |
+
:param fps:
|
25 |
+
(optional) The frames per second.
|
26 |
+
:type fps:
|
27 |
+
int or None
|
28 |
+
|
29 |
+
:param resolution:
|
30 |
(optional) Alias to ``res``.
|
31 |
+
:type res:
|
32 |
+
str or None
|
33 |
+
|
34 |
+
:param res:
|
35 |
+
(optional) The video resolution.
|
36 |
+
:type resolution:
|
37 |
+
str or None
|
38 |
+
|
39 |
+
:param mime_type:
|
40 |
(optional) Two-part identifier for file formats and format contents
|
41 |
+
composed of a "type", a "subtype".
|
42 |
+
:type mime_type:
|
43 |
+
str or None
|
44 |
+
|
45 |
+
:param type:
|
46 |
+
(optional) Type part of the ``mime_type`` (e.g.: audio, video).
|
47 |
+
:type type:
|
48 |
+
str or None
|
49 |
+
|
50 |
+
:param subtype:
|
51 |
+
(optional) Sub-type part of the ``mime_type`` (e.g.: mp4, mov).
|
52 |
+
:type subtype:
|
53 |
+
str or None
|
54 |
+
|
55 |
+
:param file_extension:
|
56 |
(optional) Alias to ``sub_type``.
|
57 |
+
:type file_extension:
|
58 |
+
str or None
|
59 |
+
|
60 |
+
:param abr:
|
61 |
(optional) Average bitrate (ABR) refers to the average amount of
|
62 |
+
data transferred per unit of time (e.g.: 64kbps, 192kbps).
|
63 |
+
:type abr:
|
64 |
+
str or None
|
65 |
+
|
66 |
+
:param bitrate:
|
67 |
(optional) Alias to ``abr``.
|
68 |
+
:type bitrate:
|
69 |
+
str or None
|
70 |
+
|
71 |
+
:param video_codec:
|
72 |
+
(optional) Video compression format.
|
73 |
+
:type video_codec:
|
74 |
+
str or None
|
75 |
+
|
76 |
+
:param audio_codec:
|
77 |
+
(optional) Audio compression format.
|
78 |
+
:type audio_codec:
|
79 |
+
str or None
|
80 |
+
|
81 |
:param bool progressive:
|
82 |
Excludes adaptive streams (one file contains both audio and video
|
83 |
tracks).
|
84 |
+
|
85 |
:param bool adaptive:
|
86 |
Excludes progressive streams (audio and video are on separate
|
87 |
tracks).
|
88 |
+
|
89 |
:param bool only_audio:
|
90 |
Excludes streams with video tracks.
|
91 |
+
|
92 |
:param bool only_video:
|
93 |
Excludes streams with audio tracks.
|
94 |
+
|
95 |
+
:param custom_filter_functions:
|
96 |
(optional) Interface for defining complex filters without
|
97 |
subclassing.
|
98 |
+
:type custom_filter_functions:
|
99 |
+
list or None
|
100 |
|
101 |
"""
|
102 |
filters = []
|
|
|
174 |
return self
|
175 |
|
176 |
def get_by_itag(self, itag):
|
177 |
+
"""Get a :class:`Stream <Stream>` for an itag, or ``None`` if not found.
|
178 |
|
179 |
:param str itag:
|
180 |
YouTube format identifier code.
|
|
|
188 |
def first(self):
|
189 |
"""Get the first element in the results.
|
190 |
|
191 |
+
Return the first result of this query or ``None`` if the result doesn't
|
192 |
contain any streams.
|
193 |
|
194 |
"""
|
|
|
200 |
def last(self):
|
201 |
"""Get the last element in the results.
|
202 |
|
203 |
+
Return the last result of this query or ``None`` if the result doesn't
|
204 |
contain any streams.
|
205 |
|
206 |
"""
|
pytube/streams.py
CHANGED
@@ -183,7 +183,7 @@ class Stream(object):
|
|
183 |
:param file_handle:
|
184 |
The file handle where the media is being written to.
|
185 |
:type file_handle:
|
186 |
-
:class
|
187 |
:param int bytes_remaining:
|
188 |
The delta between the total file size in bytes and amount already
|
189 |
downloaded.
|
@@ -206,10 +206,11 @@ class Stream(object):
|
|
206 |
|
207 |
def on_complete(self, file_handle):
|
208 |
"""On download complete handler function.
|
|
|
209 |
:param file_handle:
|
210 |
The file handle where the media is being written to.
|
211 |
:type file_handle:
|
212 |
-
:class
|
213 |
|
214 |
"""
|
215 |
logger.debug('download finished')
|
|
|
183 |
:param file_handle:
|
184 |
The file handle where the media is being written to.
|
185 |
:type file_handle:
|
186 |
+
:py:class:`io.BufferedWriter`
|
187 |
:param int bytes_remaining:
|
188 |
The delta between the total file size in bytes and amount already
|
189 |
downloaded.
|
|
|
206 |
|
207 |
def on_complete(self, file_handle):
|
208 |
"""On download complete handler function.
|
209 |
+
|
210 |
:param file_handle:
|
211 |
The file handle where the media is being written to.
|
212 |
:type file_handle:
|
213 |
+
:py:class:`io.BufferedWriter`
|
214 |
|
215 |
"""
|
216 |
logger.debug('download finished')
|
tests/requirements.txt
CHANGED
@@ -5,6 +5,7 @@ mock==1.3.0
|
|
5 |
pre-commit==0.15.0
|
6 |
pytest==3.1.3
|
7 |
pytest-cov==2.4.0
|
|
|
8 |
pytest-sugar==0.9.0
|
9 |
sphinx==1.6.4
|
10 |
sphinx-rtd-theme==0.2.4
|
|
|
5 |
pre-commit==0.15.0
|
6 |
pytest==3.1.3
|
7 |
pytest-cov==2.4.0
|
8 |
+
pytest-mock==1.6.3
|
9 |
pytest-sugar==0.9.0
|
10 |
sphinx==1.6.4
|
11 |
sphinx-rtd-theme==0.2.4
|
tests/test_streams.py
CHANGED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
import random
|
3 |
+
|
4 |
+
import mock
|
5 |
+
|
6 |
+
from pytube import request
|
7 |
+
|
8 |
+
|
9 |
+
def test_filesize(gangnam_style, mocker):
|
10 |
+
mocker.patch.object(request, 'get')
|
11 |
+
request.get.return_value = {'Content-Length': '6796391'}
|
12 |
+
assert gangnam_style.streams.first().filesize == 6796391
|
13 |
+
|
14 |
+
|
15 |
+
def test_default_filename(gangnam_style):
|
16 |
+
expected = 'PSY - GANGNAM STYLE(강남스타일) MV.mp4'
|
17 |
+
stream = gangnam_style.streams.first()
|
18 |
+
assert stream.default_filename == expected
|
19 |
+
|
20 |
+
|
21 |
+
def test_download(gangnam_style, mocker):
|
22 |
+
mocker.patch.object(request, 'get')
|
23 |
+
request.get.side_effect = [
|
24 |
+
{'Content-Length': '16384'},
|
25 |
+
{'Content-Length': '16384'},
|
26 |
+
iter([str(random.getrandbits(8 * 1024))]),
|
27 |
+
]
|
28 |
+
with mock.patch('pytube.streams.open', mock.mock_open(), create=True):
|
29 |
+
stream = gangnam_style.streams.first()
|
30 |
+
stream.download()
|
31 |
+
|
32 |
+
|
33 |
+
def test_progressive_streams_return_includes_audio_track(gangnam_style):
|
34 |
+
stream = gangnam_style.streams.filter(progressive=True).first()
|
35 |
+
assert stream.includes_audio_track
|
36 |
+
|
37 |
+
|
38 |
+
def test_progressive_streams_return_includes_video_track(gangnam_style):
|
39 |
+
stream = gangnam_style.streams.filter(progressive=True).first()
|
40 |
+
assert stream.includes_video_track
|