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