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