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