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