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