[escapist] Filter video differently (Fixes #4919)
[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     HEADRequest,
31     int_or_none,
32     RegexNotFoundError,
33     sanitize_filename,
34     unescapeHTML,
35 )
36 _NO_DEFAULT = object()
37
38
39 class InfoExtractor(object):
40     """Information Extractor class.
41
42     Information extractors are the classes that, given a URL, extract
43     information about the video (or videos) the URL refers to. This
44     information includes the real video URL, the video title, author and
45     others. The information is stored in a dictionary which is then
46     passed to the YoutubeDL. The YoutubeDL processes this
47     information possibly downloading the video to the file system, among
48     other possible outcomes.
49
50     The type field determines the the type of the result.
51     By far the most common value (and the default if _type is missing) is
52     "video", which indicates a single video.
53
54     For a video, the dictionaries must include the following fields:
55
56     id:             Video identifier.
57     title:          Video title, unescaped.
58
59     Additionally, it must contain either a formats entry or a url one:
60
61     formats:        A list of dictionaries for each format available, ordered
62                     from worst to best quality.
63
64                     Potential fields:
65                     * url        Mandatory. The URL of the video file
66                     * ext        Will be calculated from url if missing
67                     * format     A human-readable description of the format
68                                  ("mp4 container with h264/opus").
69                                  Calculated from the format_id, width, height.
70                                  and format_note fields if missing.
71                     * format_id  A short description of the format
72                                  ("mp4_h264_opus" or "19").
73                                 Technically optional, but strongly recommended.
74                     * format_note Additional info about the format
75                                  ("3D" or "DASH video")
76                     * width      Width of the video, if known
77                     * height     Height of the video, if known
78                     * resolution Textual description of width and height
79                     * tbr        Average bitrate of audio and video in KBit/s
80                     * abr        Average audio bitrate in KBit/s
81                     * acodec     Name of the audio codec in use
82                     * asr        Audio sampling rate in Hertz
83                     * vbr        Average video bitrate in KBit/s
84                     * fps        Frame rate
85                     * vcodec     Name of the video codec in use
86                     * container  Name of the container format
87                     * filesize   The number of bytes, if known in advance
88                     * filesize_approx  An estimate for the number of bytes
89                     * player_url SWF Player URL (used for rtmpdump).
90                     * protocol   The protocol that will be used for the actual
91                                  download, lower-case.
92                                  "http", "https", "rtsp", "rtmp", "rtmpe",
93                                  "m3u8", or "m3u8_native".
94                     * preference Order number of this format. If this field is
95                                  present and not None, the formats get sorted
96                                  by this field, regardless of all other values.
97                                  -1 for default (order by other properties),
98                                  -2 or smaller for less than default.
99                                  < -1000 to hide the format (if there is
100                                     another one which is strictly better)
101                     * language_preference  Is this in the correct requested
102                                  language?
103                                  10 if it's what the URL is about,
104                                  -1 for default (don't know),
105                                  -10 otherwise, other values reserved for now.
106                     * quality    Order number of the video quality of this
107                                  format, irrespective of the file format.
108                                  -1 for default (order by other properties),
109                                  -2 or smaller for less than default.
110                     * source_preference  Order number for this video source
111                                   (quality takes higher priority)
112                                  -1 for default (order by other properties),
113                                  -2 or smaller for less than default.
114                     * http_method  HTTP method to use for the download.
115                     * http_headers  A dictionary of additional HTTP headers
116                                  to add to the request.
117                     * http_post_data  Additional data to send with a POST
118                                  request.
119                     * stretched_ratio  If given and not 1, indicates that the
120                                  video's pixels are not square.
121                                  width : height ratio as float.
122                     * no_resume  The server does not support resuming the
123                                  (HTTP or RTMP) download. Boolean.
124
125     url:            Final video URL.
126     ext:            Video filename extension.
127     format:         The video format, defaults to ext (used for --get-format)
128     player_url:     SWF Player URL (used for rtmpdump).
129
130     The following fields are optional:
131
132     alt_title:      A secondary title of the video.
133     display_id      An alternative identifier for the video, not necessarily
134                     unique, but available before title. Typically, id is
135                     something like "4234987", title "Dancing naked mole rats",
136                     and display_id "dancing-naked-mole-rats"
137     thumbnails:     A list of dictionaries, with the following entries:
138                         * "id" (optional, string) - Thumbnail format ID
139                         * "url"
140                         * "preference" (optional, int) - quality of the image
141                         * "width" (optional, int)
142                         * "height" (optional, int)
143                         * "resolution" (optional, string "{width}x{height"},
144                                         deprecated)
145     thumbnail:      Full URL to a video thumbnail image.
146     description:    Full video description.
147     uploader:       Full name of the video uploader.
148     creator:        The main artist who created the video.
149     timestamp:      UNIX timestamp of the moment the video became available.
150     upload_date:    Video upload date (YYYYMMDD).
151                     If not explicitly set, calculated from timestamp.
152     uploader_id:    Nickname or id of the video uploader.
153     location:       Physical location where the video was filmed.
154     subtitles:      The subtitle file contents as a dictionary in the format
155                     {language: subtitles}.
156     duration:       Length of the video in seconds, as an integer.
157     view_count:     How many users have watched the video on the platform.
158     like_count:     Number of positive ratings of the video
159     dislike_count:  Number of negative ratings of the video
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
395         return content
396
397     def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, tries=1, timeout=5):
398         """ Returns the data of the page as a string """
399         success = False
400         try_count = 0
401         while success is False:
402             try:
403                 res = self._download_webpage_handle(url_or_request, video_id, note, errnote, fatal)
404                 success = True
405             except compat_http_client.IncompleteRead as e:
406                 try_count += 1
407                 if try_count >= tries:
408                     raise e
409                 self._sleep(timeout, video_id)
410         if res is False:
411             return res
412         else:
413             content, _ = res
414             return content
415
416     def _download_xml(self, url_or_request, video_id,
417                       note='Downloading XML', errnote='Unable to download XML',
418                       transform_source=None, fatal=True):
419         """Return the xml as an xml.etree.ElementTree.Element"""
420         xml_string = self._download_webpage(
421             url_or_request, video_id, note, errnote, fatal=fatal)
422         if xml_string is False:
423             return xml_string
424         if transform_source:
425             xml_string = transform_source(xml_string)
426         return xml.etree.ElementTree.fromstring(xml_string.encode('utf-8'))
427
428     def _download_json(self, url_or_request, video_id,
429                        note='Downloading JSON metadata',
430                        errnote='Unable to download JSON metadata',
431                        transform_source=None,
432                        fatal=True):
433         json_string = self._download_webpage(
434             url_or_request, video_id, note, errnote, fatal=fatal)
435         if (not fatal) and json_string is False:
436             return None
437         return self._parse_json(
438             json_string, video_id, transform_source=transform_source, fatal=fatal)
439
440     def _parse_json(self, json_string, video_id, transform_source=None, fatal=True):
441         if transform_source:
442             json_string = transform_source(json_string)
443         try:
444             return json.loads(json_string)
445         except ValueError as ve:
446             errmsg = '%s: Failed to parse JSON ' % video_id
447             if fatal:
448                 raise ExtractorError(errmsg, cause=ve)
449             else:
450                 self.report_warning(errmsg + str(ve))
451
452     def report_warning(self, msg, video_id=None):
453         idstr = '' if video_id is None else '%s: ' % video_id
454         self._downloader.report_warning(
455             '[%s] %s%s' % (self.IE_NAME, idstr, msg))
456
457     def to_screen(self, msg):
458         """Print msg to screen, prefixing it with '[ie_name]'"""
459         self._downloader.to_screen('[%s] %s' % (self.IE_NAME, msg))
460
461     def report_extraction(self, id_or_name):
462         """Report information extraction."""
463         self.to_screen('%s: Extracting information' % id_or_name)
464
465     def report_download_webpage(self, video_id):
466         """Report webpage download."""
467         self.to_screen('%s: Downloading webpage' % video_id)
468
469     def report_age_confirmation(self):
470         """Report attempt to confirm age."""
471         self.to_screen('Confirming age')
472
473     def report_login(self):
474         """Report attempt to log in."""
475         self.to_screen('Logging in')
476
477     # Methods for following #608
478     @staticmethod
479     def url_result(url, ie=None, video_id=None):
480         """Returns a url that points to a page that should be processed"""
481         # TODO: ie should be the class used for getting the info
482         video_info = {'_type': 'url',
483                       'url': url,
484                       'ie_key': ie}
485         if video_id is not None:
486             video_info['id'] = video_id
487         return video_info
488
489     @staticmethod
490     def playlist_result(entries, playlist_id=None, playlist_title=None, playlist_description=None):
491         """Returns a playlist"""
492         video_info = {'_type': 'playlist',
493                       'entries': entries}
494         if playlist_id:
495             video_info['id'] = playlist_id
496         if playlist_title:
497             video_info['title'] = playlist_title
498         if playlist_description:
499             video_info['description'] = playlist_description
500         return video_info
501
502     def _search_regex(self, pattern, string, name, default=_NO_DEFAULT, fatal=True, flags=0, group=None):
503         """
504         Perform a regex search on the given string, using a single or a list of
505         patterns returning the first matching group.
506         In case of failure return a default value or raise a WARNING or a
507         RegexNotFoundError, depending on fatal, specifying the field name.
508         """
509         if isinstance(pattern, (str, compat_str, compiled_regex_type)):
510             mobj = re.search(pattern, string, flags)
511         else:
512             for p in pattern:
513                 mobj = re.search(p, string, flags)
514                 if mobj:
515                     break
516
517         if not self._downloader.params.get('no_color') and os.name != 'nt' and sys.stderr.isatty():
518             _name = '\033[0;34m%s\033[0m' % name
519         else:
520             _name = name
521
522         if mobj:
523             if group is None:
524                 # return the first matching group
525                 return next(g for g in mobj.groups() if g is not None)
526             else:
527                 return mobj.group(group)
528         elif default is not _NO_DEFAULT:
529             return default
530         elif fatal:
531             raise RegexNotFoundError('Unable to extract %s' % _name)
532         else:
533             self._downloader.report_warning('unable to extract %s; '
534                                             'please report this issue on http://yt-dl.org/bug' % _name)
535             return None
536
537     def _html_search_regex(self, pattern, string, name, default=_NO_DEFAULT, fatal=True, flags=0, group=None):
538         """
539         Like _search_regex, but strips HTML tags and unescapes entities.
540         """
541         res = self._search_regex(pattern, string, name, default, fatal, flags, group)
542         if res:
543             return clean_html(res).strip()
544         else:
545             return res
546
547     def _get_login_info(self):
548         """
549         Get the the login info as (username, password)
550         It will look in the netrc file using the _NETRC_MACHINE value
551         If there's no info available, return (None, None)
552         """
553         if self._downloader is None:
554             return (None, None)
555
556         username = None
557         password = None
558         downloader_params = self._downloader.params
559
560         # Attempt to use provided username and password or .netrc data
561         if downloader_params.get('username', None) is not None:
562             username = downloader_params['username']
563             password = downloader_params['password']
564         elif downloader_params.get('usenetrc', False):
565             try:
566                 info = netrc.netrc().authenticators(self._NETRC_MACHINE)
567                 if info is not None:
568                     username = info[0]
569                     password = info[2]
570                 else:
571                     raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
572             except (IOError, netrc.NetrcParseError) as err:
573                 self._downloader.report_warning('parsing .netrc: %s' % compat_str(err))
574
575         return (username, password)
576
577     def _get_tfa_info(self):
578         """
579         Get the two-factor authentication info
580         TODO - asking the user will be required for sms/phone verify
581         currently just uses the command line option
582         If there's no info available, return None
583         """
584         if self._downloader is None:
585             return None
586         downloader_params = self._downloader.params
587
588         if downloader_params.get('twofactor', None) is not None:
589             return downloader_params['twofactor']
590
591         return None
592
593     # Helper functions for extracting OpenGraph info
594     @staticmethod
595     def _og_regexes(prop):
596         content_re = r'content=(?:"([^>]+?)"|\'([^>]+?)\')'
597         property_re = r'(?:name|property)=[\'"]og:%s[\'"]' % re.escape(prop)
598         template = r'<meta[^>]+?%s[^>]+?%s'
599         return [
600             template % (property_re, content_re),
601             template % (content_re, property_re),
602         ]
603
604     def _og_search_property(self, prop, html, name=None, **kargs):
605         if name is None:
606             name = 'OpenGraph %s' % prop
607         escaped = self._search_regex(self._og_regexes(prop), html, name, flags=re.DOTALL, **kargs)
608         if escaped is None:
609             return None
610         return unescapeHTML(escaped)
611
612     def _og_search_thumbnail(self, html, **kargs):
613         return self._og_search_property('image', html, 'thumbnail url', fatal=False, **kargs)
614
615     def _og_search_description(self, html, **kargs):
616         return self._og_search_property('description', html, fatal=False, **kargs)
617
618     def _og_search_title(self, html, **kargs):
619         return self._og_search_property('title', html, **kargs)
620
621     def _og_search_video_url(self, html, name='video url', secure=True, **kargs):
622         regexes = self._og_regexes('video') + self._og_regexes('video:url')
623         if secure:
624             regexes = self._og_regexes('video:secure_url') + regexes
625         return self._html_search_regex(regexes, html, name, **kargs)
626
627     def _og_search_url(self, html, **kargs):
628         return self._og_search_property('url', html, **kargs)
629
630     def _html_search_meta(self, name, html, display_name=None, fatal=False, **kwargs):
631         if display_name is None:
632             display_name = name
633         return self._html_search_regex(
634             r'''(?isx)<meta
635                     (?=[^>]+(?:itemprop|name|property)=(["\']?)%s\1)
636                     [^>]+?content=(["\'])(?P<content>.*?)\2''' % re.escape(name),
637             html, display_name, fatal=fatal, group='content', **kwargs)
638
639     def _dc_search_uploader(self, html):
640         return self._html_search_meta('dc.creator', html, 'uploader')
641
642     def _rta_search(self, html):
643         # See http://www.rtalabel.org/index.php?content=howtofaq#single
644         if re.search(r'(?ix)<meta\s+name="rating"\s+'
645                      r'     content="RTA-5042-1996-1400-1577-RTA"',
646                      html):
647             return 18
648         return 0
649
650     def _media_rating_search(self, html):
651         # See http://www.tjg-designs.com/WP/metadata-code-examples-adding-metadata-to-your-web-pages/
652         rating = self._html_search_meta('rating', html)
653
654         if not rating:
655             return None
656
657         RATING_TABLE = {
658             'safe for kids': 0,
659             'general': 8,
660             '14 years': 14,
661             'mature': 17,
662             'restricted': 19,
663         }
664         return RATING_TABLE.get(rating.lower(), None)
665
666     def _family_friendly_search(self, html):
667         # See http://schema.org/VideoObj
668         family_friendly = self._html_search_meta('isFamilyFriendly', html)
669
670         if not family_friendly:
671             return None
672
673         RATING_TABLE = {
674             '1': 0,
675             'true': 0,
676             '0': 18,
677             'false': 18,
678         }
679         return RATING_TABLE.get(family_friendly.lower(), None)
680
681     def _twitter_search_player(self, html):
682         return self._html_search_meta('twitter:player', html,
683                                       'twitter card player')
684
685     def _sort_formats(self, formats):
686         if not formats:
687             raise ExtractorError('No video formats found')
688
689         def _formats_key(f):
690             # TODO remove the following workaround
691             from ..utils import determine_ext
692             if not f.get('ext') and 'url' in f:
693                 f['ext'] = determine_ext(f['url'])
694
695             preference = f.get('preference')
696             if preference is None:
697                 proto = f.get('protocol')
698                 if proto is None:
699                     proto = compat_urllib_parse_urlparse(f.get('url', '')).scheme
700
701                 preference = 0 if proto in ['http', 'https'] else -0.1
702                 if f.get('ext') in ['f4f', 'f4m']:  # Not yet supported
703                     preference -= 0.5
704
705             if f.get('vcodec') == 'none':  # audio only
706                 if self._downloader.params.get('prefer_free_formats'):
707                     ORDER = ['aac', 'mp3', 'm4a', 'webm', 'ogg', 'opus']
708                 else:
709                     ORDER = ['webm', 'opus', 'ogg', 'mp3', 'aac', 'm4a']
710                 ext_preference = 0
711                 try:
712                     audio_ext_preference = ORDER.index(f['ext'])
713                 except ValueError:
714                     audio_ext_preference = -1
715             else:
716                 if self._downloader.params.get('prefer_free_formats'):
717                     ORDER = ['flv', 'mp4', 'webm']
718                 else:
719                     ORDER = ['webm', 'flv', 'mp4']
720                 try:
721                     ext_preference = ORDER.index(f['ext'])
722                 except ValueError:
723                     ext_preference = -1
724                 audio_ext_preference = 0
725
726             return (
727                 preference,
728                 f.get('language_preference') if f.get('language_preference') is not None else -1,
729                 f.get('quality') if f.get('quality') is not None else -1,
730                 f.get('tbr') if f.get('tbr') is not None else -1,
731                 f.get('vbr') if f.get('vbr') is not None else -1,
732                 f.get('height') if f.get('height') is not None else -1,
733                 f.get('width') if f.get('width') is not None else -1,
734                 ext_preference,
735                 f.get('abr') if f.get('abr') is not None else -1,
736                 audio_ext_preference,
737                 f.get('fps') if f.get('fps') is not None else -1,
738                 f.get('filesize') if f.get('filesize') is not None else -1,
739                 f.get('filesize_approx') if f.get('filesize_approx') is not None else -1,
740                 f.get('source_preference') if f.get('source_preference') is not None else -1,
741                 f.get('format_id'),
742             )
743         formats.sort(key=_formats_key)
744
745     def _check_formats(self, formats, video_id):
746         if formats:
747             formats[:] = filter(
748                 lambda f: self._is_valid_url(
749                     f['url'], video_id,
750                     item='%s video format' % f.get('format_id') if f.get('format_id') else 'video'),
751                 formats)
752
753     def _is_valid_url(self, url, video_id, item='video'):
754         try:
755             self._request_webpage(
756                 HEADRequest(url), video_id,
757                 'Checking %s URL' % item)
758             return True
759         except ExtractorError as e:
760             if isinstance(e.cause, compat_HTTPError):
761                 self.report_warning(
762                     '%s URL is invalid, skipping' % item, video_id)
763                 return False
764             raise
765
766     def http_scheme(self):
767         """ Either "http:" or "https:", depending on the user's preferences """
768         return (
769             'http:'
770             if self._downloader.params.get('prefer_insecure', False)
771             else 'https:')
772
773     def _proto_relative_url(self, url, scheme=None):
774         if url is None:
775             return url
776         if url.startswith('//'):
777             if scheme is None:
778                 scheme = self.http_scheme()
779             return scheme + url
780         else:
781             return url
782
783     def _sleep(self, timeout, video_id, msg_template=None):
784         if msg_template is None:
785             msg_template = '%(video_id)s: Waiting for %(timeout)s seconds'
786         msg = msg_template % {'video_id': video_id, 'timeout': timeout}
787         self.to_screen(msg)
788         time.sleep(timeout)
789
790     def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None):
791         manifest = self._download_xml(
792             manifest_url, video_id, 'Downloading f4m manifest',
793             'Unable to download f4m manifest')
794
795         formats = []
796         manifest_version = '1.0'
797         media_nodes = manifest.findall('{http://ns.adobe.com/f4m/1.0}media')
798         if not media_nodes:
799             manifest_version = '2.0'
800             media_nodes = manifest.findall('{http://ns.adobe.com/f4m/2.0}media')
801         for i, media_el in enumerate(media_nodes):
802             if manifest_version == '2.0':
803                 manifest_url = ('/'.join(manifest_url.split('/')[:-1]) + '/'
804                                 + (media_el.attrib.get('href') or media_el.attrib.get('url')))
805             tbr = int_or_none(media_el.attrib.get('bitrate'))
806             formats.append({
807                 'format_id': '-'.join(filter(None, [f4m_id, 'f4m-%d' % (i if tbr is None else tbr)])),
808                 'url': manifest_url,
809                 'ext': 'flv',
810                 'tbr': tbr,
811                 'width': int_or_none(media_el.attrib.get('width')),
812                 'height': int_or_none(media_el.attrib.get('height')),
813                 'preference': preference,
814             })
815         self._sort_formats(formats)
816
817         return formats
818
819     def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
820                               entry_protocol='m3u8', preference=None,
821                               m3u8_id=None):
822
823         formats = [{
824             'format_id': '-'.join(filter(None, [m3u8_id, 'm3u8-meta'])),
825             'url': m3u8_url,
826             'ext': ext,
827             'protocol': 'm3u8',
828             'preference': -1,
829             'resolution': 'multiple',
830             'format_note': 'Quality selection URL',
831         }]
832
833         format_url = lambda u: (
834             u
835             if re.match(r'^https?://', u)
836             else compat_urlparse.urljoin(m3u8_url, u))
837
838         m3u8_doc = self._download_webpage(
839             m3u8_url, video_id,
840             note='Downloading m3u8 information',
841             errnote='Failed to download m3u8 information')
842         last_info = None
843         kv_rex = re.compile(
844             r'(?P<key>[a-zA-Z_-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)')
845         for line in m3u8_doc.splitlines():
846             if line.startswith('#EXT-X-STREAM-INF:'):
847                 last_info = {}
848                 for m in kv_rex.finditer(line):
849                     v = m.group('val')
850                     if v.startswith('"'):
851                         v = v[1:-1]
852                     last_info[m.group('key')] = v
853             elif line.startswith('#') or not line.strip():
854                 continue
855             else:
856                 if last_info is None:
857                     formats.append({'url': format_url(line)})
858                     continue
859                 tbr = int_or_none(last_info.get('BANDWIDTH'), scale=1000)
860                 f = {
861                     'format_id': '-'.join(filter(None, [m3u8_id, 'm3u8-%d' % (tbr if tbr else len(formats))])),
862                     'url': format_url(line.strip()),
863                     'tbr': tbr,
864                     'ext': ext,
865                     'protocol': entry_protocol,
866                     'preference': preference,
867                 }
868                 codecs = last_info.get('CODECS')
869                 if codecs:
870                     # TODO: looks like video codec is not always necessarily goes first
871                     va_codecs = codecs.split(',')
872                     if va_codecs[0]:
873                         f['vcodec'] = va_codecs[0].partition('.')[0]
874                     if len(va_codecs) > 1 and va_codecs[1]:
875                         f['acodec'] = va_codecs[1].partition('.')[0]
876                 resolution = last_info.get('RESOLUTION')
877                 if resolution:
878                     width_str, height_str = resolution.split('x')
879                     f['width'] = int(width_str)
880                     f['height'] = int(height_str)
881                 formats.append(f)
882                 last_info = {}
883         self._sort_formats(formats)
884         return formats
885
886     # TODO: improve extraction
887     def _extract_smil_formats(self, smil_url, video_id, fatal=True):
888         smil = self._download_xml(
889             smil_url, video_id, 'Downloading SMIL file',
890             'Unable to download SMIL file', fatal=fatal)
891         if smil is False:
892             assert not fatal
893             return []
894
895         base = smil.find('./head/meta').get('base')
896
897         formats = []
898         rtmp_count = 0
899         for video in smil.findall('./body/switch/video'):
900             src = video.get('src')
901             if not src:
902                 continue
903             bitrate = int_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
904             width = int_or_none(video.get('width'))
905             height = int_or_none(video.get('height'))
906             proto = video.get('proto')
907             if not proto:
908                 if base:
909                     if base.startswith('rtmp'):
910                         proto = 'rtmp'
911                     elif base.startswith('http'):
912                         proto = 'http'
913             ext = video.get('ext')
914             if proto == 'm3u8':
915                 formats.extend(self._extract_m3u8_formats(src, video_id, ext))
916             elif proto == 'rtmp':
917                 rtmp_count += 1
918                 streamer = video.get('streamer') or base
919                 formats.append({
920                     'url': streamer,
921                     'play_path': src,
922                     'ext': 'flv',
923                     'format_id': 'rtmp-%d' % (rtmp_count if bitrate is None else bitrate),
924                     'tbr': bitrate,
925                     'width': width,
926                     'height': height,
927                 })
928         self._sort_formats(formats)
929
930         return formats
931
932     def _live_title(self, name):
933         """ Generate the title for a live video """
934         now = datetime.datetime.now()
935         now_str = now.strftime("%Y-%m-%d %H:%M")
936         return name + ' ' + now_str
937
938     def _int(self, v, name, fatal=False, **kwargs):
939         res = int_or_none(v, **kwargs)
940         if 'get_attr' in kwargs:
941             print(getattr(v, kwargs['get_attr']))
942         if res is None:
943             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
944             if fatal:
945                 raise ExtractorError(msg)
946             else:
947                 self._downloader.report_warning(msg)
948         return res
949
950     def _float(self, v, name, fatal=False, **kwargs):
951         res = float_or_none(v, **kwargs)
952         if res is None:
953             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
954             if fatal:
955                 raise ExtractorError(msg)
956             else:
957                 self._downloader.report_warning(msg)
958         return res
959
960     def _set_cookie(self, domain, name, value, expire_time=None):
961         cookie = compat_cookiejar.Cookie(
962             0, name, value, None, None, domain, None,
963             None, '/', True, False, expire_time, '', None, None, None)
964         self._downloader.cookiejar.set_cookie(cookie)
965
966     def get_testcases(self, include_onlymatching=False):
967         t = getattr(self, '_TEST', None)
968         if t:
969             assert not hasattr(self, '_TESTS'), \
970                 '%s has _TEST and _TESTS' % type(self).__name__
971             tests = [t]
972         else:
973             tests = getattr(self, '_TESTS', [])
974         for t in tests:
975             if not include_onlymatching and t.get('only_matching', False):
976                 continue
977             t['name'] = type(self).__name__[:-len('IE')]
978             yield t
979
980     def is_suitable(self, age_limit):
981         """ Test whether the extractor is generally suitable for the given
982         age limit (i.e. pornographic sites are not, all others usually are) """
983
984         any_restricted = False
985         for tc in self.get_testcases(include_onlymatching=False):
986             if 'playlist' in tc:
987                 tc = tc['playlist'][0]
988             is_restricted = age_restricted(
989                 tc.get('info_dict', {}).get('age_limit'), age_limit)
990             if not is_restricted:
991                 return True
992             any_restricted = any_restricted or is_restricted
993         return not any_restricted
994
995
996 class SearchInfoExtractor(InfoExtractor):
997     """
998     Base class for paged search queries extractors.
999     They accept urls in the format _SEARCH_KEY(|all|[0-9]):{query}
1000     Instances should define _SEARCH_KEY and _MAX_RESULTS.
1001     """
1002
1003     @classmethod
1004     def _make_valid_url(cls):
1005         return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
1006
1007     @classmethod
1008     def suitable(cls, url):
1009         return re.match(cls._make_valid_url(), url) is not None
1010
1011     def _real_extract(self, query):
1012         mobj = re.match(self._make_valid_url(), query)
1013         if mobj is None:
1014             raise ExtractorError('Invalid search query "%s"' % query)
1015
1016         prefix = mobj.group('prefix')
1017         query = mobj.group('query')
1018         if prefix == '':
1019             return self._get_n_results(query, 1)
1020         elif prefix == 'all':
1021             return self._get_n_results(query, self._MAX_RESULTS)
1022         else:
1023             n = int(prefix)
1024             if n <= 0:
1025                 raise ExtractorError('invalid download number %s for query "%s"' % (n, query))
1026             elif n > self._MAX_RESULTS:
1027                 self._downloader.report_warning('%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n))
1028                 n = self._MAX_RESULTS
1029             return self._get_n_results(query, n)
1030
1031     def _get_n_results(self, query, n):
1032         """Get a specified number of results for a query"""
1033         raise NotImplementedError("This method must be implemented by subclasses")
1034
1035     @property
1036     def SEARCH_KEY(self):
1037         return self._SEARCH_KEY