mirror of
https://code.hackerspace.pl/q3k/youtube-dl
synced 2025-03-16 11:43:02 +00:00
1459 lines
46 KiB
Python
1459 lines
46 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import calendar
|
|
import codecs
|
|
import contextlib
|
|
import ctypes
|
|
import datetime
|
|
import email.utils
|
|
import errno
|
|
import getpass
|
|
import gzip
|
|
import itertools
|
|
import io
|
|
import json
|
|
import locale
|
|
import math
|
|
import os
|
|
import pipes
|
|
import platform
|
|
import re
|
|
import ssl
|
|
import socket
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import traceback
|
|
import xml.etree.ElementTree
|
|
import zlib
|
|
|
|
try:
|
|
import urllib.request as compat_urllib_request
|
|
except ImportError: # Python 2
|
|
import urllib2 as compat_urllib_request
|
|
|
|
try:
|
|
import urllib.error as compat_urllib_error
|
|
except ImportError: # Python 2
|
|
import urllib2 as compat_urllib_error
|
|
|
|
try:
|
|
import urllib.parse as compat_urllib_parse
|
|
except ImportError: # Python 2
|
|
import urllib as compat_urllib_parse
|
|
|
|
try:
|
|
from urllib.parse import urlparse as compat_urllib_parse_urlparse
|
|
except ImportError: # Python 2
|
|
from urlparse import urlparse as compat_urllib_parse_urlparse
|
|
|
|
try:
|
|
import urllib.parse as compat_urlparse
|
|
except ImportError: # Python 2
|
|
import urlparse as compat_urlparse
|
|
|
|
try:
|
|
import http.cookiejar as compat_cookiejar
|
|
except ImportError: # Python 2
|
|
import cookielib as compat_cookiejar
|
|
|
|
try:
|
|
import html.entities as compat_html_entities
|
|
except ImportError: # Python 2
|
|
import htmlentitydefs as compat_html_entities
|
|
|
|
try:
|
|
import html.parser as compat_html_parser
|
|
except ImportError: # Python 2
|
|
import HTMLParser as compat_html_parser
|
|
|
|
try:
|
|
import http.client as compat_http_client
|
|
except ImportError: # Python 2
|
|
import httplib as compat_http_client
|
|
|
|
try:
|
|
from urllib.error import HTTPError as compat_HTTPError
|
|
except ImportError: # Python 2
|
|
from urllib2 import HTTPError as compat_HTTPError
|
|
|
|
try:
|
|
from urllib.request import urlretrieve as compat_urlretrieve
|
|
except ImportError: # Python 2
|
|
from urllib import urlretrieve as compat_urlretrieve
|
|
|
|
|
|
try:
|
|
from subprocess import DEVNULL
|
|
compat_subprocess_get_DEVNULL = lambda: DEVNULL
|
|
except ImportError:
|
|
compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
|
|
|
|
try:
|
|
from urllib.parse import parse_qs as compat_parse_qs
|
|
except ImportError: # Python 2
|
|
# HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
|
|
# Python 2's version is apparently totally broken
|
|
def _unquote(string, encoding='utf-8', errors='replace'):
|
|
if string == '':
|
|
return string
|
|
res = string.split('%')
|
|
if len(res) == 1:
|
|
return string
|
|
if encoding is None:
|
|
encoding = 'utf-8'
|
|
if errors is None:
|
|
errors = 'replace'
|
|
# pct_sequence: contiguous sequence of percent-encoded bytes, decoded
|
|
pct_sequence = b''
|
|
string = res[0]
|
|
for item in res[1:]:
|
|
try:
|
|
if not item:
|
|
raise ValueError
|
|
pct_sequence += item[:2].decode('hex')
|
|
rest = item[2:]
|
|
if not rest:
|
|
# This segment was just a single percent-encoded character.
|
|
# May be part of a sequence of code units, so delay decoding.
|
|
# (Stored in pct_sequence).
|
|
continue
|
|
except ValueError:
|
|
rest = '%' + item
|
|
# Encountered non-percent-encoded characters. Flush the current
|
|
# pct_sequence.
|
|
string += pct_sequence.decode(encoding, errors) + rest
|
|
pct_sequence = b''
|
|
if pct_sequence:
|
|
# Flush the final pct_sequence
|
|
string += pct_sequence.decode(encoding, errors)
|
|
return string
|
|
|
|
def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
|
|
encoding='utf-8', errors='replace'):
|
|
qs, _coerce_result = qs, unicode
|
|
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
|
r = []
|
|
for name_value in pairs:
|
|
if not name_value and not strict_parsing:
|
|
continue
|
|
nv = name_value.split('=', 1)
|
|
if len(nv) != 2:
|
|
if strict_parsing:
|
|
raise ValueError("bad query field: %r" % (name_value,))
|
|
# Handle case of a control-name with no equal sign
|
|
if keep_blank_values:
|
|
nv.append('')
|
|
else:
|
|
continue
|
|
if len(nv[1]) or keep_blank_values:
|
|
name = nv[0].replace('+', ' ')
|
|
name = _unquote(name, encoding=encoding, errors=errors)
|
|
name = _coerce_result(name)
|
|
value = nv[1].replace('+', ' ')
|
|
value = _unquote(value, encoding=encoding, errors=errors)
|
|
value = _coerce_result(value)
|
|
r.append((name, value))
|
|
return r
|
|
|
|
def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
|
|
encoding='utf-8', errors='replace'):
|
|
parsed_result = {}
|
|
pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
|
|
encoding=encoding, errors=errors)
|
|
for name, value in pairs:
|
|
if name in parsed_result:
|
|
parsed_result[name].append(value)
|
|
else:
|
|
parsed_result[name] = [value]
|
|
return parsed_result
|
|
|
|
try:
|
|
compat_str = unicode # Python 2
|
|
except NameError:
|
|
compat_str = str
|
|
|
|
try:
|
|
compat_chr = unichr # Python 2
|
|
except NameError:
|
|
compat_chr = chr
|
|
|
|
try:
|
|
from xml.etree.ElementTree import ParseError as compat_xml_parse_error
|
|
except ImportError: # Python 2.6
|
|
from xml.parsers.expat import ExpatError as compat_xml_parse_error
|
|
|
|
def compat_ord(c):
|
|
if type(c) is int: return c
|
|
else: return ord(c)
|
|
|
|
# This is not clearly defined otherwise
|
|
compiled_regex_type = type(re.compile(''))
|
|
|
|
std_headers = {
|
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 (Chrome)',
|
|
'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Encoding': 'gzip, deflate',
|
|
'Accept-Language': 'en-us,en;q=0.5',
|
|
}
|
|
|
|
def preferredencoding():
|
|
"""Get preferred encoding.
|
|
|
|
Returns the best encoding scheme for the system, based on
|
|
locale.getpreferredencoding() and some further tweaks.
|
|
"""
|
|
try:
|
|
pref = locale.getpreferredencoding()
|
|
u'TEST'.encode(pref)
|
|
except:
|
|
pref = 'UTF-8'
|
|
|
|
return pref
|
|
|
|
if sys.version_info < (3,0):
|
|
def compat_print(s):
|
|
print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
|
|
else:
|
|
def compat_print(s):
|
|
assert type(s) == type(u'')
|
|
print(s)
|
|
|
|
# In Python 2.x, json.dump expects a bytestream.
|
|
# In Python 3.x, it writes to a character stream
|
|
if sys.version_info < (3,0):
|
|
def write_json_file(obj, fn):
|
|
with open(fn, 'wb') as f:
|
|
json.dump(obj, f)
|
|
else:
|
|
def write_json_file(obj, fn):
|
|
with open(fn, 'w', encoding='utf-8') as f:
|
|
json.dump(obj, f)
|
|
|
|
if sys.version_info >= (2,7):
|
|
def find_xpath_attr(node, xpath, key, val):
|
|
""" Find the xpath xpath[@key=val] """
|
|
assert re.match(r'^[a-zA-Z]+$', key)
|
|
assert re.match(r'^[a-zA-Z0-9@\s:._]*$', val)
|
|
expr = xpath + u"[@%s='%s']" % (key, val)
|
|
return node.find(expr)
|
|
else:
|
|
def find_xpath_attr(node, xpath, key, val):
|
|
for f in node.findall(xpath):
|
|
if f.attrib.get(key) == val:
|
|
return f
|
|
return None
|
|
|
|
# On python2.6 the xml.etree.ElementTree.Element methods don't support
|
|
# the namespace parameter
|
|
def xpath_with_ns(path, ns_map):
|
|
components = [c.split(':') for c in path.split('/')]
|
|
replaced = []
|
|
for c in components:
|
|
if len(c) == 1:
|
|
replaced.append(c[0])
|
|
else:
|
|
ns, tag = c
|
|
replaced.append('{%s}%s' % (ns_map[ns], tag))
|
|
return '/'.join(replaced)
|
|
|
|
def htmlentity_transform(matchobj):
|
|
"""Transforms an HTML entity to a character.
|
|
|
|
This function receives a match object and is intended to be used with
|
|
the re.sub() function.
|
|
"""
|
|
entity = matchobj.group(1)
|
|
|
|
# Known non-numeric HTML entity
|
|
if entity in compat_html_entities.name2codepoint:
|
|
return compat_chr(compat_html_entities.name2codepoint[entity])
|
|
|
|
mobj = re.match(u'(?u)#(x?\\d+)', entity)
|
|
if mobj is not None:
|
|
numstr = mobj.group(1)
|
|
if numstr.startswith(u'x'):
|
|
base = 16
|
|
numstr = u'0%s' % numstr
|
|
else:
|
|
base = 10
|
|
return compat_chr(int(numstr, base))
|
|
|
|
# Unknown entity in name, return its literal representation
|
|
return (u'&%s;' % entity)
|
|
|
|
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
|
|
class BaseHTMLParser(compat_html_parser.HTMLParser):
|
|
def __init(self):
|
|
compat_html_parser.HTMLParser.__init__(self)
|
|
self.html = None
|
|
|
|
def loads(self, html):
|
|
self.html = html
|
|
self.feed(html)
|
|
self.close()
|
|
|
|
class AttrParser(BaseHTMLParser):
|
|
"""Modified HTMLParser that isolates a tag with the specified attribute"""
|
|
def __init__(self, attribute, value):
|
|
self.attribute = attribute
|
|
self.value = value
|
|
self.result = None
|
|
self.started = False
|
|
self.depth = {}
|
|
self.watch_startpos = False
|
|
self.error_count = 0
|
|
BaseHTMLParser.__init__(self)
|
|
|
|
def error(self, message):
|
|
if self.error_count > 10 or self.started:
|
|
raise compat_html_parser.HTMLParseError(message, self.getpos())
|
|
self.rawdata = '\n'.join(self.html.split('\n')[self.getpos()[0]:]) # skip one line
|
|
self.error_count += 1
|
|
self.goahead(1)
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
attrs = dict(attrs)
|
|
if self.started:
|
|
self.find_startpos(None)
|
|
if self.attribute in attrs and attrs[self.attribute] == self.value:
|
|
self.result = [tag]
|
|
self.started = True
|
|
self.watch_startpos = True
|
|
if self.started:
|
|
if not tag in self.depth: self.depth[tag] = 0
|
|
self.depth[tag] += 1
|
|
|
|
def handle_endtag(self, tag):
|
|
if self.started:
|
|
if tag in self.depth: self.depth[tag] -= 1
|
|
if self.depth[self.result[0]] == 0:
|
|
self.started = False
|
|
self.result.append(self.getpos())
|
|
|
|
def find_startpos(self, x):
|
|
"""Needed to put the start position of the result (self.result[1])
|
|
after the opening tag with the requested id"""
|
|
if self.watch_startpos:
|
|
self.watch_startpos = False
|
|
self.result.append(self.getpos())
|
|
handle_entityref = handle_charref = handle_data = handle_comment = \
|
|
handle_decl = handle_pi = unknown_decl = find_startpos
|
|
|
|
def get_result(self):
|
|
if self.result is None:
|
|
return None
|
|
if len(self.result) != 3:
|
|
return None
|
|
lines = self.html.split('\n')
|
|
lines = lines[self.result[1][0]-1:self.result[2][0]]
|
|
lines[0] = lines[0][self.result[1][1]:]
|
|
if len(lines) == 1:
|
|
lines[-1] = lines[-1][:self.result[2][1]-self.result[1][1]]
|
|
lines[-1] = lines[-1][:self.result[2][1]]
|
|
return '\n'.join(lines).strip()
|
|
# Hack for https://github.com/rg3/youtube-dl/issues/662
|
|
if sys.version_info < (2, 7, 3):
|
|
AttrParser.parse_endtag = (lambda self, i:
|
|
i + len("</scr'+'ipt>")
|
|
if self.rawdata[i:].startswith("</scr'+'ipt>")
|
|
else compat_html_parser.HTMLParser.parse_endtag(self, i))
|
|
|
|
def get_element_by_id(id, html):
|
|
"""Return the content of the tag with the specified ID in the passed HTML document"""
|
|
return get_element_by_attribute("id", id, html)
|
|
|
|
def get_element_by_attribute(attribute, value, html):
|
|
"""Return the content of the tag with the specified attribute in the passed HTML document"""
|
|
parser = AttrParser(attribute, value)
|
|
try:
|
|
parser.loads(html)
|
|
except compat_html_parser.HTMLParseError:
|
|
pass
|
|
return parser.get_result()
|
|
|
|
class MetaParser(BaseHTMLParser):
|
|
"""
|
|
Modified HTMLParser that isolates a meta tag with the specified name
|
|
attribute.
|
|
"""
|
|
def __init__(self, name):
|
|
BaseHTMLParser.__init__(self)
|
|
self.name = name
|
|
self.content = None
|
|
self.result = None
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
if tag != 'meta':
|
|
return
|
|
attrs = dict(attrs)
|
|
if attrs.get('name') == self.name:
|
|
self.result = attrs.get('content')
|
|
|
|
def get_result(self):
|
|
return self.result
|
|
|
|
def get_meta_content(name, html):
|
|
"""
|
|
Return the content attribute from the meta tag with the given name attribute.
|
|
"""
|
|
parser = MetaParser(name)
|
|
try:
|
|
parser.loads(html)
|
|
except compat_html_parser.HTMLParseError:
|
|
pass
|
|
return parser.get_result()
|
|
|
|
|
|
def clean_html(html):
|
|
"""Clean an HTML snippet into a readable string"""
|
|
# Newline vs <br />
|
|
html = html.replace('\n', ' ')
|
|
html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
|
|
html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
|
|
# Strip html tags
|
|
html = re.sub('<.*?>', '', html)
|
|
# Replace html entities
|
|
html = unescapeHTML(html)
|
|
return html.strip()
|
|
|
|
|
|
def sanitize_open(filename, open_mode):
|
|
"""Try to open the given filename, and slightly tweak it if this fails.
|
|
|
|
Attempts to open the given filename. If this fails, it tries to change
|
|
the filename slightly, step by step, until it |