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