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