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