Merge branch 'progress-as-hook2'
[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
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/VideoObject
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('filesize') if f.get('filesize') is not None else -1,
732                 f.get('vbr') if f.get('vbr') is not None else -1,
733                 f.get('height') if f.get('height') is not None else -1,
734                 f.get('width') if f.get('width') is not None else -1,
735                 ext_preference,
736                 f.get('abr') if f.get('abr') is not None else -1,
737                 audio_ext_preference,
738                 f.get('fps') if f.get('fps') 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(url, video_id, 'Checking %s URL' % item)
756             return True
757         except ExtractorError as e:
758             if isinstance(e.cause, compat_HTTPError):
759                 self.report_warning(
760                     '%s URL is invalid, skipping' % item, video_id)
761                 return False
762             raise
763
764     def http_scheme(self):
765         """ Either "http:" or "https:", depending on the user's preferences """
766         return (
767             'http:'
768             if self._downloader.params.get('prefer_insecure', False)
769             else 'https:')
770
771     def _proto_relative_url(self, url, scheme=None):
772         if url is None:
773             return url
774         if url.startswith('//'):
775             if scheme is None:
776                 scheme = self.http_scheme()
777             return scheme + url
778         else:
779             return url
780
781     def _sleep(self, timeout, video_id, msg_template=None):
782         if msg_template is None:
783             msg_template = '%(video_id)s: Waiting for %(timeout)s seconds'
784         msg = msg_template % {'video_id': video_id, 'timeout': timeout}
785         self.to_screen(msg)
786         time.sleep(timeout)
787
788     def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None):
789         manifest = self._download_xml(
790             manifest_url, video_id, 'Downloading f4m manifest',
791             'Unable to download f4m manifest')
792
793         formats = []
794         manifest_version = '1.0'
795         media_nodes = manifest.findall('{http://ns.adobe.com/f4m/1.0}media')
796         if not media_nodes:
797             manifest_version = '2.0'
798             media_nodes = manifest.findall('{http://ns.adobe.com/f4m/2.0}media')
799         for i, media_el in enumerate(media_nodes):
800             if manifest_version == '2.0':
801                 manifest_url = ('/'.join(manifest_url.split('/')[:-1]) + '/'
802                                 + (media_el.attrib.get('href') or media_el.attrib.get('url')))
803             tbr = int_or_none(media_el.attrib.get('bitrate'))
804             formats.append({
805                 'format_id': '-'.join(filter(None, [f4m_id, 'f4m-%d' % (i if tbr is None else tbr)])),
806                 'url': manifest_url,
807                 'ext': 'flv',
808                 'tbr': tbr,
809                 'width': int_or_none(media_el.attrib.get('width')),
810                 'height': int_or_none(media_el.attrib.get('height')),
811                 'preference': preference,
812             })
813         self._sort_formats(formats)
814
815         return formats
816
817     def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
818                               entry_protocol='m3u8', preference=None,
819                               m3u8_id=None):
820
821         formats = [{
822             'format_id': '-'.join(filter(None, [m3u8_id, 'm3u8-meta'])),
823             'url': m3u8_url,
824             'ext': ext,
825             'protocol': 'm3u8',
826             'preference': -1,
827             'resolution': 'multiple',
828             'format_note': 'Quality selection URL',
829         }]
830
831         format_url = lambda u: (
832             u
833             if re.match(r'^https?://', u)
834             else compat_urlparse.urljoin(m3u8_url, u))
835
836         m3u8_doc = self._download_webpage(
837             m3u8_url, video_id,
838             note='Downloading m3u8 information',
839             errnote='Failed to download m3u8 information')
840         last_info = None
841         last_media = None
842         kv_rex = re.compile(
843             r'(?P<key>[a-zA-Z_-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)')
844         for line in m3u8_doc.splitlines():
845             if line.startswith('#EXT-X-STREAM-INF:'):
846                 last_info = {}
847                 for m in kv_rex.finditer(line):
848                     v = m.group('val')
849                     if v.startswith('"'):
850                         v = v[1:-1]
851                     last_info[m.group('key')] = v
852             elif line.startswith('#EXT-X-MEDIA:'):
853                 last_media = {}
854                 for m in kv_rex.finditer(line):
855                     v = m.group('val')
856                     if v.startswith('"'):
857                         v = v[1:-1]
858                     last_media[m.group('key')] = v
859             elif line.startswith('#') or not line.strip():
860                 continue
861             else:
862                 if last_info is None:
863                     formats.append({'url': format_url(line)})
864                     continue
865                 tbr = int_or_none(last_info.get('BANDWIDTH'), scale=1000)
866                 f = {
867                     'format_id': '-'.join(filter(None, [m3u8_id, 'm3u8-%d' % (tbr if tbr else len(formats))])),
868                     'url': format_url(line.strip()),
869                     'tbr': tbr,
870                     'ext': ext,
871                     'protocol': entry_protocol,
872                     'preference': preference,
873                 }
874                 codecs = last_info.get('CODECS')
875                 if codecs:
876                     # TODO: looks like video codec is not always necessarily goes first
877                     va_codecs = codecs.split(',')
878                     if va_codecs[0]:
879                         f['vcodec'] = va_codecs[0].partition('.')[0]
880                     if len(va_codecs) > 1 and va_codecs[1]:
881                         f['acodec'] = va_codecs[1].partition('.')[0]
882                 resolution = last_info.get('RESOLUTION')
883                 if resolution:
884                     width_str, height_str = resolution.split('x')
885                     f['width'] = int(width_str)
886                     f['height'] = int(height_str)
887                 if last_media is not None:
888                     f['m3u8_media'] = last_media
889                     last_media = None
890                 formats.append(f)
891                 last_info = {}
892         self._sort_formats(formats)
893         return formats
894
895     # TODO: improve extraction
896     def _extract_smil_formats(self, smil_url, video_id, fatal=True):
897         smil = self._download_xml(
898             smil_url, video_id, 'Downloading SMIL file',
899             'Unable to download SMIL file', fatal=fatal)
900         if smil is False:
901             assert not fatal
902             return []
903
904         base = smil.find('./head/meta').get('base')
905
906         formats = []
907         rtmp_count = 0
908         for video in smil.findall('./body/switch/video'):
909             src = video.get('src')
910             if not src:
911                 continue
912             bitrate = int_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
913             width = int_or_none(video.get('width'))
914             height = int_or_none(video.get('height'))
915             proto = video.get('proto')
916             if not proto:
917                 if base:
918                     if base.startswith('rtmp'):
919                         proto = 'rtmp'
920                     elif base.startswith('http'):
921                         proto = 'http'
922             ext = video.get('ext')
923             if proto == 'm3u8':
924                 formats.extend(self._extract_m3u8_formats(src, video_id, ext))
925             elif proto == 'rtmp':
926                 rtmp_count += 1
927                 streamer = video.get('streamer') or base
928                 formats.append({
929                     'url': streamer,
930                     'play_path': src,
931                     'ext': 'flv',
932                     'format_id': 'rtmp-%d' % (rtmp_count if bitrate is None else bitrate),
933                     'tbr': bitrate,
934                     'width': width,
935                     'height': height,
936                 })
937         self._sort_formats(formats)
938
939         return formats
940
941     def _live_title(self, name):
942         """ Generate the title for a live video """
943         now = datetime.datetime.now()
944         now_str = now.strftime("%Y-%m-%d %H:%M")
945         return name + ' ' + now_str
946
947     def _int(self, v, name, fatal=False, **kwargs):
948         res = int_or_none(v, **kwargs)
949         if 'get_attr' in kwargs:
950             print(getattr(v, kwargs['get_attr']))
951         if res is None:
952             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
953             if fatal:
954                 raise ExtractorError(msg)
955             else:
956                 self._downloader.report_warning(msg)
957         return res
958
959     def _float(self, v, name, fatal=False, **kwargs):
960         res = float_or_none(v, **kwargs)
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 _set_cookie(self, domain, name, value, expire_time=None):
970         cookie = compat_cookiejar.Cookie(
971             0, name, value, None, None, domain, None,
972             None, '/', True, False, expire_time, '', None, None, None)
973         self._downloader.cookiejar.set_cookie(cookie)
974
975     def get_testcases(self, include_onlymatching=False):
976         t = getattr(self, '_TEST', None)
977         if t:
978             assert not hasattr(self, '_TESTS'), \
979                 '%s has _TEST and _TESTS' % type(self).__name__
980             tests = [t]
981         else:
982             tests = getattr(self, '_TESTS', [])
983         for t in tests:
984             if not include_onlymatching and t.get('only_matching', False):
985                 continue
986             t['name'] = type(self).__name__[:-len('IE')]
987             yield t
988
989     def is_suitable(self, age_limit):
990         """ Test whether the extractor is generally suitable for the given
991         age limit (i.e. pornographic sites are not, all others usually are) """
992
993         any_restricted = False
994         for tc in self.get_testcases(include_onlymatching=False):
995             if 'playlist' in tc:
996                 tc = tc['playlist'][0]
997             is_restricted = age_restricted(
998                 tc.get('info_dict', {}).get('age_limit'), age_limit)
999             if not is_restricted:
1000                 return True
1001             any_restricted = any_restricted or is_restricted
1002         return not any_restricted
1003
1004
1005 class SearchInfoExtractor(InfoExtractor):
1006     """
1007     Base class for paged search queries extractors.
1008     They accept urls in the format _SEARCH_KEY(|all|[0-9]):{query}
1009     Instances should define _SEARCH_KEY and _MAX_RESULTS.
1010     """
1011
1012     @classmethod
1013     def _make_valid_url(cls):
1014         return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
1015
1016     @classmethod
1017     def suitable(cls, url):
1018         return re.match(cls._make_valid_url(), url) is not None
1019
1020     def _real_extract(self, query):
1021         mobj = re.match(self._make_valid_url(), query)
1022         if mobj is None:
1023             raise ExtractorError('Invalid search query "%s"' % query)
1024
1025         prefix = mobj.group('prefix')
1026         query = mobj.group('query')
1027         if prefix == '':
1028             return self._get_n_results(query, 1)
1029         elif prefix == 'all':
1030             return self._get_n_results(query, self._MAX_RESULTS)
1031         else:
1032             n = int(prefix)
1033             if n <= 0:
1034                 raise ExtractorError('invalid download number %s for query "%s"' % (n, query))
1035             elif n > self._MAX_RESULTS:
1036                 self._downloader.report_warning('%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n))
1037                 n = self._MAX_RESULTS
1038             return self._get_n_results(query, n)
1039
1040     def _get_n_results(self, query, n):
1041         """Get a specified number of results for a query"""
1042         raise NotImplementedError("This method must be implemented by subclasses")
1043
1044     @property
1045     def SEARCH_KEY(self):
1046         return self._SEARCH_KEY