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