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