# -*- coding: utf-8 -*- """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) Copyright (C) 2016-2018 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. """ from six.moves import range from six import PY2 from six.moves import urllib try: from six.moves import html_parser unescape = html_parser.HTMLParser().unescape except AttributeError: import html unescape = html.unescape import copy import re import json import random import requests from ...kodion.utils import is_httpd_live, make_dirs, DataCache from ..youtube_exceptions import YouTubeException from .signature.cipher import Cipher from .subtitles import Subtitles import xbmcvfs class VideoInfo(object): FORMAT = { # === Non-DASH === '5': {'container': 'flv', 'title': '240p', 'sort': [240, 0], 'video': {'height': 240, 'encoding': 'h.263'}, 'audio': {'bitrate': 64, 'encoding': 'mp3'}}, '6': {'container': 'flv', # Discontinued 'discontinued': True, 'video': {'height': 270, 'encoding': 'h.263'}, 'audio': {'bitrate': 64, 'encoding': 'mp3'}}, '13': {'container': '3gp', # Discontinued 'discontinued': True, 'video': {'encoding': 'mpeg-4'}, 'audio': {'encoding': 'aac'}}, '17': {'container': '3gp', 'title': '144p', 'sort': [144, -20], 'video': {'height': 144, 'encoding': 'mpeg-4'}, 'audio': {'bitrate': 24, 'encoding': 'aac'}}, '18': {'container': 'mp4', 'title': '360p', 'sort': [360, 0], 'video': {'height': 360, 'encoding': 'h.264'}, 'audio': {'bitrate': 96, 'encoding': 'aac'}}, '22': {'container': 'mp4', 'title': '720p', 'sort': [720, 0], 'video': {'height': 720, 'encoding': 'h.264'}, 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '34': {'container': 'flv', # Discontinued 'discontinued': True, 'video': {'height': 360, 'encoding': 'h.264'}, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '35': {'container': 'flv', # Discontinued 'discontinued': True, 'video': {'height': 480, 'encoding': 'h.264'}, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '36': {'container': '3gp', 'title': '240p', 'sort': [240, -20], 'video': {'height': 240, 'encoding': 'mpeg-4'}, 'audio': {'bitrate': 32, 'encoding': 'aac'}}, '37': {'container': 'mp4', 'title': '1080p', 'sort': [1080, 0], 'video': {'height': 1080, 'encoding': 'h.264'}, 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '38': {'container': 'mp4', 'title': '3072p', 'sort': [3072, 0], 'video': {'height': 3072, 'encoding': 'h.264'}, 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '43': {'container': 'webm', 'title': '360p', 'sort': [360, -1], 'video': {'height': 360, 'encoding': 'vp8'}, 'audio': {'bitrate': 128, 'encoding': 'vorbis'}}, '44': {'container': 'webm', # Discontinued 'discontinued': True, 'video': {'height': 480, 'encoding': 'vp8'}, 'audio': {'bitrate': 128, 'encoding': 'vorbis'}}, '45': {'container': 'webm', # Discontinued 'discontinued': True, 'video': {'height': 720, 'encoding': 'vp8'}, 'audio': {'bitrate': 192, 'encoding': 'vorbis'}}, '46': {'container': 'webm', # Discontinued 'discontinued': True, 'video': {'height': 1080, 'encoding': 'vp8'}, 'audio': {'bitrate': 192, 'encoding': 'vorbis'}}, '59': {'container': 'mp4', 'title': '480p', 'sort': [480, 0], 'video': {'height': 480, 'encoding': 'h.264'}, 'audio': {'bitrate': 96, 'encoding': 'aac'}}, '78': {'container': 'mp4', 'title': '360p', 'sort': [360, 0], 'video': {'height': 360, 'encoding': 'h.264'}, 'audio': {'bitrate': 96, 'encoding': 'aac'}}, # === 3D === '82': {'container': 'mp4', '3D': True, 'title': '3D@360p', 'sort': [360, 0], 'video': {'height': 360, 'encoding': 'h.264'}, 'audio': {'bitrate': 96, 'encoding': 'aac'}}, '83': {'container': 'mp4', '3D': True, 'title': '3D@240p', 'sort': [240, 0], 'video': {'height': 240, 'encoding': 'h.264'}, 'audio': {'bitrate': 96, 'encoding': 'aac'}}, '84': {'container': 'mp4', '3D': True, 'title': '3D@720p', 'sort': [720, 0], 'video': {'height': 720, 'encoding': 'h.264'}, 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '85': {'container': 'mp4', '3D': True, 'title': '3D@1080p', 'sort': [1080, 0], 'video': {'height': 1080, 'encoding': 'h.264'}, 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '100': {'container': 'webm', '3D': True, 'title': '3D@360p', 'sort': [360, -1], 'video': {'height': 360, 'encoding': 'vp8'}, 'audio': {'bitrate': 128, 'encoding': 'vorbis'}}, '101': {'container': 'webm', # Discontinued 'discontinued': True, '3D': True, 'title': '3D@360p', 'sort': [360, -1], 'video': {'height': 360, 'encoding': 'vp8'}, 'audio': {'bitrate': 192, 'encoding': 'vorbis'}}, '102': {'container': 'webm', # Discontinued 'discontinued': True, '3D': True, 'video': {'height': 720, 'encoding': 'vp8'}, 'audio': {'bitrate': 192, 'encoding': 'vorbis'}}, # === Live Streams === '91': {'container': 'ts', 'Live': True, 'title': 'Live@144p', 'sort': [144, 0], 'video': {'height': 144, 'encoding': 'h.264'}, 'audio': {'bitrate': 48, 'encoding': 'aac'}}, '92': {'container': 'ts', 'Live': True, 'title': 'Live@240p', 'sort': [240, 0], 'video': {'height': 240, 'encoding': 'h.264'}, 'audio': {'bitrate': 48, 'encoding': 'aac'}}, '93': {'container': 'ts', 'Live': True, 'title': 'Live@360p', 'sort': [360, 0], 'video': {'height': 360, 'encoding': 'h.264'}, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '94': {'container': 'ts', 'Live': True, 'title': 'Live@480p', 'sort': [480, 0], 'video': {'height': 480, 'encoding': 'h.264'}, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '95': {'container': 'ts', 'Live': True, 'title': 'Live@720p', 'sort': [720, 0], 'video': {'height': 720, 'encoding': 'h.264'}, 'audio': {'bitrate': 256, 'encoding': 'aac'}}, '96': {'container': 'ts', 'Live': True, 'title': 'Live@1080p', 'sort': [1080, 0], 'video': {'height': 1080, 'encoding': 'h.264'}, 'audio': {'bitrate': 256, 'encoding': 'aac'}}, '120': {'container': 'flv', # Discontinued 'discontinued': True, 'Live': True, 'title': 'Live@720p', 'sort': [720, -10], 'video': {'height': 720, 'encoding': 'h.264'}, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '127': {'container': 'ts', 'Live': True, 'audio': {'bitrate': 96, 'encoding': 'aac'}}, '128': {'container': 'ts', 'Live': True, 'audio': {'bitrate': 96, 'encoding': 'aac'}}, '132': {'container': 'ts', 'Live': True, 'title': 'Live@240p', 'sort': [240, 0], 'video': {'height': 240, 'encoding': 'h.264'}, 'audio': {'bitrate': 48, 'encoding': 'aac'}}, '151': {'container': 'ts', 'Live': True, 'unsupported': True, 'title': 'Live@72p', 'sort': [72, 0], 'video': {'height': 72, 'encoding': 'h.264'}, 'audio': {'bitrate': 24, 'encoding': 'aac'}}, '300': {'container': 'ts', 'Live': True, 'title': 'Live@720p', 'sort': [720, 0], 'video': {'height': 720, 'encoding': 'h.264'}, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '301': {'container': 'ts', 'Live': True, 'title': 'Live@1080p', 'sort': [1080, 0], 'video': {'height': 1080, 'encoding': 'h.264'}, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, # === DASH (video only) '133': {'container': 'mp4', 'dash/video': True, 'video': {'height': 240, 'encoding': 'h.264'}}, '134': {'container': 'mp4', 'dash/video': True, 'video': {'height': 360, 'encoding': 'h.264'}}, '135': {'container': 'mp4', 'dash/video': True, 'video': {'height': 480, 'encoding': 'h.264'}}, '136': {'container': 'mp4', 'dash/video': True, 'video': {'height': 720, 'encoding': 'h.264'}}, '137': {'container': 'mp4', 'dash/video': True, 'video': {'height': 1080, 'encoding': 'h.264'}}, '138': {'container': 'mp4', # Discontinued 'discontinued': True, 'dash/video': True, 'video': {'height': 2160, 'encoding': 'h.264'}}, '160': {'container': 'mp4', 'dash/video': True, 'video': {'height': 144, 'encoding': 'h.264'}}, '167': {'container': 'webm', 'dash/video': True, 'video': {'height': 360, 'encoding': 'vp8'}}, '168': {'container': 'webm', 'dash/video': True, 'video': {'height': 480, 'encoding': 'vp8'}}, '169': {'container': 'webm', 'dash/video': True, 'video': {'height': 720, 'encoding': 'vp8'}}, '170': {'container': 'webm', 'dash/video': True, 'video': {'height': 1080, 'encoding': 'vp8'}}, '218': {'container': 'webm', 'dash/video': True, 'video': {'height': 480, 'encoding': 'vp8'}}, '219': {'container': 'webm', 'dash/video': True, 'video': {'height': 480, 'encoding': 'vp8'}}, '242': {'container': 'webm', 'dash/video': True, 'video': {'height': 240, 'encoding': 'vp9'}}, '243': {'container': 'webm', 'dash/video': True, 'video': {'height': 360, 'encoding': 'vp9'}}, '244': {'container': 'webm', 'dash/video': True, 'video': {'height': 480, 'encoding': 'vp9'}}, '247': {'container': 'webm', 'dash/video': True, 'video': {'height': 720, 'encoding': 'vp9'}}, '248': {'container': 'webm', 'dash/video': True, 'video': {'height': 1080, 'encoding': 'vp9'}}, '264': {'container': 'mp4', 'dash/video': True, 'video': {'height': 1440, 'encoding': 'h.264'}}, '266': {'container': 'mp4', 'dash/video': True, 'video': {'height': 2160, 'encoding': 'h.264'}}, '271': {'container': 'webm', 'dash/video': True, 'video': {'height': 1440, 'encoding': 'vp9'}}, '272': {'container': 'webm', 'dash/video': True, 'video': {'height': 2160, 'encoding': 'vp9'}}, '278': {'container': 'webm', 'dash/video': True, 'video': {'height': 144, 'encoding': 'vp9'}}, '298': {'container': 'mp4', 'dash/video': True, 'fps': 60, 'video': {'height': 720, 'encoding': 'h.264'}}, '299': {'container': 'mp4', 'dash/video': True, 'fps': 60, 'video': {'height': 1080, 'encoding': 'h.264'}}, '302': {'container': 'webm', 'dash/video': True, 'fps': 60, 'video': {'height': 720, 'encoding': 'vp9'}}, '303': {'container': 'webm', 'dash/video': True, 'fps': 60, 'video': {'height': 1080, 'encoding': 'vp9'}}, '308': {'container': 'webm', 'dash/video': True, 'fps': 60, 'video': {'height': 1440, 'encoding': 'vp9'}}, '313': {'container': 'webm', 'dash/video': True, 'video': {'height': 2160, 'encoding': 'vp9'}}, '315': {'container': 'webm', 'dash/video': True, 'fps': 60, 'video': {'height': 2160, 'encoding': 'vp9'}}, '330': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 144, 'encoding': 'vp9.2'}}, '331': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 240, 'encoding': 'vp9.2'}}, '332': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 360, 'encoding': 'vp9.2'}}, '333': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 480, 'encoding': 'vp9.2'}}, '334': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 720, 'encoding': 'vp9.2'}}, '335': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 1080, 'encoding': 'vp9.2'}}, '336': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 1440, 'encoding': 'vp9.2'}}, '337': {'container': 'webm', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 2160, 'encoding': 'vp9.2'}}, '400': {'container': 'mp4', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 1440, 'encoding': 'av1'}}, '401': {'container': 'mp4', 'dash/video': True, 'fps': 60, 'hdr': True, 'video': {'height': 2160, 'encoding': 'av1'}}, '394': {'container': 'mp4', 'dash/video': True, 'fps': 30, 'video': {'height': 144, 'encoding': 'av1'}}, '395': {'container': 'mp4', 'dash/video': True, 'fps': 30, 'video': {'height': 240, 'encoding': 'av1'}}, '396': {'container': 'mp4', 'dash/video': True, 'fps': 30, 'video': {'height': 360, 'encoding': 'av1'}}, '397': {'container': 'mp4', 'dash/video': True, 'fps': 30, 'video': {'height': 480, 'encoding': 'av1'}}, '398': {'container': 'mp4', 'dash/video': True, 'fps': 30, 'video': {'height': 720, 'encoding': 'av1'}}, '399': {'container': 'mp4', 'dash/video': True, 'fps': 30, 'video': {'height': 1080, 'encoding': 'av1'}}, # === Dash (audio only) '139': {'container': 'mp4', 'sort': [48, 0], 'title': 'aac@48', 'dash/audio': True, 'audio': {'bitrate': 48, 'encoding': 'aac'}}, '140': {'container': 'mp4', 'sort': [129, 0], 'title': 'aac@128', 'dash/audio': True, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '141': {'container': 'mp4', 'sort': [143, 0], 'title': 'aac@256', 'dash/audio': True, 'audio': {'bitrate': 256, 'encoding': 'aac'}}, '256': {'container': 'mp4', 'title': 'aac/itag 256', 'dash/audio': True, 'unsupported': True, 'audio': {'bitrate': 0, 'encoding': 'aac'}}, '258': {'container': 'mp4', 'title': 'aac/itag 258', 'dash/audio': True, 'unsupported': True, 'audio': {'bitrate': 0, 'encoding': 'aac'}}, '325': {'container': 'mp4', 'title': 'dtse/itag 325', 'dash/audio': True, 'unsupported': True, 'audio': {'bitrate': 0, 'encoding': 'aac'}}, '328': {'container': 'mp4', 'title': 'ec-3/itag 328', 'dash/audio': True, 'unsupported': True, 'audio': {'bitrate': 0, 'encoding': 'aac'}}, '171': {'container': 'webm', 'sort': [128, 0], 'title': 'vorbis@128', 'dash/audio': True, 'audio': {'bitrate': 128, 'encoding': 'vorbis'}}, '172': {'container': 'webm', 'sort': [142, 0], 'title': 'vorbis@192', 'dash/audio': True, 'audio': {'bitrate': 192, 'encoding': 'vorbis'}}, '249': {'container': 'webm', 'sort': [50, 0], 'title': 'opus@50', 'dash/audio': True, 'audio': {'bitrate': 50, 'encoding': 'opus'}}, '250': {'container': 'webm', 'sort': [70, 0], 'title': 'opus@70', 'dash/audio': True, 'audio': {'bitrate': 70, 'encoding': 'opus'}}, '251': {'container': 'webm', 'sort': [141, 0], 'title': 'opus@160', 'dash/audio': True, 'audio': {'bitrate': 160, 'encoding': 'opus'}}, # === DASH adaptive audio only '9997': {'container': 'mpd', 'sort': [-1, 0], 'title': 'DASH Audio', 'dash/audio': True, 'audio': {'bitrate': 0, 'encoding': ''}}, # === Live DASH adaptive '9998': {'container': 'mpd', 'Live': True, 'sort': [1080, 1], 'title': 'Live DASH', 'dash/audio': True, 'dash/video': True, 'audio': {'bitrate': 0, 'encoding': ''}, 'video': {'height': 0, 'encoding': ''}}, # === DASH adaptive '9999': {'container': 'mpd', 'sort': [1080, 1], 'title': 'DASH', 'dash/audio': True, 'dash/video': True, 'audio': {'bitrate': 0, 'encoding': ''}, 'video': {'height': 0, 'encoding': ''}} } def __init__(self, context, access_token='', language='en-US'): self._context = context self._data_cache = self._context.get_data_cache() self._verify = context.get_settings().verify_ssl() self._language = language.replace('-', '_') self.language = context.get_settings().get_string('youtube.language', 'en_US').replace('-', '_') self.region = context.get_settings().get_string('youtube.region', 'US') self._access_token = access_token @staticmethod def generate_cpn(): # https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L1381 # LICENSE: The Unlicense # cpn generation algorithm is reverse engineered from base.js. # In fact it works even with dummy cpn. cpn_alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' cpn = ''.join((cpn_alphabet[random.randint(0, 256) & 63] for _ in range(0, 16))) return cpn def load_stream_infos(self, video_id): return self._method_get_video_info(video_id) def get_watch_page(self, video_id): headers = {'Host': 'www.youtube.com', 'Connection': 'keep-alive', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36', 'Accept': '*/*', 'DNT': '1', 'Referer': 'https://www.youtube.com', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'} params = {'v': video_id, 'hl': self.language, 'gl': self.region} if self._access_token: params['access_token'] = self._access_token url = 'https://www.youtube.com/watch' result = requests.get(url, params=params, headers=headers, verify=self._verify, allow_redirects=True) return {'html': result.text, 'cookies': result.cookies} def get_embed_page(self, video_id): headers = {'Host': 'www.youtube.com', 'Connection': 'keep-alive', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36', 'Accept': '*/*', 'DNT': '1', 'Referer': 'https://www.youtube.com', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'} params = {'hl': self.language, 'gl': self.region} if self._access_token: params['access_token'] = self._access_token url = 'https://www.youtube.com/embed/{video_id}'.format(video_id=video_id) result = requests.get(url, params=params, headers=headers, verify=self._verify, allow_redirects=True) return {'html': result.text, 'cookies': result.cookies} @staticmethod def get_player_client(html): context = {} found = re.search( r'ytcfg\.set\((?P{"INNERTUBE_CONTEXT":.+?)\)\s*;', html ) if found: context = json.loads(found.group('context')) return context.get('INNERTUBE_CONTEXT', {}).get('client', {}) @staticmethod def get_player_config(html): config = {} found = re.search( r'window\.ytplayer\s*=\s*{}\s*;\s*ytcfg\.set\((?P.+?)\)\s*;\s*ytcfg', html ) if found: config = json.loads(found.group('config')) return config def get_player_js(self, video_id, javascript_url=''): def _normalize(url): if url in ['http://', 'https://']: url = '' if url and not url.startswith('http'): url = 'https://www.youtube.com/%s' % \ url.lstrip('/').replace('www.youtube.com/', '') if url: self._data_cache.set('player_javascript', json.dumps({'url': url})) return url cached_js = self._data_cache.get_item(DataCache.ONE_HOUR * 4, 'player_javascript') if cached_js and cached_js.get('player_javascript', {}).get('url'): cached_url = cached_js.get('player_javascript', {}).get('url') if cached_url not in ['http://', 'https://']: return cached_url if javascript_url: return _normalize(javascript_url) page_result = self.get_embed_page(video_id) html = page_result.get('html').encode('ascii', 'ignore').decode('ascii') if not html: return '' found = re.search(r'