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