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