add-trailing-comma
Browse files- .pre-commit-config.yaml +4 -0
- pytube/__main__.py +21 -13
- pytube/api.py +30 -16
- pytube/jsinterp.py +42 -21
- pytube/models.py +13 -7
- pytube/utils.py +8 -4
.pre-commit-config.yaml
CHANGED
@@ -20,3 +20,7 @@
|
|
20 |
sha: v1.1.0
|
21 |
hooks:
|
22 |
- id: python-safety-dependencies-check
|
|
|
|
|
|
|
|
|
|
20 |
sha: v1.1.0
|
21 |
hooks:
|
22 |
- id: python-safety-dependencies-check
|
23 |
+
- repo: https://github.com/asottile/add-trailing-comma
|
24 |
+
sha: v0.6.4
|
25 |
+
hooks:
|
26 |
+
- id: add-trailing-comma
|
pytube/__main__.py
CHANGED
@@ -17,19 +17,19 @@ def main():
|
|
17 |
parser = argparse.ArgumentParser(description='YouTube video downloader')
|
18 |
parser.add_argument(
|
19 |
'url',
|
20 |
-
help='The URL of the Video to be downloaded'
|
21 |
)
|
22 |
parser.add_argument(
|
23 |
'--extension',
|
24 |
'-e',
|
25 |
dest='ext',
|
26 |
-
help='The requested format of the video'
|
27 |
)
|
28 |
parser.add_argument(
|
29 |
'--resolution',
|
30 |
'-r',
|
31 |
dest='res',
|
32 |
-
help='The requested resolution'
|
33 |
)
|
34 |
parser.add_argument(
|
35 |
'--path',
|
@@ -37,7 +37,7 @@ def main():
|
|
37 |
action=FullPaths,
|
38 |
default=os.getcwd(),
|
39 |
dest='path',
|
40 |
-
help='The path to save the video to.'
|
41 |
)
|
42 |
parser.add_argument(
|
43 |
'--filename',
|
@@ -50,7 +50,7 @@ def main():
|
|
50 |
'-s',
|
51 |
action='store_true',
|
52 |
dest='show_available',
|
53 |
-
help='Prints a list of available formats for download.'
|
54 |
)
|
55 |
|
56 |
args = parser.parse_args()
|
@@ -75,8 +75,10 @@ def main():
|
|
75 |
|
76 |
if args.ext or args.res:
|
77 |
if not all([args.ext, args.res]):
|
78 |
-
print(
|
79 |
-
|
|
|
|
|
80 |
print_available_vids(videos)
|
81 |
sys.exit(1)
|
82 |
|
@@ -85,8 +87,10 @@ def main():
|
|
85 |
vid = yt.get(args.ext, args.res)
|
86 |
# Check if there's a video returned
|
87 |
if not vid:
|
88 |
-
print(
|
89 |
-
|
|
|
|
|
90 |
pprint(videos)
|
91 |
sys.exit(1)
|
92 |
|
@@ -104,8 +108,10 @@ def main():
|
|
104 |
videos = yt.filter(resolution=args.res)
|
105 |
# Check if we have a video
|
106 |
if not videos:
|
107 |
-
print(
|
108 |
-
|
|
|
|
|
109 |
sys.exit(1)
|
110 |
# Select the highest resolution one
|
111 |
vid = max(videos)
|
@@ -134,8 +140,10 @@ def print_available_vids(videos):
|
|
134 |
formatString = '{:<2} {:<15} {:<15}'
|
135 |
print(formatString.format('', 'Resolution', 'Extension'))
|
136 |
print('-' * 28)
|
137 |
-
print('\n'.join([
|
138 |
-
|
|
|
|
|
139 |
|
140 |
|
141 |
if __name__ == '__main__':
|
|
|
17 |
parser = argparse.ArgumentParser(description='YouTube video downloader')
|
18 |
parser.add_argument(
|
19 |
'url',
|
20 |
+
help='The URL of the Video to be downloaded',
|
21 |
)
|
22 |
parser.add_argument(
|
23 |
'--extension',
|
24 |
'-e',
|
25 |
dest='ext',
|
26 |
+
help='The requested format of the video',
|
27 |
)
|
28 |
parser.add_argument(
|
29 |
'--resolution',
|
30 |
'-r',
|
31 |
dest='res',
|
32 |
+
help='The requested resolution',
|
33 |
)
|
34 |
parser.add_argument(
|
35 |
'--path',
|
|
|
37 |
action=FullPaths,
|
38 |
default=os.getcwd(),
|
39 |
dest='path',
|
40 |
+
help='The path to save the video to.',
|
41 |
)
|
42 |
parser.add_argument(
|
43 |
'--filename',
|
|
|
50 |
'-s',
|
51 |
action='store_true',
|
52 |
dest='show_available',
|
53 |
+
help='Prints a list of available formats for download.',
|
54 |
)
|
55 |
|
56 |
args = parser.parse_args()
|
|
|
75 |
|
76 |
if args.ext or args.res:
|
77 |
if not all([args.ext, args.res]):
|
78 |
+
print(
|
79 |
+
'Make sure you give either of the below specified '
|
80 |
+
'format/resolution combination.',
|
81 |
+
)
|
82 |
print_available_vids(videos)
|
83 |
sys.exit(1)
|
84 |
|
|
|
87 |
vid = yt.get(args.ext, args.res)
|
88 |
# Check if there's a video returned
|
89 |
if not vid:
|
90 |
+
print(
|
91 |
+
"There's no video with the specified format/resolution "
|
92 |
+
'combination.',
|
93 |
+
)
|
94 |
pprint(videos)
|
95 |
sys.exit(1)
|
96 |
|
|
|
108 |
videos = yt.filter(resolution=args.res)
|
109 |
# Check if we have a video
|
110 |
if not videos:
|
111 |
+
print(
|
112 |
+
'There are no videos in the specified in the specified '
|
113 |
+
'resolution.',
|
114 |
+
)
|
115 |
sys.exit(1)
|
116 |
# Select the highest resolution one
|
117 |
vid = max(videos)
|
|
|
140 |
formatString = '{:<2} {:<15} {:<15}'
|
141 |
print(formatString.format('', 'Resolution', 'Extension'))
|
142 |
print('-' * 28)
|
143 |
+
print('\n'.join([
|
144 |
+
formatString.format(index, *formatTuple)
|
145 |
+
for index, formatTuple in enumerate(videos)
|
146 |
+
]))
|
147 |
|
148 |
|
149 |
if __name__ == '__main__':
|
pytube/api.py
CHANGED
@@ -67,7 +67,7 @@ QUALITY_PROFILES = {
|
|
67 |
303: ('webm', '1080p HFR', 'vp9', 'n/a', '5', '', ''),
|
68 |
308: ('webm', '1440p HFR', 'vp9', 'n/a', '10', '', ''),
|
69 |
313: ('webm', '2160p', 'vp9', 'n/a', '13-15', '', ''),
|
70 |
-
315: ('webm', '2160p HFR', 'vp9', 'n/a', '20-25', '', '')
|
71 |
}
|
72 |
|
73 |
# The keys corresponding to the quality/codec map above.
|
@@ -78,7 +78,7 @@ QUALITY_PROFILE_KEYS = (
|
|
78 |
'profile',
|
79 |
'video_bitrate',
|
80 |
'audio_codec',
|
81 |
-
'audio_bitrate'
|
82 |
)
|
83 |
|
84 |
|
@@ -112,8 +112,10 @@ class YouTube(object):
|
|
112 |
:param str url:
|
113 |
The url to the YouTube video.
|
114 |
"""
|
115 |
-
warnings.warn(
|
116 |
-
|
|
|
|
|
117 |
self.from_url(url)
|
118 |
|
119 |
@property
|
@@ -144,8 +146,10 @@ class YouTube(object):
|
|
144 |
:param str filename:
|
145 |
The filename of the video.
|
146 |
"""
|
147 |
-
warnings.warn(
|
148 |
-
|
|
|
|
|
149 |
self.set_filename(filename)
|
150 |
|
151 |
def set_filename(self, filename):
|
@@ -171,8 +175,10 @@ class YouTube(object):
|
|
171 |
"""Gets all videos. (This method is deprecated. Use `get_videos()`
|
172 |
instead.
|
173 |
"""
|
174 |
-
warnings.warn(
|
175 |
-
|
|
|
|
|
176 |
return self._videos
|
177 |
|
178 |
def from_url(self, url):
|
@@ -222,8 +228,10 @@ class YouTube(object):
|
|
222 |
# Check if we have the signature, otherwise we'll need to get the
|
223 |
# cipher from the js.
|
224 |
if 'signature=' not in url:
|
225 |
-
log.debug(
|
226 |
-
|
|
|
|
|
227 |
signature = self._get_cipher(stream_map['s'][i], js_url)
|
228 |
url = '{0}&signature={1}'.format(url, signature)
|
229 |
self._add_video(url, self.filename, **quality_profile)
|
@@ -300,8 +308,10 @@ class YouTube(object):
|
|
300 |
restriction_pattern = bytes('og:restrictions:age', 'utf-8')
|
301 |
|
302 |
if restriction_pattern in html:
|
303 |
-
raise AgeRestricted(
|
304 |
-
|
|
|
|
|
305 |
|
306 |
# Extract out the json data from the html response body.
|
307 |
json_object = self._get_json_data(html)
|
@@ -309,9 +319,11 @@ class YouTube(object):
|
|
309 |
# Here we decode the stream map and bundle it into the json object. We
|
310 |
# do this just so we just can return one object for the video data.
|
311 |
encoded_stream_map = json_object.get('args', {}).get(
|
312 |
-
'url_encoded_fmt_stream_map'
|
|
|
313 |
json_object['args']['stream_map'] = self._parse_stream_map(
|
314 |
-
encoded_stream_map
|
|
|
315 |
return json_object
|
316 |
|
317 |
def _parse_stream_map(self, blob):
|
@@ -441,8 +453,10 @@ class YouTube(object):
|
|
441 |
raise PytubeError('Unable to get encoding profile, no itag found.')
|
442 |
elif len(itag) > 1:
|
443 |
log.warn('Multiple itags found: %s', itag)
|
444 |
-
raise PytubeError(
|
445 |
-
|
|
|
|
|
446 |
return False
|
447 |
|
448 |
def _add_video(self, url, filename, **kwargs):
|
|
|
67 |
303: ('webm', '1080p HFR', 'vp9', 'n/a', '5', '', ''),
|
68 |
308: ('webm', '1440p HFR', 'vp9', 'n/a', '10', '', ''),
|
69 |
313: ('webm', '2160p', 'vp9', 'n/a', '13-15', '', ''),
|
70 |
+
315: ('webm', '2160p HFR', 'vp9', 'n/a', '20-25', '', ''),
|
71 |
}
|
72 |
|
73 |
# The keys corresponding to the quality/codec map above.
|
|
|
78 |
'profile',
|
79 |
'video_bitrate',
|
80 |
'audio_codec',
|
81 |
+
'audio_bitrate',
|
82 |
)
|
83 |
|
84 |
|
|
|
112 |
:param str url:
|
113 |
The url to the YouTube video.
|
114 |
"""
|
115 |
+
warnings.warn(
|
116 |
+
'url setter deprecated, use `from_url()` '
|
117 |
+
'instead.', DeprecationWarning,
|
118 |
+
)
|
119 |
self.from_url(url)
|
120 |
|
121 |
@property
|
|
|
146 |
:param str filename:
|
147 |
The filename of the video.
|
148 |
"""
|
149 |
+
warnings.warn(
|
150 |
+
'filename setter deprecated. Use `set_filename()` '
|
151 |
+
'instead.', DeprecationWarning,
|
152 |
+
)
|
153 |
self.set_filename(filename)
|
154 |
|
155 |
def set_filename(self, filename):
|
|
|
175 |
"""Gets all videos. (This method is deprecated. Use `get_videos()`
|
176 |
instead.
|
177 |
"""
|
178 |
+
warnings.warn(
|
179 |
+
'videos property deprecated. Use `get_videos()` '
|
180 |
+
'instead.', DeprecationWarning,
|
181 |
+
)
|
182 |
return self._videos
|
183 |
|
184 |
def from_url(self, url):
|
|
|
228 |
# Check if we have the signature, otherwise we'll need to get the
|
229 |
# cipher from the js.
|
230 |
if 'signature=' not in url:
|
231 |
+
log.debug(
|
232 |
+
'signature not in url, attempting to resolve the '
|
233 |
+
'cipher.',
|
234 |
+
)
|
235 |
signature = self._get_cipher(stream_map['s'][i], js_url)
|
236 |
url = '{0}&signature={1}'.format(url, signature)
|
237 |
self._add_video(url, self.filename, **quality_profile)
|
|
|
308 |
restriction_pattern = bytes('og:restrictions:age', 'utf-8')
|
309 |
|
310 |
if restriction_pattern in html:
|
311 |
+
raise AgeRestricted(
|
312 |
+
'Age restricted video. Unable to download '
|
313 |
+
'without being signed in.',
|
314 |
+
)
|
315 |
|
316 |
# Extract out the json data from the html response body.
|
317 |
json_object = self._get_json_data(html)
|
|
|
319 |
# Here we decode the stream map and bundle it into the json object. We
|
320 |
# do this just so we just can return one object for the video data.
|
321 |
encoded_stream_map = json_object.get('args', {}).get(
|
322 |
+
'url_encoded_fmt_stream_map',
|
323 |
+
)
|
324 |
json_object['args']['stream_map'] = self._parse_stream_map(
|
325 |
+
encoded_stream_map,
|
326 |
+
)
|
327 |
return json_object
|
328 |
|
329 |
def _parse_stream_map(self, blob):
|
|
|
453 |
raise PytubeError('Unable to get encoding profile, no itag found.')
|
454 |
elif len(itag) > 1:
|
455 |
log.warn('Multiple itags found: %s', itag)
|
456 |
+
raise PytubeError(
|
457 |
+
'Unable to get encoding profile, multiple itags '
|
458 |
+
'found.',
|
459 |
+
)
|
460 |
return False
|
461 |
|
462 |
def _add_video(self, url, filename, **kwargs):
|
pytube/jsinterp.py
CHANGED
@@ -71,7 +71,8 @@ class JSInterpreter(object):
|
|
71 |
if parens_count == 0:
|
72 |
sub_expr = expr[1:m.start()]
|
73 |
sub_result = self.interpret_expression(
|
74 |
-
sub_expr, local_vars, allow_recursion
|
|
|
75 |
remaining_expr = expr[m.end():].strip()
|
76 |
if not remaining_expr:
|
77 |
return sub_result
|
@@ -82,19 +83,23 @@ class JSInterpreter(object):
|
|
82 |
raise ExtractorError('Premature end of parens in %r' % expr)
|
83 |
|
84 |
for op, opfunc in _ASSIGN_OPERATORS:
|
85 |
-
m = re.match(
|
|
|
86 |
(?P<out>%s)(?:\[(?P<index>[^\]]+?)\])?
|
87 |
\s*%s
|
88 |
-
(?P<expr>.*)$''' % (_NAME_RE, re.escape(op)), expr
|
|
|
89 |
if not m:
|
90 |
continue
|
91 |
right_val = self.interpret_expression(
|
92 |
-
m.group('expr'), local_vars, allow_recursion - 1
|
|
|
93 |
|
94 |
if m.groupdict().get('index'):
|
95 |
lvar = local_vars[m.group('out')]
|
96 |
idx = self.interpret_expression(
|
97 |
-
m.group('index'), local_vars, allow_recursion
|
|
|
98 |
assert isinstance(idx, int)
|
99 |
cur = lvar[idx]
|
100 |
val = opfunc(cur, right_val)
|
@@ -111,7 +116,8 @@ class JSInterpreter(object):
|
|
111 |
|
112 |
var_m = re.match(
|
113 |
r'(?!if|return|true|false)(?P<name>%s)$' % _NAME_RE,
|
114 |
-
expr
|
|
|
115 |
if var_m:
|
116 |
return local_vars[var_m.group('name')]
|
117 |
|
@@ -123,7 +129,8 @@ class JSInterpreter(object):
|
|
123 |
m = re.match(
|
124 |
r'(?P<var>%s)\.(?P<member>[^(]+)'
|
125 |
'(?:\(+(?P<args>[^()]*)\))?$' % _NAME_RE,
|
126 |
-
expr
|
|
|
127 |
if m:
|
128 |
variable = m.group('var')
|
129 |
member = m.group('member')
|
@@ -149,7 +156,8 @@ class JSInterpreter(object):
|
|
149 |
else:
|
150 |
argvals = tuple([
|
151 |
self.interpret_expression(v, local_vars, allow_recursion)
|
152 |
-
for v in arg_str.split(',')
|
|
|
153 |
|
154 |
if member == 'split':
|
155 |
assert argvals == ('',)
|
@@ -175,11 +183,13 @@ class JSInterpreter(object):
|
|
175 |
return obj[member](argvals)
|
176 |
|
177 |
m = re.match(
|
178 |
-
r'(?P<in>%s)\[(?P<idx>.+)\]$' % _NAME_RE, expr
|
|
|
179 |
if m:
|
180 |
val = local_vars[m.group('in')]
|
181 |
idx = self.interpret_expression(
|
182 |
-
m.group('idx'), local_vars, allow_recursion - 1
|
|
|
183 |
return val[idx]
|
184 |
|
185 |
for op, opfunc in _OPERATORS:
|
@@ -187,24 +197,30 @@ class JSInterpreter(object):
|
|
187 |
if not m:
|
188 |
continue
|
189 |
x, abort = self.interpret_statement(
|
190 |
-
m.group('x'), local_vars, allow_recursion - 1
|
|
|
191 |
if abort:
|
192 |
raise ExtractorError(
|
193 |
-
'Premature left-side return of %s in %r' % (op, expr)
|
|
|
194 |
y, abort = self.interpret_statement(
|
195 |
-
m.group('y'), local_vars, allow_recursion - 1
|
|
|
196 |
if abort:
|
197 |
raise ExtractorError(
|
198 |
-
'Premature right-side return of %s in %r' % (op, expr)
|
|
|
199 |
return opfunc(x, y)
|
200 |
|
201 |
m = re.match(
|
202 |
-
r'^(?P<func>%s)\((?P<args>[a-zA-Z0-9_$,]+)\)$' % _NAME_RE, expr
|
|
|
203 |
if m:
|
204 |
fname = m.group('func')
|
205 |
argvals = tuple([
|
206 |
int(v) if v.isdigit() else local_vars[v]
|
207 |
-
for v in m.group('args').split(',')
|
|
|
208 |
if fname not in self._functions:
|
209 |
self._functions[fname] = self.extract_function(fname)
|
210 |
return self._functions[fname](argvals)
|
@@ -217,17 +233,20 @@ class JSInterpreter(object):
|
|
217 |
(r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
|
218 |
r'\s*(?P<fields>([a-zA-Z$0-9]+\s*:\s*function\(.*?\)\s*\{.*?\}'
|
219 |
r'(?:,\s*)?)*)' + r'\}\s*;',
|
220 |
-
self.code
|
|
|
221 |
fields = obj_m.group('fields')
|
222 |
# Currently, it only supports function definitions
|
223 |
fields_m = re.finditer(
|
224 |
r'(?P<key>[a-zA-Z$0-9]+)\s*:\s*function'
|
225 |
r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
|
226 |
-
fields
|
|
|
227 |
for f in fields_m:
|
228 |
argnames = f.group('args').split(',')
|
229 |
obj[f.group('key')] = self.build_function(
|
230 |
-
argnames, f.group('code')
|
|
|
231 |
|
232 |
return obj
|
233 |
|
@@ -237,8 +256,10 @@ class JSInterpreter(object):
|
|
237 |
(?:function\s+%s|[{;,]\s*%s\s*=\s*function|var\s+%s\s*=\s*function)\s*
|
238 |
\((?P<args>[^)]*)\)\s*
|
239 |
\{(?P<code>[^}]+)\}''' % (
|
240 |
-
re.escape(funcname), re.escape(funcname), re.escape(funcname)
|
241 |
-
|
|
|
|
|
242 |
if func_m is None:
|
243 |
raise ExtractorError('Could not find JS function %r' % funcname)
|
244 |
argnames = func_m.group('args').split(',')
|
|
|
71 |
if parens_count == 0:
|
72 |
sub_expr = expr[1:m.start()]
|
73 |
sub_result = self.interpret_expression(
|
74 |
+
sub_expr, local_vars, allow_recursion,
|
75 |
+
)
|
76 |
remaining_expr = expr[m.end():].strip()
|
77 |
if not remaining_expr:
|
78 |
return sub_result
|
|
|
83 |
raise ExtractorError('Premature end of parens in %r' % expr)
|
84 |
|
85 |
for op, opfunc in _ASSIGN_OPERATORS:
|
86 |
+
m = re.match(
|
87 |
+
r'''(?x)
|
88 |
(?P<out>%s)(?:\[(?P<index>[^\]]+?)\])?
|
89 |
\s*%s
|
90 |
+
(?P<expr>.*)$''' % (_NAME_RE, re.escape(op)), expr,
|
91 |
+
)
|
92 |
if not m:
|
93 |
continue
|
94 |
right_val = self.interpret_expression(
|
95 |
+
m.group('expr'), local_vars, allow_recursion - 1,
|
96 |
+
)
|
97 |
|
98 |
if m.groupdict().get('index'):
|
99 |
lvar = local_vars[m.group('out')]
|
100 |
idx = self.interpret_expression(
|
101 |
+
m.group('index'), local_vars, allow_recursion,
|
102 |
+
)
|
103 |
assert isinstance(idx, int)
|
104 |
cur = lvar[idx]
|
105 |
val = opfunc(cur, right_val)
|
|
|
116 |
|
117 |
var_m = re.match(
|
118 |
r'(?!if|return|true|false)(?P<name>%s)$' % _NAME_RE,
|
119 |
+
expr,
|
120 |
+
)
|
121 |
if var_m:
|
122 |
return local_vars[var_m.group('name')]
|
123 |
|
|
|
129 |
m = re.match(
|
130 |
r'(?P<var>%s)\.(?P<member>[^(]+)'
|
131 |
'(?:\(+(?P<args>[^()]*)\))?$' % _NAME_RE,
|
132 |
+
expr,
|
133 |
+
)
|
134 |
if m:
|
135 |
variable = m.group('var')
|
136 |
member = m.group('member')
|
|
|
156 |
else:
|
157 |
argvals = tuple([
|
158 |
self.interpret_expression(v, local_vars, allow_recursion)
|
159 |
+
for v in arg_str.split(',')
|
160 |
+
])
|
161 |
|
162 |
if member == 'split':
|
163 |
assert argvals == ('',)
|
|
|
183 |
return obj[member](argvals)
|
184 |
|
185 |
m = re.match(
|
186 |
+
r'(?P<in>%s)\[(?P<idx>.+)\]$' % _NAME_RE, expr,
|
187 |
+
)
|
188 |
if m:
|
189 |
val = local_vars[m.group('in')]
|
190 |
idx = self.interpret_expression(
|
191 |
+
m.group('idx'), local_vars, allow_recursion - 1,
|
192 |
+
)
|
193 |
return val[idx]
|
194 |
|
195 |
for op, opfunc in _OPERATORS:
|
|
|
197 |
if not m:
|
198 |
continue
|
199 |
x, abort = self.interpret_statement(
|
200 |
+
m.group('x'), local_vars, allow_recursion - 1,
|
201 |
+
)
|
202 |
if abort:
|
203 |
raise ExtractorError(
|
204 |
+
'Premature left-side return of %s in %r' % (op, expr),
|
205 |
+
)
|
206 |
y, abort = self.interpret_statement(
|
207 |
+
m.group('y'), local_vars, allow_recursion - 1,
|
208 |
+
)
|
209 |
if abort:
|
210 |
raise ExtractorError(
|
211 |
+
'Premature right-side return of %s in %r' % (op, expr),
|
212 |
+
)
|
213 |
return opfunc(x, y)
|
214 |
|
215 |
m = re.match(
|
216 |
+
r'^(?P<func>%s)\((?P<args>[a-zA-Z0-9_$,]+)\)$' % _NAME_RE, expr,
|
217 |
+
)
|
218 |
if m:
|
219 |
fname = m.group('func')
|
220 |
argvals = tuple([
|
221 |
int(v) if v.isdigit() else local_vars[v]
|
222 |
+
for v in m.group('args').split(',')
|
223 |
+
])
|
224 |
if fname not in self._functions:
|
225 |
self._functions[fname] = self.extract_function(fname)
|
226 |
return self._functions[fname](argvals)
|
|
|
233 |
(r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
|
234 |
r'\s*(?P<fields>([a-zA-Z$0-9]+\s*:\s*function\(.*?\)\s*\{.*?\}'
|
235 |
r'(?:,\s*)?)*)' + r'\}\s*;',
|
236 |
+
self.code,
|
237 |
+
)
|
238 |
fields = obj_m.group('fields')
|
239 |
# Currently, it only supports function definitions
|
240 |
fields_m = re.finditer(
|
241 |
r'(?P<key>[a-zA-Z$0-9]+)\s*:\s*function'
|
242 |
r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
|
243 |
+
fields,
|
244 |
+
)
|
245 |
for f in fields_m:
|
246 |
argnames = f.group('args').split(',')
|
247 |
obj[f.group('key')] = self.build_function(
|
248 |
+
argnames, f.group('code'),
|
249 |
+
)
|
250 |
|
251 |
return obj
|
252 |
|
|
|
256 |
(?:function\s+%s|[{;,]\s*%s\s*=\s*function|var\s+%s\s*=\s*function)\s*
|
257 |
\((?P<args>[^)]*)\)\s*
|
258 |
\{(?P<code>[^}]+)\}''' % (
|
259 |
+
re.escape(funcname), re.escape(funcname), re.escape(funcname),
|
260 |
+
),
|
261 |
+
self.code,
|
262 |
+
)
|
263 |
if func_m is None:
|
264 |
raise ExtractorError('Could not find JS function %r' % funcname)
|
265 |
argnames = func_m.group('args').split(',')
|
pytube/models.py
CHANGED
@@ -15,9 +15,11 @@ class Video(object):
|
|
15 |
"""Class representation of a single instance of a YouTube video.
|
16 |
"""
|
17 |
|
18 |
-
def __init__(
|
19 |
-
|
20 |
-
|
|
|
|
|
21 |
"""Sets-up the video object.
|
22 |
|
23 |
:param str url:
|
@@ -49,8 +51,10 @@ class Video(object):
|
|
49 |
self.audio_codec = audio_codec
|
50 |
self.audio_bitrate = audio_bitrate
|
51 |
|
52 |
-
def download(
|
53 |
-
|
|
|
|
|
54 |
"""Downloads the video.
|
55 |
|
56 |
:param str path:
|
@@ -110,7 +114,8 @@ class Video(object):
|
|
110 |
# to disable this.
|
111 |
os.remove(path)
|
112 |
raise KeyboardInterrupt(
|
113 |
-
'Interrupt signal given. Deleting incomplete video.'
|
|
|
114 |
|
115 |
def file_size(self, response):
|
116 |
"""Gets the file size from the response
|
@@ -125,7 +130,8 @@ class Video(object):
|
|
125 |
def __repr__(self):
|
126 |
"""A clean representation of the class instance."""
|
127 |
return '<Video: {0} (.{1}) - {2} - {3}>'.format(
|
128 |
-
self.video_codec, self.extension, self.resolution, self.profile
|
|
|
129 |
|
130 |
def __lt__(self, other):
|
131 |
"""The "less than" (lt) method is used for comparing video object to
|
|
|
15 |
"""Class representation of a single instance of a YouTube video.
|
16 |
"""
|
17 |
|
18 |
+
def __init__(
|
19 |
+
self, url, filename, extension, resolution=None,
|
20 |
+
video_codec=None, profile=None, video_bitrate=None,
|
21 |
+
audio_codec=None, audio_bitrate=None,
|
22 |
+
):
|
23 |
"""Sets-up the video object.
|
24 |
|
25 |
:param str url:
|
|
|
51 |
self.audio_codec = audio_codec
|
52 |
self.audio_bitrate = audio_bitrate
|
53 |
|
54 |
+
def download(
|
55 |
+
self, path, chunk_size=8 * 1024, on_progress=None,
|
56 |
+
on_finish=None, force_overwrite=False,
|
57 |
+
):
|
58 |
"""Downloads the video.
|
59 |
|
60 |
:param str path:
|
|
|
114 |
# to disable this.
|
115 |
os.remove(path)
|
116 |
raise KeyboardInterrupt(
|
117 |
+
'Interrupt signal given. Deleting incomplete video.',
|
118 |
+
)
|
119 |
|
120 |
def file_size(self, response):
|
121 |
"""Gets the file size from the response
|
|
|
130 |
def __repr__(self):
|
131 |
"""A clean representation of the class instance."""
|
132 |
return '<Video: {0} (.{1}) - {2} - {3}>'.format(
|
133 |
+
self.video_codec, self.extension, self.resolution, self.profile,
|
134 |
+
)
|
135 |
|
136 |
def __lt__(self, other):
|
137 |
"""The "less than" (lt) method is used for comparing video object to
|
pytube/utils.py
CHANGED
@@ -34,8 +34,10 @@ def safe_filename(text, max_length=200):
|
|
34 |
|
35 |
# Removing these SHOULD make most filename safe for a wide range of
|
36 |
# operating systems.
|
37 |
-
paranoid = [
|
38 |
-
|
|
|
|
|
39 |
|
40 |
blacklist = re.compile('|'.join(ntfs + paranoid), re.UNICODE)
|
41 |
filename = blacklist.sub('', text)
|
@@ -73,6 +75,8 @@ def print_status(progress, file_size, start):
|
|
73 |
dt = (clock() - start)
|
74 |
if dt > 0:
|
75 |
stdout.write('\r [%s%s][%3.2f%%] %s at %s/s ' %
|
76 |
-
(
|
77 |
-
|
|
|
|
|
78 |
stdout.flush()
|
|
|
34 |
|
35 |
# Removing these SHOULD make most filename safe for a wide range of
|
36 |
# operating systems.
|
37 |
+
paranoid = [
|
38 |
+
'\"', '\#', '\$', '\%', '\'', '\*', '\,', '\.', '\/', '\:',
|
39 |
+
'\;', '\<', '\>', '\?', '\\', '\^', '\|', '\~', '\\\\',
|
40 |
+
]
|
41 |
|
42 |
blacklist = re.compile('|'.join(ntfs + paranoid), re.UNICODE)
|
43 |
filename = blacklist.sub('', text)
|
|
|
75 |
dt = (clock() - start)
|
76 |
if dt > 0:
|
77 |
stdout.write('\r [%s%s][%3.2f%%] %s at %s/s ' %
|
78 |
+
(
|
79 |
+
'=' * done, ' ' * (50 - done), percent_done,
|
80 |
+
sizeof(file_size), sizeof(progress // dt),
|
81 |
+
))
|
82 |
stdout.flush()
|