[common] lower the preference of m3u8 master manifest format
[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 math
14
15 from ..compat import (
16     compat_cookiejar,
17     compat_cookies,
18     compat_etree_fromstring,
19     compat_getpass,
20     compat_http_client,
21     compat_os_name,
22     compat_str,
23     compat_urllib_error,
24     compat_urllib_parse_urlencode,
25     compat_urllib_request,
26     compat_urlparse,
27 )
28 from ..downloader.f4m import remove_encrypted_media
29 from ..utils import (
30     NO_DEFAULT,
31     age_restricted,
32     bug_reports_message,
33     clean_html,
34     compiled_regex_type,
35     determine_ext,
36     error_to_compat_str,
37     ExtractorError,
38     fix_xml_ampersands,
39     float_or_none,
40     int_or_none,
41     parse_iso8601,
42     RegexNotFoundError,
43     sanitize_filename,
44     sanitized_Request,
45     unescapeHTML,
46     unified_strdate,
47     unified_timestamp,
48     url_basename,
49     xpath_element,
50     xpath_text,
51     xpath_with_ns,
52     determine_protocol,
53     parse_duration,
54     mimetype2ext,
55     update_Request,
56     update_url_query,
57     parse_m3u8_attributes,
58     extract_attributes,
59     parse_codecs,
60 )
61
62
63 class InfoExtractor(object):
64     """Information Extractor class.
65
66     Information extractors are the classes that, given a URL, extract
67     information about the video (or videos) the URL refers to. This
68     information includes the real video URL, the video title, author and
69     others. The information is stored in a dictionary which is then
70     passed to the YoutubeDL. The YoutubeDL processes this
71     information possibly downloading the video to the file system, among
72     other possible outcomes.
73
74     The type field determines the type of the result.
75     By far the most common value (and the default if _type is missing) is
76     "video", which indicates a single video.
77
78     For a video, the dictionaries must include the following fields:
79
80     id:             Video identifier.
81     title:          Video title, unescaped.
82
83     Additionally, it must contain either a formats entry or a url one:
84
85     formats:        A list of dictionaries for each format available, ordered
86                     from worst to best quality.
87
88                     Potential fields:
89                     * url        Mandatory. The URL of the video file
90                     * ext        Will be calculated from URL if missing
91                     * format     A human-readable description of the format
92                                  ("mp4 container with h264/opus").
93                                  Calculated from the format_id, width, height.
94                                  and format_note fields if missing.
95                     * format_id  A short description of the format
96                                  ("mp4_h264_opus" or "19").
97                                 Technically optional, but strongly recommended.
98                     * format_note Additional info about the format
99                                  ("3D" or "DASH video")
100                     * width      Width of the video, if known
101                     * height     Height of the video, if known
102                     * resolution Textual description of width and height
103                     * tbr        Average bitrate of audio and video in KBit/s
104                     * abr        Average audio bitrate in KBit/s
105                     * acodec     Name of the audio codec in use
106                     * asr        Audio sampling rate in Hertz
107                     * vbr        Average video bitrate in KBit/s
108                     * fps        Frame rate
109                     * vcodec     Name of the video codec in use
110                     * container  Name of the container format
111                     * filesize   The number of bytes, if known in advance
112                     * filesize_approx  An estimate for the number of bytes
113                     * player_url SWF Player URL (used for rtmpdump).
114                     * protocol   The protocol that will be used for the actual
115                                  download, lower-case.
116                                  "http", "https", "rtsp", "rtmp", "rtmpe",
117                                  "m3u8", "m3u8_native" or "http_dash_segments".
118                     * preference Order number of this format. If this field is
119                                  present and not None, the formats get sorted
120                                  by this field, regardless of all other values.
121                                  -1 for default (order by other properties),
122                                  -2 or smaller for less than default.
123                                  < -1000 to hide the format (if there is
124                                     another one which is strictly better)
125                     * language   Language code, e.g. "de" or "en-US".
126                     * language_preference  Is this in the language mentioned in
127                                  the URL?
128                                  10 if it's what the URL is about,
129                                  -1 for default (don't know),
130                                  -10 otherwise, other values reserved for now.
131                     * quality    Order number of the video quality of this
132                                  format, irrespective of the file format.
133                                  -1 for default (order by other properties),
134                                  -2 or smaller for less than default.
135                     * source_preference  Order number for this video source
136                                   (quality takes higher priority)
137                                  -1 for default (order by other properties),
138                                  -2 or smaller for less than default.
139                     * http_headers  A dictionary of additional HTTP headers
140                                  to add to the request.
141                     * stretched_ratio  If given and not 1, indicates that the
142                                  video's pixels are not square.
143                                  width : height ratio as float.
144                     * no_resume  The server does not support resuming the
145                                  (HTTP or RTMP) download. Boolean.
146
147     url:            Final video URL.
148     ext:            Video filename extension.
149     format:         The video format, defaults to ext (used for --get-format)
150     player_url:     SWF Player URL (used for rtmpdump).
151
152     The following fields are optional:
153
154     alt_title:      A secondary title of the video.
155     display_id      An alternative identifier for the video, not necessarily
156                     unique, but available before title. Typically, id is
157                     something like "4234987", title "Dancing naked mole rats",
158                     and display_id "dancing-naked-mole-rats"
159     thumbnails:     A list of dictionaries, with the following entries:
160                         * "id" (optional, string) - Thumbnail format ID
161                         * "url"
162                         * "preference" (optional, int) - quality of the image
163                         * "width" (optional, int)
164                         * "height" (optional, int)
165                         * "resolution" (optional, string "{width}x{height"},
166                                         deprecated)
167                         * "filesize" (optional, int)
168     thumbnail:      Full URL to a video thumbnail image.
169     description:    Full video description.
170     uploader:       Full name of the video uploader.
171     license:        License name the video is licensed under.
172     creator:        The creator of the video.
173     release_date:   The date (YYYYMMDD) when the video was released.
174     timestamp:      UNIX timestamp of the moment the video became available.
175     upload_date:    Video upload date (YYYYMMDD).
176                     If not explicitly set, calculated from timestamp.
177     uploader_id:    Nickname or id of the video uploader.
178     uploader_url:   Full URL to a personal webpage of the video uploader.
179     location:       Physical location where the video was filmed.
180     subtitles:      The available subtitles as a dictionary in the format
181                     {language: subformats}. "subformats" is a list sorted from
182                     lower to higher preference, each element is a dictionary
183                     with the "ext" entry and one of:
184                         * "data": The subtitles file contents
185                         * "url": A URL pointing to the subtitles file
186                     "ext" will be calculated from URL if missing
187     automatic_captions: Like 'subtitles', used by the YoutubeIE for
188                     automatically generated captions
189     duration:       Length of the video in seconds, as an integer or float.
190     view_count:     How many users have watched the video on the platform.
191     like_count:     Number of positive ratings of the video
192     dislike_count:  Number of negative ratings of the video
193     repost_count:   Number of reposts of the video
194     average_rating: Average rating give by users, the scale used depends on the webpage
195     comment_count:  Number of comments on the video
196     comments:       A list of comments, each with one or more of the following
197                     properties (all but one of text or html optional):
198                         * "author" - human-readable name of the comment author
199                         * "author_id" - user ID of the comment author
200                         * "id" - Comment ID
201                         * "html" - Comment as HTML
202                         * "text" - Plain text of the comment
203                         * "timestamp" - UNIX timestamp of comment
204                         * "parent" - ID of the comment this one is replying to.
205                                      Set to "root" to indicate that this is a
206                                      comment to the original video.
207     age_limit:      Age restriction for the video, as an integer (years)
208     webpage_url:    The URL to the video webpage, if given to youtube-dl it
209                     should allow to get the same result again. (It will be set
210                     by YoutubeDL if it's missing)
211     categories:     A list of categories that the video falls in, for example
212                     ["Sports", "Berlin"]
213     tags:           A list of tags assigned to the video, e.g. ["sweden", "pop music"]
214     is_live:        True, False, or None (=unknown). Whether this video is a
215                     live stream that goes on instead of a fixed-length video.
216     start_time:     Time in seconds where the reproduction should start, as
217                     specified in the URL.
218     end_time:       Time in seconds where the reproduction should end, as
219                     specified in the URL.
220
221     The following fields should only be used when the video belongs to some logical
222     chapter or section:
223
224     chapter:        Name or title of the chapter the video belongs to.
225     chapter_number: Number of the chapter the video belongs to, as an integer.
226     chapter_id:     Id of the chapter the video belongs to, as a unicode string.
227
228     The following fields should only be used when the video is an episode of some
229     series or programme:
230
231     series:         Title of the series or programme the video episode belongs to.
232     season:         Title of the season the video episode belongs to.
233     season_number:  Number of the season the video episode belongs to, as an integer.
234     season_id:      Id of the season the video episode belongs to, as a unicode string.
235     episode:        Title of the video episode. Unlike mandatory video title field,
236                     this field should denote the exact title of the video episode
237                     without any kind of decoration.
238     episode_number: Number of the video episode within a season, as an integer.
239     episode_id:     Id of the video episode, as a unicode string.
240
241     The following fields should only be used when the media is a track or a part of
242     a music album:
243
244     track:          Title of the track.
245     track_number:   Number of the track within an album or a disc, as an integer.
246     track_id:       Id of the track (useful in case of custom indexing, e.g. 6.iii),
247                     as a unicode string.
248     artist:         Artist(s) of the track.
249     genre:          Genre(s) of the track.
250     album:          Title of the album the track belongs to.
251     album_type:     Type of the album (e.g. "Demo", "Full-length", "Split", "Compilation", etc).
252     album_artist:   List of all artists appeared on the album (e.g.
253                     "Ash Borer / Fell Voices" or "Various Artists", useful for splits
254                     and compilations).
255     disc_number:    Number of the disc or other physical medium the track belongs to,
256                     as an integer.
257     release_year:   Year (YYYY) when the album was released.
258
259     Unless mentioned otherwise, the fields should be Unicode strings.
260
261     Unless mentioned otherwise, None is equivalent to absence of information.
262
263
264     _type "playlist" indicates multiple videos.
265     There must be a key "entries", which is a list, an iterable, or a PagedList
266     object, each element of which is a valid dictionary by this specification.
267
268     Additionally, playlists can have "title", "description" and "id" attributes
269     with the same semantics as videos (see above).
270
271
272     _type "multi_video" indicates that there are multiple videos that
273     form a single show, for examples multiple acts of an opera or TV episode.
274     It must have an entries key like a playlist and contain all the keys
275     required for a video at the same time.
276
277
278     _type "url" indicates that the video must be extracted from another
279     location, possibly by a different extractor. Its only required key is:
280     "url" - the next URL to extract.
281     The key "ie_key" can be set to the class name (minus the trailing "IE",
282     e.g. "Youtube") if the extractor class is known in advance.
283     Additionally, the dictionary may have any properties of the resolved entity
284     known in advance, for example "title" if the title of the referred video is
285     known ahead of time.
286
287
288     _type "url_transparent" entities have the same specification as "url", but
289     indicate that the given additional information is more precise than the one
290     associated with the resolved URL.
291     This is useful when a site employs a video service that hosts the video and
292     its technical metadata, but that video service does not embed a useful
293     title, description etc.
294
295
296     Subclasses of this one should re-define the _real_initialize() and
297     _real_extract() methods and define a _VALID_URL regexp.
298     Probably, they should also be added to the list of extractors.
299
300     Finally, the _WORKING attribute should be set to False for broken IEs
301     in order to warn the users and skip the tests.
302     """
303
304     _ready = False
305     _downloader = None
306     _WORKING = True
307
308     def __init__(self, downloader=None):
309         """Constructor. Receives an optional downloader."""
310         self._ready = False
311         self.set_downloader(downloader)
312
313     @classmethod
314     def suitable(cls, url):
315         """Receives a URL and returns True if suitable for this IE."""
316
317         # This does not use has/getattr intentionally - we want to know whether
318         # we have cached the regexp for *this* class, whereas getattr would also
319         # match the superclass
320         if '_VALID_URL_RE' not in cls.__dict__:
321             cls._VALID_URL_RE = re.compile(cls._VALID_URL)
322         return cls._VALID_URL_RE.match(url) is not None
323
324     @classmethod
325     def _match_id(cls, url):
326         if '_VALID_URL_RE' not in cls.__dict__:
327             cls._VALID_URL_RE = re.compile(cls._VALID_URL)
328         m = cls._VALID_URL_RE.match(url)
329         assert m
330         return m.group('id')
331
332     @classmethod
333     def working(cls):
334         """Getter method for _WORKING."""
335         return cls._WORKING
336
337     def initialize(self):
338         """Initializes an instance (authentication, etc)."""
339         if not self._ready:
340             self._real_initialize()
341             self._ready = True
342
343     def extract(self, url):
344         """Extracts URL information and returns it in list of dicts."""
345         try:
346             self.initialize()
347             return self._real_extract(url)
348         except ExtractorError:
349             raise
350         except compat_http_client.IncompleteRead as e:
351             raise ExtractorError('A network error has occurred.', cause=e, expected=True)
352         except (KeyError, StopIteration) as e:
353             raise ExtractorError('An extractor error has occurred.', cause=e)
354
355     def set_downloader(self, downloader):
356         """Sets the downloader for this IE."""
357         self._downloader = downloader
358
359     def _real_initialize(self):
360         """Real initialization process. Redefine in subclasses."""
361         pass
362
363     def _real_extract(self, url):
364         """Real extraction process. Redefine in subclasses."""
365         pass
366
367     @classmethod
368     def ie_key(cls):
369         """A string for getting the InfoExtractor with get_info_extractor"""
370         return compat_str(cls.__name__[:-2])
371
372     @property
373     def IE_NAME(self):
374         return compat_str(type(self).__name__[:-2])
375
376     def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
377         """ Returns the response handle """
378         if note is None:
379             self.report_download_webpage(video_id)
380         elif note is not False:
381             if video_id is None:
382                 self.to_screen('%s' % (note,))
383             else:
384                 self.to_screen('%s: %s' % (video_id, note))
385         if isinstance(url_or_request, compat_urllib_request.Request):
386             url_or_request = update_Request(
387                 url_or_request, data=data, headers=headers, query=query)
388         else:
389             if query:
390                 url_or_request = update_url_query(url_or_request, query)
391             if data is not None or headers:
392                 url_or_request = sanitized_Request(url_or_request, data, headers)
393         try:
394             return self._downloader.urlopen(url_or_request)
395         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
396             if errnote is False:
397                 return False
398             if errnote is None:
399                 errnote = 'Unable to download webpage'
400
401             errmsg = '%s: %s' % (errnote, error_to_compat_str(err))
402             if fatal:
403                 raise ExtractorError(errmsg, sys.exc_info()[2], cause=err)
404             else:
405                 self._downloader.report_warning(errmsg)
406                 return False
407
408     def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True, encoding=None, data=None, headers={}, query={}):
409         """ Returns a tuple (page content as string, URL handle) """
410         # Strip hashes from the URL (#1038)
411         if isinstance(url_or_request, (compat_str, str)):
412             url_or_request = url_or_request.partition('#')[0]
413
414         urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data, headers=headers, query=query)
415         if urlh is False:
416             assert not fatal
417             return False
418         content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal, encoding=encoding)
419         return (content, urlh)
420
421     @staticmethod
422     def _guess_encoding_from_content(content_type, webpage_bytes):
423         m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type)
424         if m:
425             encoding = m.group(1)
426         else:
427             m = re.search(br'<meta[^>]+charset=[\'"]?([^\'")]+)[ /\'">]',
428                           webpage_bytes[:1024])
429             if m:
430                 encoding = m.group(1).decode('ascii')
431             elif webpage_bytes.startswith(b'\xff\xfe'):
432                 encoding = 'utf-16'
433             else:
434                 encoding = 'utf-8'
435
436         return encoding
437
438     def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True, prefix=None, encoding=None):
439         content_type = urlh.headers.get('Content-Type', '')
440         webpage_bytes = urlh.read()
441         if prefix is not None:
442             webpage_bytes = prefix + webpage_bytes
443         if not encoding:
444             encoding = self._guess_encoding_from_content(content_type, webpage_bytes)
445         if self._downloader.params.get('dump_intermediate_pages', False):
446             try:
447                 url = url_or_request.get_full_url()
448             except AttributeError:
449                 url = url_or_request
450             self.to_screen('Dumping request to ' + url)
451             dump = base64.b64encode(webpage_bytes).decode('ascii')
452             self._downloader.to_screen(dump)
453         if self._downloader.params.get('write_pages', False):
454             try:
455                 url = url_or_request.get_full_url()
456             except AttributeError:
457                 url = url_or_request
458             basen = '%s_%s' % (video_id, url)
459             if len(basen) > 240:
460                 h = '___' + hashlib.md5(basen.encode('utf-8')).hexdigest()
461                 basen = basen[:240 - len(h)] + h
462             raw_filename = basen + '.dump'
463             filename = sanitize_filename(raw_filename, restricted=True)
464             self.to_screen('Saving request to ' + filename)
465             # Working around MAX_PATH limitation on Windows (see
466             # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
467             if compat_os_name == 'nt':
468                 absfilepath = os.path.abspath(filename)
469                 if len(absfilepath) > 259:
470                     filename = '\\\\?\\' + absfilepath
471             with open(filename, 'wb') as outf:
472                 outf.write(webpage_bytes)
473
474         try:
475             content = webpage_bytes.decode(encoding, 'replace')
476         except LookupError:
477             content = webpage_bytes.decode('utf-8', 'replace')
478
479         if ('<title>Access to this site is blocked</title>' in content and
480                 'Websense' in content[:512]):
481             msg = 'Access to this webpage has been blocked by Websense filtering software in your network.'
482             blocked_iframe = self._html_search_regex(
483                 r'<iframe src="([^"]+)"', content,
484                 'Websense information URL', default=None)
485             if blocked_iframe:
486                 msg += ' Visit %s for more details' % blocked_iframe
487             raise ExtractorError(msg, expected=True)
488         if '<title>The URL you requested has been blocked</title>' in content[:512]:
489             msg = (
490                 'Access to this webpage has been blocked by Indian censorship. '
491                 'Use a VPN or proxy server (with --proxy) to route around it.')
492             block_msg = self._html_search_regex(
493                 r'</h1><p>(.*?)</p>',
494                 content, 'block message', default=None)
495             if block_msg:
496                 msg += ' (Message: "%s")' % block_msg.replace('\n', ' ')
497             raise ExtractorError(msg, expected=True)
498
499         return content
500
501     def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, tries=1, timeout=5, encoding=None, data=None, headers={}, query={}):
502         """ Returns the data of the page as a string """
503         success = False
504         try_count = 0
505         while success is False:
506             try:
507                 res = self._download_webpage_handle(url_or_request, video_id, note, errnote, fatal, encoding=encoding, data=data, headers=headers, query=query)
508                 success = True
509             except compat_http_client.IncompleteRead as e:
510                 try_count += 1
511                 if try_count >= tries:
512                     raise e
513                 self._sleep(timeout, video_id)
514         if res is False:
515             return res
516         else:
517             content, _ = res
518             return content
519
520     def _download_xml(self, url_or_request, video_id,
521                       note='Downloading XML', errnote='Unable to download XML',
522                       transform_source=None, fatal=True, encoding=None, data=None, headers={}, query={}):
523         """Return the xml as an xml.etree.ElementTree.Element"""
524         xml_string = self._download_webpage(
525             url_or_request, video_id, note, errnote, fatal=fatal, encoding=encoding, data=data, headers=headers, query=query)
526         if xml_string is False:
527             return xml_string
528         if transform_source:
529             xml_string = transform_source(xml_string)
530         return compat_etree_fromstring(xml_string.encode('utf-8'))
531
532     def _download_json(self, url_or_request, video_id,
533                        note='Downloading JSON metadata',
534                        errnote='Unable to download JSON metadata',
535                        transform_source=None,
536                        fatal=True, encoding=None, data=None, headers={}, query={}):
537         json_string = self._download_webpage(
538             url_or_request, video_id, note, errnote, fatal=fatal,
539             encoding=encoding, data=data, headers=headers, query=query)
540         if (not fatal) and json_string is False:
541             return None
542         return self._parse_json(
543             json_string, video_id, transform_source=transform_source, fatal=fatal)
544
545     def _parse_json(self, json_string, video_id, transform_source=None, fatal=True):
546         if transform_source:
547             json_string = transform_source(json_string)
548         try:
549             return json.loads(json_string)
550         except ValueError as ve:
551             errmsg = '%s: Failed to parse JSON ' % video_id
552             if fatal:
553                 raise ExtractorError(errmsg, cause=ve)
554             else:
555                 self.report_warning(errmsg + str(ve))
556
557     def report_warning(self, msg, video_id=None):
558         idstr = '' if video_id is None else '%s: ' % video_id
559         self._downloader.report_warning(
560             '[%s] %s%s' % (self.IE_NAME, idstr, msg))
561
562     def to_screen(self, msg):
563         """Print msg to screen, prefixing it with '[ie_name]'"""
564         self._downloader.to_screen('[%s] %s' % (self.IE_NAME, msg))
565
566     def report_extraction(self, id_or_name):
567         """Report information extraction."""
568         self.to_screen('%s: Extracting information' % id_or_name)
569
570     def report_download_webpage(self, video_id):
571         """Report webpage download."""
572         self.to_screen('%s: Downloading webpage' % video_id)
573
574     def report_age_confirmation(self):
575         """Report attempt to confirm age."""
576         self.to_screen('Confirming age')
577
578     def report_login(self):
579         """Report attempt to log in."""
580         self.to_screen('Logging in')
581
582     @staticmethod
583     def raise_login_required(msg='This video is only available for registered users'):
584         raise ExtractorError(
585             '%s. Use --username and --password or --netrc to provide account credentials.' % msg,
586             expected=True)
587
588     @staticmethod
589     def raise_geo_restricted(msg='This video is not available from your location due to geo restriction'):
590         raise ExtractorError(
591             '%s. You might want to use --proxy to workaround.' % msg,
592             expected=True)
593
594     # Methods for following #608
595     @staticmethod
596     def url_result(url, ie=None, video_id=None, video_title=None):
597         """Returns a URL that points to a page that should be processed"""
598         # TODO: ie should be the class used for getting the info
599         video_info = {'_type': 'url',
600                       'url': url,
601                       'ie_key': ie}
602         if video_id is not None:
603             video_info['id'] = video_id
604         if video_title is not None:
605             video_info['title'] = video_title
606         return video_info
607
608     @staticmethod
609     def playlist_result(entries, playlist_id=None, playlist_title=None, playlist_description=None):
610         """Returns a playlist"""
611         video_info = {'_type': 'playlist',
612                       'entries': entries}
613         if playlist_id:
614             video_info['id'] = playlist_id
615         if playlist_title:
616             video_info['title'] = playlist_title
617         if playlist_description:
618             video_info['description'] = playlist_description
619         return video_info
620
621     def _search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
622         """
623         Perform a regex search on the given string, using a single or a list of
624         patterns returning the first matching group.
625         In case of failure return a default value or raise a WARNING or a
626         RegexNotFoundError, depending on fatal, specifying the field name.
627         """
628         if isinstance(pattern, (str, compat_str, compiled_regex_type)):
629             mobj = re.search(pattern, string, flags)
630         else:
631             for p in pattern:
632                 mobj = re.search(p, string, flags)
633                 if mobj:
634                     break
635
636         if not self._downloader.params.get('no_color') and compat_os_name != 'nt' and sys.stderr.isatty():
637             _name = '\033[0;34m%s\033[0m' % name
638         else:
639             _name = name
640
641         if mobj:
642             if group is None:
643                 # return the first matching group
644                 return next(g for g in mobj.groups() if g is not None)
645             else:
646                 return mobj.group(group)
647         elif default is not NO_DEFAULT:
648             return default
649         elif fatal:
650             raise RegexNotFoundError('Unable to extract %s' % _name)
651         else:
652             self._downloader.report_warning('unable to extract %s' % _name + bug_reports_message())
653             return None
654
655     def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
656         """
657         Like _search_regex, but strips HTML tags and unescapes entities.
658         """
659         res = self._search_regex(pattern, string, name, default, fatal, flags, group)
660         if res:
661             return clean_html(res).strip()
662         else:
663             return res
664
665     def _get_login_info(self):
666         """
667         Get the login info as (username, password)
668         It will look in the netrc file using the _NETRC_MACHINE value
669         If there's no info available, return (None, None)
670         """
671         if self._downloader is None:
672             return (None, None)
673
674         username = None
675         password = None
676         downloader_params = self._downloader.params
677
678         # Attempt to use provided username and password or .netrc data
679         if downloader_params.get('username') is not None:
680             username = downloader_params['username']
681             password = downloader_params['password']
682         elif downloader_params.get('usenetrc', False):
683             try:
684                 info = netrc.netrc().authenticators(self._NETRC_MACHINE)
685                 if info is not None:
686                     username = info[0]
687                     password = info[2]
688                 else:
689                     raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
690             except (IOError, netrc.NetrcParseError) as err:
691                 self._downloader.report_warning('parsing .netrc: %s' % error_to_compat_str(err))
692
693         return (username, password)
694
695     def _get_tfa_info(self, note='two-factor verification code'):
696         """
697         Get the two-factor authentication info
698         TODO - asking the user will be required for sms/phone verify
699         currently just uses the command line option
700         If there's no info available, return None
701         """
702         if self._downloader is None:
703             return None
704         downloader_params = self._downloader.params
705
706         if downloader_params.get('twofactor') is not None:
707             return downloader_params['twofactor']
708
709         return compat_getpass('Type %s and press [Return]: ' % note)
710
711     # Helper functions for extracting OpenGraph info
712     @staticmethod
713     def _og_regexes(prop):
714         content_re = r'content=(?:"([^"]+?)"|\'([^\']+?)\'|\s*([^\s"\'=<>`]+?))'
715         property_re = (r'(?:name|property)=(?:\'og:%(prop)s\'|"og:%(prop)s"|\s*og:%(prop)s\b)'
716                        % {'prop': re.escape(prop)})
717         template = r'<meta[^>]+?%s[^>]+?%s'
718         return [
719             template % (property_re, content_re),
720             template % (content_re, property_re),
721         ]
722
723     @staticmethod
724     def _meta_regex(prop):
725         return r'''(?isx)<meta
726                     (?=[^>]+(?:itemprop|name|property|id|http-equiv)=(["\']?)%s\1)
727                     [^>]+?content=(["\'])(?P<content>.*?)\2''' % re.escape(prop)
728
729     def _og_search_property(self, prop, html, name=None, **kargs):
730         if not isinstance(prop, (list, tuple)):
731             prop = [prop]
732         if name is None:
733             name = 'OpenGraph %s' % prop[0]
734         og_regexes = []
735         for p in prop:
736             og_regexes.extend(self._og_regexes(p))
737         escaped = self._search_regex(og_regexes, html, name, flags=re.DOTALL, **kargs)
738         if escaped is None:
739             return None
740         return unescapeHTML(escaped)
741
742     def _og_search_thumbnail(self, html, **kargs):
743         return self._og_search_property('image', html, 'thumbnail URL', fatal=False, **kargs)
744
745     def _og_search_description(self, html, **kargs):
746         return self._og_search_property('description', html, fatal=False, **kargs)
747
748     def _og_search_title(self, html, **kargs):
749         return self._og_search_property('title', html, **kargs)
750
751     def _og_search_video_url(self, html, name='video url', secure=True, **kargs):
752         regexes = self._og_regexes('video') + self._og_regexes('video:url')
753         if secure:
754             regexes = self._og_regexes('video:secure_url') + regexes
755         return self._html_search_regex(regexes, html, name, **kargs)
756
757     def _og_search_url(self, html, **kargs):
758         return self._og_search_property('url', html, **kargs)
759
760     def _html_search_meta(self, name, html, display_name=None, fatal=False, **kwargs):
761         if not isinstance(name, (list, tuple)):
762             name = [name]
763         if display_name is None:
764             display_name = name[0]
765         return self._html_search_regex(
766             [self._meta_regex(n) for n in name],
767             html, display_name, fatal=fatal, group='content', **kwargs)
768
769     def _dc_search_uploader(self, html):
770         return self._html_search_meta('dc.creator', html, 'uploader')
771
772     def _rta_search(self, html):
773         # See http://www.rtalabel.org/index.php?content=howtofaq#single
774         if re.search(r'(?ix)<meta\s+name="rating"\s+'
775                      r'     content="RTA-5042-1996-1400-1577-RTA"',
776                      html):
777             return 18
778         return 0
779
780     def _media_rating_search(self, html):
781         # See http://www.tjg-designs.com/WP/metadata-code-examples-adding-metadata-to-your-web-pages/
782         rating = self._html_search_meta('rating', html)
783
784         if not rating:
785             return None
786
787         RATING_TABLE = {
788             'safe for kids': 0,
789             'general': 8,
790             '14 years': 14,
791             'mature': 17,
792             'restricted': 19,
793         }
794         return RATING_TABLE.get(rating.lower())
795
796     def _family_friendly_search(self, html):
797         # See http://schema.org/VideoObject
798         family_friendly = self._html_search_meta('isFamilyFriendly', html)
799
800         if not family_friendly:
801             return None
802
803         RATING_TABLE = {
804             '1': 0,
805             'true': 0,
806             '0': 18,
807             'false': 18,
808         }
809         return RATING_TABLE.get(family_friendly.lower())
810
811     def _twitter_search_player(self, html):
812         return self._html_search_meta('twitter:player', html,
813                                       'twitter card player')
814
815     def _search_json_ld(self, html, video_id, expected_type=None, **kwargs):
816         json_ld = self._search_regex(
817             r'(?s)<script[^>]+type=(["\'])application/ld\+json\1[^>]*>(?P<json_ld>.+?)</script>',
818             html, 'JSON-LD', group='json_ld', **kwargs)
819         if not json_ld:
820             return {}
821         return self._json_ld(
822             json_ld, video_id, fatal=kwargs.get('fatal', True),
823             expected_type=expected_type)
824
825     def _json_ld(self, json_ld, video_id, fatal=True, expected_type=None):
826         if isinstance(json_ld, compat_str):
827             json_ld = self._parse_json(json_ld, video_id, fatal=fatal)
828         if not json_ld:
829             return {}
830         info = {}
831         if not isinstance(json_ld, (list, tuple, dict)):
832             return info
833         if isinstance(json_ld, dict):
834             json_ld = [json_ld]
835         for e in json_ld:
836             if e.get('@context') == 'http://schema.org':
837                 item_type = e.get('@type')
838                 if expected_type is not None and expected_type != item_type:
839                     return info
840                 if item_type == 'TVEpisode':
841                     info.update({
842                         'episode': unescapeHTML(e.get('name')),
843                         'episode_number': int_or_none(e.get('episodeNumber')),
844                         'description': unescapeHTML(e.get('description')),
845                     })
846                     part_of_season = e.get('partOfSeason')
847                     if isinstance(part_of_season, dict) and part_of_season.get('@type') == 'TVSeason':
848                         info['season_number'] = int_or_none(part_of_season.get('seasonNumber'))
849                     part_of_series = e.get('partOfSeries')
850                     if isinstance(part_of_series, dict) and part_of_series.get('@type') == 'TVSeries':
851                         info['series'] = unescapeHTML(part_of_series.get('name'))
852                 elif item_type == 'Article':
853                     info.update({
854                         'timestamp': parse_iso8601(e.get('datePublished')),
855                         'title': unescapeHTML(e.get('headline')),
856                         'description': unescapeHTML(e.get('articleBody')),
857                     })
858                 elif item_type == 'VideoObject':
859                     info.update({
860                         'url': e.get('contentUrl'),
861                         'title': unescapeHTML(e.get('name')),
862                         'description': unescapeHTML(e.get('description')),
863                         'thumbnail': e.get('thumbnailUrl'),
864                         'duration': parse_duration(e.get('duration')),
865                         'timestamp': unified_timestamp(e.get('uploadDate')),
866                         'filesize': float_or_none(e.get('contentSize')),
867                         'tbr': int_or_none(e.get('bitrate')),
868                         'width': int_or_none(e.get('width')),
869                         'height': int_or_none(e.get('height')),
870                     })
871                 break
872         return dict((k, v) for k, v in info.items() if v is not None)
873
874     @staticmethod
875     def _hidden_inputs(html):
876         html = re.sub(r'<!--(?:(?!<!--).)*-->', '', html)
877         hidden_inputs = {}
878         for input in re.findall(r'(?i)<input([^>]+)>', html):
879             if not re.search(r'type=(["\'])(?:hidden|submit)\1', input):
880                 continue
881             name = re.search(r'(?:name|id)=(["\'])(?P<value>.+?)\1', input)
882             if not name:
883                 continue
884             value = re.search(r'value=(["\'])(?P<value>.*?)\1', input)
885             if not value:
886                 continue
887             hidden_inputs[name.group('value')] = value.group('value')
888         return hidden_inputs
889
890     def _form_hidden_inputs(self, form_id, html):
891         form = self._search_regex(
892             r'(?is)<form[^>]+?id=(["\'])%s\1[^>]*>(?P<form>.+?)</form>' % form_id,
893             html, '%s form' % form_id, group='form')
894         return self._hidden_inputs(form)
895
896     def _sort_formats(self, formats, field_preference=None):
897         if not formats:
898             raise ExtractorError('No video formats found')
899
900         for f in formats:
901             # Automatically determine tbr when missing based on abr and vbr (improves
902             # formats sorting in some cases)
903             if 'tbr' not in f and f.get('abr') is not None and f.get('vbr') is not None:
904                 f['tbr'] = f['abr'] + f['vbr']
905
906         def _formats_key(f):
907             # TODO remove the following workaround
908             from ..utils import determine_ext
909             if not f.get('ext') and 'url' in f:
910                 f['ext'] = determine_ext(f['url'])
911
912             if isinstance(field_preference, (list, tuple)):
913                 return tuple(
914                     f.get(field)
915                     if f.get(field) is not None
916                     else ('' if field == 'format_id' else -1)
917                     for field in field_preference)
918
919             preference = f.get('preference')
920             if preference is None:
921                 preference = 0
922                 if f.get('ext') in ['f4f', 'f4m']:  # Not yet supported
923                     preference -= 0.5
924
925             protocol = f.get('protocol') or determine_protocol(f)
926             proto_preference = 0 if protocol in ['http', 'https'] else (-0.5 if protocol == 'rtsp' else -0.1)
927
928             if f.get('vcodec') == 'none':  # audio only
929                 preference -= 50
930                 if self._downloader.params.get('prefer_free_formats'):
931                     ORDER = ['aac', 'mp3', 'm4a', 'webm', 'ogg', 'opus']
932                 else:
933                     ORDER = ['webm', 'opus', 'ogg', 'mp3', 'aac', 'm4a']
934                 ext_preference = 0
935                 try:
936                     audio_ext_preference = ORDER.index(f['ext'])
937                 except ValueError:
938                     audio_ext_preference = -1
939             else:
940                 if f.get('acodec') == 'none':  # video only
941                     preference -= 40
942                 if self._downloader.params.get('prefer_free_formats'):
943                     ORDER = ['flv', 'mp4', 'webm']
944                 else:
945                     ORDER = ['webm', 'flv', 'mp4']
946                 try:
947                     ext_preference = ORDER.index(f['ext'])
948                 except ValueError:
949                     ext_preference = -1
950                 audio_ext_preference = 0
951
952             return (
953                 preference,
954                 f.get('language_preference') if f.get('language_preference') is not None else -1,
955                 f.get('quality') if f.get('quality') is not None else -1,
956                 f.get('tbr') if f.get('tbr') is not None else -1,
957                 f.get('filesize') if f.get('filesize') is not None else -1,
958                 f.get('vbr') if f.get('vbr') is not None else -1,
959                 f.get('height') if f.get('height') is not None else -1,
960                 f.get('width') if f.get('width') is not None else -1,
961                 proto_preference,
962                 ext_preference,
963                 f.get('abr') if f.get('abr') is not None else -1,
964                 audio_ext_preference,
965                 f.get('fps') if f.get('fps') is not None else -1,
966                 f.get('filesize_approx') if f.get('filesize_approx') is not None else -1,
967                 f.get('source_preference') if f.get('source_preference') is not None else -1,
968                 f.get('format_id') if f.get('format_id') is not None else '',
969             )
970         formats.sort(key=_formats_key)
971
972     def _check_formats(self, formats, video_id):
973         if formats:
974             formats[:] = filter(
975                 lambda f: self._is_valid_url(
976                     f['url'], video_id,
977                     item='%s video format' % f.get('format_id') if f.get('format_id') else 'video'),
978                 formats)
979
980     @staticmethod
981     def _remove_duplicate_formats(formats):
982         format_urls = set()
983         unique_formats = []
984         for f in formats:
985             if f['url'] not in format_urls:
986                 format_urls.add(f['url'])
987                 unique_formats.append(f)
988         formats[:] = unique_formats
989
990     def _is_valid_url(self, url, video_id, item='video'):
991         url = self._proto_relative_url(url, scheme='http:')
992         # For now assume non HTTP(S) URLs always valid
993         if not (url.startswith('http://') or url.startswith('https://')):
994             return True
995         try:
996             self._request_webpage(url, video_id, 'Checking %s URL' % item)
997             return True
998         except ExtractorError as e:
999             if isinstance(e.cause, compat_urllib_error.URLError):
1000                 self.to_screen(
1001                     '%s: %s URL is invalid, skipping' % (video_id, item))
1002                 return False
1003             raise
1004
1005     def http_scheme(self):
1006         """ Either "http:" or "https:", depending on the user's preferences """
1007         return (
1008             'http:'
1009             if self._downloader.params.get('prefer_insecure', False)
1010             else 'https:')
1011
1012     def _proto_relative_url(self, url, scheme=None):
1013         if url is None:
1014             return url
1015         if url.startswith('//'):
1016             if scheme is None:
1017                 scheme = self.http_scheme()
1018             return scheme + url
1019         else:
1020             return url
1021
1022     def _sleep(self, timeout, video_id, msg_template=None):
1023         if msg_template is None:
1024             msg_template = '%(video_id)s: Waiting for %(timeout)s seconds'
1025         msg = msg_template % {'video_id': video_id, 'timeout': timeout}
1026         self.to_screen(msg)
1027         time.sleep(timeout)
1028
1029     def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None,
1030                              transform_source=lambda s: fix_xml_ampersands(s).strip(),
1031                              fatal=True, m3u8_id=None):
1032         manifest = self._download_xml(
1033             manifest_url, video_id, 'Downloading f4m manifest',
1034             'Unable to download f4m manifest',
1035             # Some manifests may be malformed, e.g. prosiebensat1 generated manifests
1036             # (see https://github.com/rg3/youtube-dl/issues/6215#issuecomment-121704244)
1037             transform_source=transform_source,
1038             fatal=fatal)
1039
1040         if manifest is False:
1041             return []
1042
1043         return self._parse_f4m_formats(
1044             manifest, manifest_url, video_id, preference=preference, f4m_id=f4m_id,
1045             transform_source=transform_source, fatal=fatal, m3u8_id=m3u8_id)
1046
1047     def _parse_f4m_formats(self, manifest, manifest_url, video_id, preference=None, f4m_id=None,
1048                            transform_source=lambda s: fix_xml_ampersands(s).strip(),
1049                            fatal=True, m3u8_id=None):
1050         # currently youtube-dl cannot decode the playerVerificationChallenge as Akamai uses Adobe Alchemy
1051         akamai_pv = manifest.find('{http://ns.adobe.com/f4m/1.0}pv-2.0')
1052         if akamai_pv is not None and ';' in akamai_pv.text:
1053             playerVerificationChallenge = akamai_pv.text.split(';')[0]
1054             if playerVerificationChallenge.strip() != '':
1055                 return []
1056
1057         formats = []
1058         manifest_version = '1.0'
1059         media_nodes = manifest.findall('{http://ns.adobe.com/f4m/1.0}media')
1060         if not media_nodes:
1061             manifest_version = '2.0'
1062             media_nodes = manifest.findall('{http://ns.adobe.com/f4m/2.0}media')
1063         # Remove unsupported DRM protected media from final formats
1064         # rendition (see https://github.com/rg3/youtube-dl/issues/8573).
1065         media_nodes = remove_encrypted_media(media_nodes)
1066         if not media_nodes:
1067             return formats
1068         base_url = xpath_text(
1069             manifest, ['{http://ns.adobe.com/f4m/1.0}baseURL', '{http://ns.adobe.com/f4m/2.0}baseURL'],
1070             'base URL', default=None)
1071         if base_url:
1072             base_url = base_url.strip()
1073
1074         bootstrap_info = xpath_element(
1075             manifest, ['{http://ns.adobe.com/f4m/1.0}bootstrapInfo', '{http://ns.adobe.com/f4m/2.0}bootstrapInfo'],
1076             'bootstrap info', default=None)
1077
1078         for i, media_el in enumerate(media_nodes):
1079             tbr = int_or_none(media_el.attrib.get('bitrate'))
1080             width = int_or_none(media_el.attrib.get('width'))
1081             height = int_or_none(media_el.attrib.get('height'))
1082             format_id = '-'.join(filter(None, [f4m_id, compat_str(i if tbr is None else tbr)]))
1083             # If <bootstrapInfo> is present, the specified f4m is a
1084             # stream-level manifest, and only set-level manifests may refer to
1085             # external resources.  See section 11.4 and section 4 of F4M spec
1086             if bootstrap_info is None:
1087                 media_url = None
1088                 # @href is introduced in 2.0, see section 11.6 of F4M spec
1089                 if manifest_version == '2.0':
1090                     media_url = media_el.attrib.get('href')
1091                 if media_url is None:
1092                     media_url = media_el.attrib.get('url')
1093                 if not media_url:
1094                     continue
1095                 manifest_url = (
1096                     media_url if media_url.startswith('http://') or media_url.startswith('https://')
1097                     else ((base_url or '/'.join(manifest_url.split('/')[:-1])) + '/' + media_url))
1098                 # If media_url is itself a f4m manifest do the recursive extraction
1099                 # since bitrates in parent manifest (this one) and media_url manifest
1100                 # may differ leading to inability to resolve the format by requested
1101                 # bitrate in f4m downloader
1102                 ext = determine_ext(manifest_url)
1103                 if ext == 'f4m':
1104                     f4m_formats = self._extract_f4m_formats(
1105                         manifest_url, video_id, preference=preference, f4m_id=f4m_id,
1106                         transform_source=transform_source, fatal=fatal)
1107                     # Sometimes stream-level manifest contains single media entry that
1108                     # does not contain any quality metadata (e.g. http://matchtv.ru/#live-player).
1109                     # At the same time parent's media entry in set-level manifest may
1110                     # contain it. We will copy it from parent in such cases.
1111                     if len(f4m_formats) == 1:
1112                         f = f4m_formats[0]
1113                         f.update({
1114                             'tbr': f.get('tbr') or tbr,
1115                             'width': f.get('width') or width,
1116                             'height': f.get('height') or height,
1117                             'format_id': f.get('format_id') if not tbr else format_id,
1118                         })
1119                     formats.extend(f4m_formats)
1120                     continue
1121                 elif ext == 'm3u8':
1122                     formats.extend(self._extract_m3u8_formats(
1123                         manifest_url, video_id, 'mp4', preference=preference,
1124                         m3u8_id=m3u8_id, fatal=fatal))
1125                     continue
1126             formats.append({
1127                 'format_id': format_id,
1128                 'url': manifest_url,
1129                 'ext': 'flv' if bootstrap_info is not None else None,
1130                 'tbr': tbr,
1131                 'width': width,
1132                 'height': height,
1133                 'preference': preference,
1134             })
1135         return formats
1136
1137     def _m3u8_meta_format(self, m3u8_url, ext=None, preference=None, m3u8_id=None):
1138         return {
1139             'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
1140             'url': m3u8_url,
1141             'ext': ext,
1142             'protocol': 'm3u8',
1143             'preference': -100,
1144             'resolution': 'multiple',
1145             'format_note': 'Quality selection URL',
1146         }
1147
1148     def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
1149                               entry_protocol='m3u8', preference=None,
1150                               m3u8_id=None, note=None, errnote=None,
1151                               fatal=True, live=False):
1152
1153         formats = [self._m3u8_meta_format(m3u8_url, ext, preference, m3u8_id)]
1154
1155         format_url = lambda u: (
1156             u
1157             if re.match(r'^https?://', u)
1158             else compat_urlparse.urljoin(m3u8_url, u))
1159
1160         res = self._download_webpage_handle(
1161             m3u8_url, video_id,
1162             note=note or 'Downloading m3u8 information',
1163             errnote=errnote or 'Failed to download m3u8 information',
1164             fatal=fatal)
1165         if res is False:
1166             return []
1167         m3u8_doc, urlh = res
1168         m3u8_url = urlh.geturl()
1169
1170         # We should try extracting formats only from master playlists [1], i.e.
1171         # playlists that describe available qualities. On the other hand media
1172         # playlists [2] should be returned as is since they contain just the media
1173         # without qualities renditions.
1174         # Fortunately, master playlist can be easily distinguished from media
1175         # playlist based on particular tags availability. As of [1, 2] master
1176         # playlist tags MUST NOT appear in a media playist and vice versa.
1177         # As of [3] #EXT-X-TARGETDURATION tag is REQUIRED for every media playlist
1178         # and MUST NOT appear in master playlist thus we can clearly detect media
1179         # playlist with this criterion.
1180         # 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.4
1181         # 2. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3
1182         # 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.1
1183         if '#EXT-X-TARGETDURATION' in m3u8_doc:  # media playlist, return as is
1184             return [{
1185                 'url': m3u8_url,
1186                 'format_id': m3u8_id,
1187                 'ext': ext,
1188                 'protocol': entry_protocol,
1189                 'preference': preference,
1190             }]
1191         last_info = None
1192         last_media = None
1193         for line in m3u8_doc.splitlines():
1194             if line.startswith('#EXT-X-STREAM-INF:'):
1195                 last_info = parse_m3u8_attributes(line)
1196             elif line.startswith('#EXT-X-MEDIA:'):
1197                 last_media = parse_m3u8_attributes(line)
1198             elif line.startswith('#') or not line.strip():
1199                 continue
1200             else:
1201                 if last_info is None:
1202                     formats.append({'url': format_url(line)})
1203                     continue
1204                 tbr = int_or_none(last_info.get('BANDWIDTH'), scale=1000)
1205                 format_id = []
1206                 if m3u8_id:
1207                     format_id.append(m3u8_id)
1208                 last_media_name = last_media.get('NAME') if last_media and last_media.get('TYPE') not in ('SUBTITLES', 'CLOSED-CAPTIONS') else None
1209                 # Despite specification does not mention NAME attribute for
1210                 # EXT-X-STREAM-INF it still sometimes may be present
1211                 stream_name = last_info.get('NAME') or last_media_name
1212                 # Bandwidth of live streams may differ over time thus making
1213                 # format_id unpredictable. So it's better to keep provided
1214                 # format_id intact.
1215                 if not live:
1216                     format_id.append(stream_name if stream_name else '%d' % (tbr if tbr else len(formats)))
1217                 f = {
1218                     'format_id': '-'.join(format_id),
1219                     'url': format_url(line.strip()),
1220                     'tbr': tbr,
1221                     'ext': ext,
1222                     'fps': float_or_none(last_info.get('FRAME-RATE')),
1223                     'protocol': entry_protocol,
1224                     'preference': preference,
1225                 }
1226                 resolution = last_info.get('RESOLUTION')
1227                 if resolution:
1228                     width_str, height_str = resolution.split('x')
1229                     f['width'] = int(width_str)
1230                     f['height'] = int(height_str)
1231                 # Unified Streaming Platform
1232                 mobj = re.search(
1233                     r'audio.*?(?:%3D|=)(\d+)(?:-video.*?(?:%3D|=)(\d+))?', f['url'])
1234                 if mobj:
1235                     abr, vbr = mobj.groups()
1236                     abr, vbr = float_or_none(abr, 1000), float_or_none(vbr, 1000)
1237                     f.update({
1238                         'vbr': vbr,
1239                         'abr': abr,
1240                     })
1241                 f.update(parse_codecs(last_info.get('CODECS')))
1242                 if last_media is not None:
1243                     f['m3u8_media'] = last_media
1244                     last_media = None
1245                 formats.append(f)
1246                 last_info = {}
1247         return formats
1248
1249     @staticmethod
1250     def _xpath_ns(path, namespace=None):
1251         if not namespace:
1252             return path
1253         out = []
1254         for c in path.split('/'):
1255             if not c or c == '.':
1256                 out.append(c)
1257             else:
1258                 out.append('{%s}%s' % (namespace, c))
1259         return '/'.join(out)
1260
1261     def _extract_smil_formats(self, smil_url, video_id, fatal=True, f4m_params=None, transform_source=None):
1262         smil = self._download_smil(smil_url, video_id, fatal=fatal, transform_source=transform_source)
1263
1264         if smil is False:
1265             assert not fatal
1266             return []
1267
1268         namespace = self._parse_smil_namespace(smil)
1269
1270         return self._parse_smil_formats(
1271             smil, smil_url, video_id, namespace=namespace, f4m_params=f4m_params)
1272
1273     def _extract_smil_info(self, smil_url, video_id, fatal=True, f4m_params=None):
1274         smil = self._download_smil(smil_url, video_id, fatal=fatal)
1275         if smil is False:
1276             return {}
1277         return self._parse_smil(smil, smil_url, video_id, f4m_params=f4m_params)
1278
1279     def _download_smil(self, smil_url, video_id, fatal=True, transform_source=None):
1280         return self._download_xml(
1281             smil_url, video_id, 'Downloading SMIL file',
1282             'Unable to download SMIL file', fatal=fatal, transform_source=transform_source)
1283
1284     def _parse_smil(self, smil, smil_url, video_id, f4m_params=None):
1285         namespace = self._parse_smil_namespace(smil)
1286
1287         formats = self._parse_smil_formats(
1288             smil, smil_url, video_id, namespace=namespace, f4m_params=f4m_params)
1289         subtitles = self._parse_smil_subtitles(smil, namespace=namespace)
1290
1291         video_id = os.path.splitext(url_basename(smil_url))[0]
1292         title = None
1293         description = None
1294         upload_date = None
1295         for meta in smil.findall(self._xpath_ns('./head/meta', namespace)):
1296             name = meta.attrib.get('name')
1297             content = meta.attrib.get('content')
1298             if not name or not content:
1299                 continue
1300             if not title and name == 'title':
1301                 title = content
1302             elif not description and name in ('description', 'abstract'):
1303                 description = content
1304             elif not upload_date and name == 'date':
1305                 upload_date = unified_strdate(content)
1306
1307         thumbnails = [{
1308             'id': image.get('type'),
1309             'url': image.get('src'),
1310             'width': int_or_none(image.get('width')),
1311             'height': int_or_none(image.get('height')),
1312         } for image in smil.findall(self._xpath_ns('.//image', namespace)) if image.get('src')]
1313
1314         return {
1315             'id': video_id,
1316             'title': title or video_id,
1317             'description': description,
1318             'upload_date': upload_date,
1319             'thumbnails': thumbnails,
1320             'formats': formats,
1321             'subtitles': subtitles,
1322         }
1323
1324     def _parse_smil_namespace(self, smil):
1325         return self._search_regex(
1326             r'(?i)^{([^}]+)?}smil$', smil.tag, 'namespace', default=None)
1327
1328     def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
1329         base = smil_url
1330         for meta in smil.findall(self._xpath_ns('./head/meta', namespace)):
1331             b = meta.get('base') or meta.get('httpBase')
1332             if b:
1333                 base = b
1334                 break
1335
1336         formats = []
1337         rtmp_count = 0
1338         http_count = 0
1339         m3u8_count = 0
1340
1341         srcs = []
1342         media = smil.findall(self._xpath_ns('.//video', namespace)) + smil.findall(self._xpath_ns('.//audio', namespace))
1343         for medium in media:
1344             src = medium.get('src')
1345             if not src or src in srcs:
1346                 continue
1347             srcs.append(src)
1348
1349             bitrate = float_or_none(medium.get('system-bitrate') or medium.get('systemBitrate'), 1000)
1350             filesize = int_or_none(medium.get('size') or medium.get('fileSize'))
1351             width = int_or_none(medium.get('width'))
1352             height = int_or_none(medium.get('height'))
1353             proto = medium.get('proto')
1354             ext = medium.get('ext')
1355             src_ext = determine_ext(src)
1356             streamer = medium.get('streamer') or base
1357
1358             if proto == 'rtmp' or streamer.startswith('rtmp'):
1359                 rtmp_count += 1
1360                 formats.append({
1361                     'url': streamer,
1362                     'play_path': src,
1363                     'ext': 'flv',
1364                     'format_id': 'rtmp-%d' % (rtmp_count if bitrate is None else bitrate),
1365                     'tbr': bitrate,
1366                     'filesize': filesize,
1367                     'width': width,
1368                     'height': height,
1369                 })
1370                 if transform_rtmp_url:
1371                     streamer, src = transform_rtmp_url(streamer, src)
1372                     formats[-1].update({
1373                         'url': streamer,
1374                         'play_path': src,
1375                     })
1376                 continue
1377
1378             src_url = src if src.startswith('http') else compat_urlparse.urljoin(base, src)
1379             src_url = src_url.strip()
1380
1381             if proto == 'm3u8' or src_ext == 'm3u8':
1382                 m3u8_formats = self._extract_m3u8_formats(
1383                     src_url, video_id, ext or 'mp4', m3u8_id='hls', fatal=False)
1384                 if len(m3u8_formats) == 1:
1385                     m3u8_count += 1
1386                     m3u8_formats[0].update({
1387                         'format_id': 'hls-%d' % (m3u8_count if bitrate is None else bitrate),
1388                         'tbr': bitrate,
1389                         'width': width,
1390                         'height': height,
1391                     })
1392                 formats.extend(m3u8_formats)
1393                 continue
1394
1395             if src_ext == 'f4m':
1396                 f4m_url = src_url
1397                 if not f4m_params:
1398                     f4m_params = {
1399                         'hdcore': '3.2.0',
1400                         'plugin': 'flowplayer-3.2.0.1',
1401                     }
1402                 f4m_url += '&' if '?' in f4m_url else '?'
1403                 f4m_url += compat_urllib_parse_urlencode(f4m_params)
1404                 formats.extend(self._extract_f4m_formats(f4m_url, video_id, f4m_id='hds', fatal=False))
1405                 continue
1406
1407             if src_url.startswith('http') and self._is_valid_url(src, video_id):
1408                 http_count += 1
1409                 formats.append({
1410                     'url': src_url,
1411                     'ext': ext or src_ext or 'flv',
1412                     'format_id': 'http-%d' % (bitrate or http_count),
1413                     'tbr': bitrate,
1414                     'filesize': filesize,
1415                     'width': width,
1416                     'height': height,
1417                 })
1418                 continue
1419
1420         return formats
1421
1422     def _parse_smil_subtitles(self, smil, namespace=None, subtitles_lang='en'):
1423         urls = []
1424         subtitles = {}
1425         for num, textstream in enumerate(smil.findall(self._xpath_ns('.//textstream', namespace))):
1426             src = textstream.get('src')
1427             if not src or src in urls:
1428                 continue
1429             urls.append(src)
1430             ext = textstream.get('ext') or mimetype2ext(textstream.get('type')) or determine_ext(src)
1431             lang = textstream.get('systemLanguage') or textstream.get('systemLanguageName') or textstream.get('lang') or subtitles_lang
1432             subtitles.setdefault(lang, []).append({
1433                 'url': src,
1434                 'ext': ext,
1435             })
1436         return subtitles
1437
1438     def _extract_xspf_playlist(self, playlist_url, playlist_id, fatal=True):
1439         xspf = self._download_xml(
1440             playlist_url, playlist_id, 'Downloading xpsf playlist',
1441             'Unable to download xspf manifest', fatal=fatal)
1442         if xspf is False:
1443             return []
1444         return self._parse_xspf(xspf, playlist_id)
1445
1446     def _parse_xspf(self, playlist, playlist_id):
1447         NS_MAP = {
1448             'xspf': 'http://xspf.org/ns/0/',
1449             's1': 'http://static.streamone.nl/player/ns/0',
1450         }
1451
1452         entries = []
1453         for track in playlist.findall(xpath_with_ns('./xspf:trackList/xspf:track', NS_MAP)):
1454             title = xpath_text(
1455                 track, xpath_with_ns('./xspf:title', NS_MAP), 'title', default=playlist_id)
1456             description = xpath_text(
1457                 track, xpath_with_ns('./xspf:annotation', NS_MAP), 'description')
1458             thumbnail = xpath_text(
1459                 track, xpath_with_ns('./xspf:image', NS_MAP), 'thumbnail')
1460             duration = float_or_none(
1461                 xpath_text(track, xpath_with_ns('./xspf:duration', NS_MAP), 'duration'), 1000)
1462
1463             formats = [{
1464                 'url': location.text,
1465                 'format_id': location.get(xpath_with_ns('s1:label', NS_MAP)),
1466                 'width': int_or_none(location.get(xpath_with_ns('s1:width', NS_MAP))),
1467                 'height': int_or_none(location.get(xpath_with_ns('s1:height', NS_MAP))),
1468             } for location in track.findall(xpath_with_ns('./xspf:location', NS_MAP))]
1469             self._sort_formats(formats)
1470
1471             entries.append({
1472                 'id': playlist_id,
1473                 'title': title,
1474                 'description': description,
1475                 'thumbnail': thumbnail,
1476                 'duration': duration,
1477                 'formats': formats,
1478             })
1479         return entries
1480
1481     def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, formats_dict={}):
1482         res = self._download_webpage_handle(
1483             mpd_url, video_id,
1484             note=note or 'Downloading MPD manifest',
1485             errnote=errnote or 'Failed to download MPD manifest',
1486             fatal=fatal)
1487         if res is False:
1488             return []
1489         mpd, urlh = res
1490         mpd_base_url = re.match(r'https?://.+/', urlh.geturl()).group()
1491
1492         return self._parse_mpd_formats(
1493             compat_etree_fromstring(mpd.encode('utf-8')), mpd_id, mpd_base_url, formats_dict=formats_dict)
1494
1495     def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', formats_dict={}):
1496         """
1497         Parse formats from MPD manifest.
1498         References:
1499          1. MPEG-DASH Standard, ISO/IEC 23009-1:2014(E),
1500             http://standards.iso.org/ittf/PubliclyAvailableStandards/c065274_ISO_IEC_23009-1_2014.zip
1501          2. https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP
1502         """
1503         if mpd_doc.get('type') == 'dynamic':
1504             return []
1505
1506         namespace = self._search_regex(r'(?i)^{([^}]+)?}MPD$', mpd_doc.tag, 'namespace', default=None)
1507
1508         def _add_ns(path):
1509             return self._xpath_ns(path, namespace)
1510
1511         def is_drm_protected(element):
1512             return element.find(_add_ns('ContentProtection')) is not None
1513
1514         def extract_multisegment_info(element, ms_parent_info):
1515             ms_info = ms_parent_info.copy()
1516             segment_list = element.find(_add_ns('SegmentList'))
1517             if segment_list is not None:
1518                 segment_urls_e = segment_list.findall(_add_ns('SegmentURL'))
1519                 if segment_urls_e:
1520                     ms_info['segment_urls'] = [segment.attrib['media'] for segment in segment_urls_e]
1521                 initialization = segment_list.find(_add_ns('Initialization'))
1522                 if initialization is not None:
1523                     ms_info['initialization_url'] = initialization.attrib['sourceURL']
1524             else:
1525                 segment_template = element.find(_add_ns('SegmentTemplate'))
1526                 if segment_template is not None:
1527                     start_number = segment_template.get('startNumber')
1528                     if start_number:
1529                         ms_info['start_number'] = int(start_number)
1530                     segment_timeline = segment_template.find(_add_ns('SegmentTimeline'))
1531                     if segment_timeline is not None:
1532                         s_e = segment_timeline.findall(_add_ns('S'))
1533                         if s_e:
1534                             ms_info['total_number'] = 0
1535                             ms_info['s'] = []
1536                             for s in s_e:
1537                                 r = int(s.get('r', 0))
1538                                 ms_info['total_number'] += 1 + r
1539                                 ms_info['s'].append({
1540                                     't': int(s.get('t', 0)),
1541                                     # @d is mandatory (see [1, 5.3.9.6.2, Table 17, page 60])
1542                                     'd': int(s.attrib['d']),
1543                                     'r': r,
1544                                 })
1545                     else:
1546                         timescale = segment_template.get('timescale')
1547                         if timescale:
1548                             ms_info['timescale'] = int(timescale)
1549                         segment_duration = segment_template.get('duration')
1550                         if segment_duration:
1551                             ms_info['segment_duration'] = int(segment_duration)
1552                     media_template = segment_template.get('media')
1553                     if media_template:
1554                         ms_info['media_template'] = media_template
1555                     initialization = segment_template.get('initialization')
1556                     if initialization:
1557                         ms_info['initialization_url'] = initialization
1558                     else:
1559                         initialization = segment_template.find(_add_ns('Initialization'))
1560                         if initialization is not None:
1561                             ms_info['initialization_url'] = initialization.attrib['sourceURL']
1562             return ms_info
1563
1564         mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
1565         formats = []
1566         for period in mpd_doc.findall(_add_ns('Period')):
1567             period_duration = parse_duration(period.get('duration')) or mpd_duration
1568             period_ms_info = extract_multisegment_info(period, {
1569                 'start_number': 1,
1570                 'timescale': 1,
1571             })
1572             for adaptation_set in period.findall(_add_ns('AdaptationSet')):
1573                 if is_drm_protected(adaptation_set):
1574                     continue
1575                 adaption_set_ms_info = extract_multisegment_info(adaptation_set, period_ms_info)
1576                 for representation in adaptation_set.findall(_add_ns('Representation')):
1577                     if is_drm_protected(representation):
1578                         continue
1579                     representation_attrib = adaptation_set.attrib.copy()
1580                     representation_attrib.update(representation.attrib)
1581                     # According to [1, 5.3.7.2, Table 9, page 41], @mimeType is mandatory
1582                     mime_type = representation_attrib['mimeType']
1583                     content_type = mime_type.split('/')[0]
1584                     if content_type == 'text':
1585                         # TODO implement WebVTT downloading
1586                         pass
1587                     elif content_type == 'video' or content_type == 'audio':
1588                         base_url = ''
1589                         for element in (representation, adaptation_set, period, mpd_doc):
1590                             base_url_e = element.find(_add_ns('BaseURL'))
1591                             if base_url_e is not None:
1592                                 base_url = base_url_e.text + base_url
1593                                 if re.match(r'^https?://', base_url):
1594                                     break
1595                         if mpd_base_url and not re.match(r'^https?://', base_url):
1596                             if not mpd_base_url.endswith('/') and not base_url.startswith('/'):
1597                                 mpd_base_url += '/'
1598                             base_url = mpd_base_url + base_url
1599                         representation_id = representation_attrib.get('id')
1600                         lang = representation_attrib.get('lang')
1601                         url_el = representation.find(_add_ns('BaseURL'))
1602                         filesize = int_or_none(url_el.attrib.get('{http://youtube.com/yt/2012/10/10}contentLength') if url_el is not None else None)
1603                         f = {
1604                             'format_id': '%s-%s' % (mpd_id, representation_id) if mpd_id else representation_id,
1605                             'url': base_url,
1606                             'ext': mimetype2ext(mime_type),
1607                             'width': int_or_none(representation_attrib.get('width')),
1608                             'height': int_or_none(representation_attrib.get('height')),
1609                             'tbr': int_or_none(representation_attrib.get('bandwidth'), 1000),
1610                             'asr': int_or_none(representation_attrib.get('audioSamplingRate')),
1611                             'fps': int_or_none(representation_attrib.get('frameRate')),
1612                             'vcodec': 'none' if content_type == 'audio' else representation_attrib.get('codecs'),
1613                             'acodec': 'none' if content_type == 'video' else representation_attrib.get('codecs'),
1614                             'language': lang if lang not in ('mul', 'und', 'zxx', 'mis') else None,
1615                             'format_note': 'DASH %s' % content_type,
1616                             'filesize': filesize,
1617                         }
1618                         representation_ms_info = extract_multisegment_info(representation, adaption_set_ms_info)
1619                         if 'segment_urls' not in representation_ms_info and 'media_template' in representation_ms_info:
1620                             if 'total_number' not in representation_ms_info and 'segment_duration':
1621                                 segment_duration = float(representation_ms_info['segment_duration']) / float(representation_ms_info['timescale'])
1622                                 representation_ms_info['total_number'] = int(math.ceil(float(period_duration) / segment_duration))
1623                             media_template = representation_ms_info['media_template']
1624                             media_template = media_template.replace('$RepresentationID$', representation_id)
1625                             media_template = re.sub(r'\$(Number|Bandwidth|Time)\$', r'%(\1)d', media_template)
1626                             media_template = re.sub(r'\$(Number|Bandwidth|Time)%([^$]+)\$', r'%(\1)\2', media_template)
1627                             media_template.replace('$$', '$')
1628
1629                             # As per [1, 5.3.9.4.4, Table 16, page 55] $Number$ and $Time$
1630                             # can't be used at the same time
1631                             if '%(Number' in media_template:
1632                                 representation_ms_info['segment_urls'] = [
1633                                     media_template % {
1634                                         'Number': segment_number,
1635                                         'Bandwidth': representation_attrib.get('bandwidth'),
1636                                     }
1637                                     for segment_number in range(
1638                                         representation_ms_info['start_number'],
1639                                         representation_ms_info['total_number'] + representation_ms_info['start_number'])]
1640                             else:
1641                                 representation_ms_info['segment_urls'] = []
1642                                 segment_time = 0
1643
1644                                 def add_segment_url():
1645                                     representation_ms_info['segment_urls'].append(
1646                                         media_template % {
1647                                             'Time': segment_time,
1648                                             'Bandwidth': representation_attrib.get('bandwidth'),
1649                                         }
1650                                     )
1651
1652                                 for num, s in enumerate(representation_ms_info['s']):
1653                                     segment_time = s.get('t') or segment_time
1654                                     add_segment_url()
1655                                     for r in range(s.get('r', 0)):
1656                                         segment_time += s['d']
1657                                         add_segment_url()
1658                                     segment_time += s['d']
1659                         if 'segment_urls' in representation_ms_info:
1660                             f.update({
1661                                 'segment_urls': representation_ms_info['segment_urls'],
1662                                 'protocol': 'http_dash_segments',
1663                             })
1664                             if 'initialization_url' in representation_ms_info:
1665                                 initialization_url = representation_ms_info['initialization_url'].replace('$RepresentationID$', representation_id)
1666                                 f.update({
1667                                     'initialization_url': initialization_url,
1668                                 })
1669                                 if not f.get('url'):
1670                                     f['url'] = initialization_url
1671                         try:
1672                             existing_format = next(
1673                                 fo for fo in formats
1674                                 if fo['format_id'] == representation_id)
1675                         except StopIteration:
1676                             full_info = formats_dict.get(representation_id, {}).copy()
1677                             full_info.update(f)
1678                             formats.append(full_info)
1679                         else:
1680                             existing_format.update(f)
1681                     else:
1682                         self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
1683         return formats
1684
1685     def _parse_html5_media_entries(self, base_url, webpage):
1686         def absolute_url(video_url):
1687             return compat_urlparse.urljoin(base_url, video_url)
1688
1689         def parse_content_type(content_type):
1690             if not content_type:
1691                 return {}
1692             ctr = re.search(r'(?P<mimetype>[^/]+/[^;]+)(?:;\s*codecs="?(?P<codecs>[^"]+))?', content_type)
1693             if ctr:
1694                 mimetype, codecs = ctr.groups()
1695                 f = parse_codecs(codecs)
1696                 f['ext'] = mimetype2ext(mimetype)
1697                 return f
1698             return {}
1699
1700         entries = []
1701         for media_tag, media_type, media_content in re.findall(r'(?s)(<(?P<tag>video|audio)[^>]*>)(.*?)</(?P=tag)>', webpage):
1702             media_info = {
1703                 'formats': [],
1704                 'subtitles': {},
1705             }
1706             media_attributes = extract_attributes(media_tag)
1707             src = media_attributes.get('src')
1708             if src:
1709                 media_info['formats'].append({
1710                     'url': absolute_url(src),
1711                     'vcodec': 'none' if media_type == 'audio' else None,
1712                 })
1713             media_info['thumbnail'] = media_attributes.get('poster')
1714             if media_content:
1715                 for source_tag in re.findall(r'<source[^>]+>', media_content):
1716                     source_attributes = extract_attributes(source_tag)
1717                     src = source_attributes.get('src')
1718                     if not src:
1719                         continue
1720                     f = parse_content_type(source_attributes.get('type'))
1721                     f.update({
1722                         'url': absolute_url(src),
1723                         'vcodec': 'none' if media_type == 'audio' else None,
1724                     })
1725                     media_info['formats'].append(f)
1726                 for track_tag in re.findall(r'<track[^>]+>', media_content):
1727                     track_attributes = extract_attributes(track_tag)
1728                     kind = track_attributes.get('kind')
1729                     if not kind or kind == 'subtitles':
1730                         src = track_attributes.get('src')
1731                         if not src:
1732                             continue
1733                         lang = track_attributes.get('srclang') or track_attributes.get('lang') or track_attributes.get('label')
1734                         media_info['subtitles'].setdefault(lang, []).append({
1735                             'url': absolute_url(src),
1736                         })
1737             if media_info['formats']:
1738                 entries.append(media_info)
1739         return entries
1740
1741     def _live_title(self, name):
1742         """ Generate the title for a live video """
1743         now = datetime.datetime.now()
1744         now_str = now.strftime('%Y-%m-%d %H:%M')
1745         return name + ' ' + now_str
1746
1747     def _int(self, v, name, fatal=False, **kwargs):
1748         res = int_or_none(v, **kwargs)
1749         if 'get_attr' in kwargs:
1750             print(getattr(v, kwargs['get_attr']))
1751         if res is None:
1752             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
1753             if fatal:
1754                 raise ExtractorError(msg)
1755             else:
1756                 self._downloader.report_warning(msg)
1757         return res
1758
1759     def _float(self, v, name, fatal=False, **kwargs):
1760         res = float_or_none(v, **kwargs)
1761         if res is None:
1762             msg = 'Failed to extract %s: Could not parse value %r' % (name, v)
1763             if fatal:
1764                 raise ExtractorError(msg)
1765             else:
1766                 self._downloader.report_warning(msg)
1767         return res
1768
1769     def _set_cookie(self, domain, name, value, expire_time=None):
1770         cookie = compat_cookiejar.Cookie(
1771             0, name, value, None, None, domain, None,
1772             None, '/', True, False, expire_time, '', None, None, None)
1773         self._downloader.cookiejar.set_cookie(cookie)
1774
1775     def _get_cookies(self, url):
1776         """ Return a compat_cookies.SimpleCookie with the cookies for the url """
1777         req = sanitized_Request(url)
1778         self._downloader.cookiejar.add_cookie_header(req)
1779         return compat_cookies.SimpleCookie(req.get_header('Cookie'))
1780
1781     def get_testcases(self, include_onlymatching=False):
1782         t = getattr(self, '_TEST', None)
1783         if t:
1784             assert not hasattr(self, '_TESTS'), \
1785                 '%s has _TEST and _TESTS' % type(self).__name__
1786             tests = [t]
1787         else:
1788             tests = getattr(self, '_TESTS', [])
1789         for t in tests:
1790             if not include_onlymatching and t.get('only_matching', False):
1791                 continue
1792             t['name'] = type(self).__name__[:-len('IE')]
1793             yield t
1794
1795     def is_suitable(self, age_limit):
1796         """ Test whether the extractor is generally suitable for the given
1797         age limit (i.e. pornographic sites are not, all others usually are) """
1798
1799         any_restricted = False
1800         for tc in self.get_testcases(include_onlymatching=False):
1801             if tc.get('playlist', []):
1802                 tc = tc['playlist'][0]
1803             is_restricted = age_restricted(
1804                 tc.get('info_dict', {}).get('age_limit'), age_limit)
1805             if not is_restricted:
1806                 return True
1807             any_restricted = any_restricted or is_restricted
1808         return not any_restricted
1809
1810     def extract_subtitles(self, *args, **kwargs):
1811         if (self._downloader.params.get('writesubtitles', False) or
1812                 self._downloader.params.get('listsubtitles')):
1813             return self._get_subtitles(*args, **kwargs)
1814         return {}
1815
1816     def _get_subtitles(self, *args, **kwargs):
1817         raise NotImplementedError('This method must be implemented by subclasses')
1818
1819     @staticmethod
1820     def _merge_subtitle_items(subtitle_list1, subtitle_list2):
1821         """ Merge subtitle items for one language. Items with duplicated URLs
1822         will be dropped. """
1823         list1_urls = set([item['url'] for item in subtitle_list1])
1824         ret = list(subtitle_list1)
1825         ret.extend([item for item in subtitle_list2 if item['url'] not in list1_urls])
1826         return ret
1827
1828     @classmethod
1829     def _merge_subtitles(cls, subtitle_dict1, subtitle_dict2):
1830         """ Merge two subtitle dictionaries, language by language. """
1831         ret = dict(subtitle_dict1)
1832         for lang in subtitle_dict2:
1833             ret[lang] = cls._merge_subtitle_items(subtitle_dict1.get(lang, []), subtitle_dict2[lang])
1834         return ret
1835
1836     def extract_automatic_captions(self, *args, **kwargs):
1837         if (self._downloader.params.get('writeautomaticsub', False) or
1838                 self._downloader.params.get('listsubtitles')):
1839             return self._get_automatic_captions(*args, **kwargs)
1840         return {}
1841
1842     def _get_automatic_captions(self, *args, **kwargs):
1843         raise NotImplementedError('This method must be implemented by subclasses')
1844
1845     def mark_watched(self, *args, **kwargs):
1846         if (self._downloader.params.get('mark_watched', False) and
1847                 (self._get_login_info()[0] is not None or
1848                     self._downloader.params.get('cookiefile') is not None)):
1849             self._mark_watched(*args, **kwargs)
1850
1851     def _mark_watched(self, *args, **kwargs):
1852         raise NotImplementedError('This method must be implemented by subclasses')
1853
1854     def geo_verification_headers(self):
1855         headers = {}
1856         geo_verification_proxy = self._downloader.params.get('geo_verification_proxy')
1857         if geo_verification_proxy:
1858             headers['Ytdl-request-proxy'] = geo_verification_proxy
1859         return headers
1860
1861
1862 class SearchInfoExtractor(InfoExtractor):
1863     """
1864     Base class for paged search queries extractors.
1865     They accept URLs in the format _SEARCH_KEY(|all|[0-9]):{query}
1866     Instances should define _SEARCH_KEY and _MAX_RESULTS.
1867     """
1868
1869     @classmethod
1870     def _make_valid_url(cls):
1871         return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
1872
1873     @classmethod
1874     def suitable(cls, url):
1875         return re.match(cls._make_valid_url(), url) is not None
1876
1877     def _real_extract(self, query):
1878         mobj = re.match(self._make_valid_url(), query)
1879         if mobj is None:
1880             raise ExtractorError('Invalid search query "%s"' % query)
1881
1882         prefix = mobj.group('prefix')
1883         query = mobj.group('query')
1884         if prefix == '':
1885             return self._get_n_results(query, 1)
1886         elif prefix == 'all':
1887             return self._get_n_results(query, self._MAX_RESULTS)
1888         else:
1889             n = int(prefix)
1890             if n <= 0:
1891                 raise ExtractorError('invalid download number %s for query "%s"' % (n, query))
1892             elif n > self._MAX_RESULTS:
1893                 self._downloader.report_warning('%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n))
1894                 n = self._MAX_RESULTS
1895             return self._get_n_results(query, n)
1896
1897     def _get_n_results(self, query, n):
1898         """Get a specified number of results for a query"""
1899         raise NotImplementedError('This method must be implemented by subclasses')
1900
1901     @property
1902     def SEARCH_KEY(self):
1903         return self._SEARCH_KEY