]> Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/utils.py
debian/NEWS: Write news about new behavior of youtube-dl.
[youtubedl] / youtube_dl / utils.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import ctypes
5 import datetime
6 import email.utils
7 import errno
8 import gzip
9 import itertools
10 import io
11 import json
12 import locale
13 import math
14 import os
15 import pipes
16 import platform
17 import re
18 import ssl
19 import socket
20 import struct
21 import subprocess
22 import sys
23 import traceback
24 import zlib
25
26 try:
27 import urllib.request as compat_urllib_request
28 except ImportError: # Python 2
29 import urllib2 as compat_urllib_request
30
31 try:
32 import urllib.error as compat_urllib_error
33 except ImportError: # Python 2
34 import urllib2 as compat_urllib_error
35
36 try:
37 import urllib.parse as compat_urllib_parse
38 except ImportError: # Python 2
39 import urllib as compat_urllib_parse
40
41 try:
42 from urllib.parse import urlparse as compat_urllib_parse_urlparse
43 except ImportError: # Python 2
44 from urlparse import urlparse as compat_urllib_parse_urlparse
45
46 try:
47 import urllib.parse as compat_urlparse
48 except ImportError: # Python 2
49 import urlparse as compat_urlparse
50
51 try:
52 import http.cookiejar as compat_cookiejar
53 except ImportError: # Python 2
54 import cookielib as compat_cookiejar
55
56 try:
57 import html.entities as compat_html_entities
58 except ImportError: # Python 2
59 import htmlentitydefs as compat_html_entities
60
61 try:
62 import html.parser as compat_html_parser
63 except ImportError: # Python 2
64 import HTMLParser as compat_html_parser
65
66 try:
67 import http.client as compat_http_client
68 except ImportError: # Python 2
69 import httplib as compat_http_client
70
71 try:
72 from urllib.error import HTTPError as compat_HTTPError
73 except ImportError: # Python 2
74 from urllib2 import HTTPError as compat_HTTPError
75
76 try:
77 from urllib.request import urlretrieve as compat_urlretrieve
78 except ImportError: # Python 2
79 from urllib import urlretrieve as compat_urlretrieve
80
81
82 try:
83 from subprocess import DEVNULL
84 compat_subprocess_get_DEVNULL = lambda: DEVNULL
85 except ImportError:
86 compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
87
88 try:
89 from urllib.parse import parse_qs as compat_parse_qs
90 except ImportError: # Python 2
91 # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
92 # Python 2's version is apparently totally broken
93 def _unquote(string, encoding='utf-8', errors='replace'):
94 if string == '':
95 return string
96 res = string.split('%')
97 if len(res) == 1:
98 return string
99 if encoding is None:
100 encoding = 'utf-8'
101 if errors is None:
102 errors = 'replace'
103 # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
104 pct_sequence = b''
105 string = res[0]
106 for item in res[1:]:
107 try:
108 if not item:
109 raise ValueError
110 pct_sequence += item[:2].decode('hex')
111 rest = item[2:]
112 if not rest:
113 # This segment was just a single percent-encoded character.
114 # May be part of a sequence of code units, so delay decoding.
115 # (Stored in pct_sequence).
116 continue
117 except ValueError:
118 rest = '%' + item
119 # Encountered non-percent-encoded characters. Flush the current
120 # pct_sequence.
121 string += pct_sequence.decode(encoding, errors) + rest
122 pct_sequence = b''
123 if pct_sequence:
124 # Flush the final pct_sequence
125 string += pct_sequence.decode(encoding, errors)
126 return string
127
128 def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
129 encoding='utf-8', errors='replace'):
130 qs, _coerce_result = qs, unicode
131 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
132 r = []
133 for name_value in pairs:
134 if not name_value and not strict_parsing:
135 continue
136 nv = name_value.split('=', 1)
137 if len(nv) != 2:
138 if strict_parsing:
139 raise ValueError("bad query field: %r" % (name_value,))
140 # Handle case of a control-name with no equal sign
141 if keep_blank_values:
142 nv.append('')
143 else:
144 continue
145 if len(nv[1]) or keep_blank_values:
146 name = nv[0].replace('+', ' ')
147 name = _unquote(name, encoding=encoding, errors=errors)
148 name = _coerce_result(name)
149 value = nv[1].replace('+', ' ')
150 value = _unquote(value, encoding=encoding, errors=errors)
151 value = _coerce_result(value)
152 r.append((name, value))
153 return r
154
155 def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
156 encoding='utf-8', errors='replace'):
157 parsed_result = {}
158 pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
159 encoding=encoding, errors=errors)
160 for name, value in pairs:
161 if name in parsed_result:
162 parsed_result[name].append(value)
163 else:
164 parsed_result[name] = [value]
165 return parsed_result
166
167 try:
168 compat_str = unicode # Python 2
169 except NameError:
170 compat_str = str
171
172 try:
173 compat_chr = unichr # Python 2
174 except NameError:
175 compat_chr = chr
176
177 def compat_ord(c):
178 if type(c) is int: return c
179 else: return ord(c)
180
181 # This is not clearly defined otherwise
182 compiled_regex_type = type(re.compile(''))
183
184 std_headers = {
185 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 (Chrome)',
186 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
187 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
188 'Accept-Encoding': 'gzip, deflate',
189 'Accept-Language': 'en-us,en;q=0.5',
190 }
191
192 def preferredencoding():
193 """Get preferred encoding.
194
195 Returns the best encoding scheme for the system, based on
196 locale.getpreferredencoding() and some further tweaks.
197 """
198 try:
199 pref = locale.getpreferredencoding()
200 u'TEST'.encode(pref)
201 except:
202 pref = 'UTF-8'
203
204 return pref
205
206 if sys.version_info < (3,0):
207 def compat_print(s):
208 print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
209 else:
210 def compat_print(s):
211 assert type(s) == type(u'')
212 print(s)
213
214 # In Python 2.x, json.dump expects a bytestream.
215 # In Python 3.x, it writes to a character stream
216 if sys.version_info < (3,0):
217 def write_json_file(obj, fn):
218 with open(fn, 'wb') as f:
219 json.dump(obj, f)
220 else:
221 def write_json_file(obj, fn):
222 with open(fn, 'w', encoding='utf-8') as f:
223 json.dump(obj, f)
224
225 if sys.version_info >= (2,7):
226 def find_xpath_attr(node, xpath, key, val):
227 """ Find the xpath xpath[@key=val] """
228 assert re.match(r'^[a-zA-Z]+$', key)
229 assert re.match(r'^[a-zA-Z0-9@\s:._]*$', val)
230 expr = xpath + u"[@%s='%s']" % (key, val)
231 return node.find(expr)
232 else:
233 def find_xpath_attr(node, xpath, key, val):
234 for f in node.findall(xpath):
235 if f.attrib.get(key) == val:
236 return f
237 return None
238
239 # On python2.6 the xml.etree.ElementTree.Element methods don't support
240 # the namespace parameter
241 def xpath_with_ns(path, ns_map):
242 components = [c.split(':') for c in path.split('/')]
243 replaced = []
244 for c in components:
245 if len(c) == 1:
246 replaced.append(c[0])
247 else:
248 ns, tag = c
249 replaced.append('{%s}%s' % (ns_map[ns], tag))
250 return '/'.join(replaced)
251
252 def htmlentity_transform(matchobj):
253 """Transforms an HTML entity to a character.
254
255 This function receives a match object and is intended to be used with
256 the re.sub() function.
257 """
258 entity = matchobj.group(1)
259
260 # Known non-numeric HTML entity
261 if entity in compat_html_entities.name2codepoint:
262 return compat_chr(compat_html_entities.name2codepoint[entity])
263
264 mobj = re.match(u'(?u)#(x?\\d+)', entity)
265 if mobj is not None:
266 numstr = mobj.group(1)
267 if numstr.startswith(u'x'):
268 base = 16
269 numstr = u'0%s' % numstr
270 else:
271 base = 10
272 return compat_chr(int(numstr, base))
273
274 # Unknown entity in name, return its literal representation
275 return (u'&%s;' % entity)
276
277 compat_html_parser.locatestarttagend = re.compile(r"""<[a-zA-Z][-.a-zA-Z0-9:_]*(?:\s+(?:(?<=['"\s])[^\s/>][^\s/=>]*(?:\s*=+\s*(?:'[^']*'|"[^"]*"|(?!['"])[^>\s]*))?\s*)*)?\s*""", re.VERBOSE) # backport bugfix
278 class BaseHTMLParser(compat_html_parser.HTMLParser):
279 def __init(self):
280 compat_html_parser.HTMLParser.__init__(self)
281 self.html = None
282
283 def loads(self, html):
284 self.html = html
285 self.feed(html)
286 self.close()
287
288 class AttrParser(BaseHTMLParser):
289 """Modified HTMLParser that isolates a tag with the specified attribute"""
290 def __init__(self, attribute, value):
291 self.attribute = attribute
292 self.value = value
293 self.result = None
294 self.started = False
295 self.depth = {}
296 self.watch_startpos = False
297 self.error_count = 0
298 BaseHTMLParser.__init__(self)
299
300 def error(self, message):
301 if self.error_count > 10 or self.started:
302 raise compat_html_parser.HTMLParseError(message, self.getpos())
303 self.rawdata = '\n'.join(self.html.split('\n')[self.getpos()[0]:]) # skip one line
304 self.error_count += 1
305 self.goahead(1)
306
307 def handle_starttag(self, tag, attrs):
308 attrs = dict(attrs)
309 if self.started:
310 self.find_startpos(None)
311 if self.attribute in attrs and attrs[self.attribute] == self.value:
312 self.result = [tag]
313 self.started = True
314 self.watch_startpos = True
315 if self.started:
316 if not tag in self.depth: self.depth[tag] = 0
317 self.depth[tag] += 1
318
319 def handle_endtag(self, tag):
320 if self.started:
321 if tag in self.depth: self.depth[tag] -= 1
322 if self.depth[self.result[0]] == 0:
323 self.started = False
324 self.result.append(self.getpos())
325
326 def find_startpos(self, x):
327 """Needed to put the start position of the result (self.result[1])
328 after the opening tag with the requested id"""
329 if self.watch_startpos:
330 self.watch_startpos = False
331 self.result.append(self.getpos())
332 handle_entityref = handle_charref = handle_data = handle_comment = \
333 handle_decl = handle_pi = unknown_decl = find_startpos
334
335 def get_result(self):
336 if self.result is None:
337 return None
338 if len(self.result) != 3:
339 return None
340 lines = self.html.split('\n')
341 lines = lines[self.result[1][0]-1:self.result[2][0]]
342 lines[0] = lines[0][self.result[1][1]:]
343 if len(lines) == 1:
344 lines[-1] = lines[-1][:self.result[2][1]-self.result[1][1]]
345 lines[-1] = lines[-1][:self.result[2][1]]
346 return '\n'.join(lines).strip()
347 # Hack for https://github.com/rg3/youtube-dl/issues/662
348 if sys.version_info < (2, 7, 3):
349 AttrParser.parse_endtag = (lambda self, i:
350 i + len("</scr'+'ipt>")
351 if self.rawdata[i:].startswith("</scr'+'ipt>")
352 else compat_html_parser.HTMLParser.parse_endtag(self, i))
353
354 def get_element_by_id(id, html):
355 """Return the content of the tag with the specified ID in the passed HTML document"""
356 return get_element_by_attribute("id", id, html)
357
358 def get_element_by_attribute(attribute, value, html):
359 """Return the content of the tag with the specified attribute in the passed HTML document"""
360 parser = AttrParser(attribute, value)
361 try:
362 parser.loads(html)
363 except compat_html_parser.HTMLParseError:
364 pass
365 return parser.get_result()
366
367 class MetaParser(BaseHTMLParser):
368 """
369 Modified HTMLParser that isolates a meta tag with the specified name
370 attribute.
371 """
372 def __init__(self, name):
373 BaseHTMLParser.__init__(self)
374 self.name = name
375 self.content = None
376 self.result = None
377
378 def handle_starttag(self, tag, attrs):
379 if tag != 'meta':
380 return
381 attrs = dict(attrs)
382 if attrs.get('name') == self.name:
383 self.result = attrs.get('content')
384
385 def get_result(self):
386 return self.result
387
388 def get_meta_content(name, html):
389 """
390 Return the content attribute from the meta tag with the given name attribute.
391 """
392 parser = MetaParser(name)
393 try:
394 parser.loads(html)
395 except compat_html_parser.HTMLParseError:
396 pass
397 return parser.get_result()
398
399
400 def clean_html(html):
401 """Clean an HTML snippet into a readable string"""
402 # Newline vs <br />
403 html = html.replace('\n', ' ')
404 html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
405 html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
406 # Strip html tags
407 html = re.sub('<.*?>', '', html)
408 # Replace html entities
409 html = unescapeHTML(html)
410 return html.strip()
411
412
413 def sanitize_open(filename, open_mode):
414 """Try to open the given filename, and slightly tweak it if this fails.
415
416 Attempts to open the given filename. If this fails, it tries to change
417 the filename slightly, step by step, until it's either able to open it
418 or it fails and raises a final exception, like the standard open()
419 function.
420
421 It returns the tuple (stream, definitive_file_name).
422 """
423 try:
424 if filename == u'-':
425 if sys.platform == 'win32':
426 import msvcrt
427 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
428 return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
429 stream = open(encodeFilename(filename), open_mode)
430 return (stream, filename)
431 except (IOError, OSError) as err:
432 if err.errno in (errno.EACCES,):
433 raise
434
435 # In case of error, try to remove win32 forbidden chars
436 alt_filename = os.path.join(
437 re.sub(u'[/<>:"\\|\\\\?\\*]', u'#', path_part)
438 for path_part in os.path.split(filename)
439 )
440 if alt_filename == filename:
441 raise
442 else:
443 # An exception here should be caught in the caller
444 stream = open(encodeFilename(filename), open_mode)
445 return (stream, alt_filename)
446
447
448 def timeconvert(timestr):
449 """Convert RFC 2822 defined time string into system timestamp"""
450 timestamp = None
451 timetuple = email.utils.parsedate_tz(timestr)
452 if timetuple is not None:
453 timestamp = email.utils.mktime_tz(timetuple)
454 return timestamp
455
456 def sanitize_filename(s, restricted=False, is_id=False):
457 """Sanitizes a string so it could be used as part of a filename.
458 If restricted is set, use a stricter subset of allowed characters.
459 Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
460 """
461 def replace_insane(char):
462 if char == '?' or ord(char) < 32 or ord(char) == 127:
463 return ''
464 elif char == '"':
465 return '' if restricted else '\''
466 elif char == ':':
467 return '_-' if restricted else ' -'
468 elif char in '\\/|*<>':
469 return '_'
470 if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
471 return '_'
472 if restricted and ord(char) > 127:
473 return '_'
474 return char
475
476 result = u''.join(map(replace_insane, s))
477 if not is_id:
478 while '__' in result:
479 result = result.replace('__', '_')
480 result = result.strip('_')
481 # Common case of "Foreign band name - English song title"
482 if restricted and result.startswith('-_'):
483 result = result[2:]
484 if not result:
485 result = '_'
486 return result
487
488 def orderedSet(iterable):
489 """ Remove all duplicates from the input iterable """
490 res = []
491 for el in iterable:
492 if el not in res:
493 res.append(el)
494 return res
495
496 def unescapeHTML(s):
497 """
498 @param s a string
499 """
500 assert type(s) == type(u'')
501
502 result = re.sub(u'(?u)&(.+?);', htmlentity_transform, s)
503 return result
504
505
506 def encodeFilename(s, for_subprocess=False):
507 """
508 @param s The name of the file
509 """
510
511 assert type(s) == compat_str
512
513 # Python 3 has a Unicode API
514 if sys.version_info >= (3, 0):
515 return s
516
517 if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
518 # Pass u'' directly to use Unicode APIs on Windows 2000 and up
519 # (Detecting Windows NT 4 is tricky because 'major >= 4' would
520 # match Windows 9x series as well. Besides, NT 4 is obsolete.)
521 if not for_subprocess:
522 return s
523 else:
524 # For subprocess calls, encode with locale encoding
525 # Refer to http://stackoverflow.com/a/9951851/35070
526 encoding = preferredencoding()
527 else:
528 encoding = sys.getfilesystemencoding()
529 if encoding is None:
530 encoding = 'utf-8'
531 return s.encode(encoding, 'ignore')
532
533
534 def decodeOption(optval):
535 if optval is None:
536 return optval
537 if isinstance(optval, bytes):
538 optval = optval.decode(preferredencoding())
539
540 assert isinstance(optval, compat_str)
541 return optval
542
543 def formatSeconds(secs):
544 if secs > 3600:
545 return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
546 elif secs > 60:
547 return '%d:%02d' % (secs // 60, secs % 60)
548 else:
549 return '%d' % secs
550
551
552 def make_HTTPS_handler(opts_no_check_certificate, **kwargs):
553 if sys.version_info < (3, 2):
554 import httplib
555
556 class HTTPSConnectionV3(httplib.HTTPSConnection):
557 def __init__(self, *args, **kwargs):
558 httplib.HTTPSConnection.__init__(self, *args, **kwargs)
559
560 def connect(self):
561 sock = socket.create_connection((self.host, self.port), self.timeout)
562 if getattr(self, '_tunnel_host', False):
563 self.sock = sock
564 self._tunnel()
565 try:
566 self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv3)
567 except ssl.SSLError:
568 self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv23)
569
570 class HTTPSHandlerV3(compat_urllib_request.HTTPSHandler):
571 def https_open(self, req):
572 return self.do_open(HTTPSConnectionV3, req)
573 return HTTPSHandlerV3(**kwargs)
574 else:
575 context = ssl.SSLContext(ssl.PROTOCOL_SSLv3)
576 context.verify_mode = (ssl.CERT_NONE
577 if opts_no_check_certificate
578 else ssl.CERT_REQUIRED)
579 context.set_default_verify_paths()
580 try:
581 context.load_default_certs()
582 except AttributeError:
583 pass # Python < 3.4
584 return compat_urllib_request.HTTPSHandler(context=context, **kwargs)
585
586 class ExtractorError(Exception):
587 """Error during info extraction."""
588 def __init__(self, msg, tb=None, expected=False, cause=None):
589 """ tb, if given, is the original traceback (so that it can be printed out).
590 If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
591 """
592
593 if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
594 expected = True
595 if not expected:
596 msg = msg + u'; please report this issue on https://yt-dl.org/bug . Be sure to call youtube-dl with the --verbose flag and include its complete output. Make sure you are using the latest version; type youtube-dl -U to update.'
597 super(ExtractorError, self).__init__(msg)
598
599 self.traceback = tb
600 self.exc_info = sys.exc_info() # preserve original exception
601 self.cause = cause
602
603 def format_traceback(self):
604 if self.traceback is None:
605 return None
606 return u''.join(traceback.format_tb(self.traceback))
607
608
609 class RegexNotFoundError(ExtractorError):
610 """Error when a regex didn't match"""
611 pass
612
613
614 class DownloadError(Exception):
615 """Download Error exception.
616
617 This exception may be thrown by FileDownloader objects if they are not
618 configured to continue on errors. They will contain the appropriate
619 error message.
620 """
621 def __init__(self, msg, exc_info=None):
622 """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
623 super(DownloadError, self).__init__(msg)
624 self.exc_info = exc_info
625
626
627 class SameFileError(Exception):
628 """Same File exception.
629
630 This exception will be thrown by FileDownloader objects if they detect
631 multiple files would have to be downloaded to the same file on disk.
632 """
633 pass
634
635
636 class PostProcessingError(Exception):
637 """Post Processing exception.
638
639 This exception may be raised by PostProcessor's .run() method to
640 indicate an error in the postprocessing task.
641 """
642 def __init__(self, msg):
643 self.msg = msg
644
645 class MaxDownloadsReached(Exception):
646 """ --max-downloads limit has been reached. """
647 pass
648
649
650 class UnavailableVideoError(Exception):
651 """Unavailable Format exception.
652
653 This exception will be thrown when a video is requested
654 in a format that is not available for that video.
655 """
656 pass
657
658
659 class ContentTooShortError(Exception):
660 """Content Too Short exception.
661
662 This exception may be raised by FileDownloader objects when a file they
663 download is too small for what the server announced first, indicating
664 the connection was probably interrupted.
665 """
666 # Both in bytes
667 downloaded = None
668 expected = None
669
670 def __init__(self, downloaded, expected):
671 self.downloaded = downloaded
672 self.expected = expected
673
674 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
675 """Handler for HTTP requests and responses.
676
677 This class, when installed with an OpenerDirector, automatically adds
678 the standard headers to every HTTP request and handles gzipped and
679 deflated responses from web servers. If compression is to be avoided in
680 a particular request, the original request in the program code only has
681 to include the HTTP header "Youtubedl-No-Compression", which will be
682 removed before making the real request.
683
684 Part of this code was copied from:
685
686 http://techknack.net/python-urllib2-handlers/
687
688 Andrew Rowls, the author of that code, agreed to release it to the
689 public domain.
690 """
691
692 @staticmethod
693 def deflate(data):
694 try:
695 return zlib.decompress(data, -zlib.MAX_WBITS)
696 except zlib.error:
697 return zlib.decompress(data)
698
699 @staticmethod
700 def addinfourl_wrapper(stream, headers, url, code):
701 if hasattr(compat_urllib_request.addinfourl, 'getcode'):
702 return compat_urllib_request.addinfourl(stream, headers, url, code)
703 ret = compat_urllib_request.addinfourl(stream, headers, url)
704 ret.code = code
705 return ret
706
707 def http_request(self, req):
708 for h,v in std_headers.items():
709 if h in req.headers:
710 del req.headers[h]
711 req.add_header(h, v)
712 if 'Youtubedl-no-compression' in req.headers:
713 if 'Accept-encoding' in req.headers:
714 del req.headers['Accept-encoding']
715 del req.headers['Youtubedl-no-compression']
716 if 'Youtubedl-user-agent' in req.headers:
717 if 'User-agent' in req.headers:
718 del req.headers['User-agent']
719 req.headers['User-agent'] = req.headers['Youtubedl-user-agent']
720 del req.headers['Youtubedl-user-agent']
721 return req
722
723 def http_response(self, req, resp):
724 old_resp = resp
725 # gzip
726 if resp.headers.get('Content-encoding', '') == 'gzip':
727 content = resp.read()
728 gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
729 try:
730 uncompressed = io.BytesIO(gz.read())
731 except IOError as original_ioerror:
732 # There may be junk add the end of the file
733 # See http://stackoverflow.com/q/4928560/35070 for details
734 for i in range(1, 1024):
735 try:
736 gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
737 uncompressed = io.BytesIO(gz.read())
738 except IOError:
739 continue
740 break
741 else:
742 raise original_ioerror
743 resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
744 resp.msg = old_resp.msg
745 # deflate
746 if resp.headers.get('Content-encoding', '') == 'deflate':
747 gz = io.BytesIO(self.deflate(resp.read()))
748 resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
749 resp.msg = old_resp.msg
750 return resp
751
752 https_request = http_request
753 https_response = http_response
754
755
756 def unified_strdate(date_str):
757 """Return a string with the date in the format YYYYMMDD"""
758 upload_date = None
759 #Replace commas
760 date_str = date_str.replace(',', ' ')
761 # %z (UTC offset) is only supported in python>=3.2
762 date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
763 format_expressions = [
764 '%d %B %Y',
765 '%d %b %Y',
766 '%B %d %Y',
767 '%b %d %Y',
768 '%Y-%m-%d',
769 '%d/%m/%Y',
770 '%Y/%m/%d %H:%M:%S',
771 '%Y-%m-%d %H:%M:%S',
772 '%d.%m.%Y %H:%M',
773 '%Y-%m-%dT%H:%M:%SZ',
774 '%Y-%m-%dT%H:%M:%S.%fZ',
775 '%Y-%m-%dT%H:%M:%S.%f0Z',
776 '%Y-%m-%dT%H:%M:%S',
777 '%Y-%m-%dT%H:%M',
778 ]
779 for expression in format_expressions:
780 try:
781 upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
782 except ValueError:
783 pass
784 if upload_date is None:
785 timetuple = email.utils.parsedate_tz(date_str)
786 if timetuple:
787 upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
788 return upload_date
789
790 def determine_ext(url, default_ext=u'unknown_video'):
791 guess = url.partition(u'?')[0].rpartition(u'.')[2]
792 if re.match(r'^[A-Za-z0-9]+$', guess):
793 return guess
794 else:
795 return default_ext
796
797 def subtitles_filename(filename, sub_lang, sub_format):
798 return filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
799
800 def date_from_str(date_str):
801 """
802 Return a datetime object from a string in the format YYYYMMDD or
803 (now|today)[+-][0-9](day|week|month|year)(s)?"""
804 today = datetime.date.today()
805 if date_str == 'now'or date_str == 'today':
806 return today
807 match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
808 if match is not None:
809 sign = match.group('sign')
810 time = int(match.group('time'))
811 if sign == '-':
812 time = -time
813 unit = match.group('unit')
814 #A bad aproximation?
815 if unit == 'month':
816 unit = 'day'
817 time *= 30
818 elif unit == 'year':
819 unit = 'day'
820 time *= 365
821 unit += 's'
822 delta = datetime.timedelta(**{unit: time})
823 return today + delta
824 return datetime.datetime.strptime(date_str, "%Y%m%d").date()
825
826 def hyphenate_date(date_str):
827 """
828 Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
829 match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
830 if match is not None:
831 return '-'.join(match.groups())
832 else:
833 return date_str
834
835 class DateRange(object):
836 """Represents a time interval between two dates"""
837 def __init__(self, start=None, end=None):
838 """start and end must be strings in the format accepted by date"""
839 if start is not None:
840 self.start = date_from_str(start)
841 else:
842 self.start = datetime.datetime.min.date()
843 if end is not None:
844 self.end = date_from_str(end)
845 else:
846 self.end = datetime.datetime.max.date()
847 if self.start > self.end:
848 raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
849 @classmethod
850 def day(cls, day):
851 """Returns a range that only contains the given day"""
852 return cls(day,day)
853 def __contains__(self, date):
854 """Check if the date is in the range"""
855 if not isinstance(date, datetime.date):
856 date = date_from_str(date)
857 return self.start <= date <= self.end
858 def __str__(self):
859 return '%s - %s' % ( self.start.isoformat(), self.end.isoformat())
860
861
862 def platform_name():
863 """ Returns the platform name as a compat_str """
864 res = platform.platform()
865 if isinstance(res, bytes):
866 res = res.decode(preferredencoding())
867
868 assert isinstance(res, compat_str)
869 return res
870
871
872 def write_string(s, out=None):
873 if out is None:
874 out = sys.stderr
875 assert type(s) == compat_str
876
877 if ('b' in getattr(out, 'mode', '') or
878 sys.version_info[0] < 3): # Python 2 lies about mode of sys.stderr
879 s = s.encode(preferredencoding(), 'ignore')
880 try:
881 out.write(s)
882 except UnicodeEncodeError:
883 # In Windows shells, this can fail even when the codec is just charmap!?
884 # See https://wiki.python.org/moin/PrintFails#Issue
885 if sys.platform == 'win32' and hasattr(out, 'encoding'):
886 s = s.encode(out.encoding, 'ignore').decode(out.encoding)
887 out.write(s)
888 else:
889 raise
890
891 out.flush()
892
893
894 def bytes_to_intlist(bs):
895 if not bs:
896 return []
897 if isinstance(bs[0], int): # Python 3
898 return list(bs)
899 else:
900 return [ord(c) for c in bs]
901
902
903 def intlist_to_bytes(xs):
904 if not xs:
905 return b''
906 if isinstance(chr(0), bytes): # Python 2
907 return ''.join([chr(x) for x in xs])
908 else:
909 return bytes(xs)
910
911
912 def get_cachedir(params={}):
913 cache_root = os.environ.get('XDG_CACHE_HOME',
914 os.path.expanduser('~/.cache'))
915 return params.get('cachedir', os.path.join(cache_root, 'youtube-dl'))
916
917
918 # Cross-platform file locking
919 if sys.platform == 'win32':
920 import ctypes.wintypes
921 import msvcrt
922
923 class OVERLAPPED(ctypes.Structure):
924 _fields_ = [
925 ('Internal', ctypes.wintypes.LPVOID),
926 ('InternalHigh', ctypes.wintypes.LPVOID),
927 ('Offset', ctypes.wintypes.DWORD),
928 ('OffsetHigh', ctypes.wintypes.DWORD),
929 ('hEvent', ctypes.wintypes.HANDLE),
930 ]
931
932 kernel32 = ctypes.windll.kernel32
933 LockFileEx = kernel32.LockFileEx
934 LockFileEx.argtypes = [
935 ctypes.wintypes.HANDLE, # hFile
936 ctypes.wintypes.DWORD, # dwFlags
937 ctypes.wintypes.DWORD, # dwReserved
938 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
939 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
940 ctypes.POINTER(OVERLAPPED) # Overlapped
941 ]
942 LockFileEx.restype = ctypes.wintypes.BOOL
943 UnlockFileEx = kernel32.UnlockFileEx
944 UnlockFileEx.argtypes = [
945 ctypes.wintypes.HANDLE, # hFile
946 ctypes.wintypes.DWORD, # dwReserved
947 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
948 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
949 ctypes.POINTER(OVERLAPPED) # Overlapped
950 ]
951 UnlockFileEx.restype = ctypes.wintypes.BOOL
952 whole_low = 0xffffffff
953 whole_high = 0x7fffffff
954
955 def _lock_file(f, exclusive):
956 overlapped = OVERLAPPED()
957 overlapped.Offset = 0
958 overlapped.OffsetHigh = 0
959 overlapped.hEvent = 0
960 f._lock_file_overlapped_p = ctypes.pointer(overlapped)
961 handle = msvcrt.get_osfhandle(f.fileno())
962 if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
963 whole_low, whole_high, f._lock_file_overlapped_p):
964 raise OSError('Locking file failed: %r' % ctypes.FormatError())
965
966 def _unlock_file(f):
967 assert f._lock_file_overlapped_p
968 handle = msvcrt.get_osfhandle(f.fileno())
969 if not UnlockFileEx(handle, 0,
970 whole_low, whole_high, f._lock_file_overlapped_p):
971 raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
972
973 else:
974 import fcntl
975
976 def _lock_file(f, exclusive):
977 fcntl.lockf(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
978
979 def _unlock_file(f):
980 fcntl.lockf(f, fcntl.LOCK_UN)
981
982
983 class locked_file(object):
984 def __init__(self, filename, mode, encoding=None):
985 assert mode in ['r', 'a', 'w']
986 self.f = io.open(filename, mode, encoding=encoding)
987 self.mode = mode
988
989 def __enter__(self):
990 exclusive = self.mode != 'r'
991 try:
992 _lock_file(self.f, exclusive)
993 except IOError:
994 self.f.close()
995 raise
996 return self
997
998 def __exit__(self, etype, value, traceback):
999 try:
1000 _unlock_file(self.f)
1001 finally:
1002 self.f.close()
1003
1004 def __iter__(self):
1005 return iter(self.f)
1006
1007 def write(self, *args):
1008 return self.f.write(*args)
1009
1010 def read(self, *args):
1011 return self.f.read(*args)
1012
1013
1014 def shell_quote(args):
1015 quoted_args = []
1016 encoding = sys.getfilesystemencoding()
1017 if encoding is None:
1018 encoding = 'utf-8'
1019 for a in args:
1020 if isinstance(a, bytes):
1021 # We may get a filename encoded with 'encodeFilename'
1022 a = a.decode(encoding)
1023 quoted_args.append(pipes.quote(a))
1024 return u' '.join(quoted_args)
1025
1026
1027 def takewhile_inclusive(pred, seq):
1028 """ Like itertools.takewhile, but include the latest evaluated element
1029 (the first element so that Not pred(e)) """
1030 for e in seq:
1031 yield e
1032 if not pred(e):
1033 return
1034
1035
1036 def smuggle_url(url, data):
1037 """ Pass additional data in a URL for internal use. """
1038
1039 sdata = compat_urllib_parse.urlencode(
1040 {u'__youtubedl_smuggle': json.dumps(data)})
1041 return url + u'#' + sdata
1042
1043
1044 def unsmuggle_url(smug_url, default=None):
1045 if not '#__youtubedl_smuggle' in smug_url:
1046 return smug_url, default
1047 url, _, sdata = smug_url.rpartition(u'#')
1048 jsond = compat_parse_qs(sdata)[u'__youtubedl_smuggle'][0]
1049 data = json.loads(jsond)
1050 return url, data
1051
1052
1053 def format_bytes(bytes):
1054 if bytes is None:
1055 return u'N/A'
1056 if type(bytes) is str:
1057 bytes = float(bytes)
1058 if bytes == 0.0:
1059 exponent = 0
1060 else:
1061 exponent = int(math.log(bytes, 1024.0))
1062 suffix = [u'B', u'KiB', u'MiB', u'GiB', u'TiB', u'PiB', u'EiB', u'ZiB', u'YiB'][exponent]
1063 converted = float(bytes) / float(1024 ** exponent)
1064 return u'%.2f%s' % (converted, suffix)
1065
1066
1067 def str_to_int(int_str):
1068 int_str = re.sub(r'[,\.]', u'', int_str)
1069 return int(int_str)
1070
1071
1072 def get_term_width():
1073 columns = os.environ.get('COLUMNS', None)
1074 if columns:
1075 return int(columns)
1076
1077 try:
1078 sp = subprocess.Popen(
1079 ['stty', 'size'],
1080 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1081 out, err = sp.communicate()
1082 return int(out.split()[1])
1083 except:
1084 pass
1085 return None
1086
1087
1088 def month_by_name(name):
1089 """ Return the number of a month by (locale-independently) English name """
1090
1091 ENGLISH_NAMES = [
1092 u'January', u'February', u'March', u'April', u'May', u'June',
1093 u'July', u'August', u'September', u'October', u'November', u'December']
1094 try:
1095 return ENGLISH_NAMES.index(name) + 1
1096 except ValueError:
1097 return None
1098
1099
1100 def fix_xml_ampersands(xml_str):
1101 """Replace all the '&' by '&amp;' in XML"""
1102 return re.sub(
1103 r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1104 u'&amp;',
1105 xml_str)
1106
1107
1108 def setproctitle(title):
1109 assert isinstance(title, compat_str)
1110 try:
1111 libc = ctypes.cdll.LoadLibrary("libc.so.6")
1112 except OSError:
1113 return
1114 title = title
1115 buf = ctypes.create_string_buffer(len(title) + 1)
1116 buf.value = title.encode('utf-8')
1117 try:
1118 libc.prctl(15, ctypes.byref(buf), 0, 0, 0)
1119 except AttributeError:
1120 return # Strange libc, just skip this
1121
1122
1123 def remove_start(s, start):
1124 if s.startswith(start):
1125 return s[len(start):]
1126 return s
1127
1128
1129 def url_basename(url):
1130 path = compat_urlparse.urlparse(url).path
1131 return path.strip(u'/').split(u'/')[-1]
1132
1133
1134 class HEADRequest(compat_urllib_request.Request):
1135 def get_method(self):
1136 return "HEAD"
1137
1138
1139 def int_or_none(v, scale=1):
1140 return v if v is None else (int(v) // scale)
1141
1142
1143 def parse_duration(s):
1144 if s is None:
1145 return None
1146
1147 m = re.match(
1148 r'(?:(?:(?P<hours>[0-9]+)[:h])?(?P<mins>[0-9]+)[:m])?(?P<secs>[0-9]+)s?$', s)
1149 if not m:
1150 return None
1151 res = int(m.group('secs'))
1152 if m.group('mins'):
1153 res += int(m.group('mins')) * 60
1154 if m.group('hours'):
1155 res += int(m.group('hours')) * 60 * 60
1156 return res
1157
1158
1159 def prepend_extension(filename, ext):
1160 name, real_ext = os.path.splitext(filename)
1161 return u'{0}.{1}{2}'.format(name, ext, real_ext)
1162
1163
1164 def check_executable(exe, args=[]):
1165 """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1166 args can be a list of arguments for a short output (like -version) """
1167 try:
1168 subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1169 except OSError:
1170 return False
1171 return exe
1172
1173
1174 class PagedList(object):
1175 def __init__(self, pagefunc, pagesize):
1176 self._pagefunc = pagefunc
1177 self._pagesize = pagesize
1178
1179 def __len__(self):
1180 # This is only useful for tests
1181 return len(self.getslice())
1182
1183 def getslice(self, start=0, end=None):
1184 res = []
1185 for pagenum in itertools.count(start // self._pagesize):
1186 firstid = pagenum * self._pagesize
1187 nextfirstid = pagenum * self._pagesize + self._pagesize
1188 if start >= nextfirstid:
1189 continue
1190
1191 page_results = list(self._pagefunc(pagenum))
1192
1193 startv = (
1194 start % self._pagesize
1195 if firstid <= start < nextfirstid
1196 else 0)
1197
1198 endv = (
1199 ((end - 1) % self._pagesize) + 1
1200 if (end is not None and firstid <= end <= nextfirstid)
1201 else None)
1202
1203 if startv != 0 or endv is not None:
1204 page_results = page_results[startv:endv]
1205 res.extend(page_results)
1206
1207 # A little optimization - if current page is not "full", ie. does
1208 # not contain page_size videos then we can assume that this page
1209 # is the last one - there are no more ids on further pages -
1210 # i.e. no need to query again.
1211 if len(page_results) + startv < self._pagesize:
1212 break
1213
1214 # If we got the whole page, but the next page is not interesting,
1215 # break out early as well
1216 if end == nextfirstid:
1217 break
1218 return res
1219
1220
1221 def uppercase_escape(s):
1222 return re.sub(
1223 r'\\U([0-9a-fA-F]{8})',
1224 lambda m: compat_chr(int(m.group(1), base=16)), s)
1225
1226 try:
1227 struct.pack(u'!I', 0)
1228 except TypeError:
1229 # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1230 def struct_pack(spec, *args):
1231 if isinstance(spec, compat_str):
1232 spec = spec.encode('ascii')
1233 return struct.pack(spec, *args)
1234
1235 def struct_unpack(spec, *args):
1236 if isinstance(spec, compat_str):
1237 spec = spec.encode('ascii')
1238 return struct.unpack(spec, *args)
1239 else:
1240 struct_pack = struct.pack
1241 struct_unpack = struct.unpack