]> Raphaël G. Git Repositories - youtubedl/blob - youtube_dl/utils.py
Prepare for release.
[youtubedl] / youtube_dl / utils.py
1 #!/usr/bin/env python
2 # coding: utf-8
3
4 from __future__ import unicode_literals
5
6 import base64
7 import binascii
8 import calendar
9 import codecs
10 import contextlib
11 import ctypes
12 import datetime
13 import email.utils
14 import errno
15 import functools
16 import gzip
17 import io
18 import itertools
19 import json
20 import locale
21 import math
22 import operator
23 import os
24 import pipes
25 import platform
26 import re
27 import socket
28 import ssl
29 import subprocess
30 import sys
31 import tempfile
32 import traceback
33 import xml.etree.ElementTree
34 import zlib
35
36 from .compat import (
37 compat_HTMLParser,
38 compat_basestring,
39 compat_chr,
40 compat_etree_fromstring,
41 compat_html_entities,
42 compat_html_entities_html5,
43 compat_http_client,
44 compat_kwargs,
45 compat_os_name,
46 compat_parse_qs,
47 compat_shlex_quote,
48 compat_socket_create_connection,
49 compat_str,
50 compat_struct_pack,
51 compat_struct_unpack,
52 compat_urllib_error,
53 compat_urllib_parse,
54 compat_urllib_parse_urlencode,
55 compat_urllib_parse_urlparse,
56 compat_urllib_parse_unquote_plus,
57 compat_urllib_request,
58 compat_urlparse,
59 compat_xpath,
60 )
61
62 from .socks import (
63 ProxyType,
64 sockssocket,
65 )
66
67
68 def register_socks_protocols():
69 # "Register" SOCKS protocols
70 # In Python < 2.6.5, urlsplit() suffers from bug https://bugs.python.org/issue7904
71 # URLs with protocols not in urlparse.uses_netloc are not handled correctly
72 for scheme in ('socks', 'socks4', 'socks4a', 'socks5'):
73 if scheme not in compat_urlparse.uses_netloc:
74 compat_urlparse.uses_netloc.append(scheme)
75
76
77 # This is not clearly defined otherwise
78 compiled_regex_type = type(re.compile(''))
79
80 std_headers = {
81 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)',
82 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
83 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
84 'Accept-Encoding': 'gzip, deflate',
85 'Accept-Language': 'en-us,en;q=0.5',
86 }
87
88
89 NO_DEFAULT = object()
90
91 ENGLISH_MONTH_NAMES = [
92 'January', 'February', 'March', 'April', 'May', 'June',
93 'July', 'August', 'September', 'October', 'November', 'December']
94
95 MONTH_NAMES = {
96 'en': ENGLISH_MONTH_NAMES,
97 'fr': [
98 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
99 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
100 }
101
102 KNOWN_EXTENSIONS = (
103 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'aac',
104 'flv', 'f4v', 'f4a', 'f4b',
105 'webm', 'ogg', 'ogv', 'oga', 'ogx', 'spx', 'opus',
106 'mkv', 'mka', 'mk3d',
107 'avi', 'divx',
108 'mov',
109 'asf', 'wmv', 'wma',
110 '3gp', '3g2',
111 'mp3',
112 'flac',
113 'ape',
114 'wav',
115 'f4f', 'f4m', 'm3u8', 'smil')
116
117 # needed for sanitizing filenames in restricted mode
118 ACCENT_CHARS = dict(zip('ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖŐØŒÙÚÛÜŰÝÞßàáâãäåæçèéêëìíîïðñòóôõöőøœùúûüűýþÿ',
119 itertools.chain('AAAAAA', ['AE'], 'CEEEEIIIIDNOOOOOOO', ['OE'], 'UUUUUYP', ['ss'],
120 'aaaaaa', ['ae'], 'ceeeeiiiionooooooo', ['oe'], 'uuuuuypy')))
121
122 DATE_FORMATS = (
123 '%d %B %Y',
124 '%d %b %Y',
125 '%B %d %Y',
126 '%b %d %Y',
127 '%b %dst %Y %I:%M',
128 '%b %dnd %Y %I:%M',
129 '%b %dth %Y %I:%M',
130 '%Y %m %d',
131 '%Y-%m-%d',
132 '%Y/%m/%d',
133 '%Y/%m/%d %H:%M',
134 '%Y/%m/%d %H:%M:%S',
135 '%Y-%m-%d %H:%M:%S',
136 '%Y-%m-%d %H:%M:%S.%f',
137 '%d.%m.%Y %H:%M',
138 '%d.%m.%Y %H.%M',
139 '%Y-%m-%dT%H:%M:%SZ',
140 '%Y-%m-%dT%H:%M:%S.%fZ',
141 '%Y-%m-%dT%H:%M:%S.%f0Z',
142 '%Y-%m-%dT%H:%M:%S',
143 '%Y-%m-%dT%H:%M:%S.%f',
144 '%Y-%m-%dT%H:%M',
145 '%b %d %Y at %H:%M',
146 '%b %d %Y at %H:%M:%S',
147 )
148
149 DATE_FORMATS_DAY_FIRST = list(DATE_FORMATS)
150 DATE_FORMATS_DAY_FIRST.extend([
151 '%d-%m-%Y',
152 '%d.%m.%Y',
153 '%d.%m.%y',
154 '%d/%m/%Y',
155 '%d/%m/%y',
156 '%d/%m/%Y %H:%M:%S',
157 ])
158
159 DATE_FORMATS_MONTH_FIRST = list(DATE_FORMATS)
160 DATE_FORMATS_MONTH_FIRST.extend([
161 '%m-%d-%Y',
162 '%m.%d.%Y',
163 '%m/%d/%Y',
164 '%m/%d/%y',
165 '%m/%d/%Y %H:%M:%S',
166 ])
167
168 PACKED_CODES_RE = r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)"
169
170
171 def preferredencoding():
172 """Get preferred encoding.
173
174 Returns the best encoding scheme for the system, based on
175 locale.getpreferredencoding() and some further tweaks.
176 """
177 try:
178 pref = locale.getpreferredencoding()
179 'TEST'.encode(pref)
180 except Exception:
181 pref = 'UTF-8'
182
183 return pref
184
185
186 def write_json_file(obj, fn):
187 """ Encode obj as JSON and write it to fn, atomically if possible """
188
189 fn = encodeFilename(fn)
190 if sys.version_info < (3, 0) and sys.platform != 'win32':
191 encoding = get_filesystem_encoding()
192 # os.path.basename returns a bytes object, but NamedTemporaryFile
193 # will fail if the filename contains non ascii characters unless we
194 # use a unicode object
195 path_basename = lambda f: os.path.basename(fn).decode(encoding)
196 # the same for os.path.dirname
197 path_dirname = lambda f: os.path.dirname(fn).decode(encoding)
198 else:
199 path_basename = os.path.basename
200 path_dirname = os.path.dirname
201
202 args = {
203 'suffix': '.tmp',
204 'prefix': path_basename(fn) + '.',
205 'dir': path_dirname(fn),
206 'delete': False,
207 }
208
209 # In Python 2.x, json.dump expects a bytestream.
210 # In Python 3.x, it writes to a character stream
211 if sys.version_info < (3, 0):
212 args['mode'] = 'wb'
213 else:
214 args.update({
215 'mode': 'w',
216 'encoding': 'utf-8',
217 })
218
219 tf = tempfile.NamedTemporaryFile(**compat_kwargs(args))
220
221 try:
222 with tf:
223 json.dump(obj, tf)
224 if sys.platform == 'win32':
225 # Need to remove existing file on Windows, else os.rename raises
226 # WindowsError or FileExistsError.
227 try:
228 os.unlink(fn)
229 except OSError:
230 pass
231 os.rename(tf.name, fn)
232 except Exception:
233 try:
234 os.remove(tf.name)
235 except OSError:
236 pass
237 raise
238
239
240 if sys.version_info >= (2, 7):
241 def find_xpath_attr(node, xpath, key, val=None):
242 """ Find the xpath xpath[@key=val] """
243 assert re.match(r'^[a-zA-Z_-]+$', key)
244 expr = xpath + ('[@%s]' % key if val is None else "[@%s='%s']" % (key, val))
245 return node.find(expr)
246 else:
247 def find_xpath_attr(node, xpath, key, val=None):
248 for f in node.findall(compat_xpath(xpath)):
249 if key not in f.attrib:
250 continue
251 if val is None or f.attrib.get(key) == val:
252 return f
253 return None
254
255 # On python2.6 the xml.etree.ElementTree.Element methods don't support
256 # the namespace parameter
257
258
259 def xpath_with_ns(path, ns_map):
260 components = [c.split(':') for c in path.split('/')]
261 replaced = []
262 for c in components:
263 if len(c) == 1:
264 replaced.append(c[0])
265 else:
266 ns, tag = c
267 replaced.append('{%s}%s' % (ns_map[ns], tag))
268 return '/'.join(replaced)
269
270
271 def xpath_element(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
272 def _find_xpath(xpath):
273 return node.find(compat_xpath(xpath))
274
275 if isinstance(xpath, (str, compat_str)):
276 n = _find_xpath(xpath)
277 else:
278 for xp in xpath:
279 n = _find_xpath(xp)
280 if n is not None:
281 break
282
283 if n is None:
284 if default is not NO_DEFAULT:
285 return default
286 elif fatal:
287 name = xpath if name is None else name
288 raise ExtractorError('Could not find XML element %s' % name)
289 else:
290 return None
291 return n
292
293
294 def xpath_text(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
295 n = xpath_element(node, xpath, name, fatal=fatal, default=default)
296 if n is None or n == default:
297 return n
298 if n.text is None:
299 if default is not NO_DEFAULT:
300 return default
301 elif fatal:
302 name = xpath if name is None else name
303 raise ExtractorError('Could not find XML element\'s text %s' % name)
304 else:
305 return None
306 return n.text
307
308
309 def xpath_attr(node, xpath, key, name=None, fatal=False, default=NO_DEFAULT):
310 n = find_xpath_attr(node, xpath, key)
311 if n is None:
312 if default is not NO_DEFAULT:
313 return default
314 elif fatal:
315 name = '%s[@%s]' % (xpath, key) if name is None else name
316 raise ExtractorError('Could not find XML attribute %s' % name)
317 else:
318 return None
319 return n.attrib[key]
320
321
322 def get_element_by_id(id, html):
323 """Return the content of the tag with the specified ID in the passed HTML document"""
324 return get_element_by_attribute('id', id, html)
325
326
327 def get_element_by_class(class_name, html):
328 return get_element_by_attribute(
329 'class', r'[^\'"]*\b%s\b[^\'"]*' % re.escape(class_name),
330 html, escape_value=False)
331
332
333 def get_element_by_attribute(attribute, value, html, escape_value=True):
334 """Return the content of the tag with the specified attribute in the passed HTML document"""
335
336 value = re.escape(value) if escape_value else value
337
338 m = re.search(r'''(?xs)
339 <([a-zA-Z0-9:._-]+)
340 (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'))*?
341 \s+%s=['"]?%s['"]?
342 (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'))*?
343 \s*>
344 (?P<content>.*?)
345 </\1>
346 ''' % (re.escape(attribute), value), html)
347
348 if not m:
349 return None
350 res = m.group('content')
351
352 if res.startswith('"') or res.startswith("'"):
353 res = res[1:-1]
354
355 return unescapeHTML(res)
356
357
358 class HTMLAttributeParser(compat_HTMLParser):
359 """Trivial HTML parser to gather the attributes for a single element"""
360 def __init__(self):
361 self.attrs = {}
362 compat_HTMLParser.__init__(self)
363
364 def handle_starttag(self, tag, attrs):
365 self.attrs = dict(attrs)
366
367
368 def extract_attributes(html_element):
369 """Given a string for an HTML element such as
370 <el
371 a="foo" B="bar" c="&98;az" d=boz
372 empty= noval entity="&amp;"
373 sq='"' dq="'"
374 >
375 Decode and return a dictionary of attributes.
376 {
377 'a': 'foo', 'b': 'bar', c: 'baz', d: 'boz',
378 'empty': '', 'noval': None, 'entity': '&',
379 'sq': '"', 'dq': '\''
380 }.
381 NB HTMLParser is stricter in Python 2.6 & 3.2 than in later versions,
382 but the cases in the unit test will work for all of 2.6, 2.7, 3.2-3.5.
383 """
384 parser = HTMLAttributeParser()
385 parser.feed(html_element)
386 parser.close()
387 return parser.attrs
388
389
390 def clean_html(html):
391 """Clean an HTML snippet into a readable string"""
392
393 if html is None: # Convenience for sanitizing descriptions etc.
394 return html
395
396 # Newline vs <br />
397 html = html.replace('\n', ' ')
398 html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
399 html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
400 # Strip html tags
401 html = re.sub('<.*?>', '', html)
402 # Replace html entities
403 html = unescapeHTML(html)
404 return html.strip()
405
406
407 def sanitize_open(filename, open_mode):
408 """Try to open the given filename, and slightly tweak it if this fails.
409
410 Attempts to open the given filename. If this fails, it tries to change
411 the filename slightly, step by step, until it's either able to open it
412 or it fails and raises a final exception, like the standard open()
413 function.
414
415 It returns the tuple (stream, definitive_file_name).
416 """
417 try:
418 if filename == '-':
419 if sys.platform == 'win32':
420 import msvcrt
421 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
422 return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
423 stream = open(encodeFilename(filename), open_mode)
424 return (stream, filename)
425 except (IOError, OSError) as err:
426 if err.errno in (errno.EACCES,):
427 raise
428
429 # In case of error, try to remove win32 forbidden chars
430 alt_filename = sanitize_path(filename)
431 if alt_filename == filename:
432 raise
433 else:
434 # An exception here should be caught in the caller
435 stream = open(encodeFilename(alt_filename), open_mode)
436 return (stream, alt_filename)
437
438
439 def timeconvert(timestr):
440 """Convert RFC 2822 defined time string into system timestamp"""
441 timestamp = None
442 timetuple = email.utils.parsedate_tz(timestr)
443 if timetuple is not None:
444 timestamp = email.utils.mktime_tz(timetuple)
445 return timestamp
446
447
448 def sanitize_filename(s, restricted=False, is_id=False):
449 """Sanitizes a string so it could be used as part of a filename.
450 If restricted is set, use a stricter subset of allowed characters.
451 Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
452 """
453 def replace_insane(char):
454 if restricted and char in ACCENT_CHARS:
455 return ACCENT_CHARS[char]
456 if char == '?' or ord(char) < 32 or ord(char) == 127:
457 return ''
458 elif char == '"':
459 return '' if restricted else '\''
460 elif char == ':':
461 return '_-' if restricted else ' -'
462 elif char in '\\/|*<>':
463 return '_'
464 if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
465 return '_'
466 if restricted and ord(char) > 127:
467 return '_'
468 return char
469
470 # Handle timestamps
471 s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s)
472 result = ''.join(map(replace_insane, s))
473 if not is_id:
474 while '__' in result:
475 result = result.replace('__', '_')
476 result = result.strip('_')
477 # Common case of "Foreign band name - English song title"
478 if restricted and result.startswith('-_'):
479 result = result[2:]
480 if result.startswith('-'):
481 result = '_' + result[len('-'):]
482 result = result.lstrip('.')
483 if not result:
484 result = '_'
485 return result
486
487
488 def sanitize_path(s):
489 """Sanitizes and normalizes path on Windows"""
490 if sys.platform != 'win32':
491 return s
492 drive_or_unc, _ = os.path.splitdrive(s)
493 if sys.version_info < (2, 7) and not drive_or_unc:
494 drive_or_unc, _ = os.path.splitunc(s)
495 norm_path = os.path.normpath(remove_start(s, drive_or_unc)).split(os.path.sep)
496 if drive_or_unc:
497 norm_path.pop(0)
498 sanitized_path = [
499 path_part if path_part in ['.', '..'] else re.sub('(?:[/<>:"\\|\\\\?\\*]|[\s.]$)', '#', path_part)
500 for path_part in norm_path]
501 if drive_or_unc:
502 sanitized_path.insert(0, drive_or_unc + os.path.sep)
503 return os.path.join(*sanitized_path)
504
505
506 # Prepend protocol-less URLs with `http:` scheme in order to mitigate the number of
507 # unwanted failures due to missing protocol
508 def sanitize_url(url):
509 return 'http:%s' % url if url.startswith('//') else url
510
511
512 def sanitized_Request(url, *args, **kwargs):
513 return compat_urllib_request.Request(sanitize_url(url), *args, **kwargs)
514
515
516 def orderedSet(iterable):
517 """ Remove all duplicates from the input iterable """
518 res = []
519 for el in iterable:
520 if el not in res:
521 res.append(el)
522 return res
523
524
525 def _htmlentity_transform(entity_with_semicolon):
526 """Transforms an HTML entity to a character."""
527 entity = entity_with_semicolon[:-1]
528
529 # Known non-numeric HTML entity
530 if entity in compat_html_entities.name2codepoint:
531 return compat_chr(compat_html_entities.name2codepoint[entity])
532
533 # TODO: HTML5 allows entities without a semicolon. For example,
534 # '&Eacuteric' should be decoded as 'Éric'.
535 if entity_with_semicolon in compat_html_entities_html5:
536 return compat_html_entities_html5[entity_with_semicolon]
537
538 mobj = re.match(r'#(x[0-9a-fA-F]+|[0-9]+)', entity)
539 if mobj is not None:
540 numstr = mobj.group(1)
541 if numstr.startswith('x'):
542 base = 16
543 numstr = '0%s' % numstr
544 else:
545 base = 10
546 # See https://github.com/rg3/youtube-dl/issues/7518
547 try:
548 return compat_chr(int(numstr, base))
549 except ValueError:
550 pass
551
552 # Unknown entity in name, return its literal representation
553 return '&%s;' % entity
554
555
556 def unescapeHTML(s):
557 if s is None:
558 return None
559 assert type(s) == compat_str
560
561 return re.sub(
562 r'&([^;]+;)', lambda m: _htmlentity_transform(m.group(1)), s)
563
564
565 def get_subprocess_encoding():
566 if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
567 # For subprocess calls, encode with locale encoding
568 # Refer to http://stackoverflow.com/a/9951851/35070
569 encoding = preferredencoding()
570 else:
571 encoding = sys.getfilesystemencoding()
572 if encoding is None:
573 encoding = 'utf-8'
574 return encoding
575
576
577 def encodeFilename(s, for_subprocess=False):
578 """
579 @param s The name of the file
580 """
581
582 assert type(s) == compat_str
583
584 # Python 3 has a Unicode API
585 if sys.version_info >= (3, 0):
586 return s
587
588 # Pass '' directly to use Unicode APIs on Windows 2000 and up
589 # (Detecting Windows NT 4 is tricky because 'major >= 4' would
590 # match Windows 9x series as well. Besides, NT 4 is obsolete.)
591 if not for_subprocess and sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
592 return s
593
594 # Jython assumes filenames are Unicode strings though reported as Python 2.x compatible
595 if sys.platform.startswith('java'):
596 return s
597
598 return s.encode(get_subprocess_encoding(), 'ignore')
599
600
601 def decodeFilename(b, for_subprocess=False):
602
603 if sys.version_info >= (3, 0):
604 return b
605
606 if not isinstance(b, bytes):
607 return b
608
609 return b.decode(get_subprocess_encoding(), 'ignore')
610
611
612 def encodeArgument(s):
613 if not isinstance(s, compat_str):
614 # Legacy code that uses byte strings
615 # Uncomment the following line after fixing all post processors
616 # assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s))
617 s = s.decode('ascii')
618 return encodeFilename(s, True)
619
620
621 def decodeArgument(b):
622 return decodeFilename(b, True)
623
624
625 def decodeOption(optval):
626 if optval is None:
627 return optval
628 if isinstance(optval, bytes):
629 optval = optval.decode(preferredencoding())
630
631 assert isinstance(optval, compat_str)
632 return optval
633
634
635 def formatSeconds(secs):
636 if secs > 3600:
637 return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
638 elif secs > 60:
639 return '%d:%02d' % (secs // 60, secs % 60)
640 else:
641 return '%d' % secs
642
643
644 def make_HTTPS_handler(params, **kwargs):
645 opts_no_check_certificate = params.get('nocheckcertificate', False)
646 if hasattr(ssl, 'create_default_context'): # Python >= 3.4 or 2.7.9
647 context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
648 if opts_no_check_certificate:
649 context.check_hostname = False
650 context.verify_mode = ssl.CERT_NONE
651 try:
652 return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
653 except TypeError:
654 # Python 2.7.8
655 # (create_default_context present but HTTPSHandler has no context=)
656 pass
657
658 if sys.version_info < (3, 2):
659 return YoutubeDLHTTPSHandler(params, **kwargs)
660 else: # Python < 3.4
661 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
662 context.verify_mode = (ssl.CERT_NONE
663 if opts_no_check_certificate
664 else ssl.CERT_REQUIRED)
665 context.set_default_verify_paths()
666 return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
667
668
669 def bug_reports_message():
670 if ytdl_is_updateable():
671 update_cmd = 'type youtube-dl -U to update'
672 else:
673 update_cmd = 'see https://yt-dl.org/update on how to update'
674 msg = '; please report this issue on https://yt-dl.org/bug .'
675 msg += ' Make sure you are using the latest version; %s.' % update_cmd
676 msg += ' Be sure to call youtube-dl with the --verbose flag and include its complete output.'
677 return msg
678
679
680 class ExtractorError(Exception):
681 """Error during info extraction."""
682
683 def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None):
684 """ tb, if given, is the original traceback (so that it can be printed out).
685 If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
686 """
687
688 if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
689 expected = True
690 if video_id is not None:
691 msg = video_id + ': ' + msg
692 if cause:
693 msg += ' (caused by %r)' % cause
694 if not expected:
695 msg += bug_reports_message()
696 super(ExtractorError, self).__init__(msg)
697
698 self.traceback = tb
699 self.exc_info = sys.exc_info() # preserve original exception
700 self.cause = cause
701 self.video_id = video_id
702
703 def format_traceback(self):
704 if self.traceback is None:
705 return None
706 return ''.join(traceback.format_tb(self.traceback))
707
708
709 class UnsupportedError(ExtractorError):
710 def __init__(self, url):
711 super(UnsupportedError, self).__init__(
712 'Unsupported URL: %s' % url, expected=True)
713 self.url = url
714
715
716 class RegexNotFoundError(ExtractorError):
717 """Error when a regex didn't match"""
718 pass
719
720
721 class DownloadError(Exception):
722 """Download Error exception.
723
724 This exception may be thrown by FileDownloader objects if they are not
725 configured to continue on errors. They will contain the appropriate
726 error message.
727 """
728
729 def __init__(self, msg, exc_info=None):
730 """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
731 super(DownloadError, self).__init__(msg)
732 self.exc_info = exc_info
733
734
735 class SameFileError(Exception):
736 """Same File exception.
737
738 This exception will be thrown by FileDownloader objects if they detect
739 multiple files would have to be downloaded to the same file on disk.
740 """
741 pass
742
743
744 class PostProcessingError(Exception):
745 """Post Processing exception.
746
747 This exception may be raised by PostProcessor's .run() method to
748 indicate an error in the postprocessing task.
749 """
750
751 def __init__(self, msg):
752 self.msg = msg
753
754
755 class MaxDownloadsReached(Exception):
756 """ --max-downloads limit has been reached. """
757 pass
758
759
760 class UnavailableVideoError(Exception):
761 """Unavailable Format exception.
762
763 This exception will be thrown when a video is requested
764 in a format that is not available for that video.
765 """
766 pass
767
768
769 class ContentTooShortError(Exception):
770 """Content Too Short exception.
771
772 This exception may be raised by FileDownloader objects when a file they
773 download is too small for what the server announced first, indicating
774 the connection was probably interrupted.
775 """
776
777 def __init__(self, downloaded, expected):
778 # Both in bytes
779 self.downloaded = downloaded
780 self.expected = expected
781
782
783 class XAttrMetadataError(Exception):
784 def __init__(self, code=None, msg='Unknown error'):
785 super(XAttrMetadataError, self).__init__(msg)
786 self.code = code
787 self.msg = msg
788
789 # Parsing code and msg
790 if (self.code in (errno.ENOSPC, errno.EDQUOT) or
791 'No space left' in self.msg or 'Disk quota excedded' in self.msg):
792 self.reason = 'NO_SPACE'
793 elif self.code == errno.E2BIG or 'Argument list too long' in self.msg:
794 self.reason = 'VALUE_TOO_LONG'
795 else:
796 self.reason = 'NOT_SUPPORTED'
797
798
799 class XAttrUnavailableError(Exception):
800 pass
801
802
803 def _create_http_connection(ydl_handler, http_class, is_https, *args, **kwargs):
804 # Working around python 2 bug (see http://bugs.python.org/issue17849) by limiting
805 # expected HTTP responses to meet HTTP/1.0 or later (see also
806 # https://github.com/rg3/youtube-dl/issues/6727)
807 if sys.version_info < (3, 0):
808 kwargs[b'strict'] = True
809 hc = http_class(*args, **kwargs)
810 source_address = ydl_handler._params.get('source_address')
811 if source_address is not None:
812 sa = (source_address, 0)
813 if hasattr(hc, 'source_address'): # Python 2.7+
814 hc.source_address = sa
815 else: # Python 2.6
816 def _hc_connect(self, *args, **kwargs):
817 sock = compat_socket_create_connection(
818 (self.host, self.port), self.timeout, sa)
819 if is_https:
820 self.sock = ssl.wrap_socket(
821 sock, self.key_file, self.cert_file,
822 ssl_version=ssl.PROTOCOL_TLSv1)
823 else:
824 self.sock = sock
825 hc.connect = functools.partial(_hc_connect, hc)
826
827 return hc
828
829
830 def handle_youtubedl_headers(headers):
831 filtered_headers = headers
832
833 if 'Youtubedl-no-compression' in filtered_headers:
834 filtered_headers = dict((k, v) for k, v in filtered_headers.items() if k.lower() != 'accept-encoding')
835 del filtered_headers['Youtubedl-no-compression']
836
837 return filtered_headers
838
839
840 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
841 """Handler for HTTP requests and responses.
842
843 This class, when installed with an OpenerDirector, automatically adds
844 the standard headers to every HTTP request and handles gzipped and
845 deflated responses from web servers. If compression is to be avoided in
846 a particular request, the original request in the program code only has
847 to include the HTTP header "Youtubedl-no-compression", which will be
848 removed before making the real request.
849
850 Part of this code was copied from:
851
852 http://techknack.net/python-urllib2-handlers/
853
854 Andrew Rowls, the author of that code, agreed to release it to the
855 public domain.
856 """
857
858 def __init__(self, params, *args, **kwargs):
859 compat_urllib_request.HTTPHandler.__init__(self, *args, **kwargs)
860 self._params = params
861
862 def http_open(self, req):
863 conn_class = compat_http_client.HTTPConnection
864
865 socks_proxy = req.headers.get('Ytdl-socks-proxy')
866 if socks_proxy:
867 conn_class = make_socks_conn_class(conn_class, socks_proxy)
868 del req.headers['Ytdl-socks-proxy']
869
870 return self.do_open(functools.partial(
871 _create_http_connection, self, conn_class, False),
872 req)
873
874 @staticmethod
875 def deflate(data):
876 try:
877 return zlib.decompress(data, -zlib.MAX_WBITS)
878 except zlib.error:
879 return zlib.decompress(data)
880
881 @staticmethod
882 def addinfourl_wrapper(stream, headers, url, code):
883 if hasattr(compat_urllib_request.addinfourl, 'getcode'):
884 return compat_urllib_request.addinfourl(stream, headers, url, code)
885 ret = compat_urllib_request.addinfourl(stream, headers, url)
886 ret.code = code
887 return ret
888
889 def http_request(self, req):
890 # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
891 # always respected by websites, some tend to give out URLs with non percent-encoded
892 # non-ASCII characters (see telemb.py, ard.py [#3412])
893 # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
894 # To work around aforementioned issue we will replace request's original URL with
895 # percent-encoded one
896 # Since redirects are also affected (e.g. http://www.southpark.de/alle-episoden/s18e09)
897 # the code of this workaround has been moved here from YoutubeDL.urlopen()
898 url = req.get_full_url()
899 url_escaped = escape_url(url)
900
901 # Substitute URL if any change after escaping
902 if url != url_escaped:
903 req = update_Request(req, url=url_escaped)
904
905 for h, v in std_headers.items():
906 # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275
907 # The dict keys are capitalized because of this bug by urllib
908 if h.capitalize() not in req.headers:
909 req.add_header(h, v)
910
911 req.headers = handle_youtubedl_headers(req.headers)
912
913 if sys.version_info < (2, 7) and '#' in req.get_full_url():
914 # Python 2.6 is brain-dead when it comes to fragments
915 req._Request__original = req._Request__original.partition('#')[0]
916 req._Request__r_type = req._Request__r_type.partition('#')[0]
917
918 return req
919
920 def http_response(self, req, resp):
921 old_resp = resp
922 # gzip
923 if resp.headers.get('Content-encoding', '') == 'gzip':
924 content = resp.read()
925 gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
926 try:
927 uncompressed = io.BytesIO(gz.read())
928 except IOError as original_ioerror:
929 # There may be junk add the end of the file
930 # See http://stackoverflow.com/q/4928560/35070 for details
931 for i in range(1, 1024):
932 try:
933 gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
934 uncompressed = io.BytesIO(gz.read())
935 except IOError:
936 continue
937 break
938 else:
939 raise original_ioerror
940 resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
941 resp.msg = old_resp.msg
942 del resp.headers['Content-encoding']
943 # deflate
944 if resp.headers.get('Content-encoding', '') == 'deflate':
945 gz = io.BytesIO(self.deflate(resp.read()))
946 resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
947 resp.msg = old_resp.msg
948 del resp.headers['Content-encoding']
949 # Percent-encode redirect URL of Location HTTP header to satisfy RFC 3986 (see
950 # https://github.com/rg3/youtube-dl/issues/6457).
951 if 300 <= resp.code < 400:
952 location = resp.headers.get('Location')
953 if location:
954 # As of RFC 2616 default charset is iso-8859-1 that is respected by python 3
955 if sys.version_info >= (3, 0):
956 location = location.encode('iso-8859-1').decode('utf-8')
957 else:
958 location = location.decode('utf-8')
959 location_escaped = escape_url(location)
960 if location != location_escaped:
961 del resp.headers['Location']
962 if sys.version_info < (3, 0):
963 location_escaped = location_escaped.encode('utf-8')
964 resp.headers['Location'] = location_escaped
965 return resp
966
967 https_request = http_request
968 https_response = http_response
969
970
971 def make_socks_conn_class(base_class, socks_proxy):
972 assert issubclass(base_class, (
973 compat_http_client.HTTPConnection, compat_http_client.HTTPSConnection))
974
975 url_components = compat_urlparse.urlparse(socks_proxy)
976 if url_components.scheme.lower() == 'socks5':
977 socks_type = ProxyType.SOCKS5
978 elif url_components.scheme.lower() in ('socks', 'socks4'):
979 socks_type = ProxyType.SOCKS4
980 elif url_components.scheme.lower() == 'socks4a':
981 socks_type = ProxyType.SOCKS4A
982
983 def unquote_if_non_empty(s):
984 if not s:
985 return s
986 return compat_urllib_parse_unquote_plus(s)
987
988 proxy_args = (
989 socks_type,
990 url_components.hostname, url_components.port or 1080,
991 True, # Remote DNS
992 unquote_if_non_empty(url_components.username),
993 unquote_if_non_empty(url_components.password),
994 )
995
996 class SocksConnection(base_class):
997 def connect(self):
998 self.sock = sockssocket()
999 self.sock.setproxy(*proxy_args)
1000 if type(self.timeout) in (int, float):
1001 self.sock.settimeout(self.timeout)
1002 self.sock.connect((self.host, self.port))
1003
1004 if isinstance(self, compat_http_client.HTTPSConnection):
1005 if hasattr(self, '_context'): # Python > 2.6
1006 self.sock = self._context.wrap_socket(
1007 self.sock, server_hostname=self.host)
1008 else:
1009 self.sock = ssl.wrap_socket(self.sock)
1010
1011 return SocksConnection
1012
1013
1014 class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler):
1015 def __init__(self, params, https_conn_class=None, *args, **kwargs):
1016 compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs)
1017 self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection
1018 self._params = params
1019
1020 def https_open(self, req):
1021 kwargs = {}
1022 conn_class = self._https_conn_class
1023
1024 if hasattr(self, '_context'): # python > 2.6
1025 kwargs['context'] = self._context
1026 if hasattr(self, '_check_hostname'): # python 3.x
1027 kwargs['check_hostname'] = self._check_hostname
1028
1029 socks_proxy = req.headers.get('Ytdl-socks-proxy')
1030 if socks_proxy:
1031 conn_class = make_socks_conn_class(conn_class, socks_proxy)
1032 del req.headers['Ytdl-socks-proxy']
1033
1034 return self.do_open(functools.partial(
1035 _create_http_connection, self, conn_class, True),
1036 req, **kwargs)
1037
1038
1039 class YoutubeDLCookieProcessor(compat_urllib_request.HTTPCookieProcessor):
1040 def __init__(self, cookiejar=None):
1041 compat_urllib_request.HTTPCookieProcessor.__init__(self, cookiejar)
1042
1043 def http_response(self, request, response):
1044 # Python 2 will choke on next HTTP request in row if there are non-ASCII
1045 # characters in Set-Cookie HTTP header of last response (see
1046 # https://github.com/rg3/youtube-dl/issues/6769).
1047 # In order to at least prevent crashing we will percent encode Set-Cookie
1048 # header before HTTPCookieProcessor starts processing it.
1049 # if sys.version_info < (3, 0) and response.headers:
1050 # for set_cookie_header in ('Set-Cookie', 'Set-Cookie2'):
1051 # set_cookie = response.headers.get(set_cookie_header)
1052 # if set_cookie:
1053 # set_cookie_escaped = compat_urllib_parse.quote(set_cookie, b"%/;:@&=+$,!~*'()?#[] ")
1054 # if set_cookie != set_cookie_escaped:
1055 # del response.headers[set_cookie_header]
1056 # response.headers[set_cookie_header] = set_cookie_escaped
1057 return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response)
1058
1059 https_request = compat_urllib_request.HTTPCookieProcessor.http_request
1060 https_response = http_response
1061
1062
1063 def extract_timezone(date_str):
1064 m = re.search(
1065 r'^.{8,}?(?P<tz>Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
1066 date_str)
1067 if not m:
1068 timezone = datetime.timedelta()
1069 else:
1070 date_str = date_str[:-len(m.group('tz'))]
1071 if not m.group('sign'):
1072 timezone = datetime.timedelta()
1073 else:
1074 sign = 1 if m.group('sign') == '+' else -1
1075 timezone = datetime.timedelta(
1076 hours=sign * int(m.group('hours')),
1077 minutes=sign * int(m.group('minutes')))
1078 return timezone, date_str
1079
1080
1081 def parse_iso8601(date_str, delimiter='T', timezone=None):
1082 """ Return a UNIX timestamp from the given date """
1083
1084 if date_str is None:
1085 return None
1086
1087 date_str = re.sub(r'\.[0-9]+', '', date_str)
1088
1089 if timezone is None:
1090 timezone, date_str = extract_timezone(date_str)
1091
1092 try:
1093 date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
1094 dt = datetime.datetime.strptime(date_str, date_format) - timezone
1095 return calendar.timegm(dt.timetuple())
1096 except ValueError:
1097 pass
1098
1099
1100 def date_formats(day_first=True):
1101 return DATE_FORMATS_DAY_FIRST if day_first else DATE_FORMATS_MONTH_FIRST
1102
1103
1104 def unified_strdate(date_str, day_first=True):
1105 """Return a string with the date in the format YYYYMMDD"""
1106
1107 if date_str is None:
1108 return None
1109 upload_date = None
1110 # Replace commas
1111 date_str = date_str.replace(',', ' ')
1112 # Remove AM/PM + timezone
1113 date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
1114 _, date_str = extract_timezone(date_str)
1115
1116 for expression in date_formats(day_first):
1117 try:
1118 upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
1119 except ValueError:
1120 pass
1121 if upload_date is None:
1122 timetuple = email.utils.parsedate_tz(date_str)
1123 if timetuple:
1124 try:
1125 upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
1126 except ValueError:
1127 pass
1128 if upload_date is not None:
1129 return compat_str(upload_date)
1130
1131
1132 def unified_timestamp(date_str, day_first=True):
1133 if date_str is None:
1134 return None
1135
1136 date_str = date_str.replace(',', ' ')
1137
1138 pm_delta = 12 if re.search(r'(?i)PM', date_str) else 0
1139 timezone, date_str = extract_timezone(date_str)
1140
1141 # Remove AM/PM + timezone
1142 date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
1143
1144 for expression in date_formats(day_first):
1145 try:
1146 dt = datetime.datetime.strptime(date_str, expression) - timezone + datetime.timedelta(hours=pm_delta)
1147 return calendar.timegm(dt.timetuple())
1148 except ValueError:
1149 pass
1150 timetuple = email.utils.parsedate_tz(date_str)
1151 if timetuple:
1152 return calendar.timegm(timetuple) + pm_delta * 3600
1153
1154
1155 def determine_ext(url, default_ext='unknown_video'):
1156 if url is None:
1157 return default_ext
1158 guess = url.partition('?')[0].rpartition('.')[2]
1159 if re.match(r'^[A-Za-z0-9]+$', guess):
1160 return guess
1161 # Try extract ext from URLs like http://example.com/foo/bar.mp4/?download
1162 elif guess.rstrip('/') in KNOWN_EXTENSIONS:
1163 return guess.rstrip('/')
1164 else:
1165 return default_ext
1166
1167
1168 def subtitles_filename(filename, sub_lang, sub_format):
1169 return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format
1170
1171
1172 def date_from_str(date_str):
1173 """
1174 Return a datetime object from a string in the format YYYYMMDD or
1175 (now|today)[+-][0-9](day|week|month|year)(s)?"""
1176 today = datetime.date.today()
1177 if date_str in ('now', 'today'):
1178 return today
1179 if date_str == 'yesterday':
1180 return today - datetime.timedelta(days=1)
1181 match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
1182 if match is not None:
1183 sign = match.group('sign')
1184 time = int(match.group('time'))
1185 if sign == '-':
1186 time = -time
1187 unit = match.group('unit')
1188 # A bad approximation?
1189 if unit == 'month':
1190 unit = 'day'
1191 time *= 30
1192 elif unit == 'year':
1193 unit = 'day'
1194 time *= 365
1195 unit += 's'
1196 delta = datetime.timedelta(**{unit: time})
1197 return today + delta
1198 return datetime.datetime.strptime(date_str, '%Y%m%d').date()
1199
1200
1201 def hyphenate_date(date_str):
1202 """
1203 Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
1204 match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
1205 if match is not None:
1206 return '-'.join(match.groups())
1207 else:
1208 return date_str
1209
1210
1211 class DateRange(object):
1212 """Represents a time interval between two dates"""
1213
1214 def __init__(self, start=None, end=None):
1215 """start and end must be strings in the format accepted by date"""
1216 if start is not None:
1217 self.start = date_from_str(start)
1218 else:
1219 self.start = datetime.datetime.min.date()
1220 if end is not None:
1221 self.end = date_from_str(end)
1222 else:
1223 self.end = datetime.datetime.max.date()
1224 if self.start > self.end:
1225 raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
1226
1227 @classmethod
1228 def day(cls, day):
1229 """Returns a range that only contains the given day"""
1230 return cls(day, day)
1231
1232 def __contains__(self, date):
1233 """Check if the date is in the range"""
1234 if not isinstance(date, datetime.date):
1235 date = date_from_str(date)
1236 return self.start <= date <= self.end
1237
1238 def __str__(self):
1239 return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
1240
1241
1242 def platform_name():
1243 """ Returns the platform name as a compat_str """
1244 res = platform.platform()
1245 if isinstance(res, bytes):
1246 res = res.decode(preferredencoding())
1247
1248 assert isinstance(res, compat_str)
1249 return res
1250
1251
1252 def _windows_write_string(s, out):
1253 """ Returns True if the string was written using special methods,
1254 False if it has yet to be written out."""
1255 # Adapted from http://stackoverflow.com/a/3259271/35070
1256
1257 import ctypes
1258 import ctypes.wintypes
1259
1260 WIN_OUTPUT_IDS = {
1261 1: -11,
1262 2: -12,
1263 }
1264
1265 try:
1266 fileno = out.fileno()
1267 except AttributeError:
1268 # If the output stream doesn't have a fileno, it's virtual
1269 return False
1270 except io.UnsupportedOperation:
1271 # Some strange Windows pseudo files?
1272 return False
1273 if fileno not in WIN_OUTPUT_IDS:
1274 return False
1275
1276 GetStdHandle = ctypes.WINFUNCTYPE(
1277 ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
1278 (b'GetStdHandle', ctypes.windll.kernel32))
1279 h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
1280
1281 WriteConsoleW = ctypes.WINFUNCTYPE(
1282 ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
1283 ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
1284 ctypes.wintypes.LPVOID)((b'WriteConsoleW', ctypes.windll.kernel32))
1285 written = ctypes.wintypes.DWORD(0)
1286
1287 GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)((b'GetFileType', ctypes.windll.kernel32))
1288 FILE_TYPE_CHAR = 0x0002
1289 FILE_TYPE_REMOTE = 0x8000
1290 GetConsoleMode = ctypes.WINFUNCTYPE(
1291 ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
1292 ctypes.POINTER(ctypes.wintypes.DWORD))(
1293 (b'GetConsoleMode', ctypes.windll.kernel32))
1294 INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
1295
1296 def not_a_console(handle):
1297 if handle == INVALID_HANDLE_VALUE or handle is None:
1298 return True
1299 return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR or
1300 GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
1301
1302 if not_a_console(h):
1303 return False
1304
1305 def next_nonbmp_pos(s):
1306 try:
1307 return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
1308 except StopIteration:
1309 return len(s)
1310
1311 while s:
1312 count = min(next_nonbmp_pos(s), 1024)
1313
1314 ret = WriteConsoleW(
1315 h, s, count if count else 2, ctypes.byref(written), None)
1316 if ret == 0:
1317 raise OSError('Failed to write string')
1318 if not count: # We just wrote a non-BMP character
1319 assert written.value == 2
1320 s = s[1:]
1321 else:
1322 assert written.value > 0
1323 s = s[written.value:]
1324 return True
1325
1326
1327 def write_string(s, out=None, encoding=None):
1328 if out is None:
1329 out = sys.stderr
1330 assert type(s) == compat_str
1331
1332 if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
1333 if _windows_write_string(s, out):
1334 return
1335
1336 if ('b' in getattr(out, 'mode', '') or
1337 sys.version_info[0] < 3): # Python 2 lies about mode of sys.stderr
1338 byt = s.encode(encoding or preferredencoding(), 'ignore')
1339 out.write(byt)
1340 elif hasattr(out, 'buffer'):
1341 enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
1342 byt = s.encode(enc, 'ignore')
1343 out.buffer.write(byt)
1344 else:
1345 out.write(s)
1346 out.flush()
1347
1348
1349 def bytes_to_intlist(bs):
1350 if not bs:
1351 return []
1352 if isinstance(bs[0], int): # Python 3
1353 return list(bs)
1354 else:
1355 return [ord(c) for c in bs]
1356
1357
1358 def intlist_to_bytes(xs):
1359 if not xs:
1360 return b''
1361 return compat_struct_pack('%dB' % len(xs), *xs)
1362
1363
1364 # Cross-platform file locking
1365 if sys.platform == 'win32':
1366 import ctypes.wintypes
1367 import msvcrt
1368
1369 class OVERLAPPED(ctypes.Structure):
1370 _fields_ = [
1371 ('Internal', ctypes.wintypes.LPVOID),
1372 ('InternalHigh', ctypes.wintypes.LPVOID),
1373 ('Offset', ctypes.wintypes.DWORD),
1374 ('OffsetHigh', ctypes.wintypes.DWORD),
1375 ('hEvent', ctypes.wintypes.HANDLE),
1376 ]
1377
1378 kernel32 = ctypes.windll.kernel32
1379 LockFileEx = kernel32.LockFileEx
1380 LockFileEx.argtypes = [
1381 ctypes.wintypes.HANDLE, # hFile
1382 ctypes.wintypes.DWORD, # dwFlags
1383 ctypes.wintypes.DWORD, # dwReserved
1384 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
1385 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
1386 ctypes.POINTER(OVERLAPPED) # Overlapped
1387 ]
1388 LockFileEx.restype = ctypes.wintypes.BOOL
1389 UnlockFileEx = kernel32.UnlockFileEx
1390 UnlockFileEx.argtypes = [
1391 ctypes.wintypes.HANDLE, # hFile
1392 ctypes.wintypes.DWORD, # dwReserved
1393 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
1394 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
1395 ctypes.POINTER(OVERLAPPED) # Overlapped
1396 ]
1397 UnlockFileEx.restype = ctypes.wintypes.BOOL
1398 whole_low = 0xffffffff
1399 whole_high = 0x7fffffff
1400
1401 def _lock_file(f, exclusive):
1402 overlapped = OVERLAPPED()
1403 overlapped.Offset = 0
1404 overlapped.OffsetHigh = 0
1405 overlapped.hEvent = 0
1406 f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1407 handle = msvcrt.get_osfhandle(f.fileno())
1408 if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1409 whole_low, whole_high, f._lock_file_overlapped_p):
1410 raise OSError('Locking file failed: %r' % ctypes.FormatError())
1411
1412 def _unlock_file(f):
1413 assert f._lock_file_overlapped_p
1414 handle = msvcrt.get_osfhandle(f.fileno())
1415 if not UnlockFileEx(handle, 0,
1416 whole_low, whole_high, f._lock_file_overlapped_p):
1417 raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1418
1419 else:
1420 # Some platforms, such as Jython, is missing fcntl
1421 try:
1422 import fcntl
1423
1424 def _lock_file(f, exclusive):
1425 fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1426
1427 def _unlock_file(f):
1428 fcntl.flock(f, fcntl.LOCK_UN)
1429 except ImportError:
1430 UNSUPPORTED_MSG = 'file locking is not supported on this platform'
1431
1432 def _lock_file(f, exclusive):
1433 raise IOError(UNSUPPORTED_MSG)
1434
1435 def _unlock_file(f):
1436 raise IOError(UNSUPPORTED_MSG)
1437
1438
1439 class locked_file(object):
1440 def __init__(self, filename, mode, encoding=None):
1441 assert mode in ['r', 'a', 'w']
1442 self.f = io.open(filename, mode, encoding=encoding)
1443 self.mode = mode
1444
1445 def __enter__(self):
1446 exclusive = self.mode != 'r'
1447 try:
1448 _lock_file(self.f, exclusive)
1449 except IOError:
1450 self.f.close()
1451 raise
1452 return self
1453
1454 def __exit__(self, etype, value, traceback):
1455 try:
1456 _unlock_file(self.f)
1457 finally:
1458 self.f.close()
1459
1460 def __iter__(self):
1461 return iter(self.f)
1462
1463 def write(self, *args):
1464 return self.f.write(*args)
1465
1466 def read(self, *args):
1467 return self.f.read(*args)
1468
1469
1470 def get_filesystem_encoding():
1471 encoding = sys.getfilesystemencoding()
1472 return encoding if encoding is not None else 'utf-8'
1473
1474
1475 def shell_quote(args):
1476 quoted_args = []
1477 encoding = get_filesystem_encoding()
1478 for a in args:
1479 if isinstance(a, bytes):
1480 # We may get a filename encoded with 'encodeFilename'
1481 a = a.decode(encoding)
1482 quoted_args.append(pipes.quote(a))
1483 return ' '.join(quoted_args)
1484
1485
1486 def smuggle_url(url, data):
1487 """ Pass additional data in a URL for internal use. """
1488
1489 url, idata = unsmuggle_url(url, {})
1490 data.update(idata)
1491 sdata = compat_urllib_parse_urlencode(
1492 {'__youtubedl_smuggle': json.dumps(data)})
1493 return url + '#' + sdata
1494
1495
1496 def unsmuggle_url(smug_url, default=None):
1497 if '#__youtubedl_smuggle' not in smug_url:
1498 return smug_url, default
1499 url, _, sdata = smug_url.rpartition('#')
1500 jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1501 data = json.loads(jsond)
1502 return url, data
1503
1504
1505 def format_bytes(bytes):
1506 if bytes is None:
1507 return 'N/A'
1508 if type(bytes) is str:
1509 bytes = float(bytes)
1510 if bytes == 0.0:
1511 exponent = 0
1512 else:
1513 exponent = int(math.log(bytes, 1024.0))
1514 suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1515 converted = float(bytes) / float(1024 ** exponent)
1516 return '%.2f%s' % (converted, suffix)
1517
1518
1519 def lookup_unit_table(unit_table, s):
1520 units_re = '|'.join(re.escape(u) for u in unit_table)
1521 m = re.match(
1522 r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)\b' % units_re, s)
1523 if not m:
1524 return None
1525 num_str = m.group('num').replace(',', '.')
1526 mult = unit_table[m.group('unit')]
1527 return int(float(num_str) * mult)
1528
1529
1530 def parse_filesize(s):
1531 if s is None:
1532 return None
1533
1534 # The lower-case forms are of course incorrect and unofficial,
1535 # but we support those too
1536 _UNIT_TABLE = {
1537 'B': 1,
1538 'b': 1,
1539 'bytes': 1,
1540 'KiB': 1024,
1541 'KB': 1000,
1542 'kB': 1024,
1543 'Kb': 1000,
1544 'kb': 1000,
1545 'kilobytes': 1000,
1546 'kibibytes': 1024,
1547 'MiB': 1024 ** 2,
1548 'MB': 1000 ** 2,
1549 'mB': 1024 ** 2,
1550 'Mb': 1000 ** 2,
1551 'mb': 1000 ** 2,
1552 'megabytes': 1000 ** 2,
1553 'mebibytes': 1024 ** 2,
1554 'GiB': 1024 ** 3,
1555 'GB': 1000 ** 3,
1556 'gB': 1024 ** 3,
1557 'Gb': 1000 ** 3,
1558 'gb': 1000 ** 3,
1559 'gigabytes': 1000 ** 3,
1560 'gibibytes': 1024 ** 3,
1561 'TiB': 1024 ** 4,
1562 'TB': 1000 ** 4,
1563 'tB': 1024 ** 4,
1564 'Tb': 1000 ** 4,
1565 'tb': 1000 ** 4,
1566 'terabytes': 1000 ** 4,
1567 'tebibytes': 1024 ** 4,
1568 'PiB': 1024 ** 5,
1569 'PB': 1000 ** 5,
1570 'pB': 1024 ** 5,
1571 'Pb': 1000 ** 5,
1572 'pb': 1000 ** 5,
1573 'petabytes': 1000 ** 5,
1574 'pebibytes': 1024 ** 5,
1575 'EiB': 1024 ** 6,
1576 'EB': 1000 ** 6,
1577 'eB': 1024 ** 6,
1578 'Eb': 1000 ** 6,
1579 'eb': 1000 ** 6,
1580 'exabytes': 1000 ** 6,
1581 'exbibytes': 1024 ** 6,
1582 'ZiB': 1024 ** 7,
1583 'ZB': 1000 ** 7,
1584 'zB': 1024 ** 7,
1585 'Zb': 1000 ** 7,
1586 'zb': 1000 ** 7,
1587 'zettabytes': 1000 ** 7,
1588 'zebibytes': 1024 ** 7,
1589 'YiB': 1024 ** 8,
1590 'YB': 1000 ** 8,
1591 'yB': 1024 ** 8,
1592 'Yb': 1000 ** 8,
1593 'yb': 1000 ** 8,
1594 'yottabytes': 1000 ** 8,
1595 'yobibytes': 1024 ** 8,
1596 }
1597
1598 return lookup_unit_table(_UNIT_TABLE, s)
1599
1600
1601 def parse_count(s):
1602 if s is None:
1603 return None
1604
1605 s = s.strip()
1606
1607 if re.match(r'^[\d,.]+$', s):
1608 return str_to_int(s)
1609
1610 _UNIT_TABLE = {
1611 'k': 1000,
1612 'K': 1000,
1613 'm': 1000 ** 2,
1614 'M': 1000 ** 2,
1615 'kk': 1000 ** 2,
1616 'KK': 1000 ** 2,
1617 }
1618
1619 return lookup_unit_table(_UNIT_TABLE, s)
1620
1621
1622 def month_by_name(name, lang='en'):
1623 """ Return the number of a month by (locale-independently) English name """
1624
1625 month_names = MONTH_NAMES.get(lang, MONTH_NAMES['en'])
1626
1627 try:
1628 return month_names.index(name) + 1
1629 except ValueError:
1630 return None
1631
1632
1633 def month_by_abbreviation(abbrev):
1634 """ Return the number of a month by (locale-independently) English
1635 abbreviations """
1636
1637 try:
1638 return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1639 except ValueError:
1640 return None
1641
1642
1643 def fix_xml_ampersands(xml_str):
1644 """Replace all the '&' by '&amp;' in XML"""
1645 return re.sub(
1646 r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1647 '&amp;',
1648 xml_str)
1649
1650
1651 def setproctitle(title):
1652 assert isinstance(title, compat_str)
1653
1654 # ctypes in Jython is not complete
1655 # http://bugs.jython.org/issue2148
1656 if sys.platform.startswith('java'):
1657 return
1658
1659 try:
1660 libc = ctypes.cdll.LoadLibrary('libc.so.6')
1661 except OSError:
1662 return
1663 title_bytes = title.encode('utf-8')
1664 buf = ctypes.create_string_buffer(len(title_bytes))
1665 buf.value = title_bytes
1666 try:
1667 libc.prctl(15, buf, 0, 0, 0)
1668 except AttributeError:
1669 return # Strange libc, just skip this
1670
1671
1672 def remove_start(s, start):
1673 return s[len(start):] if s is not None and s.startswith(start) else s
1674
1675
1676 def remove_end(s, end):
1677 return s[:-len(end)] if s is not None and s.endswith(end) else s
1678
1679
1680 def remove_quotes(s):
1681 if s is None or len(s) < 2:
1682 return s
1683 for quote in ('"', "'", ):
1684 if s[0] == quote and s[-1] == quote:
1685 return s[1:-1]
1686 return s
1687
1688
1689 def url_basename(url):
1690 path = compat_urlparse.urlparse(url).path
1691 return path.strip('/').split('/')[-1]
1692
1693
1694 def base_url(url):
1695 return re.match(r'https?://[^?#&]+/', url).group()
1696
1697
1698 class HEADRequest(compat_urllib_request.Request):
1699 def get_method(self):
1700 return 'HEAD'
1701
1702
1703 class PUTRequest(compat_urllib_request.Request):
1704 def get_method(self):
1705 return 'PUT'
1706
1707
1708 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1709 if get_attr:
1710 if v is not None:
1711 v = getattr(v, get_attr, None)
1712 if v == '':
1713 v = None
1714 if v is None:
1715 return default
1716 try:
1717 return int(v) * invscale // scale
1718 except ValueError:
1719 return default
1720
1721
1722 def str_or_none(v, default=None):
1723 return default if v is None else compat_str(v)
1724
1725
1726 def str_to_int(int_str):
1727 """ A more relaxed version of int_or_none """
1728 if int_str is None:
1729 return None
1730 int_str = re.sub(r'[,\.\+]', '', int_str)
1731 return int(int_str)
1732
1733
1734 def float_or_none(v, scale=1, invscale=1, default=None):
1735 if v is None:
1736 return default
1737 try:
1738 return float(v) * invscale / scale
1739 except ValueError:
1740 return default
1741
1742
1743 def strip_or_none(v):
1744 return None if v is None else v.strip()
1745
1746
1747 def parse_duration(s):
1748 if not isinstance(s, compat_basestring):
1749 return None
1750
1751 s = s.strip()
1752
1753 days, hours, mins, secs, ms = [None] * 5
1754 m = re.match(r'(?:(?:(?:(?P<days>[0-9]+):)?(?P<hours>[0-9]+):)?(?P<mins>[0-9]+):)?(?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?$', s)
1755 if m:
1756 days, hours, mins, secs, ms = m.groups()
1757 else:
1758 m = re.match(
1759 r'''(?ix)(?:P?T)?
1760 (?:
1761 (?P<days>[0-9]+)\s*d(?:ays?)?\s*
1762 )?
1763 (?:
1764 (?P<hours>[0-9]+)\s*h(?:ours?)?\s*
1765 )?
1766 (?:
1767 (?P<mins>[0-9]+)\s*m(?:in(?:ute)?s?)?\s*
1768 )?
1769 (?:
1770 (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*s(?:ec(?:ond)?s?)?\s*
1771 )?$''', s)
1772 if m:
1773 days, hours, mins, secs, ms = m.groups()
1774 else:
1775 m = re.match(r'(?i)(?:(?P<hours>[0-9.]+)\s*(?:hours?)|(?P<mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*)$', s)
1776 if m:
1777 hours, mins = m.groups()
1778 else:
1779 return None
1780
1781 duration = 0
1782 if secs:
1783 duration += float(secs)
1784 if mins:
1785 duration += float(mins) * 60
1786 if hours:
1787 duration += float(hours) * 60 * 60
1788 if days:
1789 duration += float(days) * 24 * 60 * 60
1790 if ms:
1791 duration += float(ms)
1792 return duration
1793
1794
1795 def prepend_extension(filename, ext, expected_real_ext=None):
1796 name, real_ext = os.path.splitext(filename)
1797 return (
1798 '{0}.{1}{2}'.format(name, ext, real_ext)
1799 if not expected_real_ext or real_ext[1:] == expected_real_ext
1800 else '{0}.{1}'.format(filename, ext))
1801
1802
1803 def replace_extension(filename, ext, expected_real_ext=None):
1804 name, real_ext = os.path.splitext(filename)
1805 return '{0}.{1}'.format(
1806 name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1807 ext)
1808
1809
1810 def check_executable(exe, args=[]):
1811 """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1812 args can be a list of arguments for a short output (like -version) """
1813 try:
1814 subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1815 except OSError:
1816 return False
1817 return exe
1818
1819
1820 def get_exe_version(exe, args=['--version'],
1821 version_re=None, unrecognized='present'):
1822 """ Returns the version of the specified executable,
1823 or False if the executable is not present """
1824 try:
1825 # STDIN should be redirected too. On UNIX-like systems, ffmpeg triggers
1826 # SIGTTOU if youtube-dl is run in the background.
1827 # See https://github.com/rg3/youtube-dl/issues/955#issuecomment-209789656
1828 out, _ = subprocess.Popen(
1829 [encodeArgument(exe)] + args,
1830 stdin=subprocess.PIPE,
1831 stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1832 except OSError:
1833 return False
1834 if isinstance(out, bytes): # Python 2.x
1835 out = out.decode('ascii', 'ignore')
1836 return detect_exe_version(out, version_re, unrecognized)
1837
1838
1839 def detect_exe_version(output, version_re=None, unrecognized='present'):
1840 assert isinstance(output, compat_str)
1841 if version_re is None:
1842 version_re = r'version\s+([-0-9._a-zA-Z]+)'
1843 m = re.search(version_re, output)
1844 if m:
1845 return m.group(1)
1846 else:
1847 return unrecognized
1848
1849
1850 class PagedList(object):
1851 def __len__(self):
1852 # This is only useful for tests
1853 return len(self.getslice())
1854
1855
1856 class OnDemandPagedList(PagedList):
1857 def __init__(self, pagefunc, pagesize, use_cache=False):
1858 self._pagefunc = pagefunc
1859 self._pagesize = pagesize
1860 self._use_cache = use_cache
1861 if use_cache:
1862 self._cache = {}
1863
1864 def getslice(self, start=0, end=None):
1865 res = []
1866 for pagenum in itertools.count(start // self._pagesize):
1867 firstid = pagenum * self._pagesize
1868 nextfirstid = pagenum * self._pagesize + self._pagesize
1869 if start >= nextfirstid:
1870 continue
1871
1872 page_results = None
1873 if self._use_cache:
1874 page_results = self._cache.get(pagenum)
1875 if page_results is None:
1876 page_results = list(self._pagefunc(pagenum))
1877 if self._use_cache:
1878 self._cache[pagenum] = page_results
1879
1880 startv = (
1881 start % self._pagesize
1882 if firstid <= start < nextfirstid
1883 else 0)
1884
1885 endv = (
1886 ((end - 1) % self._pagesize) + 1
1887 if (end is not None and firstid <= end <= nextfirstid)
1888 else None)
1889
1890 if startv != 0 or endv is not None:
1891 page_results = page_results[startv:endv]
1892 res.extend(page_results)
1893
1894 # A little optimization - if current page is not "full", ie. does
1895 # not contain page_size videos then we can assume that this page
1896 # is the last one - there are no more ids on further pages -
1897 # i.e. no need to query again.
1898 if len(page_results) + startv < self._pagesize:
1899 break
1900
1901 # If we got the whole page, but the next page is not interesting,
1902 # break out early as well
1903 if end == nextfirstid:
1904 break
1905 return res
1906
1907
1908 class InAdvancePagedList(PagedList):
1909 def __init__(self, pagefunc, pagecount, pagesize):
1910 self._pagefunc = pagefunc
1911 self._pagecount = pagecount
1912 self._pagesize = pagesize
1913
1914 def getslice(self, start=0, end=None):
1915 res = []
1916 start_page = start // self._pagesize
1917 end_page = (
1918 self._pagecount if end is None else (end // self._pagesize + 1))
1919 skip_elems = start - start_page * self._pagesize
1920 only_more = None if end is None else end - start
1921 for pagenum in range(start_page, end_page):
1922 page = list(self._pagefunc(pagenum))
1923 if skip_elems:
1924 page = page[skip_elems:]
1925 skip_elems = None
1926 if only_more is not None:
1927 if len(page) < only_more:
1928 only_more -= len(page)
1929 else:
1930 page = page[:only_more]
1931 res.extend(page)
1932 break
1933 res.extend(page)
1934 return res
1935
1936
1937 def uppercase_escape(s):
1938 unicode_escape = codecs.getdecoder('unicode_escape')
1939 return re.sub(
1940 r'\\U[0-9a-fA-F]{8}',
1941 lambda m: unicode_escape(m.group(0))[0],
1942 s)
1943
1944
1945 def lowercase_escape(s):
1946 unicode_escape = codecs.getdecoder('unicode_escape')
1947 return re.sub(
1948 r'\\u[0-9a-fA-F]{4}',
1949 lambda m: unicode_escape(m.group(0))[0],
1950 s)
1951
1952
1953 def escape_rfc3986(s):
1954 """Escape non-ASCII characters as suggested by RFC 3986"""
1955 if sys.version_info < (3, 0) and isinstance(s, compat_str):
1956 s = s.encode('utf-8')
1957 return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1958
1959
1960 def escape_url(url):
1961 """Escape URL as suggested by RFC 3986"""
1962 url_parsed = compat_urllib_parse_urlparse(url)
1963 return url_parsed._replace(
1964 netloc=url_parsed.netloc.encode('idna').decode('ascii'),
1965 path=escape_rfc3986(url_parsed.path),
1966 params=escape_rfc3986(url_parsed.params),
1967 query=escape_rfc3986(url_parsed.query),
1968 fragment=escape_rfc3986(url_parsed.fragment)
1969 ).geturl()
1970
1971
1972 def read_batch_urls(batch_fd):
1973 def fixup(url):
1974 if not isinstance(url, compat_str):
1975 url = url.decode('utf-8', 'replace')
1976 BOM_UTF8 = '\xef\xbb\xbf'
1977 if url.startswith(BOM_UTF8):
1978 url = url[len(BOM_UTF8):]
1979 url = url.strip()
1980 if url.startswith(('#', ';', ']')):
1981 return False
1982 return url
1983
1984 with contextlib.closing(batch_fd) as fd:
1985 return [url for url in map(fixup, fd) if url]
1986
1987
1988 def urlencode_postdata(*args, **kargs):
1989 return compat_urllib_parse_urlencode(*args, **kargs).encode('ascii')
1990
1991
1992 def update_url_query(url, query):
1993 if not query:
1994 return url
1995 parsed_url = compat_urlparse.urlparse(url)
1996 qs = compat_parse_qs(parsed_url.query)
1997 qs.update(query)
1998 return compat_urlparse.urlunparse(parsed_url._replace(
1999 query=compat_urllib_parse_urlencode(qs, True)))
2000
2001
2002 def update_Request(req, url=None, data=None, headers={}, query={}):
2003 req_headers = req.headers.copy()
2004 req_headers.update(headers)
2005 req_data = data or req.data
2006 req_url = update_url_query(url or req.get_full_url(), query)
2007 req_get_method = req.get_method()
2008 if req_get_method == 'HEAD':
2009 req_type = HEADRequest
2010 elif req_get_method == 'PUT':
2011 req_type = PUTRequest
2012 else:
2013 req_type = compat_urllib_request.Request
2014 new_req = req_type(
2015 req_url, data=req_data, headers=req_headers,
2016 origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
2017 if hasattr(req, 'timeout'):
2018 new_req.timeout = req.timeout
2019 return new_req
2020
2021
2022 def dict_get(d, key_or_keys, default=None, skip_false_values=True):
2023 if isinstance(key_or_keys, (list, tuple)):
2024 for key in key_or_keys:
2025 if key not in d or d[key] is None or skip_false_values and not d[key]:
2026 continue
2027 return d[key]
2028 return default
2029 return d.get(key_or_keys, default)
2030
2031
2032 def try_get(src, getter, expected_type=None):
2033 try:
2034 v = getter(src)
2035 except (AttributeError, KeyError, TypeError, IndexError):
2036 pass
2037 else:
2038 if expected_type is None or isinstance(v, expected_type):
2039 return v
2040
2041
2042 def encode_compat_str(string, encoding=preferredencoding(), errors='strict'):
2043 return string if isinstance(string, compat_str) else compat_str(string, encoding, errors)
2044
2045
2046 US_RATINGS = {
2047 'G': 0,
2048 'PG': 10,
2049 'PG-13': 13,
2050 'R': 16,
2051 'NC': 18,
2052 }
2053
2054
2055 TV_PARENTAL_GUIDELINES = {
2056 'TV-Y': 0,
2057 'TV-Y7': 7,
2058 'TV-G': 0,
2059 'TV-PG': 0,
2060 'TV-14': 14,
2061 'TV-MA': 17,
2062 }
2063
2064
2065 def parse_age_limit(s):
2066 if type(s) == int:
2067 return s if 0 <= s <= 21 else None
2068 if not isinstance(s, compat_basestring):
2069 return None
2070 m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
2071 if m:
2072 return int(m.group('age'))
2073 if s in US_RATINGS:
2074 return US_RATINGS[s]
2075 return TV_PARENTAL_GUIDELINES.get(s)
2076
2077
2078 def strip_jsonp(code):
2079 return re.sub(
2080 r'(?s)^[a-zA-Z0-9_.$]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
2081
2082
2083 def js_to_json(code):
2084 def fix_kv(m):
2085 v = m.group(0)
2086 if v in ('true', 'false', 'null'):
2087 return v
2088 elif v.startswith('/*') or v == ',':
2089 return ""
2090
2091 if v[0] in ("'", '"'):
2092 v = re.sub(r'(?s)\\.|"', lambda m: {
2093 '"': '\\"',
2094 "\\'": "'",
2095 '\\\n': '',
2096 '\\x': '\\u00',
2097 }.get(m.group(0), m.group(0)), v[1:-1])
2098
2099 INTEGER_TABLE = (
2100 (r'^(0[xX][0-9a-fA-F]+)\s*:?$', 16),
2101 (r'^(0+[0-7]+)\s*:?$', 8),
2102 )
2103
2104 for regex, base in INTEGER_TABLE:
2105 im = re.match(regex, v)
2106 if im:
2107 i = int(im.group(1), base)
2108 return '"%d":' % i if v.endswith(':') else '%d' % i
2109
2110 return '"%s"' % v
2111
2112 return re.sub(r'''(?sx)
2113 "(?:[^"\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^"\\]*"|
2114 '(?:[^'\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^'\\]*'|
2115 /\*.*?\*/|,(?=\s*[\]}])|
2116 [a-zA-Z_][.a-zA-Z_0-9]*|
2117 \b(?:0[xX][0-9a-fA-F]+|0+[0-7]+)(?:\s*:)?|
2118 [0-9]+(?=\s*:)
2119 ''', fix_kv, code)
2120
2121
2122 def qualities(quality_ids):
2123 """ Get a numeric quality value out of a list of possible values """
2124 def q(qid):
2125 try:
2126 return quality_ids.index(qid)
2127 except ValueError:
2128 return -1
2129 return q
2130
2131
2132 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
2133
2134
2135 def limit_length(s, length):
2136 """ Add ellipses to overly long strings """
2137 if s is None:
2138 return None
2139 ELLIPSES = '...'
2140 if len(s) > length:
2141 return s[:length - len(ELLIPSES)] + ELLIPSES
2142 return s
2143
2144
2145 def version_tuple(v):
2146 return tuple(int(e) for e in re.split(r'[-.]', v))
2147
2148
2149 def is_outdated_version(version, limit, assume_new=True):
2150 if not version:
2151 return not assume_new
2152 try:
2153 return version_tuple(version) < version_tuple(limit)
2154 except ValueError:
2155 return not assume_new
2156
2157
2158 def ytdl_is_updateable():
2159 """ Returns if youtube-dl can be updated with -U """
2160 from zipimport import zipimporter
2161
2162 return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
2163
2164
2165 def args_to_str(args):
2166 # Get a short string representation for a subprocess command
2167 return ' '.join(compat_shlex_quote(a) for a in args)
2168
2169
2170 def error_to_compat_str(err):
2171 err_str = str(err)
2172 # On python 2 error byte string must be decoded with proper
2173 # encoding rather than ascii
2174 if sys.version_info[0] < 3:
2175 err_str = err_str.decode(preferredencoding())
2176 return err_str
2177
2178
2179 def mimetype2ext(mt):
2180 if mt is None:
2181 return None
2182
2183 ext = {
2184 'audio/mp4': 'm4a',
2185 # Per RFC 3003, audio/mpeg can be .mp1, .mp2 or .mp3. Here use .mp3 as
2186 # it's the most popular one
2187 'audio/mpeg': 'mp3',
2188 }.get(mt)
2189 if ext is not None:
2190 return ext
2191
2192 _, _, res = mt.rpartition('/')
2193 res = res.split(';')[0].strip().lower()
2194
2195 return {
2196 '3gpp': '3gp',
2197 'smptett+xml': 'tt',
2198 'srt': 'srt',
2199 'ttaf+xml': 'dfxp',
2200 'ttml+xml': 'ttml',
2201 'vtt': 'vtt',
2202 'x-flv': 'flv',
2203 'x-mp4-fragmented': 'mp4',
2204 'x-ms-wmv': 'wmv',
2205 'mpegurl': 'm3u8',
2206 'x-mpegurl': 'm3u8',
2207 'vnd.apple.mpegurl': 'm3u8',
2208 'dash+xml': 'mpd',
2209 'f4m': 'f4m',
2210 'f4m+xml': 'f4m',
2211 'hds+xml': 'f4m',
2212 'vnd.ms-sstr+xml': 'ism',
2213 'quicktime': 'mov',
2214 }.get(res, res)
2215
2216
2217 def parse_codecs(codecs_str):
2218 # http://tools.ietf.org/html/rfc6381
2219 if not codecs_str:
2220 return {}
2221 splited_codecs = list(filter(None, map(
2222 lambda str: str.strip(), codecs_str.strip().strip(',').split(','))))
2223 vcodec, acodec = None, None
2224 for full_codec in splited_codecs:
2225 codec = full_codec.split('.')[0]
2226 if codec in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2', 'h263', 'h264', 'mp4v'):
2227 if not vcodec:
2228 vcodec = full_codec
2229 elif codec in ('mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-3'):
2230 if not acodec:
2231 acodec = full_codec
2232 else:
2233 write_string('WARNING: Unknown codec %s' % full_codec, sys.stderr)
2234 if not vcodec and not acodec:
2235 if len(splited_codecs) == 2:
2236 return {
2237 'vcodec': vcodec,
2238 'acodec': acodec,
2239 }
2240 elif len(splited_codecs) == 1:
2241 return {
2242 'vcodec': 'none',
2243 'acodec': vcodec,
2244 }
2245 else:
2246 return {
2247 'vcodec': vcodec or 'none',
2248 'acodec': acodec or 'none',
2249 }
2250 return {}
2251
2252
2253 def urlhandle_detect_ext(url_handle):
2254 getheader = url_handle.headers.get
2255
2256 cd = getheader('Content-Disposition')
2257 if cd:
2258 m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
2259 if m:
2260 e = determine_ext(m.group('filename'), default_ext=None)
2261 if e:
2262 return e
2263
2264 return mimetype2ext(getheader('Content-Type'))
2265
2266
2267 def encode_data_uri(data, mime_type):
2268 return 'data:%s;base64,%s' % (mime_type, base64.b64encode(data).decode('ascii'))
2269
2270
2271 def age_restricted(content_limit, age_limit):
2272 """ Returns True iff the content should be blocked """
2273
2274 if age_limit is None: # No limit set
2275 return False
2276 if content_limit is None:
2277 return False # Content available for everyone
2278 return age_limit < content_limit
2279
2280
2281 def is_html(first_bytes):
2282 """ Detect whether a file contains HTML by examining its first bytes. """
2283
2284 BOMS = [
2285 (b'\xef\xbb\xbf', 'utf-8'),
2286 (b'\x00\x00\xfe\xff', 'utf-32-be'),
2287 (b'\xff\xfe\x00\x00', 'utf-32-le'),
2288 (b'\xff\xfe', 'utf-16-le'),
2289 (b'\xfe\xff', 'utf-16-be'),
2290 ]
2291 for bom, enc in BOMS:
2292 if first_bytes.startswith(bom):
2293 s = first_bytes[len(bom):].decode(enc, 'replace')
2294 break
2295 else:
2296 s = first_bytes.decode('utf-8', 'replace')
2297
2298 return re.match(r'^\s*<', s)
2299
2300
2301 def determine_protocol(info_dict):
2302 protocol = info_dict.get('protocol')
2303 if protocol is not None:
2304 return protocol
2305
2306 url = info_dict['url']
2307 if url.startswith('rtmp'):
2308 return 'rtmp'
2309 elif url.startswith('mms'):
2310 return 'mms'
2311 elif url.startswith('rtsp'):
2312 return 'rtsp'
2313
2314 ext = determine_ext(url)
2315 if ext == 'm3u8':
2316 return 'm3u8'
2317 elif ext == 'f4m':
2318 return 'f4m'
2319
2320 return compat_urllib_parse_urlparse(url).scheme
2321
2322
2323 def render_table(header_row, data):
2324 """ Render a list of rows, each as a list of values """
2325 table = [header_row] + data
2326 max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
2327 format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
2328 return '\n'.join(format_str % tuple(row) for row in table)
2329
2330
2331 def _match_one(filter_part, dct):
2332 COMPARISON_OPERATORS = {
2333 '<': operator.lt,
2334 '<=': operator.le,
2335 '>': operator.gt,
2336 '>=': operator.ge,
2337 '=': operator.eq,
2338 '!=': operator.ne,
2339 }
2340 operator_rex = re.compile(r'''(?x)\s*
2341 (?P<key>[a-z_]+)
2342 \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
2343 (?:
2344 (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
2345 (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
2346 )
2347 \s*$
2348 ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
2349 m = operator_rex.search(filter_part)
2350 if m:
2351 op = COMPARISON_OPERATORS[m.group('op')]
2352 actual_value = dct.get(m.group('key'))
2353 if (m.group('strval') is not None or
2354 # If the original field is a string and matching comparisonvalue is
2355 # a number we should respect the origin of the original field
2356 # and process comparison value as a string (see
2357 # https://github.com/rg3/youtube-dl/issues/11082).
2358 actual_value is not None and m.group('intval') is not None and
2359 isinstance(actual_value, compat_str)):
2360 if m.group('op') not in ('=', '!='):
2361 raise ValueError(
2362 'Operator %s does not support string values!' % m.group('op'))
2363 comparison_value = m.group('strval') or m.group('intval')
2364 else:
2365 try:
2366 comparison_value = int(m.group('intval'))
2367 except ValueError:
2368 comparison_value = parse_filesize(m.group('intval'))
2369 if comparison_value is None:
2370 comparison_value = parse_filesize(m.group('intval') + 'B')
2371 if comparison_value is None:
2372 raise ValueError(
2373 'Invalid integer value %r in filter part %r' % (
2374 m.group('intval'), filter_part))
2375 if actual_value is None:
2376 return m.group('none_inclusive')
2377 return op(actual_value, comparison_value)
2378
2379 UNARY_OPERATORS = {
2380 '': lambda v: v is not None,
2381 '!': lambda v: v is None,
2382 }
2383 operator_rex = re.compile(r'''(?x)\s*
2384 (?P<op>%s)\s*(?P<key>[a-z_]+)
2385 \s*$
2386 ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
2387 m = operator_rex.search(filter_part)
2388 if m:
2389 op = UNARY_OPERATORS[m.group('op')]
2390 actual_value = dct.get(m.group('key'))
2391 return op(actual_value)
2392
2393 raise ValueError('Invalid filter part %r' % filter_part)
2394
2395
2396 def match_str(filter_str, dct):
2397 """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
2398
2399 return all(
2400 _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
2401
2402
2403 def match_filter_func(filter_str):
2404 def _match_func(info_dict):
2405 if match_str(filter_str, info_dict):
2406 return None
2407 else:
2408 video_title = info_dict.get('title', info_dict.get('id', 'video'))
2409 return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
2410 return _match_func
2411
2412
2413 def parse_dfxp_time_expr(time_expr):
2414 if not time_expr:
2415 return
2416
2417 mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
2418 if mobj:
2419 return float(mobj.group('time_offset'))
2420
2421 mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:(?:\.|:)\d+)?)$', time_expr)
2422 if mobj:
2423 return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3).replace(':', '.'))
2424
2425
2426 def srt_subtitles_timecode(seconds):
2427 return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
2428
2429
2430 def dfxp2srt(dfxp_data):
2431 _x = functools.partial(xpath_with_ns, ns_map={
2432 'ttml': 'http://www.w3.org/ns/ttml',
2433 'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
2434 'ttaf1_0604': 'http://www.w3.org/2006/04/ttaf1',
2435 })
2436
2437 class TTMLPElementParser(object):
2438 out = ''
2439
2440 def start(self, tag, attrib):
2441 if tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
2442 self.out += '\n'
2443
2444 def end(self, tag):
2445 pass
2446
2447 def data(self, data):
2448 self.out += data
2449
2450 def close(self):
2451 return self.out.strip()
2452
2453 def parse_node(node):
2454 target = TTMLPElementParser()
2455 parser = xml.etree.ElementTree.XMLParser(target=target)
2456 parser.feed(xml.etree.ElementTree.tostring(node))
2457 return parser.close()
2458
2459 dfxp = compat_etree_fromstring(dfxp_data.encode('utf-8'))
2460 out = []
2461 paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall(_x('.//ttaf1_0604:p')) or dfxp.findall('.//p')
2462
2463 if not paras:
2464 raise ValueError('Invalid dfxp/TTML subtitle')
2465
2466 for para, index in zip(paras, itertools.count(1)):
2467 begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
2468 end_time = parse_dfxp_time_expr(para.attrib.get('end'))
2469 dur = parse_dfxp_time_expr(para.attrib.get('dur'))
2470 if begin_time is None:
2471 continue
2472 if not end_time:
2473 if not dur:
2474 continue
2475 end_time = begin_time + dur
2476 out.append('%d\n%s --> %s\n%s\n\n' % (
2477 index,
2478 srt_subtitles_timecode(begin_time),
2479 srt_subtitles_timecode(end_time),
2480 parse_node(para)))
2481
2482 return ''.join(out)
2483
2484
2485 def cli_option(params, command_option, param):
2486 param = params.get(param)
2487 if param:
2488 param = compat_str(param)
2489 return [command_option, param] if param is not None else []
2490
2491
2492 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
2493 param = params.get(param)
2494 assert isinstance(param, bool)
2495 if separator:
2496 return [command_option + separator + (true_value if param else false_value)]
2497 return [command_option, true_value if param else false_value]
2498
2499
2500 def cli_valueless_option(params, command_option, param, expected_value=True):
2501 param = params.get(param)
2502 return [command_option] if param == expected_value else []
2503
2504
2505 def cli_configuration_args(params, param, default=[]):
2506 ex_args = params.get(param)
2507 if ex_args is None:
2508 return default
2509 assert isinstance(ex_args, list)
2510 return ex_args
2511
2512
2513 class ISO639Utils(object):
2514 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2515 _lang_map = {
2516 'aa': 'aar',
2517 'ab': 'abk',
2518 'ae': 'ave',
2519 'af': 'afr',
2520 'ak': 'aka',
2521 'am': 'amh',
2522 'an': 'arg',
2523 'ar': 'ara',
2524 'as': 'asm',
2525 'av': 'ava',
2526 'ay': 'aym',
2527 'az': 'aze',
2528 'ba': 'bak',
2529 'be': 'bel',
2530 'bg': 'bul',
2531 'bh': 'bih',
2532 'bi': 'bis',
2533 'bm': 'bam',
2534 'bn': 'ben',
2535 'bo': 'bod',
2536 'br': 'bre',
2537 'bs': 'bos',
2538 'ca': 'cat',
2539 'ce': 'che',
2540 'ch': 'cha',
2541 'co': 'cos',
2542 'cr': 'cre',
2543 'cs': 'ces',
2544 'cu': 'chu',
2545 'cv': 'chv',
2546 'cy': 'cym',
2547 'da': 'dan',
2548 'de': 'deu',
2549 'dv': 'div',
2550 'dz': 'dzo',
2551 'ee': 'ewe',
2552 'el': 'ell',
2553 'en': 'eng',
2554 'eo': 'epo',
2555 'es': 'spa',
2556 'et': 'est',
2557 'eu': 'eus',
2558 'fa': 'fas',
2559 'ff': 'ful',
2560 'fi': 'fin',
2561 'fj': 'fij',
2562 'fo': 'fao',
2563 'fr': 'fra',
2564 'fy': 'fry',
2565 'ga': 'gle',
2566 'gd': 'gla',
2567 'gl': 'glg',
2568 'gn': 'grn',
2569 'gu': 'guj',
2570 'gv': 'glv',
2571 'ha': 'hau',
2572 'he': 'heb',
2573 'hi': 'hin',
2574 'ho': 'hmo',
2575 'hr': 'hrv',
2576 'ht': 'hat',
2577 'hu': 'hun',
2578 'hy': 'hye',
2579 'hz': 'her',
2580 'ia': 'ina',
2581 'id': 'ind',
2582 'ie': 'ile',
2583 'ig': 'ibo',
2584 'ii': 'iii',
2585 'ik': 'ipk',
2586 'io': 'ido',
2587 'is': 'isl',
2588 'it': 'ita',
2589 'iu': 'iku',
2590 'ja': 'jpn',
2591 'jv': 'jav',
2592 'ka': 'kat',
2593 'kg': 'kon',
2594 'ki': 'kik',
2595 'kj': 'kua',
2596 'kk': 'kaz',
2597 'kl': 'kal',
2598 'km': 'khm',
2599 'kn': 'kan',
2600 'ko': 'kor',
2601 'kr': 'kau',
2602 'ks': 'kas',
2603 'ku': 'kur',
2604 'kv': 'kom',
2605 'kw': 'cor',
2606 'ky': 'kir',
2607 'la': 'lat',
2608 'lb': 'ltz',
2609 'lg': 'lug',
2610 'li': 'lim',
2611 'ln': 'lin',
2612 'lo': 'lao',
2613 'lt': 'lit',
2614 'lu': 'lub',
2615 'lv': 'lav',
2616 'mg': 'mlg',
2617 'mh': 'mah',
2618 'mi': 'mri',
2619 'mk': 'mkd',
2620 'ml': 'mal',
2621 'mn': 'mon',
2622 'mr': 'mar',
2623 'ms': 'msa',
2624 'mt': 'mlt',
2625 'my': 'mya',
2626 'na': 'nau',
2627 'nb': 'nob',
2628 'nd': 'nde',
2629 'ne': 'nep',
2630 'ng': 'ndo',
2631 'nl': 'nld',
2632 'nn': 'nno',
2633 'no': 'nor',
2634 'nr': 'nbl',
2635 'nv': 'nav',
2636 'ny': 'nya',
2637 'oc': 'oci',
2638 'oj': 'oji',
2639 'om': 'orm',
2640 'or': 'ori',
2641 'os': 'oss',
2642 'pa': 'pan',
2643 'pi': 'pli',
2644 'pl': 'pol',
2645 'ps': 'pus',
2646 'pt': 'por',
2647 'qu': 'que',
2648 'rm': 'roh',
2649 'rn': 'run',
2650 'ro': 'ron',
2651 'ru': 'rus',
2652 'rw': 'kin',
2653 'sa': 'san',
2654 'sc': 'srd',
2655 'sd': 'snd',
2656 'se': 'sme',
2657 'sg': 'sag',
2658 'si': 'sin',
2659 'sk': 'slk',
2660 'sl': 'slv',
2661 'sm': 'smo',
2662 'sn': 'sna',
2663 'so': 'som',
2664 'sq': 'sqi',
2665 'sr': 'srp',
2666 'ss': 'ssw',
2667 'st': 'sot',
2668 'su': 'sun',
2669 'sv': 'swe',
2670 'sw': 'swa',
2671 'ta': 'tam',
2672 'te': 'tel',
2673 'tg': 'tgk',
2674 'th': 'tha',
2675 'ti': 'tir',
2676 'tk': 'tuk',
2677 'tl': 'tgl',
2678 'tn': 'tsn',
2679 'to': 'ton',
2680 'tr': 'tur',
2681 'ts': 'tso',
2682 'tt': 'tat',
2683 'tw': 'twi',
2684 'ty': 'tah',
2685 'ug': 'uig',
2686 'uk': 'ukr',
2687 'ur': 'urd',
2688 'uz': 'uzb',
2689 've': 'ven',
2690 'vi': 'vie',
2691 'vo': 'vol',
2692 'wa': 'wln',
2693 'wo': 'wol',
2694 'xh': 'xho',
2695 'yi': 'yid',
2696 'yo': 'yor',
2697 'za': 'zha',
2698 'zh': 'zho',
2699 'zu': 'zul',
2700 }
2701
2702 @classmethod
2703 def short2long(cls, code):
2704 """Convert language code from ISO 639-1 to ISO 639-2/T"""
2705 return cls._lang_map.get(code[:2])
2706
2707 @classmethod
2708 def long2short(cls, code):
2709 """Convert language code from ISO 639-2/T to ISO 639-1"""
2710 for short_name, long_name in cls._lang_map.items():
2711 if long_name == code:
2712 return short_name
2713
2714
2715 class ISO3166Utils(object):
2716 # From http://data.okfn.org/data/core/country-list
2717 _country_map = {
2718 'AF': 'Afghanistan',
2719 'AX': 'Åland Islands',
2720 'AL': 'Albania',
2721 'DZ': 'Algeria',
2722 'AS': 'American Samoa',
2723 'AD': 'Andorra',
2724 'AO': 'Angola',
2725 'AI': 'Anguilla',
2726 'AQ': 'Antarctica',
2727 'AG': 'Antigua and Barbuda',
2728 'AR': 'Argentina',
2729 'AM': 'Armenia',
2730 'AW': 'Aruba',
2731 'AU': 'Australia',
2732 'AT': 'Austria',
2733 'AZ': 'Azerbaijan',
2734 'BS': 'Bahamas',
2735 'BH': 'Bahrain',
2736 'BD': 'Bangladesh',
2737 'BB': 'Barbados',
2738 'BY': 'Belarus',
2739 'BE': 'Belgium',
2740 'BZ': 'Belize',
2741 'BJ': 'Benin',
2742 'BM': 'Bermuda',
2743 'BT': 'Bhutan',
2744 'BO': 'Bolivia, Plurinational State of',
2745 'BQ': 'Bonaire, Sint Eustatius and Saba',
2746 'BA': 'Bosnia and Herzegovina',
2747 'BW': 'Botswana',
2748 'BV': 'Bouvet Island',
2749 'BR': 'Brazil',
2750 'IO': 'British Indian Ocean Territory',
2751 'BN': 'Brunei Darussalam',
2752 'BG': 'Bulgaria',
2753 'BF': 'Burkina Faso',
2754 'BI': 'Burundi',
2755 'KH': 'Cambodia',
2756 'CM': 'Cameroon',
2757 'CA': 'Canada',
2758 'CV': 'Cape Verde',
2759 'KY': 'Cayman Islands',
2760 'CF': 'Central African Republic',
2761 'TD': 'Chad',
2762 'CL': 'Chile',
2763 'CN': 'China',
2764 'CX': 'Christmas Island',
2765 'CC': 'Cocos (Keeling) Islands',
2766 'CO': 'Colombia',
2767 'KM': 'Comoros',
2768 'CG': 'Congo',
2769 'CD': 'Congo, the Democratic Republic of the',
2770 'CK': 'Cook Islands',
2771 'CR': 'Costa Rica',
2772 'CI': 'Côte d\'Ivoire',
2773 'HR': 'Croatia',
2774 'CU': 'Cuba',
2775 'CW': 'Curaçao',
2776 'CY': 'Cyprus',
2777 'CZ': 'Czech Republic',
2778 'DK': 'Denmark',
2779 'DJ': 'Djibouti',
2780 'DM': 'Dominica',
2781 'DO': 'Dominican Republic',
2782 'EC': 'Ecuador',
2783 'EG': 'Egypt',
2784 'SV': 'El Salvador',
2785 'GQ': 'Equatorial Guinea',
2786 'ER': 'Eritrea',
2787 'EE': 'Estonia',
2788 'ET': 'Ethiopia',
2789 'FK': 'Falkland Islands (Malvinas)',
2790 'FO': 'Faroe Islands',
2791 'FJ': 'Fiji',
2792 'FI': 'Finland',
2793 'FR': 'France',
2794 'GF': 'French Guiana',
2795 'PF': 'French Polynesia',
2796 'TF': 'French Southern Territories',
2797 'GA': 'Gabon',
2798 'GM': 'Gambia',
2799 'GE': 'Georgia',
2800 'DE': 'Germany',
2801 'GH': 'Ghana',
2802 'GI': 'Gibraltar',
2803 'GR': 'Greece',
2804 'GL': 'Greenland',
2805 'GD': 'Grenada',
2806 'GP': 'Guadeloupe',
2807 'GU': 'Guam',
2808 'GT': 'Guatemala',
2809 'GG': 'Guernsey',
2810 'GN': 'Guinea',
2811 'GW': 'Guinea-Bissau',
2812 'GY': 'Guyana',
2813 'HT': 'Haiti',
2814 'HM': 'Heard Island and McDonald Islands',
2815 'VA': 'Holy See (Vatican City State)',
2816 'HN': 'Honduras',
2817 'HK': 'Hong Kong',
2818 'HU': 'Hungary',
2819 'IS': 'Iceland',
2820 'IN': 'India',
2821 'ID': 'Indonesia',
2822 'IR': 'Iran, Islamic Republic of',
2823 'IQ': 'Iraq',
2824 'IE': 'Ireland',
2825 'IM': 'Isle of Man',
2826 'IL': 'Israel',
2827 'IT': 'Italy',
2828 'JM': 'Jamaica',
2829 'JP': 'Japan',
2830 'JE': 'Jersey',
2831 'JO': 'Jordan',
2832 'KZ': 'Kazakhstan',
2833 'KE': 'Kenya',
2834 'KI': 'Kiribati',
2835 'KP': 'Korea, Democratic People\'s Republic of',
2836 'KR': 'Korea, Republic of',
2837 'KW': 'Kuwait',
2838 'KG': 'Kyrgyzstan',
2839 'LA': 'Lao People\'s Democratic Republic',
2840 'LV': 'Latvia',
2841 'LB': 'Lebanon',
2842 'LS': 'Lesotho',
2843 'LR': 'Liberia',
2844 'LY': 'Libya',
2845 'LI': 'Liechtenstein',
2846 'LT': 'Lithuania',
2847 'LU': 'Luxembourg',
2848 'MO': 'Macao',
2849 'MK': 'Macedonia, the Former Yugoslav Republic of',
2850 'MG': 'Madagascar',
2851 'MW': 'Malawi',
2852 'MY': 'Malaysia',
2853 'MV': 'Maldives',
2854 'ML': 'Mali',
2855 'MT': 'Malta',
2856 'MH': 'Marshall Islands',
2857 'MQ': 'Martinique',
2858 'MR': 'Mauritania',
2859 'MU': 'Mauritius',
2860 'YT': 'Mayotte',
2861 'MX': 'Mexico',
2862 'FM': 'Micronesia, Federated States of',
2863 'MD': 'Moldova, Republic of',
2864 'MC': 'Monaco',
2865 'MN': 'Mongolia',
2866 'ME': 'Montenegro',
2867 'MS': 'Montserrat',
2868 'MA': 'Morocco',
2869 'MZ': 'Mozambique',
2870 'MM': 'Myanmar',
2871 'NA': 'Namibia',
2872 'NR': 'Nauru',
2873 'NP': 'Nepal',
2874 'NL': 'Netherlands',
2875 'NC': 'New Caledonia',
2876 'NZ': 'New Zealand',
2877 'NI': 'Nicaragua',
2878 'NE': 'Niger',
2879 'NG': 'Nigeria',
2880 'NU': 'Niue',
2881 'NF': 'Norfolk Island',
2882 'MP': 'Northern Mariana Islands',
2883 'NO': 'Norway',
2884 'OM': 'Oman',
2885 'PK': 'Pakistan',
2886 'PW': 'Palau',
2887 'PS': 'Palestine, State of',
2888 'PA': 'Panama',
2889 'PG': 'Papua New Guinea',
2890 'PY': 'Paraguay',
2891 'PE': 'Peru',
2892 'PH': 'Philippines',
2893 'PN': 'Pitcairn',
2894 'PL': 'Poland',
2895 'PT': 'Portugal',
2896 'PR': 'Puerto Rico',
2897 'QA': 'Qatar',
2898 'RE': 'Réunion',
2899 'RO': 'Romania',
2900 'RU': 'Russian Federation',
2901 'RW': 'Rwanda',
2902 'BL': 'Saint Barthélemy',
2903 'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2904 'KN': 'Saint Kitts and Nevis',
2905 'LC': 'Saint Lucia',
2906 'MF': 'Saint Martin (French part)',
2907 'PM': 'Saint Pierre and Miquelon',
2908 'VC': 'Saint Vincent and the Grenadines',
2909 'WS': 'Samoa',
2910 'SM': 'San Marino',
2911 'ST': 'Sao Tome and Principe',
2912 'SA': 'Saudi Arabia',
2913 'SN': 'Senegal',
2914 'RS': 'Serbia',
2915 'SC': 'Seychelles',
2916 'SL': 'Sierra Leone',
2917 'SG': 'Singapore',
2918 'SX': 'Sint Maarten (Dutch part)',
2919 'SK': 'Slovakia',
2920 'SI': 'Slovenia',
2921 'SB': 'Solomon Islands',
2922 'SO': 'Somalia',
2923 'ZA': 'South Africa',
2924 'GS': 'South Georgia and the South Sandwich Islands',
2925 'SS': 'South Sudan',
2926 'ES': 'Spain',
2927 'LK': 'Sri Lanka',
2928 'SD': 'Sudan',
2929 'SR': 'Suriname',
2930 'SJ': 'Svalbard and Jan Mayen',
2931 'SZ': 'Swaziland',
2932 'SE': 'Sweden',
2933 'CH': 'Switzerland',
2934 'SY': 'Syrian Arab Republic',
2935 'TW': 'Taiwan, Province of China',
2936 'TJ': 'Tajikistan',
2937 'TZ': 'Tanzania, United Republic of',
2938 'TH': 'Thailand',
2939 'TL': 'Timor-Leste',
2940 'TG': 'Togo',
2941 'TK': 'Tokelau',
2942 'TO': 'Tonga',
2943 'TT': 'Trinidad and Tobago',
2944 'TN': 'Tunisia',
2945 'TR': 'Turkey',
2946 'TM': 'Turkmenistan',
2947 'TC': 'Turks and Caicos Islands',
2948 'TV': 'Tuvalu',
2949 'UG': 'Uganda',
2950 'UA': 'Ukraine',
2951 'AE': 'United Arab Emirates',
2952 'GB': 'United Kingdom',
2953 'US': 'United States',
2954 'UM': 'United States Minor Outlying Islands',
2955 'UY': 'Uruguay',
2956 'UZ': 'Uzbekistan',
2957 'VU': 'Vanuatu',
2958 'VE': 'Venezuela, Bolivarian Republic of',
2959 'VN': 'Viet Nam',
2960 'VG': 'Virgin Islands, British',
2961 'VI': 'Virgin Islands, U.S.',
2962 'WF': 'Wallis and Futuna',
2963 'EH': 'Western Sahara',
2964 'YE': 'Yemen',
2965 'ZM': 'Zambia',
2966 'ZW': 'Zimbabwe',
2967 }
2968
2969 @classmethod
2970 def short2full(cls, code):
2971 """Convert an ISO 3166-2 country code to the corresponding full name"""
2972 return cls._country_map.get(code.upper())
2973
2974
2975 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2976 def __init__(self, proxies=None):
2977 # Set default handlers
2978 for type in ('http', 'https'):
2979 setattr(self, '%s_open' % type,
2980 lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2981 meth(r, proxy, type))
2982 return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2983
2984 def proxy_open(self, req, proxy, type):
2985 req_proxy = req.headers.get('Ytdl-request-proxy')
2986 if req_proxy is not None:
2987 proxy = req_proxy
2988 del req.headers['Ytdl-request-proxy']
2989
2990 if proxy == '__noproxy__':
2991 return None # No Proxy
2992 if compat_urlparse.urlparse(proxy).scheme.lower() in ('socks', 'socks4', 'socks4a', 'socks5'):
2993 req.add_header('Ytdl-socks-proxy', proxy)
2994 # youtube-dl's http/https handlers do wrapping the socket with socks
2995 return None
2996 return compat_urllib_request.ProxyHandler.proxy_open(
2997 self, req, proxy, type)
2998
2999
3000 def ohdave_rsa_encrypt(data, exponent, modulus):
3001 '''
3002 Implement OHDave's RSA algorithm. See http://www.ohdave.com/rsa/
3003
3004 Input:
3005 data: data to encrypt, bytes-like object
3006 exponent, modulus: parameter e and N of RSA algorithm, both integer
3007 Output: hex string of encrypted data
3008
3009 Limitation: supports one block encryption only
3010 '''
3011
3012 payload = int(binascii.hexlify(data[::-1]), 16)
3013 encrypted = pow(payload, exponent, modulus)
3014 return '%x' % encrypted
3015
3016
3017 def encode_base_n(num, n, table=None):
3018 FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
3019 if not table:
3020 table = FULL_TABLE[:n]
3021
3022 if n > len(table):
3023 raise ValueError('base %d exceeds table length %d' % (n, len(table)))
3024
3025 if num == 0:
3026 return table[0]
3027
3028 ret = ''
3029 while num:
3030 ret = table[num % n] + ret
3031 num = num // n
3032 return ret
3033
3034
3035 def decode_packed_codes(code):
3036 mobj = re.search(PACKED_CODES_RE, code)
3037 obfucasted_code, base, count, symbols = mobj.groups()
3038 base = int(base)
3039 count = int(count)
3040 symbols = symbols.split('|')
3041 symbol_table = {}
3042
3043 while count:
3044 count -= 1
3045 base_n_count = encode_base_n(count, base)
3046 symbol_table[base_n_count] = symbols[count] or base_n_count
3047
3048 return re.sub(
3049 r'\b(\w+)\b', lambda mobj: symbol_table[mobj.group(0)],
3050 obfucasted_code)
3051
3052
3053 def parse_m3u8_attributes(attrib):
3054 info = {}
3055 for (key, val) in re.findall(r'(?P<key>[A-Z0-9-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)', attrib):
3056 if val.startswith('"'):
3057 val = val[1:-1]
3058 info[key] = val
3059 return info
3060
3061
3062 def urshift(val, n):
3063 return val >> n if val >= 0 else (val + 0x100000000) >> n
3064
3065
3066 # Based on png2str() written by @gdkchan and improved by @yokrysty
3067 # Originally posted at https://github.com/rg3/youtube-dl/issues/9706
3068 def decode_png(png_data):
3069 # Reference: https://www.w3.org/TR/PNG/
3070 header = png_data[8:]
3071
3072 if png_data[:8] != b'\x89PNG\x0d\x0a\x1a\x0a' or header[4:8] != b'IHDR':
3073 raise IOError('Not a valid PNG file.')
3074
3075 int_map = {1: '>B', 2: '>H', 4: '>I'}
3076 unpack_integer = lambda x: compat_struct_unpack(int_map[len(x)], x)[0]
3077
3078 chunks = []
3079
3080 while header:
3081 length = unpack_integer(header[:4])
3082 header = header[4:]
3083
3084 chunk_type = header[:4]
3085 header = header[4:]
3086
3087 chunk_data = header[:length]
3088 header = header[length:]
3089
3090 header = header[4:] # Skip CRC
3091
3092 chunks.append({
3093 'type': chunk_type,
3094 'length': length,
3095 'data': chunk_data
3096 })
3097
3098 ihdr = chunks[0]['data']
3099
3100 width = unpack_integer(ihdr[:4])
3101 height = unpack_integer(ihdr[4:8])
3102
3103 idat = b''
3104
3105 for chunk in chunks:
3106 if chunk['type'] == b'IDAT':
3107 idat += chunk['data']
3108
3109 if not idat:
3110 raise IOError('Unable to read PNG data.')
3111
3112 decompressed_data = bytearray(zlib.decompress(idat))
3113
3114 stride = width * 3
3115 pixels = []
3116
3117 def _get_pixel(idx):
3118 x = idx % stride
3119 y = idx // stride
3120 return pixels[y][x]
3121
3122 for y in range(height):
3123 basePos = y * (1 + stride)
3124 filter_type = decompressed_data[basePos]
3125
3126 current_row = []
3127
3128 pixels.append(current_row)
3129
3130 for x in range(stride):
3131 color = decompressed_data[1 + basePos + x]
3132 basex = y * stride + x
3133 left = 0
3134 up = 0
3135
3136 if x > 2:
3137 left = _get_pixel(basex - 3)
3138 if y > 0:
3139 up = _get_pixel(basex - stride)
3140
3141 if filter_type == 1: # Sub
3142 color = (color + left) & 0xff
3143 elif filter_type == 2: # Up
3144 color = (color + up) & 0xff
3145 elif filter_type == 3: # Average
3146 color = (color + ((left + up) >> 1)) & 0xff
3147 elif filter_type == 4: # Paeth
3148 a = left
3149 b = up
3150 c = 0
3151
3152 if x > 2 and y > 0:
3153 c = _get_pixel(basex - stride - 3)
3154
3155 p = a + b - c
3156
3157 pa = abs(p - a)
3158 pb = abs(p - b)
3159 pc = abs(p - c)
3160
3161 if pa <= pb and pa <= pc:
3162 color = (color + a) & 0xff
3163 elif pb <= pc:
3164 color = (color + b) & 0xff
3165 else:
3166 color = (color + c) & 0xff
3167
3168 current_row.append(color)
3169
3170 return width, height, pixels
3171
3172
3173 def write_xattr(path, key, value):
3174 # This mess below finds the best xattr tool for the job
3175 try:
3176 # try the pyxattr module...
3177 import xattr
3178
3179 if hasattr(xattr, 'set'): # pyxattr
3180 # Unicode arguments are not supported in python-pyxattr until
3181 # version 0.5.0
3182 # See https://github.com/rg3/youtube-dl/issues/5498
3183 pyxattr_required_version = '0.5.0'
3184 if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
3185 # TODO: fallback to CLI tools
3186 raise XAttrUnavailableError(
3187 'python-pyxattr is detected but is too old. '
3188 'youtube-dl requires %s or above while your version is %s. '
3189 'Falling back to other xattr implementations' % (
3190 pyxattr_required_version, xattr.__version__))
3191
3192 setxattr = xattr.set
3193 else: # xattr
3194 setxattr = xattr.setxattr
3195
3196 try:
3197 setxattr(path, key, value)
3198 except EnvironmentError as e:
3199 raise XAttrMetadataError(e.errno, e.strerror)
3200
3201 except ImportError:
3202 if compat_os_name == 'nt':
3203 # Write xattrs to NTFS Alternate Data Streams:
3204 # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
3205 assert ':' not in key
3206 assert os.path.exists(path)
3207
3208 ads_fn = path + ':' + key
3209 try:
3210 with open(ads_fn, 'wb') as f:
3211 f.write(value)
3212 except EnvironmentError as e:
3213 raise XAttrMetadataError(e.errno, e.strerror)
3214 else:
3215 user_has_setfattr = check_executable('setfattr', ['--version'])
3216 user_has_xattr = check_executable('xattr', ['-h'])
3217
3218 if user_has_setfattr or user_has_xattr:
3219
3220 value = value.decode('utf-8')
3221 if user_has_setfattr:
3222 executable = 'setfattr'
3223 opts = ['-n', key, '-v', value]
3224 elif user_has_xattr:
3225 executable = 'xattr'
3226 opts = ['-w', key, value]
3227
3228 cmd = ([encodeFilename(executable, True)] +
3229 [encodeArgument(o) for o in opts] +
3230 [encodeFilename(path, True)])
3231
3232 try:
3233 p = subprocess.Popen(
3234 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
3235 except EnvironmentError as e:
3236 raise XAttrMetadataError(e.errno, e.strerror)
3237 stdout, stderr = p.communicate()
3238 stderr = stderr.decode('utf-8', 'replace')
3239 if p.returncode != 0:
3240 raise XAttrMetadataError(p.returncode, stderr)
3241
3242 else:
3243 # On Unix, and can't find pyxattr, setfattr, or xattr.
3244 if sys.platform.startswith('linux'):
3245 raise XAttrUnavailableError(
3246 "Couldn't find a tool to set the xattrs. "
3247 "Install either the python 'pyxattr' or 'xattr' "
3248 "modules, or the GNU 'attr' package "
3249 "(which contains the 'setfattr' tool).")
3250 else:
3251 raise XAttrUnavailableError(
3252 "Couldn't find a tool to set the xattrs. "
3253 "Install either the python 'xattr' module, "
3254 "or the 'xattr' binary.")