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