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