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