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