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