[extractor/common] Use NO_DEFAULT from utils
[youtube-dl] / youtube_dl / extractor / common.py
1 from __future__ import unicode_literals
2
3 import base64
4 import datetime
5 import hashlib
6 import json
7 import netrc
8 import os
9 import re
10 import socket
11 import sys
12 import time
13 import xml.etree.ElementTree
14
15 from ..compat import (
16     compat_cookiejar,
17     compat_HTTPError,
18     compat_http_client,
19     compat_urllib_error,
20     compat_urllib_parse_urlparse,
21     compat_urlparse,
22     compat_str,
23 )
24 from ..utils import (
25     NO_DEFAULT,
26     age_restricted,
27     bug_reports_message,
28     clean_html,
29     compiled_regex_type,
30     ExtractorError,
31     float_or_none,
32     int_or_none,
33     RegexNotFoundError,
34     sanitize_filename,
35     unescapeHTML,
36 )
37
38
39
40 class InfoExtractor(object):
41     """Information Extractor class.
42
43     Information extractors are the classes that, given a URL, extract
44     information about the video (or videos) the URL refers to. This
45     information includes the real video URL, the video title, author and
46     others. The information is stored in a dictionary which is then
47     passed to the YoutubeDL. The YoutubeDL processes this
48     information possibly downloading the video to the file system, among
49     other possible outcomes.
50
51     The type field determines the type of the result.
52     By far the most common value (and the default if _type is missing) is
53     "video", which indicates a single video.
54
55     For a video, the dictionaries must include the following fields:
56
57     id:             Video identifier.
58     title:          Video title, unescaped.
59
60     Additionally, it must contain either a formats entry or a url one:
61
62     formats:        A list of dictionaries for each format available, ordered
63                     from worst to best quality.
64
65                     Potential fields:
66                     * url        Mandatory. The URL of the video file
67                     * ext        Will be calculated from url if missing
68                     * format     A human-readable description of the format
69                                  ("mp4 container with h264/opus").
70                                  Calculated from the format_id, width, height.
71                                  and format_note fields if missing.
72                     * format_id  A short description of the format
73                                  ("mp4_h264_opus" or "19").
74                                 Technically optional, but strongly recommended.
75                     * format_note Additional info about the format
76                                  ("3D" or "DASH video")
77                     * width      Width of the video, if known
78                     * height     Height of the video, if known
79                     * resolution Textual description of width and height
80                     * tbr        Average bitrate of audio and video in KBit/s
81                     * abr        Average audio bitrate in KBit/s
82                     * acodec     Name of the audio codec in use
83                     * asr        Audio sampling rate in Hertz
84                     * vbr        Average video bitrate in KBit/s
85                     * fps        Frame rate
86                     * vcodec     Name of the video codec in use
87                     * container  Name of the container format
88                     * filesize   The number of bytes, if known in advance
89                     * filesize_approx  An estimate for the number of bytes
90                     * player_url SWF Player URL (used for rtmpdump).
91                     * protocol   The protocol that will be used for the actual
92                                  download, lower-case.
93                                  "http", "https", "rtsp", "rtmp", "rtmpe",
94                                  "m3u8", or "m3u8_native".
95                     * preference Order number of this format. If this field is
96                                  present and not None, the formats get sorted
97                                  by this field, regardless of all other values.
98                                  -1 for default (order by other properties),
99                                  -2 or smaller for less than default.
100                                  < -1000 to hide the format (if there is
101                                     another one which is strictly better)
102                     * language_preference  Is this in the correct requested
103                                  language?
104                                  10 if it's what the URL is about,
105                                  -1 for default (don't know),
106                                  -10 otherwise, other values reserved for now.
107                     * quality    Order number of the video quality of this
108                                  format, irrespective of the file format.
109                                  -1 for default (order by other properties),
110                                  -2 or smaller for less than default.
111                     * source_preference  Order number for this video source
112                                   (quality takes higher priority)
113                                  -1 for default (order by other properties),
114                                  -2 or smaller for less than default.
115                     * http_headers  A dictionary of additional HTTP headers
116                                  to add to the request.
117                     * stretched_ratio  If given and not 1, indicates that the
118                                  video's pixels are not square.
119                                  width : height ratio as float.
120                     * no_resume  The server does not support resuming the
121                                  (HTTP or RTMP) download. Boolean.
122
123     url:            Final video URL.
124     ext:            Video filename extension.
125     format:         The video format, defaults to ext (used for --get-format)
126     player_url:     SWF Player URL (used for rtmpdump).
127
128     The following fields are optional:
129
130     alt_title:      A secondary title of the video.
131     display_id      An alternative identifier for the video, not necessarily
132                     unique, but available before title. Typically, id is
133                     something like "4234987", title "Dancing naked mole rats",
134                     and display_id "dancing-naked-mole-rats"
135     thumbnails:     A list of dictionaries, with the following entries:
136                         * "id" (optional, string) - Thumbnail format ID
137                         * "url"
138                         * "preference" (optional, int) - quality of the image
139                         * "width" (optional, int)
140                         * "height" (optional, int)
141                         * "resolution" (optional, string "{width}x{height"},
142                                         deprecated)
143     thumbnail:      Full URL to a video thumbnail image.
144     description:    Full video description.
145     uploader:       Full name of the video uploader.
146     creator:        The main artist who created the video.
147     timestamp:      UNIX timestamp of the moment the video became available.
148     upload_date:    Video upload date (YYYYMMDD).
149                     If not explicitly set, calculated from timestamp.
150     uploader_id:    Nickname or id of the video uploader.
151     location:       Physical location where the video was filmed.
152     subtitles:      The available subtitles as a dictionary in the format
153                     {language: subformats}. "subformats" is a list sorted from
154                     lower to higher preference, each element is a dictionary
155                     with the "ext" entry and one of:
156                         * "data": The subtitles file contents
157                         * "url": A url pointing to the subtitles file
158     automatic_captions: Like 'subtitles', used by the YoutubeIE for
159                     automatically generated captions
160     duration:       Length of the video in seconds, as an integer.
161     view_count:     How many users have watched the video on the platform.
162     like_count:     Number of positive ratings of the video
163     dislike_count:  Number of negative ratings of the video
164     average_rating: Average rating give by users, the scale used depends on the webpage
165     comment_count:  Number of comments on the video
166     comments:       A list of comments, each with one or more of the following
167                     properties (all but one of text or html optional):
168                         * "author" - human-readable name of the comment author
169                         * "author_id" - user ID of the comment author
170                         * "id" - Comment ID
171                         * "html" - Comment as HTML
172                         * "text" - Plain text of the comment
173                         * "timestamp" - UNIX timestamp of comment
174                         * "parent" - ID of the comment this one is replying to.
175                                      Set to "root" to indicate that this is a
176                                      comment to the original video.
177     age_limit:      Age restriction for the video, as an integer (years)
178     webpage_url:    The url to the video webpage, if given to youtube-dl it
179                     should allow to get the same result again. (It will be set
180                     by YoutubeDL if it's missing)
181     categories:     A list of categories that the video falls in, for example
182                     ["Sports", "Berlin"]
183     is_live:        True, False, or None (=unknown). Whether this video is a
184                     live stream that goes on instead of a fixed-length video.
185
186     Unless mentioned otherwise, the fields should be Unicode strings.
187
188     Unless mentioned otherwise, None is equivalent to absence of information.
189
190
191     _type "playlist" indicates multiple videos.
192     There must be a key "entries", which is a list, an iterable, or a PagedList
193     object, each element of which is a valid dictionary by this specification.
194
195     Additionally, playlists can have "title" and "id" attributes with the same
196     semantics as videos (see above).
197
198
199     _type "multi_video" indicates that there are multiple videos that
200     form a single show, for examples multiple acts of an opera or TV episode.
201     It must have an entries key like a playlist and contain all the keys
202     required for a video at the same time.
203
204
205     _type "url" indicates that the video must be extracted from another
206     location, possibly by a different extractor. Its only required key is:
207     "url" - the next URL to extract.
208     The key "ie_key" can be set to the class name (minus the trailing "IE",
209     e.g. "Youtube") if the extractor class is known in advance.
210     Additionally, the dictionary may have any properties of the resolved entity
211     known in advance, for example "title" if the title of the referred video is
212     known ahead of time.
213
214
215     _type "url_transparent" entities have the same specification as "url", but
216     indicate that the given additional information is more precise than the one
217     associated with the resolved URL.
218     This is useful when a site employs a video service that hosts the video and
219     its technical metadata, but that video service does not embed a useful
220     title, description etc.
221
222
223     Subclasses of this one should re-define the _real_initialize() and
224     _real_extract() methods and define a _VALID_URL regexp.
225     Probably, they should also be added to the list of extractors.
226
227     Finally, the _WORKING attribute should be set to False for broken IEs
228     in order to warn the users and skip the tests.
229     """
230
231     _ready = False
232     _downloader = None
233     _WORKING = True
234
235     def __init__(self, downloader=None):
236         """Constructor. Receives an optional downloader."""
237         self._ready = False
238         self.set_downloader(downloader)
239
240     @classmethod
241     def suitable(cls, url):
242         """Receives a URL and returns True if suitable for this IE."""
243
244         # This does not use has/getattr intentionally - we want to know whether
245         # we have cached the regexp for *this* class, whereas getattr would also
246         # match the superclass
247         if '_VALID_URL_RE' not in cls.__dict__:
248             cls._VALID_URL_RE = re.compile(cls._VALID_URL)
249         return cls._VALID_URL_RE.match(url) is not None
250
251     @classmethod
252     def _match_id(cls, url):
253         if '_VALID_URL_RE' not in cls.__dict__:
254             cls._VALID_URL_RE = re.compile(cls._VALID_URL)
255         m = cls._VALID_URL_RE.match(url)
256         assert m
257         return m.group('id')
258
259     @classmethod
260     def working(cls):
261         """Getter method for _WORKING."""
262         return cls._WORKING
263
264     def initialize(self):
265         """Initializes an instance (authentication, etc)."""
266         if not self._ready:
267             self._real_initialize()
268             self._ready = True
269
270     def extract(self, url):
271         """Extracts URL information and returns it in list of dicts."""
272         try:
273             self.initialize()
274             return self._real_extract(url)
275         except ExtractorError:
276             raise
277         except compat_http_client.IncompleteRead as e:
278             raise ExtractorError('A network error has occured.', cause=e, expected=True)
279         except (KeyError, StopIteration) as e:
280             raise ExtractorError('An extractor error has occured.', cause=e)
281
282     def set_downloader(self, downloader):
283         """Sets the downloader for this IE."""
284         self._downloader = downloader
285
286     def _real_initialize(self):
287         """Real initialization process. Redefine in subclasses."""
288         pass
289
290     def _real_extract(self, url):
291         """Real extraction process. Redefine in subclasses."""
292         pass
293
294     @classmethod
295     def ie_key(cls):
296         """A string for getting the InfoExtractor with get_info_extractor"""
297         return cls.__name__[:-2]
298
299     @property
300     def IE_NAME(self):
301         return type(self).__name__[:-2]
302
303     def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True):
304         """ Returns the response handle """
305         if note is None:
306             self.report_download_webpage(video_id)
307         elif note is not False:
308             if video_id is None:
309                 self.to_screen('%s' % (note,))
310             else:
311                 self.to_screen('%s: %s' % (video_id, note))
312         try:
313             return self._downloader.urlopen(url_or_request)
314         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
315             if errnote is False:
316                 return False
317             if errnote is None:
318                 errnote = 'Unable to download webpage'
319             errmsg = '%s: %s' % (errnote, compat_str(err))
320             if fatal:
321                 raise ExtractorError(errmsg, sys.exc_info()[2], cause=err)
322             else:
323                 self._downloader.report_warning(errmsg)
324                 return False
325
326     def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True, encoding=None):
327         """ Returns a tuple (page content as string, URL handle) """
328         # Strip hashes from the URL (#1038)
329         if isinstance(url_or_request, (compat_str, str)):
330             url_or_request = url_or_request.partition('#')[0]
331
332         urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal)
333         if urlh is False:
334             assert not fatal
335             return False
336         content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal, encoding=encoding)
337         return (content, urlh)
338
339     @staticmethod
340     def _guess_encoding_from_content(content_type, webpage_bytes):
341         m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type)
342         if m:
343             encoding = m.group(1)
344         else:
345             m = re.search(br'<meta[^>]+charset=[\'"]?([^\'")]+)[ /\'">]',
346                           webpage_bytes[:1024])
347             if m:
348                 encoding = m.group(1).decode('ascii')
349             elif webpage_bytes.startswith(b'\xff\xfe'):
350                 encoding = 'utf-16'
351             else:
352                 encoding = 'utf-8'
353
354         return encoding
355
356     def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True, prefix=None, encoding=None):
357         content_type = urlh.headers.get('Content-Type', '')
358         webpage_bytes = urlh.read()
359         if prefix is not None:
360             webpage_bytes = prefix + webpage_bytes
361         if not encoding:
362             encoding = self._guess_encoding_from_content(content_type, webpage_bytes)
363         if self._downloader.params.get('dump_intermediate_pages', False):
364             try:
365                 url = url_or_request.get_full_url()
366             except AttributeError:
367                 url = url_or_request
368             self.to_screen('Dumping request to ' + url)
369             dump = base64.b64encode(webpage_bytes).decode('ascii')
370             self._downloader.to_screen(dump)
371         if self._downloader.params.get('write_pages', False):
372             try:
373                 url = url_or_request.get_full_url()
374             except AttributeError:
375                 url = url_or_request
376             basen = '%s_%s' % (video_id, url)
377             if len(basen) > 240:
378                 h = '___' + hashlib.md5(basen.encode('utf-8')).hexdigest()
379                 basen = basen[:240 - len(h)] + h
380             raw_filename = basen + '.dump'
381             filename = sanitize_filename(raw_filename, restricted=True)
382             self.to_screen('Saving request to ' + filename)
383             # Working around MAX_PATH limitation on Windows (see
384             # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
385             if os.name == 'nt':
386                 absfilepath = os.path.abspath(filename)
387                 if len(absfilepath) > 259:
388                     filename = '\\\\?\\' + absfilepath
389             with open(filename, 'wb') as outf:
390                 outf.write(webpage_bytes)
391
392         try:
393             content = webpage_bytes.decode(encoding, 'replace')
394         except LookupError:
395             content = webpage_bytes.decode('utf-8', 'replace')
396
397         if ('<title>Access to this site is blocked</title>' in content and
398                 'Websense' in content[:512]):
399             msg = 'Access to this webpage has been blocked by Websense filtering software in your network.'
400             blocked_iframe = self._html_search_regex(
401                 r'<iframe src="([^"]+)"', content,
402                 'Websense information URL', default=None)
403             if blocked_iframe:
404                 msg += ' Visit %s for more details' % blocked_iframe
405             raise ExtractorError(msg, expected=True)
406         if '<title>The URL you requested has been blocked</title>' in content[:512]:
407             msg = (
408                 'Access to this webpage has been blocked by Indian censorship. '
409                 'Use a VPN or proxy server (with --proxy) to route around it.')
410             block_msg = self._html_search_regex(
411                 r'</h1><p>(.*?)</p>',
412                 content, 'block message', default=None)
413             if block_msg:
414                 msg += ' (Message: "%s")' % block_msg.replace('\n', ' ')
415             raise ExtractorError(msg, expected=True)
416
417         return content
418
419     def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, tries=1, timeout=5, encoding=None):
420         """ Returns the data of the page as a string """
421         success = False
422         try_count = 0
423         while success is False:
424             try:
425                 res = self._download_webpage_handle(url_or_request, video_id, note, errnote, fatal, encoding=encoding)
426                 success = True
427             except compat_http_client.IncompleteRead as e:
428                 try_count += 1
429                 if try_count >= tries:
430                     raise e
431                 self._sleep(timeout, video_id)
432         if res is False:
433             return res
434         else:
435             content, _ = res
436             return content
437
438     def _download_xml(self, url_or_request, video_id,
439                       note='Downloading XML', errnote='Unable to download XML',
440                       transform_source=None, fatal=True, encoding=None):
441         """Return the xml as an xml.etree.ElementTree.Element"""
442         xml_string = self._download_webpage(
443             url_or_request, video_id, note, errnote, fatal=fatal, encoding=encoding)
444         if xml_string is False:
445             return xml_string
446         if transform_source:
447             xml_string = transform_source(xml_string)
448         return xml.etree.ElementTree.fromstring(xml_string.encode('utf-8'))
449
450     def _download_json(self, url_or_request, video_id,
451                        note='Downloading JSON metadata',
452                        errnote='Unable to download JSON metadata',
453                        transform_source=None,
454                        fatal=True, encoding=None):
455         json_string = self._download_webpage(
456             url_or_request, video_id, note, errnote, fatal=fatal,
457             encoding=encoding)
458         if (not fatal) and json_string is False:
459             return None
460         return self._parse_json(
461             json_string, video_id, transform_source=transform_source, fatal=fatal)
462
463     def _parse_json(self, json_string, video_id, transform_source=None, fatal=True):
464         if transform_source:
465             json_string = transform_source(json_string)
466         try:
467             return json.loads(json_string)
468         except ValueError as ve:
469             errmsg = '%s: Failed to parse JSON ' % video_id
470             if fatal:
471                 raise ExtractorError(errmsg, cause=ve)
472             else:
473                 self.report_warning(errmsg + str(ve))
474
475     def report_warning(self, msg, video_id=None):
476         idstr = '' if video_id is None else '%s: ' % video_id
477         self._downloader.report_warning(
478             '[%s] %s%s' % (self.IE_NAME, idstr, msg))
479
480     def to_screen(self, msg):
481         """Print msg to screen, prefixing it with '[ie_name]'"""
482         self._downloader.to_screen('[%s] %s' % (self.IE_NAME, msg))
483
484     def report_extraction(self, id_or_name):
485         """Report information extraction."""
486         self.to_screen('%s: Extracting information' % id_or_name)
487
488     def report_download_webpage(self, video_id):
489         """Report webpage download."""
490         self.to_screen('%s: Downloading webpage' % video_id)
491
492     def report_age_confirmation(self):
493         """Report attempt to confirm age."""
494         self.to_screen('Confirming age')
495
496     def report_login(self):
497         """Report attempt to log in."""
498         self.to_screen('Logging in')
499
500     # Methods for following #608
501     @staticmethod
502     def url_result(url, ie=None, video_id=None, video_title=None):
503         """Returns a url that points to a page that should be processed"""
504         # TODO: ie should be the class used for getting the info
505         video_info = {'_type': 'url',
506                       'url': url,
507                       'ie_key': ie}
508         if video_id is not None:
509             video_info['id'] = video_id
510         if video_title is not None:
511             video_info['title'] = video_title
512         return video_info
513
514     @staticmethod
515     def playlist_result(entries, playlist_id=None, playlist_title=None, playlist_description=None):
516         """Returns a playlist"""
517         video_info = {'_type': 'playlist',
518                       'entries': entries}
519         if playlist_id:
520             video_info['id'] = playlist_id
521         if playlist_title:
522             video_info['title'] = playlist_title
523         if playlist_description:
524             video_info['description'] = playlist_description
525         return video_info
526
527     def _search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
528         """
529         Perform a regex search on the given string, using a single or a list of
530         patterns returning the first matching group.
531         In case of failure return a default value or raise a WARNING or a
532         RegexNotFoundError, depending on fatal, specifying the field name.
533         """
534         if isinstance(pattern, (str, compat_str, compiled_regex_type)):
535             mobj = re.search(pattern, string, flags)
536         else:
537             for p in pattern:
538                 mobj = re.search(p, string, flags)
539                 if mobj:
540                     break
541
542         if not self._downloader.params.get('no_color') and os.name != 'nt' and sys.stderr.isatty():
543             _name = '\033[0;34m%s\033[0m' % name
544         else:
545             _name = name
546
547         if mobj:
548             if group is None:
549                 # return the first matching group
550                 return next(g for g in mobj.groups() if g is not None)
551             else:
552                 return mobj.group(group)
553         elif default is not NO_DEFAULT:
554             return default
555         elif fatal:
556             raise RegexNotFoundError('Unable to extract %s' % _name)
557         else:
558             self._downloader.report_warning('unable to extract %s' % _name + bug_reports_message())
559             return None
560
561     def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
562         """
563         Like _search_regex, but strips HTML tags and unescapes entities.
564         """
565         res = self._search_regex(pattern, string, name, default, fatal, flags, group)
566         if res:
567             return clean_html(res).strip()
568         else:
569             return res
570
571     def _get_login_info(self):
572         """
573         Get the login info as (username, password)
574         It will look in the netrc file using the _NETRC_MACHINE value
575         If there's no info available, return (None, None)
576         """
577         if self._downloader is None:
578             return (None, None)
579
580         username = None
581         password = None
582         downloader_params = self._downloader.params
583
584         # Attempt to use provided username and password or .netrc data
585         if downloader_params.get('username', None) is not None:
586             username = downloader_params['username']
587             password = downloader_params['password']
588         elif downloader_params.get('usenetrc', False):
589             try:
590                 info = netrc.netrc().authenticators(self._NETRC_MACHINE)
591                 if info is not None:
592                     username = info[0]
593                     password = info[2]
594                 else:
595                     raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
596             except (IOError, netrc.NetrcParseError) as err:
597                 self._downloader.report_warning('parsing .netrc: %s' % compat_str(err))
598
599         return (username, password)
600
601     def _get_tfa_info(self):
602         """
603         Get the two-factor authentication info
604         TODO - asking the user will be required for sms/phone verify
605         currently just uses the command line option
606         If there's no info available, return None
607         """
608         if self._downloader is None:
609             return None
610         downloader_params = self._downloader.params
611
612         if downloader_params.get('twofactor', None) is not None:
613             return downloader_params['twofactor']
614
615         return None
616
617     # Helper functions for extracting OpenGraph info
618     @staticmethod
619     def _og_regexes(prop):
620         content_re = r'content=(?:"([^>]+?)"|\'([^>]+?)\')'
621         property_re = r'(?:name|property)=[\'"]og:%s[\'"]' % re.escape(prop)
622         template = r'<meta[^>]+?%s[^>]+?%s'
623         return [
624             template % (property_re, content_re),
625             template % (content_re, property_re),
626         ]
627
628     def _og_search_property(self, prop, html, name=None, **kargs):
629         if name is None:
630             name = 'OpenGraph %s' % prop
631         escaped = self._search_regex(self._og_regexes(prop), html, name, flags=re.DOTALL, **kargs)
632         if escaped is None:
633             return None
634         return unescapeHTML(escaped)
635
636     def _og_search_thumbnail(self, html, **kargs):
637         return self._og_search_property('image', html, 'thumbnail url', fatal=False, **kargs)
638
639     def _og_search_description(self, html, **kargs):
640         return self._og_search_property('description', html, fatal=False, **kargs)
641
642     def _og_search_title(self, html, **kargs):
643         return self._og_search_property('title', html, **kargs)
644
645     def _og_search_video_url(self, html, name='video url', secure=True, **kargs):
646         regexes = self._og_regexes('video') + self._og_regexes('video:url')
647         if secure:
648             regexes = self._og_regexes('video:secure_url') + regexes
649         return self._html_search_regex(regexes, html, name, **kargs)
650
651     def _og_search_url(self, html, **kargs):
652         return self._og_search_property('url', html, **kargs)
653
654     def _html_search_meta(self, name, html, display_name=None, fatal=False, **kwargs):
655         if display_name is None:
656             display_name = name
657         return self._html_search_regex(
658             r'''(?isx)<meta
659                     (?=[^>]+(?:itemprop|name|property)=(["\']?)%s\1)
660                     [^>]+?content=(["\'])(?P<content>.*?)\2''' % re.escape(name),
661             html, display_name, fatal=fatal, group='content', **kwargs)
662
663     def _dc_search_uploader(self, html):
664         return self._html_search_meta('dc.creator', html, 'uploader')
665
666     def _rta_search(self, html):
667         # See http://www.rtalabel.org/index.php?content=howtofaq#single
668         if re.search(r'(?ix)<meta\s+name="rating"\s+'
669                      r'     content="RTA-5042-1996-1400-1577-RTA"',
670                      html):
671             return 18
672         return 0
673
674     def _media_rating_search(self, html):
675         # See http://www.tjg-designs.com/WP/metadata-code-examples-adding-metadata-to-your-web-pages/
676         rating = self._html_search_meta('rating', html)
677
678         if not rating:
679             return None
680
681         RATING_TABLE = {
682             'safe for kids': 0,
683             'general': 8,
684             '14 years': 14,
685             'mature': 17,
686             'restricted': 19,
687         }
688         return RATING_TABLE.get(rating.lower(), None)
689
690     def _family_friendly_search(self, html):
691         # See http://schema.org/VideoObject
692         family_friendly = self._html_search_meta('isFamilyFriendly', html)
693
694         if not family_friendly:
695             return None
696
697         RATING_TABLE = {
698             '1': 0,
699             'true': 0,
700             '0': 18,
701             'false': 18,
702         }
703         return RATING_TABLE.get(family_friendly.lower(), None)
704
705     def _twitter_search_player(self, html):
706         return self._html_search_meta('twitter:player', html,
707                                       'twitter card player')
708
709     def _sort_formats(self, formats, field_preference=None):
710         if not formats:
711             raise ExtractorError('No video formats found')
712
713         def _formats_key(f):
714             # TODO remove the following workaround
715             from ..utils import determine_ext
716             if not f.get('ext') and 'url' in f:
717                 f['ext'] = determine_ext(f['url'])
718
719             if isinstance(field_preference, (list, tuple)):
720                 return tuple(f.get(field) if f.get(field) is not None else -1 for field in field_preference)
721
722             preference = f.get('preference')
723             if preference is None:
724                 proto = f.get('protocol')
725                 if proto is None:
726                     proto = compat_urllib_parse_urlparse(f.get('url', '')).scheme
727
728                 preference = 0 if proto in ['http', 'https'] else -0.1
729                 if f.get('ext') in ['f4f', 'f4m']:  # Not yet supported
730                     preference -= 0.5
731
732             if f.get('vcodec') == 'none':  # audio only
733                 if self._downloader.params.get('prefer_free_formats'):
734                     ORDER = ['aac', 'mp3', 'm4a', 'webm', 'ogg', 'opus']
735                 else:
736                     ORDER = ['webm', 'opus', 'ogg', 'mp3', 'aac', 'm4a']
737                 ext_preference = 0
738                 try:
739                     audio_ext_preference = ORDER.index(f['ext'])
740                 except ValueError:
741                     audio_ext_preference = -1
742             else:
743                 if self._downloader.params.get('prefer_free_formats'):
744                     ORDER = ['flv', 'mp4', 'webm']
745                 else:
746                     ORDER = ['webm', 'flv', 'mp4']
747                 try:
748                     ext_preference = ORDER.index(f['ext'])
749                 except ValueError:
750                     ext_preference = -1
751                 audio_ext_preference = 0
752
753             return (
754                 preference,
755                 f.get('language_preference') if f.get('language_preference') is not None else -1,
756                 f.get('quality') if f.get('quality') is not None else -1,
757                 f.get('tbr') if f.get('tbr') is not None else -1,
758                 f.get('filesize') if f.get('filesize') is not None else -1,
759                 f.get('vbr') if f.get('vbr') is not None else -1,
760                 f.get('height') if f.get('height') is not None else -1,
761                 f.get('width') if f.get('width') is not None else -1,
762                 ext_preference,
763                 f.get('abr') if f.get('abr') is not None else -1,
764                 audio_ext_preference,
765                 f.get('fps') if f.get('fps') is not None else -1,
766                 f.get('filesize_approx') if f.get('filesize_approx') is not None else -1,
767                 f.get('source_preference') if f.get('source_preference') is not None else -1,
768                 f.get('format_id') if f.get('format_id') is not None else '',
769             )
770         formats.sort(key=_formats_key)
771
772     def _check_formats(self, formats, video_id):
773         if formats:
774             formats[:] = filter(
775                 lambda f: self._is_valid_url(
776                     f['url'], video_id,
777                     item='%s video format' % f.get('format_id') if f.get('format_id') else 'video'),
778                 formats)
779
780     def _is_valid_url(self, url, video_id, item='video'):
781         url = self._proto_relative_url(url, scheme='http:')
782         # For now assume non HTTP(S) URLs always valid
783         if not (url.startswith('http://') or url.startswith('https://')):
784             return True
785         try:
786             self._request_webpage(url, video_id, 'Checking %s URL' % item)
787             return True
788         except ExtractorError as e:
789             if isinstance(e.cause, compat_HTTPError):
790                 self.to_screen(
791                     '%s: %s URL is invalid, skipping' % (video_id, item))
792                 return False
793             raise
794
795     def http_scheme(self):
796         """ Either "http:" or "https:", depending on the user's preferences """
797         return (
798             'http:'
799             if self._downloader.params.get('prefer_insecure', False)
800             else 'https:')
801
802     def _proto_relative_url(self, url, scheme=None):
803         if url is None:
804             return url
805         if url.startswith('//'):
806             if scheme is None:
807                 scheme = self.http_scheme()
808             return scheme + url
809         else:
810             return url
811
812     def _sleep(self, timeout, video_id, msg_template=None):
813         if msg_template is None:
814             msg_template = '%(video_id)s: Waiting for %(timeout)s seconds'
815         msg = msg_template % {'video_id': video_id, 'timeout': timeout}
816         self.to_screen(msg)
817         time.sleep(timeout)
818
819     def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None):
820         manifest = self._download_xml(
821             manifest_url, video_id, 'Downloading f4m manifest',
822             'Unable to download f4m manifest')
823
824         formats = []
825         manifest_version = '1.0'
826         media_nodes = manifest.findall('{http://ns.adobe.com/f4m/1.0}media')
827         if not media_nodes:
828             manifest_version = '2.0'
829             media_nodes = manifest.findall('{http://ns.adobe.com/f4m/2.0}media')
830         for i, media_el in enumerate(media_nodes):
831             if manifest_version == '2.0':
832                 manifest_url = ('/'.join(manifest_url.split('/')[:-1]) + '/' +
833                                 (media_el.attrib.get('href') or media_el.attrib.get('url')))
834             tbr = int_or_none(media_el.attrib.get('bitrate'))
835             formats.append({
836                 'format_id': '-'.join(filter(None, [f4m_id, compat_str(i if tbr is None else tbr)])),
837                 'url': manifest_url,
838                 'ext': 'flv',
839                 'tbr': tbr,
840                 'width': int_or_none(media_el.attrib.get('width')),
841                 'height': int_or_none(media_el.attrib.get('height')),
842                 'preference': preference,
843             })
844         self._sort_formats(formats)
845
846         return formats
847
848     def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
849                               entry_protocol='m3u8', preference=None,
850                               m3u8_id=None, note=None, errnote=None):
851
852         formats = [{
853             'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
854             'url': m3u8_url,
855             'ext': ext,
856             'protocol': 'm3u8',
857             'preference': preference - 1 if preference else -1,
858             'resolution': 'multiple',
859             'format_note': 'Quality selection URL',
860         }]
861
862         format_url = lambda u: (
863             u
864             if re.match(r'^https?://', u)
865             else compat_urlparse.urljoin(m3u8_url, u))
866
867         m3u8_doc = self._download_webpage(
868             m3u8_url, video_id,
869             note=note or 'Downloading m3u8 information',
870             errnote=errnote or 'Failed to download m3u8 information')
871         last_info = None
872         last_media = None
873         kv_rex = re.compile(
874             r'(?P<key>[a-zA-Z_-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)')
875         for line in m3u8_doc.splitlines():
876             if line.startswith('#EXT-X-STREAM-INF:'):
877                 last_info = {}
878                 for m in kv_rex.finditer(line):
879                     v = m.group('val')
880                     if v.startswith('"'):
881                         v = v[1:-1]
882                     last_info[m.group('key')] = v
883             elif line.startswith('#EXT-X-MEDIA:'):
884                 last_media = {}
885                 for m in kv_rex.finditer(line):
886                     v = m.group('val')
887                     if v.startswith('"'):
888                         v = v[1:-1]
889                     last_media[m.group('key')] = v
890             elif line.startswith('#') or not line.strip():
891                 continue
892             else:
893                 if last_info is None:
894                     formats.append({'url': format_url(line)})
895                     continue
896                 tbr = int_or_none(last_info.get('BANDWIDTH'), scale=1000)
897                 format_id = []
898                 if m3u8_id:
899                     format_id.append(m3u8_id)
900                 last_media_name = last_media.get('NAME') if last_media and last_media.get('TYPE') != 'SUBTITLES' else None
901                 format_id.append(last_media_name if last_media_name else '%d' % (tbr if tbr else len(formats)))
902                 f = {
903                     'format_id': '-'.join(format_id),
904                     'url': format_url(line.strip()),
905                     'tbr': tbr,
906                     'ext': ext,
907                     'protocol': entry_protocol,
908                     'preference': preference,
909                 }
910                 codecs = last_info.get('CODECS')
911                 if codecs:
912                     # TODO: looks like video codec is not always necessarily goes first
913                     va_codecs = codecs.split(',')
914                     if va_codecs[0]:
915                         f['vcodec'] = va_codecs[0].partition('.')[0]
916                     if len(va_codecs) > 1 and va_codecs[1]:
917                         f['acodec'] = va_codecs[1].partition('.')[0]
918                 resolution = last_info.get('RESOLUTION')
919                 if resolution:
920                     width_str, height_str = resolution.split('x')
921                     f['width'] = int(width_str)
922                     f['height'] = int(height_str)
923                 if last_media is not None:
924                     f['m3u8_media'] = last_media
925                     last_media = None
926                 formats.append(f)
927                 last_info = {}
928         self._sort_formats(formats)
929         return formats
930
931     # TODO: improve extraction
932     def _extract_smil_formats(self, smil_url, video_id, fatal=True):
933         smil = self._download_xml(
934             smil_url, video_id, 'Downloading SMIL file',
935             'Unable to download SMIL file', fatal=fatal)
936         if smil is False:
937             assert not fatal
938             return []
939
940         base = smil.find('./head/meta').get('base')
941
942         formats = []
943         rtmp_count = 0
944         if smil.findall('./body/seq/video'):
945             video = smil.findall('./body/seq/video')[0]
946             fmts, rtmp_count = self._parse_smil_video(video, video_id, base, rtmp_count)
947             formats.extend(fmts)
948         else:
949             for video in smil.findall('./body/switch/video'):
950                 fmts, rtmp_count = self._parse_smil_video(video, video_id, base, rtmp_count)
951                 formats.extend(fmts)
952
953         self._sort_formats(formats)
954
955         return formats
956
957     def _parse_smil_video(self, video, video_id, base, rtmp_count):
958         src = video.get('src')
959         if not src:
960             return ([], rtmp_count)
961         bitrate = int_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
962         width = int_or_none(video.get('width'))
963         height = int_or_none(video.get('height'))
964         proto = video.get('proto')
965         if not proto:
966             if base:
967                 if base.startswith('rtmp'):
968                     proto = 'rtmp'
969                 elif base.startswith('http'):
970                     proto = 'http'
971         ext = video.get('ext')
972         if proto == 'm3u8':
973             return (self._extract_m3u8_formats(src, video_id, ext), rtmp_count)
974         elif proto == 'rtmp':
975             rtmp_count += 1
976             streamer = video.get('streamer') or base
977             return ([{
978                 'url': streamer,
979                 'play_path': src,
980                 'ext': 'flv',
981                 'format_id': 'rtmp-%d' % (rtmp_count if bitrate is None else bitrate),
982                 'tbr': bitrate,
983                 'width': width,
984                 'height': height,
985             }], rtmp_count)
986         elif proto.startswith('http'):
987             return ([{
988                 'url': base + src,
989                 'ext': ext or 'flv',
990                 'tbr': bitrate,
991                 'width': width,
992                 'height': height,
993             }], rtmp_count)
994
995     def _live_title(self, name):
996         """ Generate the title for a live video """
997         now = datetime.datetime.now()
998         now_str = now.strftime("%Y-%m-%d %H:%M")
999         return name + ' ' + now_str
1000
1001     def _int(self, v, name, fatal=False, **kwargs):
1002         res = int_or_none(v, **kwargs)
1003         if 'get_attr' in kwargs:
1004             print(getattr(v, kwargs['get_attr']))
1005         if res is None:
1006             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
1007             if fatal:
1008                 raise ExtractorError(msg)
1009             else:
1010                 self._downloader.report_warning(msg)
1011         return res
1012
1013     def _float(self, v, name, fatal=False, **kwargs):
1014         res = float_or_none(v, **kwargs)
1015         if res is None:
1016             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
1017             if fatal:
1018                 raise ExtractorError(msg)
1019             else:
1020                 self._downloader.report_warning(msg)
1021         return res
1022
1023     def _set_cookie(self, domain, name, value, expire_time=None):
1024         cookie = compat_cookiejar.Cookie(
1025             0, name, value, None, None, domain, None,
1026             None, '/', True, False, expire_time, '', None, None, None)
1027         self._downloader.cookiejar.set_cookie(cookie)
1028
1029     def get_testcases(self, include_onlymatching=False):
1030         t = getattr(self, '_TEST', None)
1031         if t:
1032             assert not hasattr(self, '_TESTS'), \
1033                 '%s has _TEST and _TESTS' % type(self).__name__
1034             tests = [t]
1035         else:
1036             tests = getattr(self, '_TESTS', [])
1037         for t in tests:
1038             if not include_onlymatching and t.get('only_matching', False):
1039                 continue
1040             t['name'] = type(self).__name__[:-len('IE')]
1041             yield t
1042
1043     def is_suitable(self, age_limit):
1044         """ Test whether the extractor is generally suitable for the given
1045         age limit (i.e. pornographic sites are not, all others usually are) """
1046
1047         any_restricted = False
1048         for tc in self.get_testcases(include_onlymatching=False):
1049             if 'playlist' in tc:
1050                 tc = tc['playlist'][0]
1051             is_restricted = age_restricted(
1052                 tc.get('info_dict', {}).get('age_limit'), age_limit)
1053             if not is_restricted:
1054                 return True
1055             any_restricted = any_restricted or is_restricted
1056         return not any_restricted
1057
1058     def extract_subtitles(self, *args, **kwargs):
1059         if (self._downloader.params.get('writesubtitles', False) or
1060                 self._downloader.params.get('listsubtitles')):
1061             return self._get_subtitles(*args, **kwargs)
1062         return {}
1063
1064     def _get_subtitles(self, *args, **kwargs):
1065         raise NotImplementedError("This method must be implemented by subclasses")
1066
1067     def extract_automatic_captions(self, *args, **kwargs):
1068         if (self._downloader.params.get('writeautomaticsub', False) or
1069                 self._downloader.params.get('listsubtitles')):
1070             return self._get_automatic_captions(*args, **kwargs)
1071         return {}
1072
1073     def _get_automatic_captions(self, *args, **kwargs):
1074         raise NotImplementedError("This method must be implemented by subclasses")
1075
1076
1077 class SearchInfoExtractor(InfoExtractor):
1078     """
1079     Base class for paged search queries extractors.
1080     They accept urls in the format _SEARCH_KEY(|all|[0-9]):{query}
1081     Instances should define _SEARCH_KEY and _MAX_RESULTS.
1082     """
1083
1084     @classmethod
1085     def _make_valid_url(cls):
1086         return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
1087
1088     @classmethod
1089     def suitable(cls, url):
1090         return re.match(cls._make_valid_url(), url) is not None
1091
1092     def _real_extract(self, query):
1093         mobj = re.match(self._make_valid_url(), query)
1094         if mobj is None:
1095             raise ExtractorError('Invalid search query "%s"' % query)
1096
1097         prefix = mobj.group('prefix')
1098         query = mobj.group('query')
1099         if prefix == '':
1100             return self._get_n_results(query, 1)
1101         elif prefix == 'all':
1102             return self._get_n_results(query, self._MAX_RESULTS)
1103         else:
1104             n = int(prefix)
1105             if n <= 0:
1106                 raise ExtractorError('invalid download number %s for query "%s"' % (n, query))
1107             elif n > self._MAX_RESULTS:
1108                 self._downloader.report_warning('%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n))
1109                 n = self._MAX_RESULTS
1110             return self._get_n_results(query, n)
1111
1112     def _get_n_results(self, query, n):
1113         """Get a specified number of results for a query"""
1114         raise NotImplementedError("This method must be implemented by subclasses")
1115
1116     @property
1117     def SEARCH_KEY(self):
1118         return self._SEARCH_KEY