|
|
@ -23,253 +23,7 @@ import urllib |
|
|
|
from .utils import * |
|
|
|
|
|
|
|
|
|
|
|
class InfoExtractor(object): |
|
|
|
"""Information Extractor class. |
|
|
|
|
|
|
|
Information extractors are the classes that, given a URL, extract |
|
|
|
information about the video (or videos) the URL refers to. This |
|
|
|
information includes the real video URL, the video title, author and |
|
|
|
others. The information is stored in a dictionary which is then |
|
|
|
passed to the FileDownloader. The FileDownloader processes this |
|
|
|
information possibly downloading the video to the file system, among |
|
|
|
other possible outcomes. |
|
|
|
|
|
|
|
The dictionaries must include the following fields: |
|
|
|
|
|
|
|
id: Video identifier. |
|
|
|
url: Final video URL. |
|
|
|
title: Video title, unescaped. |
|
|
|
ext: Video filename extension. |
|
|
|
|
|
|
|
The following fields are optional: |
|
|
|
|
|
|
|
format: The video format, defaults to ext (used for --get-format) |
|
|
|
thumbnail: Full URL to a video thumbnail image. |
|
|
|
description: One-line video description. |
|
|
|
uploader: Full name of the video uploader. |
|
|
|
upload_date: Video upload date (YYYYMMDD). |
|
|
|
uploader_id: Nickname or id of the video uploader. |
|
|
|
location: Physical location of the video. |
|
|
|
player_url: SWF Player URL (used for rtmpdump). |
|
|
|
subtitles: The subtitle file contents. |
|
|
|
urlhandle: [internal] The urlHandle to be used to download the file, |
|
|
|
like returned by urllib.request.urlopen |
|
|
|
|
|
|
|
The fields should all be Unicode strings. |
|
|
|
|
|
|
|
Subclasses of this one should re-define the _real_initialize() and |
|
|
|
_real_extract() methods and define a _VALID_URL regexp. |
|
|
|
Probably, they should also be added to the list of extractors. |
|
|
|
|
|
|
|
_real_extract() must return a *list* of information dictionaries as |
|
|
|
described above. |
|
|
|
|
|
|
|
Finally, the _WORKING attribute should be set to False for broken IEs |
|
|
|
in order to warn the users and skip the tests. |
|
|
|
""" |
|
|
|
|
|
|
|
_ready = False |
|
|
|
_downloader = None |
|
|
|
_WORKING = True |
|
|
|
|
|
|
|
def __init__(self, downloader=None): |
|
|
|
"""Constructor. Receives an optional downloader.""" |
|
|
|
self._ready = False |
|
|
|
self.set_downloader(downloader) |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def suitable(cls, url): |
|
|
|
"""Receives a URL and returns True if suitable for this IE.""" |
|
|
|
return re.match(cls._VALID_URL, url) is not None |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def working(cls): |
|
|
|
"""Getter method for _WORKING.""" |
|
|
|
return cls._WORKING |
|
|
|
|
|
|
|
def initialize(self): |
|
|
|
"""Initializes an instance (authentication, etc).""" |
|
|
|
if not self._ready: |
|
|
|
self._real_initialize() |
|
|
|
self._ready = True |
|
|
|
|
|
|
|
def extract(self, url): |
|
|
|
"""Extracts URL information and returns it in list of dicts.""" |
|
|
|
self.initialize() |
|
|
|
return self._real_extract(url) |
|
|
|
|
|
|
|
def set_downloader(self, downloader): |
|
|
|
"""Sets the downloader for this IE.""" |
|
|
|
self._downloader = downloader |
|
|
|
|
|
|
|
def _real_initialize(self): |
|
|
|
"""Real initialization process. Redefine in subclasses.""" |
|
|
|
pass |
|
|
|
|
|
|
|
def _real_extract(self, url): |
|
|
|
"""Real extraction process. Redefine in subclasses.""" |
|
|
|
pass |
|
|
|
|
|
|
|
@property |
|
|
|
def IE_NAME(self): |
|
|
|
return type(self).__name__[:-2] |
|
|
|
|
|
|
|
def _request_webpage(self, url_or_request, video_id, note=None, errnote=None): |
|
|
|
""" Returns the response handle """ |
|
|
|
if note is None: |
|
|
|
self.report_download_webpage(video_id) |
|
|
|
elif note is not False: |
|
|
|
self.to_screen(u'%s: %s' % (video_id, note)) |
|
|
|
try: |
|
|
|
return compat_urllib_request.urlopen(url_or_request) |
|
|
|
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: |
|
|
|
if errnote is None: |
|
|
|
errnote = u'Unable to download webpage' |
|
|
|
raise ExtractorError(u'%s: %s' % (errnote, compat_str(err)), sys.exc_info()[2]) |
|
|
|
|
|
|
|
def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None): |
|
|
|
""" Returns a tuple (page content as string, URL handle) """ |
|
|
|
urlh = self._request_webpage(url_or_request, video_id, note, errnote) |
|
|
|
content_type = urlh.headers.get('Content-Type', '') |
|
|
|
m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type) |
|
|
|
if m: |
|
|
|
encoding = m.group(1) |
|
|
|
else: |
|
|
|
encoding = 'utf-8' |
|
|
|
webpage_bytes = urlh.read() |
|
|
|
if self._downloader.params.get('dump_intermediate_pages', False): |
|
|
|
try: |
|
|
|
url = url_or_request.get_full_url() |
|
|
|
except AttributeError: |
|
|
|
url = url_or_request |
|
|
|
self.to_screen(u'Dumping request to ' + url) |
|
|
|
dump = base64.b64encode(webpage_bytes).decode('ascii') |
|
|
|
self._downloader.to_screen(dump) |
|
|
|
content = webpage_bytes.decode(encoding, 'replace') |
|
|
|
return (content, urlh) |
|
|
|
|
|
|
|
def _download_webpage(self, url_or_request, video_id, note=None, errnote=None): |
|
|
|
""" Returns the data of the page as a string """ |
|
|
|
return self._download_webpage_handle(url_or_request, video_id, note, errnote)[0] |
|
|
|
|
|
|
|
def to_screen(self, msg): |
|
|
|
"""Print msg to screen, prefixing it with '[ie_name]'""" |
|
|
|
self._downloader.to_screen(u'[%s] %s' % (self.IE_NAME, msg)) |
|
|
|
|
|
|
|
def report_extraction(self, id_or_name): |
|
|
|
"""Report information extraction.""" |
|
|
|
self.to_screen(u'%s: Extracting information' % id_or_name) |
|
|
|
|
|
|
|
def report_download_webpage(self, video_id): |
|
|
|
"""Report webpage download.""" |
|
|
|
self.to_screen(u'%s: Downloading webpage' % video_id) |
|
|
|
|
|
|
|
def report_age_confirmation(self): |
|
|
|
"""Report attempt to confirm age.""" |
|
|
|
self.to_screen(u'Confirming age') |
|
|
|
|
|
|
|
#Methods for following #608 |
|
|
|
#They set the correct value of the '_type' key |
|
|
|
def video_result(self, video_info): |
|
|
|
"""Returns a video""" |
|
|
|
video_info['_type'] = 'video' |
|
|
|
return video_info |
|
|
|
def url_result(self, url, ie=None): |
|
|
|
"""Returns a url that points to a page that should be processed""" |
|
|
|
#TODO: ie should be the class used for getting the info |
|
|
|
video_info = {'_type': 'url', |
|
|
|
'url': url, |
|
|
|
'ie_key': ie} |
|
|
|
return video_info |
|
|
|
def playlist_result(self, entries, playlist_id=None, playlist_title=None): |
|
|
|
"""Returns a playlist""" |
|
|
|
video_info = {'_type': 'playlist', |
|
|
|
'entries': entries} |
|
|
|
if playlist_id: |
|
|
|
video_info['id'] = playlist_id |
|
|
|
if playlist_title: |
|
|
|
video_info['title'] = playlist_title |
|
|
|
return video_info |
|
|
|
|
|
|
|
def _search_regex(self, pattern, string, name, default=None, fatal=True, flags=0): |
|
|
|
""" |
|
|
|
Perform a regex search on the given string, using a single or a list of |
|
|
|
patterns returning the first matching group. |
|
|
|
In case of failure return a default value or raise a WARNING or a |
|
|
|
ExtractorError, depending on fatal, specifying the field name. |
|
|
|
""" |
|
|
|
if isinstance(pattern, (str, compat_str, compiled_regex_type)): |
|
|
|
mobj = re.search(pattern, string, flags) |
|
|
|
else: |
|
|
|
for p in pattern: |
|
|
|
mobj = re.search(p, string, flags) |
|
|
|
if mobj: break |
|
|
|
|
|
|
|
if sys.stderr.isatty() and os.name != 'nt': |
|
|
|
_name = u'\033[0;34m%s\033[0m' % name |
|
|
|
else: |
|
|
|
_name = name |
|
|
|
|
|
|
|
if mobj: |
|
|
|
# return the first matching group |
|
|
|
return next(g for g in mobj.groups() if g is not None) |
|
|
|
elif default is not None: |
|
|
|
return default |
|
|
|
elif fatal: |
|
|
|
raise ExtractorError(u'Unable to extract %s' % _name) |
|
|
|
else: |
|
|
|
self._downloader.report_warning(u'unable to extract %s; ' |
|
|
|
u'please report this issue on GitHub.' % _name) |
|
|
|
return None |
|
|
|
|
|
|
|
def _html_search_regex(self, pattern, string, name, default=None, fatal=True, flags=0): |
|
|
|
""" |
|
|
|
Like _search_regex, but strips HTML tags and unescapes entities. |
|
|
|
""" |
|
|
|
res = self._search_regex(pattern, string, name, default, fatal, flags) |
|
|
|
if res: |
|
|
|
return clean_html(res).strip() |
|
|
|
else: |
|
|
|
return res |
|
|
|
|
|
|
|
class SearchInfoExtractor(InfoExtractor): |
|
|
|
""" |
|
|
|
Base class for paged search queries extractors. |
|
|
|
They accept urls in the format _SEARCH_KEY(|all|[0-9]):{query} |
|
|
|
Instances should define _SEARCH_KEY and _MAX_RESULTS. |
|
|
|
""" |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def _make_valid_url(cls): |
|
|
|
return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def suitable(cls, url): |
|
|
|
return re.match(cls._make_valid_url(), url) is not None |
|
|
|
|
|
|
|
def _real_extract(self, query): |
|
|
|
mobj = re.match(self._make_valid_url(), query) |
|
|
|
if mobj is None: |
|
|
|
raise ExtractorError(u'Invalid search query "%s"' % query) |
|
|
|
|
|
|
|
prefix = mobj.group('prefix') |
|
|
|
query = mobj.group('query') |
|
|
|
if prefix == '': |
|
|
|
return self._get_n_results(query, 1) |
|
|
|
elif prefix == 'all': |
|
|
|
return self._get_n_results(query, self._MAX_RESULTS) |
|
|
|
else: |
|
|
|
n = int(prefix) |
|
|
|
if n <= 0: |
|
|
|
raise ExtractorError(u'invalid download number %s for query "%s"' % (n, query)) |
|
|
|
elif n > self._MAX_RESULTS: |
|
|
|
self._downloader.report_warning(u'%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n)) |
|
|
|
n = self._MAX_RESULTS |
|
|
|
return self._get_n_results(query, n) |
|
|
|
|
|
|
|
def _get_n_results(self, query, n): |
|
|
|
"""Get a specified number of results for a query""" |
|
|
|
raise NotImplementedError("This method must be implemented by sublclasses") |
|
|
|
from .extractor.common import InfoExtractor, SearchInfoExtractor |
|
|
|
|
|
|
|
|
|
|
|
class YoutubeIE(InfoExtractor): |
|
|
|