2 # -*- coding: utf-8 -*-
25 import urllib
.request
as compat_urllib_request
26 except ImportError: # Python 2
27 import urllib2
as compat_urllib_request
30 import urllib
.error
as compat_urllib_error
31 except ImportError: # Python 2
32 import urllib2
as compat_urllib_error
35 import urllib
.parse
as compat_urllib_parse
36 except ImportError: # Python 2
37 import urllib
as compat_urllib_parse
40 from urllib
.parse
import urlparse
as compat_urllib_parse_urlparse
41 except ImportError: # Python 2
42 from urlparse
import urlparse
as compat_urllib_parse_urlparse
45 import urllib
.parse
as compat_urlparse
46 except ImportError: # Python 2
47 import urlparse
as compat_urlparse
50 import http
.cookiejar
as compat_cookiejar
51 except ImportError: # Python 2
52 import cookielib
as compat_cookiejar
55 import html
.entities
as compat_html_entities
56 except ImportError: # Python 2
57 import htmlentitydefs
as compat_html_entities
60 import html
.parser
as compat_html_parser
61 except ImportError: # Python 2
62 import HTMLParser
as compat_html_parser
65 import http
.client
as compat_http_client
66 except ImportError: # Python 2
67 import httplib
as compat_http_client
70 from urllib
.error
import HTTPError
as compat_HTTPError
71 except ImportError: # Python 2
72 from urllib2
import HTTPError
as compat_HTTPError
75 from urllib
.request
import urlretrieve
as compat_urlretrieve
76 except ImportError: # Python 2
77 from urllib
import urlretrieve
as compat_urlretrieve
81 from subprocess
import DEVNULL
82 compat_subprocess_get_DEVNULL
= lambda: DEVNULL
84 compat_subprocess_get_DEVNULL
= lambda: open(os
.path
.devnull
, 'w')
87 from urllib
.parse
import parse_qs
as compat_parse_qs
88 except ImportError: # Python 2
89 # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
90 # Python 2's version is apparently totally broken
91 def _unquote(string
, encoding
='utf-8', errors
='replace'):
94 res
= string
.split('%')
101 # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
108 pct_sequence
+= item
[:2].decode('hex')
111 # This segment was just a single percent-encoded character.
112 # May be part of a sequence of code units, so delay decoding.
113 # (Stored in pct_sequence).
117 # Encountered non-percent-encoded characters. Flush the current
119 string
+= pct_sequence
.decode(encoding
, errors
) + rest
122 # Flush the final pct_sequence
123 string
+= pct_sequence
.decode(encoding
, errors
)
126 def _parse_qsl(qs
, keep_blank_values
=False, strict_parsing
=False,
127 encoding
='utf-8', errors
='replace'):
128 qs
, _coerce_result
= qs
, unicode
129 pairs
= [s2
for s1
in qs
.split('&') for s2
in s1
.split(';')]
131 for name_value
in pairs
:
132 if not name_value
and not strict_parsing
:
134 nv
= name_value
.split('=', 1)
137 raise ValueError("bad query field: %r" % (name_value
,))
138 # Handle case of a control-name with no equal sign
139 if keep_blank_values
:
143 if len(nv
[1]) or keep_blank_values
:
144 name
= nv
[0].replace('+', ' ')
145 name
= _unquote(name
, encoding
=encoding
, errors
=errors
)
146 name
= _coerce_result(name
)
147 value
= nv
[1].replace('+', ' ')
148 value
= _unquote(value
, encoding
=encoding
, errors
=errors
)
149 value
= _coerce_result(value
)
150 r
.append((name
, value
))
153 def compat_parse_qs(qs
, keep_blank_values
=False, strict_parsing
=False,
154 encoding
='utf-8', errors
='replace'):
156 pairs
= _parse_qsl(qs
, keep_blank_values
, strict_parsing
,
157 encoding
=encoding
, errors
=errors
)
158 for name
, value
in pairs
:
159 if name
in parsed_result
:
160 parsed_result
[name
].append(value
)
162 parsed_result
[name
] = [value
]
166 compat_str
= unicode # Python 2
171 compat_chr
= unichr # Python 2
176 if type(c
) is int: return c
179 # This is not clearly defined otherwise
180 compiled_regex_type
= type(re
.compile(''))
183 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 (Chrome)',
184 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
185 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
186 'Accept-Encoding': 'gzip, deflate',
187 'Accept-Language': 'en-us,en;q=0.5',
190 def preferredencoding():
191 """Get preferred encoding.
193 Returns the best encoding scheme for the system, based on
194 locale.getpreferredencoding() and some further tweaks.
197 pref
= locale
.getpreferredencoding()
204 if sys
.version_info
< (3,0):
206 print(s
.encode(preferredencoding(), 'xmlcharrefreplace'))
209 assert type(s
) == type(u
'')
212 # In Python 2.x, json.dump expects a bytestream.
213 # In Python 3.x, it writes to a character stream
214 if sys
.version_info
< (3,0):
215 def write_json_file(obj
, fn
):
216 with open(fn
, 'wb') as f
:
219 def write_json_file(obj
, fn
):
220 with open(fn
, 'w', encoding
='utf-8') as f
:
223 if sys
.version_info
>= (2,7):
224 def find_xpath_attr(node
, xpath
, key
, val
):
225 """ Find the xpath xpath[@key=val] """
226 assert re
.match(r
'^[a-zA-Z]+$', key
)
227 assert re
.match(r
'^[a-zA-Z0-9@\s]*$', val
)
228 expr
= xpath
+ u
"[@%s='%s']" % (key
, val
)
229 return node
.find(expr
)
231 def find_xpath_attr(node
, xpath
, key
, val
):
232 for f
in node
.findall(xpath
):
233 if f
.attrib
.get(key
) == val
:
237 # On python2.6 the xml.etree.ElementTree.Element methods don't support
238 # the namespace parameter
239 def xpath_with_ns(path
, ns_map
):
240 components
= [c
.split(':') for c
in path
.split('/')]
244 replaced
.append(c
[0])
247 replaced
.append('{%s}%s' % (ns_map
[ns
], tag
))
248 return '/'.join(replaced
)
250 def htmlentity_transform(matchobj
):
251 """Transforms an HTML entity to a character.
253 This function receives a match object and is intended to be used with
254 the re.sub() function.
256 entity
= matchobj
.group(1)
258 # Known non-numeric HTML entity
259 if entity
in compat_html_entities
.name2codepoint
:
260 return compat_chr(compat_html_entities
.name2codepoint
[entity
])
262 mobj
= re
.match(u
'(?u)#(x?\\d+)', entity
)
264 numstr
= mobj
.group(1)
265 if numstr
.startswith(u
'x'):
267 numstr
= u
'0%s' % numstr
270 return compat_chr(int(numstr
, base
))
272 # Unknown entity in name, return its literal representation
273 return (u
'&%s;' % entity
)
275 compat_html_parser
.locatestarttagend
= re
.compile(r
"""<[a-zA-Z][-.a-zA-Z0-9:_]*(?:\s+(?:(?<=['"\s])[^\s/>][^\s/=>]*(?:\s*=+\s*(?:'[^']*'|"[^"]*"|(?!['"])[^>\s]*))?\s*)*)?\s*""", re
.VERBOSE
) # backport bugfix
276 class BaseHTMLParser(compat_html_parser
.HTMLParser
):
278 compat_html_parser
.HTMLParser
.__init
__(self
)
281 def loads(self
, html
):
286 class AttrParser(BaseHTMLParser
):
287 """Modified HTMLParser that isolates a tag with the specified attribute"""
288 def __init__(self
, attribute
, value
):
289 self
.attribute
= attribute
294 self
.watch_startpos
= False
296 BaseHTMLParser
.__init
__(self
)
298 def error(self
, message
):
299 if self
.error_count
> 10 or self
.started
:
300 raise compat_html_parser
.HTMLParseError(message
, self
.getpos())
301 self
.rawdata
= '\n'.join(self
.html
.split('\n')[self
.getpos()[0]:]) # skip one line
302 self
.error_count
+= 1
305 def handle_starttag(self
, tag
, attrs
):
308 self
.find_startpos(None)
309 if self
.attribute
in attrs
and attrs
[self
.attribute
] == self
.value
:
312 self
.watch_startpos
= True
314 if not tag
in self
.depth
: self
.depth
[tag
] = 0
317 def handle_endtag(self
, tag
):
319 if tag
in self
.depth
: self
.depth
[tag
] -= 1
320 if self
.depth
[self
.result
[0]] == 0:
322 self
.result
.append(self
.getpos())
324 def find_startpos(self
, x
):
325 """Needed to put the start position of the result (self.result[1])
326 after the opening tag with the requested id"""
327 if self
.watch_startpos
:
328 self
.watch_startpos
= False
329 self
.result
.append(self
.getpos())
330 handle_entityref
= handle_charref
= handle_data
= handle_comment
= \
331 handle_decl
= handle_pi
= unknown_decl
= find_startpos
333 def get_result(self
):
334 if self
.result
is None:
336 if len(self
.result
) != 3:
338 lines
= self
.html
.split('\n')
339 lines
= lines
[self
.result
[1][0]-1:self
.result
[2][0]]
340 lines
[0] = lines
[0][self
.result
[1][1]:]
342 lines
[-1] = lines
[-1][:self
.result
[2][1]-self
.result
[1][1]]
343 lines
[-1] = lines
[-1][:self
.result
[2][1]]
344 return '\n'.join(lines
).strip()
345 # Hack for https://github.com/rg3/youtube-dl/issues/662
346 if sys
.version_info
< (2, 7, 3):
347 AttrParser
.parse_endtag
= (lambda self
, i
:
348 i
+ len("</scr'+'ipt>")
349 if self
.rawdata
[i
:].startswith("</scr'+'ipt>")
350 else compat_html_parser
.HTMLParser
.parse_endtag(self
, i
))
352 def get_element_by_id(id, html
):
353 """Return the content of the tag with the specified ID in the passed HTML document"""
354 return get_element_by_attribute("id", id, html
)
356 def get_element_by_attribute(attribute
, value
, html
):
357 """Return the content of the tag with the specified attribute in the passed HTML document"""
358 parser
= AttrParser(attribute
, value
)
361 except compat_html_parser
.HTMLParseError
:
363 return parser
.get_result()
365 class MetaParser(BaseHTMLParser
):
367 Modified HTMLParser that isolates a meta tag with the specified name
370 def __init__(self
, name
):
371 BaseHTMLParser
.__init
__(self
)
376 def handle_starttag(self
, tag
, attrs
):
380 if attrs
.get('name') == self
.name
:
381 self
.result
= attrs
.get('content')
383 def get_result(self
):
386 def get_meta_content(name
, html
):
388 Return the content attribute from the meta tag with the given name attribute.
390 parser
= MetaParser(name
)
393 except compat_html_parser
.HTMLParseError
:
395 return parser
.get_result()
398 def clean_html(html
):
399 """Clean an HTML snippet into a readable string"""
401 html
= html
.replace('\n', ' ')
402 html
= re
.sub(r
'\s*<\s*br\s*/?\s*>\s*', '\n', html
)
403 html
= re
.sub(r
'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html
)
405 html
= re
.sub('<.*?>', '', html
)
406 # Replace html entities
407 html
= unescapeHTML(html
)
411 def sanitize_open(filename
, open_mode
):
412 """Try to open the given filename, and slightly tweak it if this fails.
414 Attempts to open the given filename. If this fails, it tries to change
415 the filename slightly, step by step, until it's either able to open it
416 or it fails and raises a final exception, like the standard open()
419 It returns the tuple (stream, definitive_file_name).
423 if sys
.platform
== 'win32':
425 msvcrt
.setmode(sys
.stdout
.fileno(), os
.O_BINARY
)
426 return (sys
.stdout
.buffer if hasattr(sys
.stdout
, 'buffer') else sys
.stdout
, filename
)
427 stream
= open(encodeFilename(filename
), open_mode
)
428 return (stream
, filename
)
429 except (IOError, OSError) as err
:
430 if err
.errno
in (errno
.EACCES
,):
433 # In case of error, try to remove win32 forbidden chars
434 alt_filename
= os
.path
.join(
435 re
.sub(u
'[/<>:"\\|\\\\?\\*]', u
'#', path_part
)
436 for path_part
in os
.path
.split(filename
)
438 if alt_filename
== filename
:
441 # An exception here should be caught in the caller
442 stream
= open(encodeFilename(filename
), open_mode
)
443 return (stream
, alt_filename
)
446 def timeconvert(timestr
):
447 """Convert RFC 2822 defined time string into system timestamp"""
449 timetuple
= email
.utils
.parsedate_tz(timestr
)
450 if timetuple
is not None:
451 timestamp
= email
.utils
.mktime_tz(timetuple
)
454 def sanitize_filename(s
, restricted
=False, is_id
=False):
455 """Sanitizes a string so it could be used as part of a filename.
456 If restricted is set, use a stricter subset of allowed characters.
457 Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
459 def replace_insane(char
):
460 if char
== '?' or ord(char
) < 32 or ord(char
) == 127:
463 return '' if restricted
else '\''
465 return '_-' if restricted
else ' -'
466 elif char
in '\\/|*<>':
468 if restricted
and (char
in '!&\'()[]{}$;`^,#' or char
.isspace()):
470 if restricted
and ord(char
) > 127:
474 result
= u
''.join(map(replace_insane
, s
))
476 while '__' in result
:
477 result
= result
.replace('__', '_')
478 result
= result
.strip('_')
479 # Common case of "Foreign band name - English song title"
480 if restricted
and result
.startswith('-_'):
486 def orderedSet(iterable
):
487 """ Remove all duplicates from the input iterable """
498 assert type(s
) == type(u
'')
500 result
= re
.sub(u
'(?u)&(.+?);', htmlentity_transform
, s
)
503 def encodeFilename(s
):
505 @param s The name of the file
508 assert type(s
) == type(u
'')
510 # Python 3 has a Unicode API
511 if sys
.version_info
>= (3, 0):
514 if sys
.platform
== 'win32' and sys
.getwindowsversion()[0] >= 5:
515 # Pass u'' directly to use Unicode APIs on Windows 2000 and up
516 # (Detecting Windows NT 4 is tricky because 'major >= 4' would
517 # match Windows 9x series as well. Besides, NT 4 is obsolete.)
520 encoding
= sys
.getfilesystemencoding()
523 return s
.encode(encoding
, 'ignore')
525 def decodeOption(optval
):
528 if isinstance(optval
, bytes):
529 optval
= optval
.decode(preferredencoding())
531 assert isinstance(optval
, compat_str
)
534 def formatSeconds(secs
):
536 return '%d:%02d:%02d' % (secs
// 3600, (secs
% 3600) // 60, secs
% 60)
538 return '%d:%02d' % (secs
// 60, secs
% 60)
543 def make_HTTPS_handler(opts_no_check_certificate
, **kwargs
):
544 if sys
.version_info
< (3, 2):
547 class HTTPSConnectionV3(httplib
.HTTPSConnection
):
548 def __init__(self
, *args
, **kwargs
):
549 httplib
.HTTPSConnection
.__init
__(self
, *args
, **kwargs
)
552 sock
= socket
.create_connection((self
.host
, self
.port
), self
.timeout
)
553 if getattr(self
, '_tunnel_host', False):
557 self
.sock
= ssl
.wrap_socket(sock
, self
.key_file
, self
.cert_file
, ssl_version
=ssl
.PROTOCOL_SSLv3
)
559 self
.sock
= ssl
.wrap_socket(sock
, self
.key_file
, self
.cert_file
, ssl_version
=ssl
.PROTOCOL_SSLv23
)
561 class HTTPSHandlerV3(compat_urllib_request
.HTTPSHandler
):
562 def https_open(self
, req
):
563 return self
.do_open(HTTPSConnectionV3
, req
)
564 return HTTPSHandlerV3(**kwargs
)
566 context
= ssl
.SSLContext(ssl
.PROTOCOL_SSLv3
)
567 context
.verify_mode
= (ssl
.CERT_NONE
568 if opts_no_check_certificate
569 else ssl
.CERT_REQUIRED
)
570 context
.set_default_verify_paths()
572 context
.load_default_certs()
573 except AttributeError:
575 return compat_urllib_request
.HTTPSHandler(context
=context
, **kwargs
)
577 class ExtractorError(Exception):
578 """Error during info extraction."""
579 def __init__(self
, msg
, tb
=None, expected
=False, cause
=None):
580 """ tb, if given, is the original traceback (so that it can be printed out).
581 If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
584 if sys
.exc_info()[0] in (compat_urllib_error
.URLError
, socket
.timeout
, UnavailableVideoError
):
587 msg
= msg
+ u
'; please report this issue on https://yt-dl.org/bug . Be sure to call youtube-dl with the --verbose flag and include its complete output. Make sure you are using the latest version; type youtube-dl -U to update.'
588 super(ExtractorError
, self
).__init
__(msg
)
591 self
.exc_info
= sys
.exc_info() # preserve original exception
594 def format_traceback(self
):
595 if self
.traceback
is None:
597 return u
''.join(traceback
.format_tb(self
.traceback
))
600 class RegexNotFoundError(ExtractorError
):
601 """Error when a regex didn't match"""
605 class DownloadError(Exception):
606 """Download Error exception.
608 This exception may be thrown by FileDownloader objects if they are not
609 configured to continue on errors. They will contain the appropriate
612 def __init__(self
, msg
, exc_info
=None):
613 """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
614 super(DownloadError
, self
).__init
__(msg
)
615 self
.exc_info
= exc_info
618 class SameFileError(Exception):
619 """Same File exception.
621 This exception will be thrown by FileDownloader objects if they detect
622 multiple files would have to be downloaded to the same file on disk.
627 class PostProcessingError(Exception):
628 """Post Processing exception.
630 This exception may be raised by PostProcessor's .run() method to
631 indicate an error in the postprocessing task.
633 def __init__(self
, msg
):
636 class MaxDownloadsReached(Exception):
637 """ --max-downloads limit has been reached. """
641 class UnavailableVideoError(Exception):
642 """Unavailable Format exception.
644 This exception will be thrown when a video is requested
645 in a format that is not available for that video.
650 class ContentTooShortError(Exception):
651 """Content Too Short exception.
653 This exception may be raised by FileDownloader objects when a file they
654 download is too small for what the server announced first, indicating
655 the connection was probably interrupted.
661 def __init__(self
, downloaded
, expected
):
662 self
.downloaded
= downloaded
663 self
.expected
= expected
665 class YoutubeDLHandler(compat_urllib_request
.HTTPHandler
):
666 """Handler for HTTP requests and responses.
668 This class, when installed with an OpenerDirector, automatically adds
669 the standard headers to every HTTP request and handles gzipped and
670 deflated responses from web servers. If compression is to be avoided in
671 a particular request, the original request in the program code only has
672 to include the HTTP header "Youtubedl-No-Compression", which will be
673 removed before making the real request.
675 Part of this code was copied from:
677 http://techknack.net/python-urllib2-handlers/
679 Andrew Rowls, the author of that code, agreed to release it to the
686 return zlib
.decompress(data
, -zlib
.MAX_WBITS
)
688 return zlib
.decompress(data
)
691 def addinfourl_wrapper(stream
, headers
, url
, code
):
692 if hasattr(compat_urllib_request
.addinfourl
, 'getcode'):
693 return compat_urllib_request
.addinfourl(stream
, headers
, url
, code
)
694 ret
= compat_urllib_request
.addinfourl(stream
, headers
, url
)
698 def http_request(self
, req
):
699 for h
,v
in std_headers
.items():
703 if 'Youtubedl-no-compression' in req
.headers
:
704 if 'Accept-encoding' in req
.headers
:
705 del req
.headers
['Accept-encoding']
706 del req
.headers
['Youtubedl-no-compression']
707 if 'Youtubedl-user-agent' in req
.headers
:
708 if 'User-agent' in req
.headers
:
709 del req
.headers
['User-agent']
710 req
.headers
['User-agent'] = req
.headers
['Youtubedl-user-agent']
711 del req
.headers
['Youtubedl-user-agent']
714 def http_response(self
, req
, resp
):
717 if resp
.headers
.get('Content-encoding', '') == 'gzip':
718 content
= resp
.read()
719 gz
= gzip
.GzipFile(fileobj
=io
.BytesIO(content
), mode
='rb')
721 uncompressed
= io
.BytesIO(gz
.read())
722 except IOError as original_ioerror
:
723 # There may be junk add the end of the file
724 # See http://stackoverflow.com/q/4928560/35070 for details
725 for i
in range(1, 1024):
727 gz
= gzip
.GzipFile(fileobj
=io
.BytesIO(content
[:-i
]), mode
='rb')
728 uncompressed
= io
.BytesIO(gz
.read())
733 raise original_ioerror
734 resp
= self
.addinfourl_wrapper(uncompressed
, old_resp
.headers
, old_resp
.url
, old_resp
.code
)
735 resp
.msg
= old_resp
.msg
737 if resp
.headers
.get('Content-encoding', '') == 'deflate':
738 gz
= io
.BytesIO(self
.deflate(resp
.read()))
739 resp
= self
.addinfourl_wrapper(gz
, old_resp
.headers
, old_resp
.url
, old_resp
.code
)
740 resp
.msg
= old_resp
.msg
743 https_request
= http_request
744 https_response
= http_response
746 def unified_strdate(date_str
):
747 """Return a string with the date in the format YYYYMMDD"""
750 date_str
= date_str
.replace(',',' ')
751 # %z (UTC offset) is only supported in python>=3.2
752 date_str
= re
.sub(r
' (\+|-)[\d]*$', '', date_str
)
753 format_expressions
= [
761 '%Y-%m-%dT%H:%M:%SZ',
762 '%Y-%m-%dT%H:%M:%S.%fZ',
763 '%Y-%m-%dT%H:%M:%S.%f0Z',
766 for expression
in format_expressions
:
768 upload_date
= datetime
.datetime
.strptime(date_str
, expression
).strftime('%Y%m%d')
771 if upload_date
is None:
772 timetuple
= email
.utils
.parsedate_tz(date_str
)
774 upload_date
= datetime
.datetime(*timetuple
[:6]).strftime('%Y%m%d')
777 def determine_ext(url
, default_ext
=u
'unknown_video'):
778 guess
= url
.partition(u
'?')[0].rpartition(u
'.')[2]
779 if re
.match(r
'^[A-Za-z0-9]+$', guess
):
784 def subtitles_filename(filename
, sub_lang
, sub_format
):
785 return filename
.rsplit('.', 1)[0] + u
'.' + sub_lang
+ u
'.' + sub_format
787 def date_from_str(date_str
):
789 Return a datetime object from a string in the format YYYYMMDD or
790 (now|today)[+-][0-9](day|week|month|year)(s)?"""
791 today
= datetime
.date
.today()
792 if date_str
== 'now'or date_str
== 'today':
794 match
= re
.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str
)
795 if match
is not None:
796 sign
= match
.group('sign')
797 time
= int(match
.group('time'))
800 unit
= match
.group('unit')
809 delta
= datetime
.timedelta(**{unit: time}
)
811 return datetime
.datetime
.strptime(date_str
, "%Y%m%d").date()
813 class DateRange(object):
814 """Represents a time interval between two dates"""
815 def __init__(self
, start
=None, end
=None):
816 """start and end must be strings in the format accepted by date"""
817 if start
is not None:
818 self
.start
= date_from_str(start
)
820 self
.start
= datetime
.datetime
.min.date()
822 self
.end
= date_from_str(end
)
824 self
.end
= datetime
.datetime
.max.date()
825 if self
.start
> self
.end
:
826 raise ValueError('Date range: "%s" , the start date must be before the end date' % self
)
829 """Returns a range that only contains the given day"""
831 def __contains__(self
, date
):
832 """Check if the date is in the range"""
833 if not isinstance(date
, datetime
.date
):
834 date
= date_from_str(date
)
835 return self
.start
<= date
<= self
.end
837 return '%s - %s' % ( self
.start
.isoformat(), self
.end
.isoformat())
841 """ Returns the platform name as a compat_str """
842 res
= platform
.platform()
843 if isinstance(res
, bytes):
844 res
= res
.decode(preferredencoding())
846 assert isinstance(res
, compat_str
)
850 def write_string(s
, out
=None):
853 assert type(s
) == type(u
'')
855 if ('b' in getattr(out
, 'mode', '') or
856 sys
.version_info
[0] < 3): # Python 2 lies about mode of sys.stderr
857 s
= s
.encode(preferredencoding(), 'ignore')
862 def bytes_to_intlist(bs
):
865 if isinstance(bs
[0], int): # Python 3
868 return [ord(c
) for c
in bs
]
871 def intlist_to_bytes(xs
):
874 if isinstance(chr(0), bytes): # Python 2
875 return ''.join([chr(x
) for x
in xs
])
880 def get_cachedir(params
={}):
881 cache_root
= os
.environ
.get('XDG_CACHE_HOME',
882 os
.path
.expanduser('~/.cache'))
883 return params
.get('cachedir', os
.path
.join(cache_root
, 'youtube-dl'))
886 # Cross-platform file locking
887 if sys
.platform
== 'win32':
888 import ctypes
.wintypes
891 class OVERLAPPED(ctypes
.Structure
):
893 ('Internal', ctypes
.wintypes
.LPVOID
),
894 ('InternalHigh', ctypes
.wintypes
.LPVOID
),
895 ('Offset', ctypes
.wintypes
.DWORD
),
896 ('OffsetHigh', ctypes
.wintypes
.DWORD
),
897 ('hEvent', ctypes
.wintypes
.HANDLE
),
900 kernel32
= ctypes
.windll
.kernel32
901 LockFileEx
= kernel32
.LockFileEx
902 LockFileEx
.argtypes
= [
903 ctypes
.wintypes
.HANDLE
, # hFile
904 ctypes
.wintypes
.DWORD
, # dwFlags
905 ctypes
.wintypes
.DWORD
, # dwReserved
906 ctypes
.wintypes
.DWORD
, # nNumberOfBytesToLockLow
907 ctypes
.wintypes
.DWORD
, # nNumberOfBytesToLockHigh
908 ctypes
.POINTER(OVERLAPPED
) # Overlapped
910 LockFileEx
.restype
= ctypes
.wintypes
.BOOL
911 UnlockFileEx
= kernel32
.UnlockFileEx
912 UnlockFileEx
.argtypes
= [
913 ctypes
.wintypes
.HANDLE
, # hFile
914 ctypes
.wintypes
.DWORD
, # dwReserved
915 ctypes
.wintypes
.DWORD
, # nNumberOfBytesToLockLow
916 ctypes
.wintypes
.DWORD
, # nNumberOfBytesToLockHigh
917 ctypes
.POINTER(OVERLAPPED
) # Overlapped
919 UnlockFileEx
.restype
= ctypes
.wintypes
.BOOL
920 whole_low
= 0xffffffff
921 whole_high
= 0x7fffffff
923 def _lock_file(f
, exclusive
):
924 overlapped
= OVERLAPPED()
925 overlapped
.Offset
= 0
926 overlapped
.OffsetHigh
= 0
927 overlapped
.hEvent
= 0
928 f
._lock
_file
_overlapped
_p
= ctypes
.pointer(overlapped
)
929 handle
= msvcrt
.get_osfhandle(f
.fileno())
930 if not LockFileEx(handle
, 0x2 if exclusive
else 0x0, 0,
931 whole_low
, whole_high
, f
._lock
_file
_overlapped
_p
):
932 raise OSError('Locking file failed: %r' % ctypes
.FormatError())
935 assert f
._lock
_file
_overlapped
_p
936 handle
= msvcrt
.get_osfhandle(f
.fileno())
937 if not UnlockFileEx(handle
, 0,
938 whole_low
, whole_high
, f
._lock
_file
_overlapped
_p
):
939 raise OSError('Unlocking file failed: %r' % ctypes
.FormatError())
944 def _lock_file(f
, exclusive
):
945 fcntl
.lockf(f
, fcntl
.LOCK_EX
if exclusive
else fcntl
.LOCK_SH
)
948 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
951 class locked_file(object):
952 def __init__(self
, filename
, mode
, encoding
=None):
953 assert mode
in ['r', 'a', 'w']
954 self
.f
= io
.open(filename
, mode
, encoding
=encoding
)
958 exclusive
= self
.mode
!= 'r'
960 _lock_file(self
.f
, exclusive
)
966 def __exit__(self
, etype
, value
, traceback
):
975 def write(self
, *args
):
976 return self
.f
.write(*args
)
978 def read(self
, *args
):
979 return self
.f
.read(*args
)
982 def shell_quote(args
):
984 encoding
= sys
.getfilesystemencoding()
988 if isinstance(a
, bytes):
989 # We may get a filename encoded with 'encodeFilename'
990 a
= a
.decode(encoding
)
991 quoted_args
.append(pipes
.quote(a
))
992 return u
' '.join(quoted_args
)
995 def takewhile_inclusive(pred
, seq
):
996 """ Like itertools.takewhile, but include the latest evaluated element
997 (the first element so that Not pred(e)) """
1004 def smuggle_url(url
, data
):
1005 """ Pass additional data in a URL for internal use. """
1007 sdata
= compat_urllib_parse
.urlencode(
1008 {u'__youtubedl_smuggle': json.dumps(data)}
)
1009 return url
+ u
'#' + sdata
1012 def unsmuggle_url(smug_url
):
1013 if not '#__youtubedl_smuggle' in smug_url
:
1014 return smug_url
, None
1015 url
, _
, sdata
= smug_url
.rpartition(u
'#')
1016 jsond
= compat_parse_qs(sdata
)[u
'__youtubedl_smuggle'][0]
1017 data
= json
.loads(jsond
)
1021 def format_bytes(bytes):
1024 if type(bytes) is str:
1025 bytes = float(bytes)
1029 exponent
= int(math
.log(bytes, 1024.0))
1030 suffix
= [u
'B', u
'KiB', u
'MiB', u
'GiB', u
'TiB', u
'PiB', u
'EiB', u
'ZiB', u
'YiB'][exponent
]
1031 converted
= float(bytes) / float(1024 ** exponent
)
1032 return u
'%.2f%s' % (converted
, suffix
)
1035 def str_to_int(int_str
):
1036 int_str
= re
.sub(r
'[,\.]', u
'', int_str
)
1040 def get_term_width():
1041 columns
= os
.environ
.get('COLUMNS', None)
1046 sp
= subprocess
.Popen(
1048 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
1049 out
, err
= sp
.communicate()
1050 return int(out
.split()[1])
1056 def month_by_name(name
):
1057 """ Return the number of a month by (locale-independently) English name """
1060 u
'January', u
'February', u
'March', u
'April', u
'May', u
'June',
1061 u
'July', u
'August', u
'September', u
'October', u
'November', u
'December']
1063 return ENGLISH_NAMES
.index(name
) + 1
1068 def fix_xml_all_ampersand(xml_str
):
1069 """Replace all the '&' by '&' in XML"""
1070 return xml_str
.replace(u
'&', u
'&')
1073 def setproctitle(title
):
1074 assert isinstance(title
, type(u
''))
1076 libc
= ctypes
.cdll
.LoadLibrary("libc.so.6")
1080 buf
= ctypes
.create_string_buffer(len(title
) + 1)
1081 buf
.value
= title
.encode('utf-8')
1083 libc
.prctl(15, ctypes
.byref(buf
), 0, 0, 0)
1084 except AttributeError:
1085 return # Strange libc, just skip this
1088 def remove_start(s
, start
):
1089 if s
.startswith(start
):
1090 return s
[len(start
):]
1094 def url_basename(url
):
1095 path
= compat_urlparse
.urlparse(url
).path
1096 return path
.strip(u
'/').split(u
'/')[-1]
1099 class HEADRequest(compat_urllib_request
.Request
):
1100 def get_method(self
):
1105 return v
if v
is None else int(v
)
1108 def parse_duration(s
):
1113 r
'(?:(?:(?P<hours>[0-9]+):)?(?P<mins>[0-9]+):)?(?P<secs>[0-9]+)$', s
)
1116 res
= int(m
.group('secs'))
1118 res
+= int(m
.group('mins')) * 60
1119 if m
.group('hours'):
1120 res
+= int(m
.group('hours')) * 60 * 60
1124 def prepend_extension(filename
, ext
):
1125 name
, real_ext
= os
.path
.splitext(filename
)
1126 return u
'{0}.{1}{2}'.format(name
, ext
, real_ext
)