Merge remote-tracking branch 'AGSPhoenix/teamcoco-fix'
[youtube-dl] / youtube_dl / extractor / common.py
1 import base64
2 import hashlib
3 import json
4 import os
5 import re
6 import socket
7 import sys
8 import netrc
9 import xml.etree.ElementTree
10
11 from ..utils import (
12     compat_http_client,
13     compat_urllib_error,
14     compat_urllib_parse_urlparse,
15     compat_str,
16
17     clean_html,
18     compiled_regex_type,
19     ExtractorError,
20     RegexNotFoundError,
21     sanitize_filename,
22     unescapeHTML,
23 )
24 _NO_DEFAULT = object()
25
26
27 class InfoExtractor(object):
28     """Information Extractor class.
29
30     Information extractors are the classes that, given a URL, extract
31     information about the video (or videos) the URL refers to. This
32     information includes the real video URL, the video title, author and
33     others. The information is stored in a dictionary which is then
34     passed to the FileDownloader. The FileDownloader processes this
35     information possibly downloading the video to the file system, among
36     other possible outcomes.
37
38     The dictionaries must include the following fields:
39
40     id:             Video identifier.
41     title:          Video title, unescaped.
42
43     Additionally, it must contain either a formats entry or a url one:
44
45     formats:        A list of dictionaries for each format available, ordered
46                     from worst to best quality.
47
48                     Potential fields:
49                     * url        Mandatory. The URL of the video file
50                     * ext        Will be calculated from url if missing
51                     * format     A human-readable description of the format
52                                  ("mp4 container with h264/opus").
53                                  Calculated from the format_id, width, height.
54                                  and format_note fields if missing.
55                     * format_id  A short description of the format
56                                  ("mp4_h264_opus" or "19").
57                                 Technically optional, but strongly recommended.
58                     * format_note Additional info about the format
59                                  ("3D" or "DASH video")
60                     * width      Width of the video, if known
61                     * height     Height of the video, if known
62                     * resolution Textual description of width and height
63                     * tbr        Average bitrate of audio and video in KBit/s
64                     * abr        Average audio bitrate in KBit/s
65                     * acodec     Name of the audio codec in use
66                     * asr        Audio sampling rate in Hertz
67                     * vbr        Average video bitrate in KBit/s
68                     * vcodec     Name of the video codec in use
69                     * container  Name of the container format
70                     * filesize   The number of bytes, if known in advance
71                     * player_url SWF Player URL (used for rtmpdump).
72                     * protocol   The protocol that will be used for the actual
73                                  download, lower-case.
74                                  "http", "https", "rtsp", "rtmp", "m3u8" or so.
75                     * preference Order number of this format. If this field is
76                                  present and not None, the formats get sorted
77                                  by this field, regardless of all other values.
78                                  -1 for default (order by other properties),
79                                  -2 or smaller for less than default.
80                     * quality    Order number of the video quality of this
81                                  format, irrespective of the file format.
82                                  -1 for default (order by other properties),
83                                  -2 or smaller for less than default.
84     url:            Final video URL.
85     ext:            Video filename extension.
86     format:         The video format, defaults to ext (used for --get-format)
87     player_url:     SWF Player URL (used for rtmpdump).
88
89     The following fields are optional:
90
91     display_id      An alternative identifier for the video, not necessarily
92                     unique, but available before title. Typically, id is
93                     something like "4234987", title "Dancing naked mole rats",
94                     and display_id "dancing-naked-mole-rats"
95     thumbnails:     A list of dictionaries (with the entries "resolution" and
96                     "url") for the varying thumbnails
97     thumbnail:      Full URL to a video thumbnail image.
98     description:    One-line video description.
99     uploader:       Full name of the video uploader.
100     timestamp:      UNIX timestamp of the moment the video became available.
101     upload_date:    Video upload date (YYYYMMDD).
102                     If not explicitly set, calculated from timestamp.
103     uploader_id:    Nickname or id of the video uploader.
104     location:       Physical location of the video.
105     subtitles:      The subtitle file contents as a dictionary in the format
106                     {language: subtitles}.
107     duration:       Length of the video in seconds, as an integer.
108     view_count:     How many users have watched the video on the platform.
109     like_count:     Number of positive ratings of the video
110     dislike_count:  Number of negative ratings of the video
111     comment_count:  Number of comments on the video
112     age_limit:      Age restriction for the video, as an integer (years)
113     webpage_url:    The url to the video webpage, if given to youtube-dl it
114                     should allow to get the same result again. (It will be set
115                     by YoutubeDL if it's missing)
116
117     Unless mentioned otherwise, the fields should be Unicode strings.
118
119     Subclasses of this one should re-define the _real_initialize() and
120     _real_extract() methods and define a _VALID_URL regexp.
121     Probably, they should also be added to the list of extractors.
122
123     Finally, the _WORKING attribute should be set to False for broken IEs
124     in order to warn the users and skip the tests.
125     """
126
127     _ready = False
128     _downloader = None
129     _WORKING = True
130
131     def __init__(self, downloader=None):
132         """Constructor. Receives an optional downloader."""
133         self._ready = False
134         self.set_downloader(downloader)
135
136     @classmethod
137     def suitable(cls, url):
138         """Receives a URL and returns True if suitable for this IE."""
139
140         # This does not use has/getattr intentionally - we want to know whether
141         # we have cached the regexp for *this* class, whereas getattr would also
142         # match the superclass
143         if '_VALID_URL_RE' not in cls.__dict__:
144             cls._VALID_URL_RE = re.compile(cls._VALID_URL)
145         return cls._VALID_URL_RE.match(url) is not None
146
147     @classmethod
148     def working(cls):
149         """Getter method for _WORKING."""
150         return cls._WORKING
151
152     def initialize(self):
153         """Initializes an instance (authentication, etc)."""
154         if not self._ready:
155             self._real_initialize()
156             self._ready = True
157
158     def extract(self, url):
159         """Extracts URL information and returns it in list of dicts."""
160         self.initialize()
161         return self._real_extract(url)
162
163     def set_downloader(self, downloader):
164         """Sets the downloader for this IE."""
165         self._downloader = downloader
166
167     def _real_initialize(self):
168         """Real initialization process. Redefine in subclasses."""
169         pass
170
171     def _real_extract(self, url):
172         """Real extraction process. Redefine in subclasses."""
173         pass
174
175     @classmethod
176     def ie_key(cls):
177         """A string for getting the InfoExtractor with get_info_extractor"""
178         return cls.__name__[:-2]
179
180     @property
181     def IE_NAME(self):
182         return type(self).__name__[:-2]
183
184     def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True):
185         """ Returns the response handle """
186         if note is None:
187             self.report_download_webpage(video_id)
188         elif note is not False:
189             if video_id is None:
190                 self.to_screen(u'%s' % (note,))
191             else:
192                 self.to_screen(u'%s: %s' % (video_id, note))
193         try:
194             return self._downloader.urlopen(url_or_request)
195         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
196             if errnote is False:
197                 return False
198             if errnote is None:
199                 errnote = u'Unable to download webpage'
200             errmsg = u'%s: %s' % (errnote, compat_str(err))
201             if fatal:
202                 raise ExtractorError(errmsg, sys.exc_info()[2], cause=err)
203             else:
204                 self._downloader.report_warning(errmsg)
205                 return False
206
207     def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True):
208         """ Returns a tuple (page content as string, URL handle) """
209
210         # Strip hashes from the URL (#1038)
211         if isinstance(url_or_request, (compat_str, str)):
212             url_or_request = url_or_request.partition('#')[0]
213
214         urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal)
215         if urlh is False:
216             assert not fatal
217             return False
218         content_type = urlh.headers.get('Content-Type', '')
219         webpage_bytes = urlh.read()
220         m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type)
221         if m:
222             encoding = m.group(1)
223         else:
224             m = re.search(br'<meta[^>]+charset=[\'"]?([^\'")]+)[ /\'">]',
225                           webpage_bytes[:1024])
226             if m:
227                 encoding = m.group(1).decode('ascii')
228             elif webpage_bytes.startswith(b'\xff\xfe'):
229                 encoding = 'utf-16'
230             else:
231                 encoding = 'utf-8'
232         if self._downloader.params.get('dump_intermediate_pages', False):
233             try:
234                 url = url_or_request.get_full_url()
235             except AttributeError:
236                 url = url_or_request
237             self.to_screen(u'Dumping request to ' + url)
238             dump = base64.b64encode(webpage_bytes).decode('ascii')
239             self._downloader.to_screen(dump)
240         if self._downloader.params.get('write_pages', False):
241             try:
242                 url = url_or_request.get_full_url()
243             except AttributeError:
244                 url = url_or_request
245             if len(url) > 200:
246                 h = u'___' + hashlib.md5(url.encode('utf-8')).hexdigest()
247                 url = url[:200 - len(h)] + h
248             raw_filename = ('%s_%s.dump' % (video_id, url))
249             filename = sanitize_filename(raw_filename, restricted=True)
250             self.to_screen(u'Saving request to ' + filename)
251             with open(filename, 'wb') as outf:
252                 outf.write(webpage_bytes)
253
254         content = webpage_bytes.decode(encoding, 'replace')
255
256         if (u'<title>Access to this site is blocked</title>' in content and
257                 u'Websense' in content[:512]):
258             msg = u'Access to this webpage has been blocked by Websense filtering software in your network.'
259             blocked_iframe = self._html_search_regex(
260                 r'<iframe src="([^"]+)"', content,
261                 u'Websense information URL', default=None)
262             if blocked_iframe:
263                 msg += u' Visit %s for more details' % blocked_iframe
264             raise ExtractorError(msg, expected=True)
265
266         return (content, urlh)
267
268     def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True):
269         """ Returns the data of the page as a string """
270         res = self._download_webpage_handle(url_or_request, video_id, note, errnote, fatal)
271         if res is False:
272             return res
273         else:
274             content, _ = res
275             return content
276
277     def _download_xml(self, url_or_request, video_id,
278                       note=u'Downloading XML', errnote=u'Unable to download XML',
279                       transform_source=None):
280         """Return the xml as an xml.etree.ElementTree.Element"""
281         xml_string = self._download_webpage(url_or_request, video_id, note, errnote)
282         if transform_source:
283             xml_string = transform_source(xml_string)
284         return xml.etree.ElementTree.fromstring(xml_string.encode('utf-8'))
285
286     def _download_json(self, url_or_request, video_id,
287                        note=u'Downloading JSON metadata',
288                        errnote=u'Unable to download JSON metadata',
289                        transform_source=None):
290         json_string = self._download_webpage(url_or_request, video_id, note, errnote)
291         if transform_source:
292             json_string = transform_source(json_string)
293         try:
294             return json.loads(json_string)
295         except ValueError as ve:
296             raise ExtractorError('Failed to download JSON', cause=ve)
297
298     def report_warning(self, msg, video_id=None):
299         idstr = u'' if video_id is None else u'%s: ' % video_id
300         self._downloader.report_warning(
301             u'[%s] %s%s' % (self.IE_NAME, idstr, msg))
302
303     def to_screen(self, msg):
304         """Print msg to screen, prefixing it with '[ie_name]'"""
305         self._downloader.to_screen(u'[%s] %s' % (self.IE_NAME, msg))
306
307     def report_extraction(self, id_or_name):
308         """Report information extraction."""
309         self.to_screen(u'%s: Extracting information' % id_or_name)
310
311     def report_download_webpage(self, video_id):
312         """Report webpage download."""
313         self.to_screen(u'%s: Downloading webpage' % video_id)
314
315     def report_age_confirmation(self):
316         """Report attempt to confirm age."""
317         self.to_screen(u'Confirming age')
318
319     def report_login(self):
320         """Report attempt to log in."""
321         self.to_screen(u'Logging in')
322
323     #Methods for following #608
324     @staticmethod
325     def url_result(url, ie=None, video_id=None):
326         """Returns a url that points to a page that should be processed"""
327         #TODO: ie should be the class used for getting the info
328         video_info = {'_type': 'url',
329                       'url': url,
330                       'ie_key': ie}
331         if video_id is not None:
332             video_info['id'] = video_id
333         return video_info
334     @staticmethod
335     def playlist_result(entries, playlist_id=None, playlist_title=None):
336         """Returns a playlist"""
337         video_info = {'_type': 'playlist',
338                       'entries': entries}
339         if playlist_id:
340             video_info['id'] = playlist_id
341         if playlist_title:
342             video_info['title'] = playlist_title
343         return video_info
344
345     def _search_regex(self, pattern, string, name, default=_NO_DEFAULT, fatal=True, flags=0):
346         """
347         Perform a regex search on the given string, using a single or a list of
348         patterns returning the first matching group.
349         In case of failure return a default value or raise a WARNING or a
350         RegexNotFoundError, depending on fatal, specifying the field name.
351         """
352         if isinstance(pattern, (str, compat_str, compiled_regex_type)):
353             mobj = re.search(pattern, string, flags)
354         else:
355             for p in pattern:
356                 mobj = re.search(p, string, flags)
357                 if mobj: break
358
359         if os.name != 'nt' and sys.stderr.isatty():
360             _name = u'\033[0;34m%s\033[0m' % name
361         else:
362             _name = name
363
364         if mobj:
365             # return the first matching group
366             return next(g for g in mobj.groups() if g is not None)
367         elif default is not _NO_DEFAULT:
368             return default
369         elif fatal:
370             raise RegexNotFoundError(u'Unable to extract %s' % _name)
371         else:
372             self._downloader.report_warning(u'unable to extract %s; '
373                 u'please report this issue on http://yt-dl.org/bug' % _name)
374             return None
375
376     def _html_search_regex(self, pattern, string, name, default=_NO_DEFAULT, fatal=True, flags=0):
377         """
378         Like _search_regex, but strips HTML tags and unescapes entities.
379         """
380         res = self._search_regex(pattern, string, name, default, fatal, flags)
381         if res:
382             return clean_html(res).strip()
383         else:
384             return res
385
386     def _get_login_info(self):
387         """
388         Get the the login info as (username, password)
389         It will look in the netrc file using the _NETRC_MACHINE value
390         If there's no info available, return (None, None)
391         """
392         if self._downloader is None:
393             return (None, None)
394
395         username = None
396         password = None
397         downloader_params = self._downloader.params
398
399         # Attempt to use provided username and password or .netrc data
400         if downloader_params.get('username', None) is not None:
401             username = downloader_params['username']
402             password = downloader_params['password']
403         elif downloader_params.get('usenetrc', False):
404             try:
405                 info = netrc.netrc().authenticators(self._NETRC_MACHINE)
406                 if info is not None:
407                     username = info[0]
408                     password = info[2]
409                 else:
410                     raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
411             except (IOError, netrc.NetrcParseError) as err:
412                 self._downloader.report_warning(u'parsing .netrc: %s' % compat_str(err))
413         
414         return (username, password)
415
416     # Helper functions for extracting OpenGraph info
417     @staticmethod
418     def _og_regexes(prop):
419         content_re = r'content=(?:"([^>]+?)"|\'([^>]+?)\')'
420         property_re = r'(?:name|property)=[\'"]og:%s[\'"]' % re.escape(prop)
421         template = r'<meta[^>]+?%s[^>]+?%s'
422         return [
423             template % (property_re, content_re),
424             template % (content_re, property_re),
425         ]
426
427     def _og_search_property(self, prop, html, name=None, **kargs):
428         if name is None:
429             name = 'OpenGraph %s' % prop
430         escaped = self._search_regex(self._og_regexes(prop), html, name, flags=re.DOTALL, **kargs)
431         if escaped is None:
432             return None
433         return unescapeHTML(escaped)
434
435     def _og_search_thumbnail(self, html, **kargs):
436         return self._og_search_property('image', html, u'thumbnail url', fatal=False, **kargs)
437
438     def _og_search_description(self, html, **kargs):
439         return self._og_search_property('description', html, fatal=False, **kargs)
440
441     def _og_search_title(self, html, **kargs):
442         return self._og_search_property('title', html, **kargs)
443
444     def _og_search_video_url(self, html, name='video url', secure=True, **kargs):
445         regexes = self._og_regexes('video')
446         if secure: regexes = self._og_regexes('video:secure_url') + regexes
447         return self._html_search_regex(regexes, html, name, **kargs)
448
449     def _html_search_meta(self, name, html, display_name=None, fatal=False):
450         if display_name is None:
451             display_name = name
452         return self._html_search_regex(
453             r'''(?ix)<meta
454                     (?=[^>]+(?:itemprop|name|property)=["\']%s["\'])
455                     [^>]+content=["\']([^"\']+)["\']''' % re.escape(name),
456             html, display_name, fatal=fatal)
457
458     def _dc_search_uploader(self, html):
459         return self._html_search_meta('dc.creator', html, 'uploader')
460
461     def _rta_search(self, html):
462         # See http://www.rtalabel.org/index.php?content=howtofaq#single
463         if re.search(r'(?ix)<meta\s+name="rating"\s+'
464                      r'     content="RTA-5042-1996-1400-1577-RTA"',
465                      html):
466             return 18
467         return 0
468
469     def _media_rating_search(self, html):
470         # See http://www.tjg-designs.com/WP/metadata-code-examples-adding-metadata-to-your-web-pages/
471         rating = self._html_search_meta('rating', html)
472
473         if not rating:
474             return None
475
476         RATING_TABLE = {
477             'safe for kids': 0,
478             'general': 8,
479             '14 years': 14,
480             'mature': 17,
481             'restricted': 19,
482         }
483         return RATING_TABLE.get(rating.lower(), None)
484
485     def _twitter_search_player(self, html):
486         return self._html_search_meta('twitter:player', html,
487             'twitter card player')
488
489     def _sort_formats(self, formats):
490         if not formats:
491             raise ExtractorError(u'No video formats found')
492
493         def _formats_key(f):
494             # TODO remove the following workaround
495             from ..utils import determine_ext
496             if not f.get('ext') and 'url' in f:
497                 f['ext'] = determine_ext(f['url'])
498
499             preference = f.get('preference')
500             if preference is None:
501                 proto = f.get('protocol')
502                 if proto is None:
503                     proto = compat_urllib_parse_urlparse(f.get('url', '')).scheme
504
505                 preference = 0 if proto in ['http', 'https'] else -0.1
506                 if f.get('ext') in ['f4f', 'f4m']:  # Not yet supported
507                     preference -= 0.5
508
509             if f.get('vcodec') == 'none':  # audio only
510                 if self._downloader.params.get('prefer_free_formats'):
511                     ORDER = [u'aac', u'mp3', u'm4a', u'webm', u'ogg', u'opus']
512                 else:
513                     ORDER = [u'webm', u'opus', u'ogg', u'mp3', u'aac', u'm4a']
514                 ext_preference = 0
515                 try:
516                     audio_ext_preference = ORDER.index(f['ext'])
517                 except ValueError:
518                     audio_ext_preference = -1
519             else:
520                 if self._downloader.params.get('prefer_free_formats'):
521                     ORDER = [u'flv', u'mp4', u'webm']
522                 else:
523                     ORDER = [u'webm', u'flv', u'mp4']
524                 try:
525                     ext_preference = ORDER.index(f['ext'])
526                 except ValueError:
527                     ext_preference = -1
528                 audio_ext_preference = 0
529
530             return (
531                 preference,
532                 f.get('quality') if f.get('quality') is not None else -1,
533                 f.get('height') if f.get('height') is not None else -1,
534                 f.get('width') if f.get('width') is not None else -1,
535                 ext_preference,
536                 f.get('tbr') if f.get('tbr') is not None else -1,
537                 f.get('vbr') if f.get('vbr') is not None else -1,
538                 f.get('abr') if f.get('abr') is not None else -1,
539                 audio_ext_preference,
540                 f.get('filesize') if f.get('filesize') is not None else -1,
541                 f.get('format_id'),
542             )
543         formats.sort(key=_formats_key)
544
545
546 class SearchInfoExtractor(InfoExtractor):
547     """
548     Base class for paged search queries extractors.
549     They accept urls in the format _SEARCH_KEY(|all|[0-9]):{query}
550     Instances should define _SEARCH_KEY and _MAX_RESULTS.
551     """
552
553     @classmethod
554     def _make_valid_url(cls):
555         return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
556
557     @classmethod
558     def suitable(cls, url):
559         return re.match(cls._make_valid_url(), url) is not None
560
561     def _real_extract(self, query):
562         mobj = re.match(self._make_valid_url(), query)
563         if mobj is None:
564             raise ExtractorError(u'Invalid search query "%s"' % query)
565
566         prefix = mobj.group('prefix')
567         query = mobj.group('query')
568         if prefix == '':
569             return self._get_n_results(query, 1)
570         elif prefix == 'all':
571             return self._get_n_results(query, self._MAX_RESULTS)
572         else:
573             n = int(prefix)
574             if n <= 0:
575                 raise ExtractorError(u'invalid download number %s for query "%s"' % (n, query))
576             elif n > self._MAX_RESULTS:
577                 self._downloader.report_warning(u'%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n))
578                 n = self._MAX_RESULTS
579             return self._get_n_results(query, n)
580
581     def _get_n_results(self, query, n):
582         """Get a specified number of results for a query"""
583         raise NotImplementedError("This method must be implemented by subclasses")
584
585     @property
586     def SEARCH_KEY(self):
587         return self._SEARCH_KEY