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