[extractor/common] Handle malformed f4m manifests
[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                 manifest_url = ('/'.join(manifest_url.split('/')[:-1]) + '/' +
855                                 (media_el.attrib.get('href') or media_el.attrib.get('url')))
856             tbr = int_or_none(media_el.attrib.get('bitrate'))
857             formats.append({
858                 'format_id': '-'.join(filter(None, [f4m_id, compat_str(i if tbr is None else tbr)])),
859                 'url': manifest_url,
860                 'ext': 'flv',
861                 'tbr': tbr,
862                 'width': int_or_none(media_el.attrib.get('width')),
863                 'height': int_or_none(media_el.attrib.get('height')),
864                 'preference': preference,
865             })
866         self._sort_formats(formats)
867
868         return formats
869
870     def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
871                               entry_protocol='m3u8', preference=None,
872                               m3u8_id=None, note=None, errnote=None,
873                               fatal=True):
874
875         formats = [{
876             'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
877             'url': m3u8_url,
878             'ext': ext,
879             'protocol': 'm3u8',
880             'preference': preference - 1 if preference else -1,
881             'resolution': 'multiple',
882             'format_note': 'Quality selection URL',
883         }]
884
885         format_url = lambda u: (
886             u
887             if re.match(r'^https?://', u)
888             else compat_urlparse.urljoin(m3u8_url, u))
889
890         m3u8_doc = self._download_webpage(
891             m3u8_url, video_id,
892             note=note or 'Downloading m3u8 information',
893             errnote=errnote or 'Failed to download m3u8 information',
894             fatal=fatal)
895         if m3u8_doc is False:
896             return m3u8_doc
897         last_info = None
898         last_media = None
899         kv_rex = re.compile(
900             r'(?P<key>[a-zA-Z_-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)')
901         for line in m3u8_doc.splitlines():
902             if line.startswith('#EXT-X-STREAM-INF:'):
903                 last_info = {}
904                 for m in kv_rex.finditer(line):
905                     v = m.group('val')
906                     if v.startswith('"'):
907                         v = v[1:-1]
908                     last_info[m.group('key')] = v
909             elif line.startswith('#EXT-X-MEDIA:'):
910                 last_media = {}
911                 for m in kv_rex.finditer(line):
912                     v = m.group('val')
913                     if v.startswith('"'):
914                         v = v[1:-1]
915                     last_media[m.group('key')] = v
916             elif line.startswith('#') or not line.strip():
917                 continue
918             else:
919                 if last_info is None:
920                     formats.append({'url': format_url(line)})
921                     continue
922                 tbr = int_or_none(last_info.get('BANDWIDTH'), scale=1000)
923                 format_id = []
924                 if m3u8_id:
925                     format_id.append(m3u8_id)
926                 last_media_name = last_media.get('NAME') if last_media and last_media.get('TYPE') != 'SUBTITLES' else None
927                 format_id.append(last_media_name if last_media_name else '%d' % (tbr if tbr else len(formats)))
928                 f = {
929                     'format_id': '-'.join(format_id),
930                     'url': format_url(line.strip()),
931                     'tbr': tbr,
932                     'ext': ext,
933                     'protocol': entry_protocol,
934                     'preference': preference,
935                 }
936                 codecs = last_info.get('CODECS')
937                 if codecs:
938                     # TODO: looks like video codec is not always necessarily goes first
939                     va_codecs = codecs.split(',')
940                     if va_codecs[0]:
941                         f['vcodec'] = va_codecs[0].partition('.')[0]
942                     if len(va_codecs) > 1 and va_codecs[1]:
943                         f['acodec'] = va_codecs[1].partition('.')[0]
944                 resolution = last_info.get('RESOLUTION')
945                 if resolution:
946                     width_str, height_str = resolution.split('x')
947                     f['width'] = int(width_str)
948                     f['height'] = int(height_str)
949                 if last_media is not None:
950                     f['m3u8_media'] = last_media
951                     last_media = None
952                 formats.append(f)
953                 last_info = {}
954         self._sort_formats(formats)
955         return formats
956
957     # TODO: improve extraction
958     def _extract_smil_formats(self, smil_url, video_id, fatal=True):
959         smil = self._download_xml(
960             smil_url, video_id, 'Downloading SMIL file',
961             'Unable to download SMIL file', fatal=fatal)
962         if smil is False:
963             assert not fatal
964             return []
965
966         base = smil.find('./head/meta').get('base')
967
968         formats = []
969         rtmp_count = 0
970         if smil.findall('./body/seq/video'):
971             video = smil.findall('./body/seq/video')[0]
972             fmts, rtmp_count = self._parse_smil_video(video, video_id, base, rtmp_count)
973             formats.extend(fmts)
974         else:
975             for video in smil.findall('./body/switch/video'):
976                 fmts, rtmp_count = self._parse_smil_video(video, video_id, base, rtmp_count)
977                 formats.extend(fmts)
978
979         self._sort_formats(formats)
980
981         return formats
982
983     def _parse_smil_video(self, video, video_id, base, rtmp_count):
984         src = video.get('src')
985         if not src:
986             return ([], rtmp_count)
987         bitrate = int_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
988         width = int_or_none(video.get('width'))
989         height = int_or_none(video.get('height'))
990         proto = video.get('proto')
991         if not proto:
992             if base:
993                 if base.startswith('rtmp'):
994                     proto = 'rtmp'
995                 elif base.startswith('http'):
996                     proto = 'http'
997         ext = video.get('ext')
998         if proto == 'm3u8':
999             return (self._extract_m3u8_formats(src, video_id, ext), rtmp_count)
1000         elif proto == 'rtmp':
1001             rtmp_count += 1
1002             streamer = video.get('streamer') or base
1003             return ([{
1004                 'url': streamer,
1005                 'play_path': src,
1006                 'ext': 'flv',
1007                 'format_id': 'rtmp-%d' % (rtmp_count if bitrate is None else bitrate),
1008                 'tbr': bitrate,
1009                 'width': width,
1010                 'height': height,
1011             }], rtmp_count)
1012         elif proto.startswith('http'):
1013             return ([{
1014                 'url': base + src,
1015                 'ext': ext or 'flv',
1016                 'tbr': bitrate,
1017                 'width': width,
1018                 'height': height,
1019             }], rtmp_count)
1020
1021     def _live_title(self, name):
1022         """ Generate the title for a live video """
1023         now = datetime.datetime.now()
1024         now_str = now.strftime("%Y-%m-%d %H:%M")
1025         return name + ' ' + now_str
1026
1027     def _int(self, v, name, fatal=False, **kwargs):
1028         res = int_or_none(v, **kwargs)
1029         if 'get_attr' in kwargs:
1030             print(getattr(v, kwargs['get_attr']))
1031         if res is None:
1032             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
1033             if fatal:
1034                 raise ExtractorError(msg)
1035             else:
1036                 self._downloader.report_warning(msg)
1037         return res
1038
1039     def _float(self, v, name, fatal=False, **kwargs):
1040         res = float_or_none(v, **kwargs)
1041         if res is None:
1042             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
1043             if fatal:
1044                 raise ExtractorError(msg)
1045             else:
1046                 self._downloader.report_warning(msg)
1047         return res
1048
1049     def _set_cookie(self, domain, name, value, expire_time=None):
1050         cookie = compat_cookiejar.Cookie(
1051             0, name, value, None, None, domain, None,
1052             None, '/', True, False, expire_time, '', None, None, None)
1053         self._downloader.cookiejar.set_cookie(cookie)
1054
1055     def get_testcases(self, include_onlymatching=False):
1056         t = getattr(self, '_TEST', None)
1057         if t:
1058             assert not hasattr(self, '_TESTS'), \
1059                 '%s has _TEST and _TESTS' % type(self).__name__
1060             tests = [t]
1061         else:
1062             tests = getattr(self, '_TESTS', [])
1063         for t in tests:
1064             if not include_onlymatching and t.get('only_matching', False):
1065                 continue
1066             t['name'] = type(self).__name__[:-len('IE')]
1067             yield t
1068
1069     def is_suitable(self, age_limit):
1070         """ Test whether the extractor is generally suitable for the given
1071         age limit (i.e. pornographic sites are not, all others usually are) """
1072
1073         any_restricted = False
1074         for tc in self.get_testcases(include_onlymatching=False):
1075             if 'playlist' in tc:
1076                 tc = tc['playlist'][0]
1077             is_restricted = age_restricted(
1078                 tc.get('info_dict', {}).get('age_limit'), age_limit)
1079             if not is_restricted:
1080                 return True
1081             any_restricted = any_restricted or is_restricted
1082         return not any_restricted
1083
1084     def extract_subtitles(self, *args, **kwargs):
1085         if (self._downloader.params.get('writesubtitles', False) or
1086                 self._downloader.params.get('listsubtitles')):
1087             return self._get_subtitles(*args, **kwargs)
1088         return {}
1089
1090     def _get_subtitles(self, *args, **kwargs):
1091         raise NotImplementedError("This method must be implemented by subclasses")
1092
1093     def extract_automatic_captions(self, *args, **kwargs):
1094         if (self._downloader.params.get('writeautomaticsub', False) or
1095                 self._downloader.params.get('listsubtitles')):
1096             return self._get_automatic_captions(*args, **kwargs)
1097         return {}
1098
1099     def _get_automatic_captions(self, *args, **kwargs):
1100         raise NotImplementedError("This method must be implemented by subclasses")
1101
1102
1103 class SearchInfoExtractor(InfoExtractor):
1104     """
1105     Base class for paged search queries extractors.
1106     They accept urls in the format _SEARCH_KEY(|all|[0-9]):{query}
1107     Instances should define _SEARCH_KEY and _MAX_RESULTS.
1108     """
1109
1110     @classmethod
1111     def _make_valid_url(cls):
1112         return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
1113
1114     @classmethod
1115     def suitable(cls, url):
1116         return re.match(cls._make_valid_url(), url) is not None
1117
1118     def _real_extract(self, query):
1119         mobj = re.match(self._make_valid_url(), query)
1120         if mobj is None:
1121             raise ExtractorError('Invalid search query "%s"' % query)
1122
1123         prefix = mobj.group('prefix')
1124         query = mobj.group('query')
1125         if prefix == '':
1126             return self._get_n_results(query, 1)
1127         elif prefix == 'all':
1128             return self._get_n_results(query, self._MAX_RESULTS)
1129         else:
1130             n = int(prefix)
1131             if n <= 0:
1132                 raise ExtractorError('invalid download number %s for query "%s"' % (n, query))
1133             elif n > self._MAX_RESULTS:
1134                 self._downloader.report_warning('%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n))
1135                 n = self._MAX_RESULTS
1136             return self._get_n_results(query, n)
1137
1138     def _get_n_results(self, query, n):
1139         """Get a specified number of results for a query"""
1140         raise NotImplementedError("This method must be implemented by subclasses")
1141
1142     @property
1143     def SEARCH_KEY(self):
1144         return self._SEARCH_KEY