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