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