extractor: youtube: Swap video dimensions to match standard practice.
[youtube-dl] / 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 DownloadError(Exception):
576     """Download Error exception.
577
578     This exception may be thrown by FileDownloader objects if they are not
579     configured to continue on errors. They will contain the appropriate
580     error message.
581     """
582     def __init__(self, msg, exc_info=None):
583         """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
584         super(DownloadError, self).__init__(msg)
585         self.exc_info = exc_info
586
587
588 class SameFileError(Exception):
589     """Same File exception.
590
591     This exception will be thrown by FileDownloader objects if they detect
592     multiple files would have to be downloaded to the same file on disk.
593     """
594     pass
595
596
597 class PostProcessingError(Exception):
598     """Post Processing exception.
599
600     This exception may be raised by PostProcessor's .run() method to
601     indicate an error in the postprocessing task.
602     """
603     def __init__(self, msg):
604         self.msg = msg
605
606 class MaxDownloadsReached(Exception):
607     """ --max-downloads limit has been reached. """
608     pass
609
610
611 class UnavailableVideoError(Exception):
612     """Unavailable Format exception.
613
614     This exception will be thrown when a video is requested
615     in a format that is not available for that video.
616     """
617     pass
618
619
620 class ContentTooShortError(Exception):
621     """Content Too Short exception.
622
623     This exception may be raised by FileDownloader objects when a file they
624     download is too small for what the server announced first, indicating
625     the connection was probably interrupted.
626     """
627     # Both in bytes
628     downloaded = None
629     expected = None
630
631     def __init__(self, downloaded, expected):
632         self.downloaded = downloaded
633         self.expected = expected
634
635 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
636     """Handler for HTTP requests and responses.
637
638     This class, when installed with an OpenerDirector, automatically adds
639     the standard headers to every HTTP request and handles gzipped and
640     deflated responses from web servers. If compression is to be avoided in
641     a particular request, the original request in the program code only has
642     to include the HTTP header "Youtubedl-No-Compression", which will be
643     removed before making the real request.
644
645     Part of this code was copied from:
646
647     http://techknack.net/python-urllib2-handlers/
648
649     Andrew Rowls, the author of that code, agreed to release it to the
650     public domain.
651     """
652
653     @staticmethod
654     def deflate(data):
655         try:
656             return zlib.decompress(data, -zlib.MAX_WBITS)
657         except zlib.error:
658             return zlib.decompress(data)
659
660     @staticmethod
661     def addinfourl_wrapper(stream, headers, url, code):
662         if hasattr(compat_urllib_request.addinfourl, 'getcode'):
663             return compat_urllib_request.addinfourl(stream, headers, url, code)
664         ret = compat_urllib_request.addinfourl(stream, headers, url)
665         ret.code = code
666         return ret
667
668     def http_request(self, req):
669         for h,v in std_headers.items():
670             if h in req.headers:
671                 del req.headers[h]
672             req.add_header(h, v)
673         if 'Youtubedl-no-compression' in req.headers:
674             if 'Accept-encoding' in req.headers:
675                 del req.headers['Accept-encoding']
676             del req.headers['Youtubedl-no-compression']
677         if 'Youtubedl-user-agent' in req.headers:
678             if 'User-agent' in req.headers:
679                 del req.headers['User-agent']
680             req.headers['User-agent'] = req.headers['Youtubedl-user-agent']
681             del req.headers['Youtubedl-user-agent']
682         return req
683
684     def http_response(self, req, resp):
685         old_resp = resp
686         # gzip
687         if resp.headers.get('Content-encoding', '') == 'gzip':
688             content = resp.read()
689             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
690             try:
691                 uncompressed = io.BytesIO(gz.read())
692             except IOError as original_ioerror:
693                 # There may be junk add the end of the file
694                 # See http://stackoverflow.com/q/4928560/35070 for details
695                 for i in range(1, 1024):
696                     try:
697                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
698                         uncompressed = io.BytesIO(gz.read())
699                     except IOError:
700                         continue
701                     break
702                 else:
703                     raise original_ioerror
704             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
705             resp.msg = old_resp.msg
706         # deflate
707         if resp.headers.get('Content-encoding', '') == 'deflate':
708             gz = io.BytesIO(self.deflate(resp.read()))
709             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
710             resp.msg = old_resp.msg
711         return resp
712
713     https_request = http_request
714     https_response = http_response
715
716 def unified_strdate(date_str):
717     """Return a string with the date in the format YYYYMMDD"""
718     upload_date = None
719     #Replace commas
720     date_str = date_str.replace(',',' ')
721     # %z (UTC offset) is only supported in python>=3.2
722     date_str = re.sub(r' (\+|-)[\d]*$', '', date_str)
723     format_expressions = [
724         '%d %B %Y',
725         '%B %d %Y',
726         '%b %d %Y',
727         '%Y-%m-%d',
728         '%d/%m/%Y',
729         '%Y/%m/%d %H:%M:%S',
730         '%d.%m.%Y %H:%M',
731         '%Y-%m-%dT%H:%M:%SZ',
732         '%Y-%m-%dT%H:%M:%S',
733     ]
734     for expression in format_expressions:
735         try:
736             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
737         except:
738             pass
739     return upload_date
740
741 def determine_ext(url, default_ext=u'unknown_video'):
742     guess = url.partition(u'?')[0].rpartition(u'.')[2]
743     if re.match(r'^[A-Za-z0-9]+$', guess):
744         return guess
745     else:
746         return default_ext
747
748 def subtitles_filename(filename, sub_lang, sub_format):
749     return filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
750
751 def date_from_str(date_str):
752     """
753     Return a datetime object from a string in the format YYYYMMDD or
754     (now|today)[+-][0-9](day|week|month|year)(s)?"""
755     today = datetime.date.today()
756     if date_str == 'now'or date_str == 'today':
757         return today
758     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
759     if match is not None:
760         sign = match.group('sign')
761         time = int(match.group('time'))
762         if sign == '-':
763             time = -time
764         unit = match.group('unit')
765         #A bad aproximation?
766         if unit == 'month':
767             unit = 'day'
768             time *= 30
769         elif unit == 'year':
770             unit = 'day'
771             time *= 365
772         unit += 's'
773         delta = datetime.timedelta(**{unit: time})
774         return today + delta
775     return datetime.datetime.strptime(date_str, "%Y%m%d").date()
776     
777 class DateRange(object):
778     """Represents a time interval between two dates"""
779     def __init__(self, start=None, end=None):
780         """start and end must be strings in the format accepted by date"""
781         if start is not None:
782             self.start = date_from_str(start)
783         else:
784             self.start = datetime.datetime.min.date()
785         if end is not None:
786             self.end = date_from_str(end)
787         else:
788             self.end = datetime.datetime.max.date()
789         if self.start > self.end:
790             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
791     @classmethod
792     def day(cls, day):
793         """Returns a range that only contains the given day"""
794         return cls(day,day)
795     def __contains__(self, date):
796         """Check if the date is in the range"""
797         if not isinstance(date, datetime.date):
798             date = date_from_str(date)
799         return self.start <= date <= self.end
800     def __str__(self):
801         return '%s - %s' % ( self.start.isoformat(), self.end.isoformat())
802
803
804 def platform_name():
805     """ Returns the platform name as a compat_str """
806     res = platform.platform()
807     if isinstance(res, bytes):
808         res = res.decode(preferredencoding())
809
810     assert isinstance(res, compat_str)
811     return res
812
813
814 def write_string(s, out=None):
815     if out is None:
816         out = sys.stderr
817     assert type(s) == type(u'')
818
819     if ('b' in getattr(out, 'mode', '') or
820             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
821         s = s.encode(preferredencoding(), 'ignore')
822     out.write(s)
823     out.flush()
824
825
826 def bytes_to_intlist(bs):
827     if not bs:
828         return []
829     if isinstance(bs[0], int):  # Python 3
830         return list(bs)
831     else:
832         return [ord(c) for c in bs]
833
834
835 def intlist_to_bytes(xs):
836     if not xs:
837         return b''
838     if isinstance(chr(0), bytes):  # Python 2
839         return ''.join([chr(x) for x in xs])
840     else:
841         return bytes(xs)
842
843
844 def get_cachedir(params={}):
845     cache_root = os.environ.get('XDG_CACHE_HOME',
846                                 os.path.expanduser('~/.cache'))
847     return params.get('cachedir', os.path.join(cache_root, 'youtube-dl'))
848
849
850 # Cross-platform file locking
851 if sys.platform == 'win32':
852     import ctypes.wintypes
853     import msvcrt
854
855     class OVERLAPPED(ctypes.Structure):
856         _fields_ = [
857             ('Internal', ctypes.wintypes.LPVOID),
858             ('InternalHigh', ctypes.wintypes.LPVOID),
859             ('Offset', ctypes.wintypes.DWORD),
860             ('OffsetHigh', ctypes.wintypes.DWORD),
861             ('hEvent', ctypes.wintypes.HANDLE),
862         ]
863
864     kernel32 = ctypes.windll.kernel32
865     LockFileEx = kernel32.LockFileEx
866     LockFileEx.argtypes = [
867         ctypes.wintypes.HANDLE,     # hFile
868         ctypes.wintypes.DWORD,      # dwFlags
869         ctypes.wintypes.DWORD,      # dwReserved
870         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
871         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
872         ctypes.POINTER(OVERLAPPED)  # Overlapped
873     ]
874     LockFileEx.restype = ctypes.wintypes.BOOL
875     UnlockFileEx = kernel32.UnlockFileEx
876     UnlockFileEx.argtypes = [
877         ctypes.wintypes.HANDLE,     # hFile
878         ctypes.wintypes.DWORD,      # dwReserved
879         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
880         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
881         ctypes.POINTER(OVERLAPPED)  # Overlapped
882     ]
883     UnlockFileEx.restype = ctypes.wintypes.BOOL
884     whole_low = 0xffffffff
885     whole_high = 0x7fffffff
886
887     def _lock_file(f, exclusive):
888         overlapped = OVERLAPPED()
889         overlapped.Offset = 0
890         overlapped.OffsetHigh = 0
891         overlapped.hEvent = 0
892         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
893         handle = msvcrt.get_osfhandle(f.fileno())
894         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
895                           whole_low, whole_high, f._lock_file_overlapped_p):
896             raise OSError('Locking file failed: %r' % ctypes.FormatError())
897
898     def _unlock_file(f):
899         assert f._lock_file_overlapped_p
900         handle = msvcrt.get_osfhandle(f.fileno())
901         if not UnlockFileEx(handle, 0,
902                             whole_low, whole_high, f._lock_file_overlapped_p):
903             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
904
905 else:
906     import fcntl
907
908     def _lock_file(f, exclusive):
909         fcntl.lockf(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
910
911     def _unlock_file(f):
912         fcntl.lockf(f, fcntl.LOCK_UN)
913
914
915 class locked_file(object):
916     def __init__(self, filename, mode, encoding=None):
917         assert mode in ['r', 'a', 'w']
918         self.f = io.open(filename, mode, encoding=encoding)
919         self.mode = mode
920
921     def __enter__(self):
922         exclusive = self.mode != 'r'
923         try:
924             _lock_file(self.f, exclusive)
925         except IOError:
926             self.f.close()
927             raise
928         return self
929
930     def __exit__(self, etype, value, traceback):
931         try:
932             _unlock_file(self.f)
933         finally:
934             self.f.close()
935
936     def __iter__(self):
937         return iter(self.f)
938
939     def write(self, *args):
940         return self.f.write(*args)
941
942     def read(self, *args):
943         return self.f.read(*args)
944
945
946 def shell_quote(args):
947     return ' '.join(map(pipes.quote, args))
948
949
950 def takewhile_inclusive(pred, seq):
951     """ Like itertools.takewhile, but include the latest evaluated element
952         (the first element so that Not pred(e)) """
953     for e in seq:
954         yield e
955         if not pred(e):
956             return
957
958
959 def smuggle_url(url, data):
960     """ Pass additional data in a URL for internal use. """
961
962     sdata = compat_urllib_parse.urlencode(
963         {u'__youtubedl_smuggle': json.dumps(data)})
964     return url + u'#' + sdata
965
966
967 def unsmuggle_url(smug_url):
968     if not '#__youtubedl_smuggle' in smug_url:
969         return smug_url, None
970     url, _, sdata = smug_url.rpartition(u'#')
971     jsond = compat_parse_qs(sdata)[u'__youtubedl_smuggle'][0]
972     data = json.loads(jsond)
973     return url, data