# -*- 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. """ import json import os import re import shutil import socket from base64 import b64decode from ..youtube.helper import yt_subscriptions from .. import kodion from ..kodion.utils import FunctionCache, strip_html_from_text, get_client_ip_address, is_httpd_live, find_video_id from ..kodion.items import * from ..youtube.client import YouTube from .helper import v3, ResourceManager, yt_specials, yt_playlist, yt_login, yt_setup_wizard, yt_video, \ yt_context_menu, yt_play, yt_old_actions, UrlResolver, UrlToItemConverter from .youtube_exceptions import InvalidGrant, LoginException import xbmc import xbmcaddon import xbmcvfs import xbmcgui import xbmcplugin try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: pass class Provider(kodion.AbstractProvider): LOCAL_MAP = {'youtube.search': 30102, 'youtube.next_page': 30106, 'youtube.watch_later': 30107, 'youtube.video.rate.none': 30108, 'youtube.remove': 30108, 'youtube.sign.in': 30111, 'youtube.sign.out': 30112, 'youtube.rename': 30113, 'youtube.delete': 30118, 'youtube.api.key': 30201, 'youtube.api.id': 30202, 'youtube.api.secret': 30203, 'youtube.channels': 30500, 'youtube.playlists': 30501, 'youtube.go_to_channel': 30502, 'youtube.subscriptions': 30504, 'youtube.unsubscribe': 30505, 'youtube.subscribe': 30506, 'youtube.my_channel': 30507, 'youtube.video.liked': 30508, 'youtube.history': 30509, 'youtube.my_subscriptions': 30510, 'youtube.video.queue': 30511, 'youtube.browse_channels': 30512, 'youtube.popular_right_now': 30513, 'youtube.related_videos': 30514, 'youtube.setting.auto_remove_watch_later': 30515, 'youtube.subscribe_to': 30517, 'youtube.sign.go_to': 30518, 'youtube.sign.enter_code': 30519, 'youtube.video.add_to_playlist': 30520, 'youtube.playlist.select': 30521, 'youtube.playlist.create': 30522, 'youtube.setup_wizard.select_language': 30524, 'youtube.setup_wizard.select_region': 30525, 'youtube.setup_wizard.adjust': 30526, 'youtube.setup_wizard.adjust.language_and_region': 30527, 'youtube.video.rate': 30528, 'youtube.video.rate.like': 30529, 'youtube.video.rate.dislike': 30530, 'youtube.playlist.play.all': 30531, 'youtube.playlist.play.default': 30532, 'youtube.playlist.play.reverse': 30533, 'youtube.playlist.play.shuffle': 30534, 'youtube.playlist.play.select': 30535, 'youtube.playlist.progress.updating': 30536, 'youtube.playlist.play.from_here': 30537, 'youtube.video.disliked': 30538, 'youtube.live': 30539, 'youtube.video.play_with': 30540, 'youtube.error.rtmpe_not_supported': 30542, 'youtube.refresh': 30543, 'youtube.video.description.links': 30544, 'youtube.video.description.links.not_found': 30545, 'youtube.sign.twice.title': 30546, 'youtube.sign.twice.text': 30547, 'youtube.video.more': 30548, 'youtube.error.no_video_streams_found': 30549, 'youtube.recommendations': 30551, 'youtube.function.cache': 30557, 'youtube.search.history': 30558, 'youtube.subtitle.language': 30560, 'youtube.none': 30561, 'youtube.prompt': 30566, 'youtube.set.as.watchlater': 30567, 'youtube.remove.as.watchlater': 30568, 'youtube.set.as.history': 30571, 'youtube.remove.as.history': 30572, 'youtube.succeeded': 30575, 'youtube.failed': 30576, 'youtube.settings': 30577, 'youtube.dash.enable.confirm': 30579, 'youtube.reset.access.manager.confirm': 30581, 'youtube.my_subscriptions_filtered': 30584, 'youtube.add.my_subscriptions.filter': 30587, 'youtube.remove.my_subscriptions.filter': 30588, 'youtube.added.my_subscriptions.filter': 30589, 'youtube.removed.my_subscriptions.filter': 30590, 'youtube.updated_': 30597, 'youtube.api.personal.enabled': 30598, 'youtube.api.personal.failed': 30599, 'youtube.subtitle._with_fallback': 30601, 'youtube.subtitle.no.auto.generated': 30602, 'youtube.quick.search': 30605, 'youtube.quick.search.incognito': 30606, 'youtube.clear_history': 30609, 'youtube.clear_history_confirmation': 30610, 'youtube.saved.playlists': 30611, 'youtube.retry': 30612, 'youtube.failed.watch_later.retry': 30614, 'youtube.cancel': 30615, 'youtube.must.be.signed.in': 30616, 'youtube.select.listen.ip': 30644, 'youtube.purchases': 30622, 'youtube.requires.krypton': 30624, 'youtube.inputstreamhelper.is.installed': 30625, 'youtube.upcoming.live': 30646, 'youtube.completed.live': 30647, 'youtube.api.key.incorrect': 30648, 'youtube.client.id.incorrect': 30649, 'youtube.client.secret.incorrect': 30650, 'youtube.perform.geolocation': 30653, 'youtube.my_location': 30654, 'youtube.switch.user': 30655, 'youtube.user.new': 30656, 'youtube.user.unnamed': 30657, 'youtube.enter.user.name': 30658, 'youtube.user.changed': 30659, 'youtube.remove.a.user': 30662, 'youtube.rename.a.user': 30663, 'youtube.switch.user.now': 30665, 'youtube.removed': 30666, 'youtube.renamed': 30667, 'youtube.playback.history': 30673, 'youtube.mark.watched': 30670, 'youtube.mark.unwatched': 30669, 'youtube.reset.resume.point': 30674, 'youtube.data.cache': 30687, 'youtube.httpd.not.running': 30699, 'youtube.client.ip': 30700, 'youtube.client.ip.failed': 30701, 'youtube.video.play_with_subtitles': 30702, 'youtube.are.you.sure': 30703, 'youtube.subtitles.download': 30705, 'youtube.pre.download.subtitles': 30706, 'youtube.untitled': 30707, 'youtube.video.play_audio_only': 30708, 'youtube.failed.watch_later.retry.2': 30709, 'youtube.failed.watch_later.retry.3': 30710, 'youtube.added.to.watch.later': 30713, 'youtube.added.to.playlist': 30714, 'youtube.removed.from.playlist': 30715, 'youtube.liked.video': 30716, 'youtube.disliked.video': 30717, 'youtube.unrated.video': 30718, 'youtube.subscribed.to.channel': 30719, 'youtube.unsubscribed.from.channel': 30720, 'youtube.uploads': 30726, 'youtube.video.play_ask_for_quality': 30730, 'youtube.key.requirement.notification': 30731, 'youtube.video.comments': 30732, 'youtube.video.comments.likes': 30733, 'youtube.video.comments.replies': 30734, 'youtube.video.comments.edited': 30735, } def __init__(self): kodion.AbstractProvider.__init__(self) self._resource_manager = None self._client = None self._is_logged_in = False self.v3_handle_error = v3.handle_error self.yt_video = yt_video def get_wizard_supported_views(self): return ['default', 'episodes'] def get_wizard_steps(self, context): return [(yt_setup_wizard.process, [self, context])] def is_logged_in(self): return self._is_logged_in @staticmethod def get_dev_config(context, addon_id, dev_configs): _dev_config = context.get_ui().get_home_window_property('configs') context.get_ui().clear_home_window_property('configs') dev_config = dict() if _dev_config is not None: context.log_debug('Using window property for developer keys is deprecated, instead use the youtube_registration module.') try: dev_config = json.loads(_dev_config) except ValueError: context.log_error('Error loading developer key: |invalid json|') if not dev_config and addon_id and dev_configs: dev_config = dev_configs.get(addon_id) if dev_config and not context.get_settings().allow_dev_keys(): context.log_debug('Developer config ignored') return None elif dev_config: if not dev_config.get('main') or not dev_config['main'].get('key') \ or not dev_config['main'].get('system') or not dev_config.get('origin') \ or not dev_config['main'].get('id') or not dev_config['main'].get('secret'): context.log_error('Error loading developer config: |invalid structure| ' 'expected: |{"origin": ADDON_ID, "main": {"system": SYSTEM_NAME, "key": API_KEY, "id": CLIENT_ID, "secret": CLIENT_SECRET}}|') return dict() else: dev_origin = dev_config['origin'] dev_main = dev_config['main'] dev_system = dev_main['system'] if dev_system == 'JSONStore': dev_key = b64decode(dev_main['key']) dev_id = b64decode(dev_main['id']) dev_secret = b64decode(dev_main['secret']) else: dev_key = dev_main['key'] dev_id = dev_main['id'] dev_secret = dev_main['secret'] context.log_debug('Using developer config: origin: |{0}| system |{1}|'.format(dev_origin, dev_system)) return {'origin': dev_origin, 'main': {'id': dev_id, 'secret': dev_secret, 'key': dev_key, 'system': dev_system}} else: return dict() def reset_client(self): self._client = None def get_client(self, context): if self._client is not None: return self._client # set the items per page (later) settings = context.get_settings() access_manager = context.get_access_manager() items_per_page = settings.get_items_per_page() language = settings.get_string('youtube.language', 'en-US') region = settings.get_string('youtube.region', 'US') api_last_origin = access_manager.get_last_origin() youtube_config = YouTube.CONFIGS.get('main') dev_id = context.get_param('addon_id', None) dev_configs = YouTube.CONFIGS.get('developer') dev_config = self.get_dev_config(context, dev_id, dev_configs) dev_keys = dict() if dev_config: dev_keys = dev_config.get('main') client = None refresh_tokens = list() if dev_id: dev_origin = dev_config.get('origin') if dev_config.get('origin') else dev_id if api_last_origin != dev_origin: context.log_debug('API key origin changed, clearing cache. |%s|' % dev_origin) access_manager.set_last_origin(dev_origin) self.get_resource_manager(context).clear() else: if api_last_origin != 'plugin.video.youtube': context.log_debug('API key origin changed, clearing cache. |plugin.video.youtube|') access_manager.set_last_origin('plugin.video.youtube') self.get_resource_manager(context).clear() if dev_id: access_tokens = access_manager.get_dev_access_token(dev_id).split('|') if len(access_tokens) != 2 or access_manager.is_dev_access_token_expired(dev_id): # reset access_token access_manager.update_dev_access_token(dev_id, '') access_tokens = list() else: access_tokens = access_manager.get_access_token().split('|') if len(access_tokens) != 2 or access_manager.is_access_token_expired(): # reset access_token access_manager.update_access_token('') access_tokens = list() if dev_id: if dev_keys: context.log_debug('Selecting YouTube developer config "%s"' % dev_id) else: context.log_debug('Selecting YouTube config "%s" w/ developer access tokens' % youtube_config['system']) if access_manager.developer_has_refresh_token(dev_id): if dev_keys: keys_changed = access_manager.dev_keys_changed(dev_id, dev_keys['key'], dev_keys['id'], dev_keys['secret']) else: keys_changed = access_manager.dev_keys_changed(dev_id, youtube_config['key'], youtube_config['id'], youtube_config['secret']) if keys_changed: context.log_warning('API key set changed: Resetting client and updating access token') self.reset_client() access_manager.update_dev_access_token(dev_id, access_token='', refresh_token='') access_tokens = access_manager.get_dev_access_token(dev_id) if access_tokens: access_tokens = access_tokens.split('|') refresh_tokens = access_manager.get_dev_refresh_token(dev_id) if refresh_tokens: refresh_tokens = refresh_tokens.split('|') context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) # create a new access_token if dev_keys: client = YouTube(language=language, region=region, items_per_page=items_per_page, config=dev_keys) else: client = YouTube(language=language, region=region, items_per_page=items_per_page, config=youtube_config) else: context.log_debug('Selecting YouTube config "%s"' % youtube_config['system']) if access_manager.has_refresh_token(): if YouTube.api_keys_changed: context.log_warning('API key set changed: Resetting client and updating access token') self.reset_client() access_manager.update_access_token(access_token='', refresh_token='') access_tokens = access_manager.get_access_token() if access_tokens: access_tokens = access_tokens.split('|') refresh_tokens = access_manager.get_refresh_token() if refresh_tokens: refresh_tokens = refresh_tokens.split('|') context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) # create a new access_token client = YouTube(language=language, region=region, items_per_page=items_per_page, config=youtube_config) if client: if len(access_tokens) != 2 and len(refresh_tokens) == 2: try: access_token_kodi, expires_in_kodi = client.refresh_token(refresh_tokens[1]) access_token_tv, expires_in_tv = client.refresh_token_tv(refresh_tokens[0]) access_tokens = [access_token_tv, access_token_kodi] access_token = '%s|%s' % (access_token_tv, access_token_kodi) expires_in = min(expires_in_tv, expires_in_kodi) if dev_id: access_manager.update_dev_access_token(dev_id, access_token, expires_in) else: access_manager.update_access_token(access_token, expires_in) except (InvalidGrant, LoginException) as ex: self.handle_exception(context, ex) access_tokens = ['', ''] # reset access_token if isinstance(ex, InvalidGrant): if dev_id: access_manager.update_dev_access_token(dev_id, access_token='', refresh_token='') else: access_manager.update_access_token(access_token='', refresh_token='') else: if dev_id: access_manager.update_dev_access_token(dev_id, '') else: access_manager.update_access_token('') # we clear the cache, so none cached data of an old account will be displayed. self.get_resource_manager(context).clear() # in debug log the login status self._is_logged_in = len(access_tokens) == 2 if self._is_logged_in: context.log_debug('User is logged in') else: context.log_debug('User is not logged in') if len(access_tokens) == 0: access_tokens = ['', ''] client.set_access_token(access_token=access_tokens[1]) client.set_access_token_tv(access_token_tv=access_tokens[0]) self._client = client self._client.set_log_error(context.log_error) else: self._client = YouTube(items_per_page=items_per_page, language=language, region=region, config=youtube_config) self._client.set_log_error(context.log_error) # in debug log the login status context.log_debug('User is not logged in') return self._client def get_resource_manager(self, context): if not self._resource_manager: # self._resource_manager = ResourceManager(weakref.proxy(context), weakref.proxy(self.get_client(context))) self._resource_manager = ResourceManager(context, self.get_client(context)) return self._resource_manager def get_alternative_fanart(self, context): return self.get_fanart(context) @staticmethod def get_fanart(context): return context.create_resource_path('media', 'fanart.jpg') # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/uri2addon/$') def on_uri2addon(self, context, re_match): uri = context.get_param('uri', '') if not uri: return False resolver = UrlResolver(context) res_url = resolver.resolve(uri) url_converter = UrlToItemConverter(flatten=True) url_converter.add_urls([res_url], self, context) items = url_converter.get_items(self, context, title_required=False) if len(items) > 0: return items[0] return False @kodion.RegisterProviderPath('^/playlist/(?P[^/]+)/$') def _on_playlist(self, context, re_match): self.set_content_type(context, kodion.constants.content_type.VIDEOS) result = [] playlist_id = re_match.group('playlist_id') page_token = context.get_param('page_token', '') # no caching json_data = self.get_client(context).get_playlist_items(playlist_id=playlist_id, page_token=page_token) if not v3.handle_error(self, context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) return result """ Lists the videos of a playlist. path : '/channel/(?P[^/]+)/playlist/(?P[^/]+)/' channel_id : ['mine'|] playlist_id: """ @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/playlist/(?P[^/]+)/$') def _on_channel_playlist(self, context, re_match): self.set_content_type(context, kodion.constants.content_type.VIDEOS) client = self.get_client(context) result = [] playlist_id = re_match.group('playlist_id') page_token = context.get_param('page_token', '') # no caching json_data = client.get_playlist_items(playlist_id=playlist_id, page_token=page_token) if not v3.handle_error(self, context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) return result """ Lists all playlists of a channel. path : '/channel/(?P[^/]+)/playlists/' channel_id: """ @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/playlists/$') def _on_channel_playlists(self, context, re_match): self.set_content_type(context, kodion.constants.content_type.FILES) result = [] channel_id = re_match.group('channel_id') page_token = context.get_param('page_token', '') resource_manager = self.get_resource_manager(context) item_params = {} incognito = str(context.get_param('incognito', False)).lower() == 'true' addon_id = context.get_param('addon_id', '') if incognito: item_params.update({'incognito': incognito}) if addon_id: item_params.update({'addon_id': addon_id}) playlists = resource_manager.get_related_playlists(channel_id) uploads_playlist = playlists.get('uploads', '') if uploads_playlist: uploads_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.uploads'])), context.create_uri(['channel', channel_id, 'playlist', uploads_playlist], item_params), image=context.create_resource_path('media', 'playlist.png')) result.append(uploads_item) # no caching json_data = self.get_client(context).get_playlists_of_channel(channel_id, page_token) if not v3.handle_error(self, context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) return result """ List live streams for channel. path : '/channel/(?P[^/]+)/live/' channel_id: """ @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/live/$') def _on_channel_live(self, context, re_match): self.set_content_type(context, kodion.constants.content_type.VIDEOS) result = [] channel_id = re_match.group('channel_id') page_token = context.get_param('page_token', '') safe_search = context.get_settings().safe_search() # no caching json_data = self.get_client(context).search(q='', search_type='video', event_type='live', channel_id=channel_id, page_token=page_token, safe_search=safe_search) if not v3.handle_error(self, context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) return result """ Lists a playlist folder and all uploaded videos of a channel. path :'/channel|user/(?P)[^/]+/' channel_id: """ @kodion.RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$') def _on_channel(self, context, re_match): listitem_channel_id = context.get_ui().get_info_label('Container.ListItem(0).Property(channel_id)') method = re_match.group('method') channel_id = re_match.group('channel_id') if method == 'channel' and channel_id and channel_id.lower() == 'property': if listitem_channel_id and listitem_channel_id.lower().startswith(('mine', 'uc')): context.execute('Container.Update(%s)' % context.create_uri(['channel', listitem_channel_id])) # redirect if keymap, without redirect results in 'invalid handle -1' if method == 'channel' and not channel_id: return False self.set_content_type(context, kodion.constants.content_type.VIDEOS) resource_manager = self.get_resource_manager(context) mine_id = '' result = [] """ This is a helper routine if we only have the username of a channel. This will retrieve the correct channel id based on the username. """ if method == 'user' or channel_id == 'mine': context.log_debug('Trying to get channel id for user "%s"' % channel_id) json_data = context.get_function_cache().get(FunctionCache.ONE_DAY, self.get_client(context).get_channel_by_username, channel_id) if not v3.handle_error(self, context, json_data): return False # we correct the channel id based on the username items = json_data.get('items', []) if len(items) > 0: if method == 'user': channel_id = items[0]['id'] else: mine_id = items[0]['id'] else: context.log_warning('Could not find channel ID for user "%s"' % channel_id) if method == 'user': return False channel_fanarts = resource_manager.get_fanarts([channel_id]) page = int(context.get_param('page', 1)) page_token = context.get_param('page_token', '') incognito = str(context.get_param('incognito', False)).lower() == 'true' addon_id = context.get_param('addon_id', '') item_params = {} if incognito: item_params.update({'incognito': incognito}) if addon_id: item_params.update({'addon_id': addon_id}) hide_folders = str(context.get_param('hide_folders', False)).lower() == 'true' if page == 1 and not hide_folders: hide_playlists = str(context.get_param('hide_playlists', False)).lower() == 'true' hide_search = str(context.get_param('hide_search', False)).lower() == 'true' hide_live = str(context.get_param('hide_live', False)).lower() == 'true' if not hide_playlists: playlists_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.playlists'])), context.create_uri(['channel', channel_id, 'playlists'], item_params), image=context.create_resource_path('media', 'playlist.png')) playlists_item.set_fanart(channel_fanarts.get(channel_id, self.get_fanart(context))) result.append(playlists_item) search_live_id = mine_id if mine_id else channel_id if not hide_search: search_item = kodion.items.NewSearchItem(context, alt_name=context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.search'])), image=context.create_resource_path('media', 'search.png'), fanart=self.get_fanart(context), channel_id=search_live_id, incognito=incognito, addon_id=addon_id) result.append(search_item) if not hide_live: live_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.live'])), context.create_uri(['channel', search_live_id, 'live'], item_params), image=context.create_resource_path('media', 'live.png')) result.append(live_item) playlists = resource_manager.get_related_playlists(channel_id) upload_playlist = playlists.get('uploads', '') if upload_playlist: json_data = context.get_function_cache().get(FunctionCache.ONE_MINUTE * 5, self.get_client(context).get_playlist_items, upload_playlist, page_token=page_token) if not v3.handle_error(self, context, json_data): return False result.extend( v3.response_to_items(self, context, json_data, sort=lambda x: x.get_aired(), reverse_sort=True)) return result # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/location/mine/$') def _on_my_location(self, context, re_match): self.set_content_type(context, kodion.constants.content_type.FILES) settings = context.get_settings() result = list() # search search_item = kodion.items.SearchItem(context, image=context.create_resource_path('media', 'search.png'), fanart=self.get_fanart(context), location=True) result.append(search_item) # completed live events if settings.get_bool('youtube.folder.completed.live.show', True): live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.completed.live']), context.create_uri(['special', 'completed_live'], params={'location': True}), image=context.create_resource_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # upcoming live events if settings.get_bool('youtube.folder.upcoming.live.show', True): live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.upcoming.live']), context.create_uri(['special', 'upcoming_live'], params={'location': True}), image=context.create_resource_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # live events live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.live']), context.create_uri(['special', 'live'], params={'location': True}), image=context.create_resource_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) return result """ Plays a video. path for video: '/play/?video_id=XXXXXXX' path for playlist: '/play/?playlist_id=XXXXXXX&mode=[OPTION]' OPTION: [normal(default)|reverse|shuffle] path for channel live streams: '/play/?channel_id=UCXXXXXXX&live=X OPTION: live parameter required, live=1 for first live stream live = index of live stream if channel has multiple live streams """ # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/play/$') def on_play(self, context, re_match): listitem_path = context.get_ui().get_info_label('Container.ListItem(0).FileNameAndPath') redirect = False params = context.get_params() if 'video_id' not in params and 'playlist_id' not in params and \ 'channel_id' not in params and 'live' not in params: if context.is_plugin_path(listitem_path, 'play'): video_id = find_video_id(listitem_path) if video_id: context.set_param('video_id', video_id) params = context.get_params() else: return False else: return False if context.get_ui().get_home_window_property('prompt_for_subtitles') != params.get('video_id'): context.get_ui().clear_home_window_property('prompt_for_subtitles') if context.get_ui().get_home_window_property('audio_only') != params.get('video_id'): context.get_ui().clear_home_window_property('audio_only') if context.get_ui().get_home_window_property('ask_for_quality') != params.get('video_id'): context.get_ui().clear_home_window_property('ask_for_quality') if 'prompt_for_subtitles' in params: prompt_subtitles = params['prompt_for_subtitles'] == '1' del params['prompt_for_subtitles'] if prompt_subtitles and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems context.get_ui().set_home_window_property('prompt_for_subtitles', params['video_id']) context.log_debug('Redirecting playback with subtitles') redirect = True elif 'audio_only' in params: audio_only = params['audio_only'] == '1' del params['audio_only'] if audio_only and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems context.get_ui().set_home_window_property('audio_only', params['video_id']) context.log_debug('Redirecting audio only playback') redirect = True elif 'ask_for_quality' in params: ask_for_quality = params['ask_for_quality'] == '1' del params['ask_for_quality'] if ask_for_quality and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems context.get_ui().set_home_window_property('ask_for_quality', params['video_id']) context.log_debug('Redirecting audio only playback') redirect = True if 'playlist_id' not in params and 'video_id' in params and (context.get_handle() == -1 or redirect): builtin = 'PlayMedia(%s)' if context.get_handle() == -1 else 'RunPlugin(%s)' if not redirect: context.log_debug('Redirecting playback, handle is -1') context.execute(builtin % context.create_uri(['play'], {'video_id': params['video_id']})) return if 'playlist_id' in params and (context.get_handle() != -1): builtin = 'RunPlugin(%s)' stream_url = context.create_uri(['play'], params) xbmcplugin.setResolvedUrl(handle=context.get_handle(), succeeded=False, listitem=xbmcgui.ListItem(path=stream_url)) context.execute(builtin % context.create_uri(['play'], params)) return if 'video_id' in params and 'playlist_id' not in params: return yt_play.play_video(self, context) elif 'playlist_id' in params: return yt_play.play_playlist(self, context) elif 'channel_id' in params and 'live' in params: if int(params['live']) > 0: return yt_play.play_channel_live(self, context) return False @kodion.RegisterProviderPath('^/video/(?P[^/]+)/$') def _on_video_x(self, context, re_match): method = re_match.group('method') return yt_video.process(method, self, context, re_match) @kodion.RegisterProviderPath('^/playlist/(?P[^/]+)/(?P[^/]+)/$') def _on_playlist_x(self, context, re_match): method = re_match.group('method') category = re_match.group('category') return yt_playlist.process(method, category, self, context) @kodion.RegisterProviderPath('^/subscriptions/(?P[^/]+)/$') def _on_subscriptions(self, context, re_match): method = re_match.group('method') if method == 'list': self.set_content_type(context, kodion.constants.content_type.FILES) return yt_subscriptions.process(method, self, context) @kodion.RegisterProviderPath('^/special/(?P[^/]+)/$') def _on_yt_specials(self, context, re_match): category = re_match.group('category') return yt_specials.process(category, self, context) # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/history/clear/$') def _on_yt_clear_history(self, context, re_match): if context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.clear_history_confirmation'])): json_data = self.get_client(context).clear_watch_history() if 'error' not in json_data: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/watch_later/playlist_id/$') def _on_yt_get_watch_later_id(self, context, re_match): client = self.get_client(context) access_manager = context.get_access_manager() if self.is_logged_in(): watch_later_id = None count = 0 while not watch_later_id: count += 1 watch_later_id = client.get_watch_later_id() if watch_later_id: access_manager.set_watch_later_id(watch_later_id) context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) break else: if count == 1: if not context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.failed.watch_later.retry']), nolabel=context.localize(self.LOCAL_MAP['youtube.cancel']), yeslabel=context.localize(self.LOCAL_MAP['youtube.retry'])): break elif count == 2: if not context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.failed.watch_later.retry.2']), nolabel=context.localize(self.LOCAL_MAP['youtube.cancel']), yeslabel=context.localize(self.LOCAL_MAP['youtube.retry'])): break else: _ = context.get_ui().on_ok(context.get_name(), context.localize(self.LOCAL_MAP['youtube.failed.watch_later.retry.3'])) break else: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.must.be.signed.in'])) @kodion.RegisterProviderPath('^/users/(?P[^/]+)/$') def _on_users(self, context, re_match): action = re_match.group('action') refresh = context.get_param('refresh', 'true').lower() == 'true' access_manager = context.get_access_manager() ui = context.get_ui() def add_user(_access_manager_users): _results = ui.on_keyboard_input(context.localize(self.LOCAL_MAP['youtube.enter.user.name'])) if _results[0] is False: return None _new_user_name = _results[1] if not _new_user_name.strip(): _new_user_name = context.localize(self.LOCAL_MAP['youtube.user.unnamed']) _new_users = {} for idx, key in enumerate(list(_access_manager_users.keys())): _new_users[str(idx)] = _access_manager_users[key] _new_users[str(len(_new_users))] = access_manager.get_new_user(_new_user_name) access_manager.set_users(_new_users) return str(len(_new_users) - 1) def switch_to_user(_user): _user_name = access_manager.get_users()[_user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) access_manager.set_user(_user, switch_to=True) ui.show_notification(context.localize(self.LOCAL_MAP['youtube.user.changed']) % _user_name, context.localize(self.LOCAL_MAP['youtube.switch.user'])) self.get_resource_manager(context).clear() if refresh: ui.refresh_container() if action == 'switch': access_manager_users = access_manager.get_users() current_user = access_manager.get_user() users = [ui.bold(context.localize(self.LOCAL_MAP['youtube.user.new']))] user_index_map = [] for k in list(access_manager_users.keys()): if k == current_user: if access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): users.append( ui.color('limegreen', ' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) ) else: users.append(' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) elif access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): users.append(ui.color('limegreen', access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])))) else: users.append(access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed']))) user_index_map.append(k) result = ui.on_select(context.localize(self.LOCAL_MAP['youtube.switch.user']), users) if result == -1: return True elif result == 0: user = add_user(access_manager_users) else: user = user_index_map[result - 1] if user and (user != access_manager.get_user()): switch_to_user(user) elif action == 'add': user = add_user(access_manager.get_users()) if user: user_name = access_manager.get_users()[user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) result = ui.on_yes_no_input(context.localize(self.LOCAL_MAP['youtube.switch.user']), context.localize(self.LOCAL_MAP['youtube.switch.user.now']) % user_name) if result: switch_to_user(user) elif action == 'remove': access_manager_users = access_manager.get_users() users = [] user_index_map = [] current_user = access_manager.get_user() current_user_dict = access_manager_users[current_user] for k in list(access_manager_users.keys()): if k == current_user: if access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): users.append( ui.color('limegreen', ' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) ) else: users.append(' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) elif access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): users.append(ui.color('limegreen', access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])))) else: users.append(access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed']))) user_index_map.append(k) result = ui.on_select(context.localize(self.LOCAL_MAP['youtube.remove.a.user']), users) if result == -1: return True else: user = user_index_map[result] user_name = access_manager_users[user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) result = ui.on_remove_content(user_name) if result: if user == current_user: access_manager.set_user('0', switch_to=True) del access_manager_users[user] new_users = {} for i, u in enumerate(list(access_manager_users.keys())): if access_manager_users[u] == current_user_dict: access_manager.set_user(str(i), switch_to=True) new_users[str(i)] = access_manager_users[u] if not new_users.get(access_manager.get_user()): access_manager.set_user('0', switch_to=True) access_manager.set_users(new_users) ui.show_notification(context.localize(self.LOCAL_MAP['youtube.removed']) % user_name, context.localize(self.LOCAL_MAP['youtube.remove'])) elif action == 'rename': access_manager_users = access_manager.get_users() users = [] user_index_map = [] current_user = access_manager.get_user() for k in list(access_manager_users.keys()): if k == current_user: if access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): users.append( ui.color('limegreen', ' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) ) else: users.append(' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) elif access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): users.append(ui.color('limegreen', access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])))) else: users.append(access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed']))) user_index_map.append(k) result = ui.on_select(context.localize(self.LOCAL_MAP['youtube.rename.a.user']), users) if result == -1: return True else: user = user_index_map[result] old_user_name = access_manager_users[user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) results = ui.on_keyboard_input(context.localize(self.LOCAL_MAP['youtube.enter.user.name']), default=old_user_name) if results[0] is False: return True new_user_name = results[1] if not new_user_name.strip() or (old_user_name == new_user_name): return True access_manager_users[user]['name'] = new_user_name access_manager.set_users(access_manager_users) ui.show_notification(context.localize(self.LOCAL_MAP['youtube.renamed']) % (old_user_name, new_user_name), context.localize(self.LOCAL_MAP['youtube.rename'])) return True @kodion.RegisterProviderPath('^/sign/(?P[^/]+)/$') def _on_sign(self, context, re_match): sign_out_confirmed = context.get_param('confirmed', '').lower() == 'true' mode = re_match.group('mode') if (mode == 'in') and context.get_access_manager().has_refresh_token(): yt_login.process('out', self, context, sign_out_refresh=False) if not sign_out_confirmed: if (mode == 'out') and context.get_ui().on_yes_no_input(context.localize(self.LOCAL_MAP['youtube.sign.out']), context.localize(self.LOCAL_MAP['youtube.are.you.sure'])): sign_out_confirmed = True if (mode == 'in') or ((mode == 'out') and sign_out_confirmed): yt_login.process(mode, self, context) return False @kodion.RegisterProviderPath('^/search/$') def endpoint_search(self, context, re_match): query = context.get_param('q', '') if not query: return [] return self.on_search(query, context, re_match) def _search_channel_or_playlist(self, context, id_string): json_data = {} result = [] if re.match(r'U[CU][0-9a-zA-Z_\-]{20,24}', id_string): json_data = self.get_client(context).get_channels(id_string) elif re.match(r'[OP]L[0-9a-zA-Z_\-]{30,40}', id_string): json_data = self.get_client(context).get_playlists(id_string) if not json_data or not v3.handle_error(self, context, json_data): return [] result.extend(v3.response_to_items(self, context, json_data)) return result def on_search(self, search_text, context, re_match): result = self._search_channel_or_playlist(context, search_text) if result: # found a channel or playlist matching search_text return result channel_id = context.get_param('channel_id', '') event_type = context.get_param('event_type', '') hide_folders = str(context.get_param('hide_folders', False)).lower() == 'true' location = str(context.get_param('location', False)).lower() == 'true' page = int(context.get_param('page', 1)) page_token = context.get_param('page_token', '') search_type = context.get_param('search_type', 'video') safe_search = context.get_settings().safe_search() context.set_param('q', search_text) if search_type == 'video': self.set_content_type(context, kodion.constants.content_type.VIDEOS) else: self.set_content_type(context, kodion.constants.content_type.FILES) if page == 1 and search_type == 'video' and not event_type and not hide_folders: if not channel_id and not location: channel_params = {} channel_params.update(context.get_params()) channel_params['search_type'] = 'channel' channel_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.channels'])), context.create_uri([context.get_path()], channel_params), image=context.create_resource_path('media', 'channels.png')) channel_item.set_fanart(self.get_fanart(context)) result.append(channel_item) if not location: playlist_params = {} playlist_params.update(context.get_params()) playlist_params['search_type'] = 'playlist' playlist_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.playlists'])), context.create_uri([context.get_path()], playlist_params), image=context.create_resource_path('media', 'playlist.png')) playlist_item.set_fanart(self.get_fanart(context)) result.append(playlist_item) if not channel_id: # live live_params = {} live_params.update(context.get_params()) live_params['search_type'] = 'video' live_params['event_type'] = 'live' live_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.live'])), context.create_uri([context.get_path().replace('input', 'query')], live_params), image=context.create_resource_path('media', 'live.png')) result.append(live_item) json_data = context.get_function_cache().get(FunctionCache.ONE_MINUTE * 10, self.get_client(context).search, q=search_text, search_type=search_type, event_type=event_type, safe_search=safe_search, page_token=page_token, channel_id=channel_id, location=location) if not v3.handle_error(self, context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) return result @kodion.RegisterProviderPath('^/config/(?P[^/]+)/$') def configure_addon(self, context, re_match): switch = re_match.group('switch') settings = context.get_settings() if switch == 'youtube': context.addon().openSettings() context.get_ui().refresh_container() elif switch == 'mpd': use_dash = context.use_inputstream_adaptive() if use_dash: xbmcaddon.Addon(id='inputstream.adaptive').openSettings() else: settings.set_bool('kodion.video.quality.mpd', False) elif switch == 'subtitles': yt_language = context.get_settings().get_string('youtube.language', 'en-US') sub_setting = context.get_settings().subtitle_languages() if yt_language.startswith('en'): sub_opts = [context.localize(self.LOCAL_MAP['youtube.none']), context.localize(self.LOCAL_MAP['youtube.prompt']), context.localize(self.LOCAL_MAP['youtube.subtitle._with_fallback']) % ('en', 'en-US/en-GB'), yt_language, '%s (%s)' % (yt_language, context.localize(self.LOCAL_MAP['youtube.subtitle.no.auto.generated']))] else: sub_opts = [context.localize(self.LOCAL_MAP['youtube.none']), context.localize(self.LOCAL_MAP['youtube.prompt']), context.localize(self.LOCAL_MAP['youtube.subtitle._with_fallback']) % (yt_language, 'en'), yt_language, '%s (%s)' % (yt_language, context.localize(self.LOCAL_MAP['youtube.subtitle.no.auto.generated']))] sub_opts[sub_setting] = context.get_ui().bold(sub_opts[sub_setting]) result = context.get_ui().on_select(context.localize(self.LOCAL_MAP['youtube.subtitle.language']), sub_opts) if result > -1: context.get_settings().set_subtitle_languages(result) result = context.get_ui().on_yes_no_input( context.localize(self.LOCAL_MAP['youtube.subtitles.download']), context.localize(self.LOCAL_MAP['youtube.pre.download.subtitles']) ) if result > -1: context.get_settings().set_subtitle_download(result == 1) elif switch == 'listen_ip': local_ranges = ('10.', '172.16.', '192.168.') addresses = [iface[4][0] for iface in socket.getaddrinfo(socket.gethostname(), None) if iface[4][0].startswith(local_ranges)] + ['127.0.0.1', '0.0.0.0'] selected_address = context.get_ui().on_select(context.localize(self.LOCAL_MAP['youtube.select.listen.ip']), addresses) if selected_address == -1: return False else: context.get_settings().set_httpd_listen(addresses[selected_address]) else: return False # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/my_subscriptions/filter/$') def manage_my_subscription_filter(self, context, re_match): params = context.get_params() action = params.get('action') channel = params.get('channel_name') if (not channel) or (not action): return filter_enabled = context.get_settings().get_bool('youtube.folder.my_subscriptions_filtered.show', False) if not filter_enabled: return channel_name = channel.lower() channel_name = channel_name.replace(',', '') filter_string = context.get_settings().get_string('youtube.filter.my_subscriptions_filtered.list', '') filter_string = filter_string.replace(', ', ',') filter_list = filter_string.split(',') filter_list = [x.lower() for x in filter_list] if action == 'add': if channel_name not in filter_list: filter_list.append(channel_name) elif action == 'remove': if channel_name in filter_list: filter_list = [chan_name for chan_name in filter_list if chan_name != channel_name] modified_string = ','.join(filter_list).lstrip(',') if filter_string != modified_string: context.get_settings().set_string('youtube.filter.my_subscriptions_filtered.list', modified_string) message = '' if action == 'add': message = context.localize(self.LOCAL_MAP['youtube.added.my_subscriptions.filter']) elif action == 'remove': message = context.localize(self.LOCAL_MAP['youtube.removed.my_subscriptions.filter']) if message: context.get_ui().show_notification(message=message) context.get_ui().refresh_container() @kodion.RegisterProviderPath('^/maintain/(?P[^/]+)/(?P[^/]+)/$') def maintenance_actions(self, context, re_match): maint_type = re_match.group('maint_type') action = re_match.group('action') if action == 'clear': if maint_type == 'function_cache': if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.function.cache'])): context.get_function_cache().clear() context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) elif maint_type == 'data_cache': if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.data.cache'])): context.get_data_cache().clear() context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) elif maint_type == 'search_cache': if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.search.history'])): context.get_search_history().clear() context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) elif maint_type == 'playback_history': if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.playback.history'])): context.get_playback_history().clear() context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) elif action == 'reset': if maint_type == 'access_manager': if context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.reset.access.manager.confirm'])): try: context.get_function_cache().clear() access_manager = context.get_access_manager() client = self.get_client(context) if access_manager.has_refresh_token(): refresh_tokens = access_manager.get_refresh_token().split('|') refresh_tokens = list(set(refresh_tokens)) for refresh_token in refresh_tokens: try: client.revoke(refresh_token) except: pass self.reset_client() access_manager.update_access_token(access_token='', refresh_token='') context.get_ui().refresh_container() context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) except: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.failed'])) elif action == 'delete': _maint_files = {'function_cache': 'cache.sqlite', 'search_cache': 'search.sqlite', 'data_cache': 'data_cache.sqlite', 'playback_history': 'playback_history', 'settings_xml': 'settings.xml', 'api_keys': 'api_keys.json', 'access_manager': 'access_manager.json', 'temp_files': 'special://temp/plugin.video.youtube/'} _file = _maint_files.get(maint_type, '') success = False if _file: if 'sqlite' in _file: _file_w_path = os.path.join(context.get_cache_path(), _file) elif maint_type == 'temp_files': _file_w_path = _file elif _file == 'playback_history': _file = ''.join([str(context.get_access_manager().get_current_user_id()), '.sqlite']) _file_w_path = os.path.join(os.path.join(context.get_data_path(), 'playback'), _file) else: _file_w_path = os.path.join(context.get_data_path(), _file) if context.get_ui().on_delete_content(_file): if maint_type == 'temp_files': _trans_path = xbmc.translatePath(_file_w_path) try: xbmcvfs.rmdir(_trans_path, force=True) except: pass if xbmcvfs.exists(_trans_path): try: shutil.rmtree(_trans_path) except: pass success = not xbmcvfs.exists(_trans_path) elif _file_w_path: success = xbmcvfs.delete(_file_w_path) if success: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) else: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.failed'])) elif action == 'install': if maint_type == 'inputstreamhelper': if context.get_system_version().get_version()[0] >= 17: try: xbmcaddon.Addon('script.module.inputstreamhelper') context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.inputstreamhelper.is.installed'])) except RuntimeError: context.execute('InstallAddon(script.module.inputstreamhelper)') else: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.requires.krypton'])) # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/api/update/$') def api_key_update(self, context, re_match): settings = context.get_settings() params = context.get_params() client_id = params.get('client_id') client_secret = params.get('client_secret') api_key = params.get('api_key') enable = params.get('enable', '').lower() == 'true' updated_list = [] log_list = [] if api_key: settings.set_string('youtube.api.key', api_key) updated_list.append(context.localize(self.LOCAL_MAP['youtube.api.key'])) log_list.append('Key') if client_id: settings.set_string('youtube.api.id', client_id) updated_list.append(context.localize(self.LOCAL_MAP['youtube.api.id'])) log_list.append('Id') if client_secret: settings.set_string('youtube.api.secret', client_secret) updated_list.append(context.localize(self.LOCAL_MAP['youtube.api.secret'])) log_list.append('Secret') if updated_list: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.updated_']) % ', '.join(updated_list)) context.log_debug('Updated API keys: %s' % ', '.join(log_list)) client_id = settings.get_string('youtube.api.id', '') client_secret = settings.get_string('youtube.api.secret', '') api_key = settings.get_string('youtube.api.key', '') missing_list = [] log_list = [] if enable and client_id and client_secret and api_key: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.api.personal.enabled'])) context.log_debug('Personal API keys enabled') elif enable: if not api_key: missing_list.append(context.localize(self.LOCAL_MAP['youtube.api.key'])) log_list.append('Key') if not client_id: missing_list.append(context.localize(self.LOCAL_MAP['youtube.api.id'])) log_list.append('Id') if not client_secret: missing_list.append(context.localize(self.LOCAL_MAP['youtube.api.secret'])) log_list.append('Secret') context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.api.personal.failed']) % ', '.join(missing_list)) context.log_debug('Failed to enable personal API keys. Missing: %s' % ', '.join(log_list)) # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/show_client_ip/$') def show_client_ip(self, context, re_match): port = context.get_settings().httpd_port() if is_httpd_live(port=port): client_ip = get_client_ip_address(port=port) if client_ip: context.get_ui().on_ok(context.get_name(), context.localize(self.LOCAL_MAP['youtube.client.ip']) % client_ip) else: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.client.ip.failed'])) else: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.httpd.not.running'])) # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/playback_history/$') def on_playback_history(self, context, re_match): params = context.get_params() video_id = params.get('video_id') action = params.get('action') if not video_id or not action: return True playback_history = context.get_playback_history() items = playback_history.get_items([video_id]) if not items or not items.get(video_id): item_dict = {'play_count': '0', 'total_time': '0.0', 'played_time': '0.0', 'played_percent': '0'} else: item_dict = items.get(video_id) if action == 'mark_unwatched': if int(item_dict.get('play_count', 0)) > 0: item_dict['play_count'] = '0' item_dict['played_time'] = '0.0' item_dict['played_percent'] = '0' elif action == 'mark_watched': if int(item_dict.get('play_count', 0)) == 0: item_dict['play_count'] = '1' elif action == 'reset_resume': item_dict['played_time'] = '0.0' item_dict['played_percent'] = '0' item_dict['play_count'] = item_dict.get('play_count', '0') item_dict['total_time'] = item_dict.get('total_time', '0.0') item_dict['played_time'] = item_dict.get('played_time', '0.0') item_dict['played_percent'] = item_dict.get('played_percent', '0') playback_history.update(video_id, item_dict['play_count'], item_dict['total_time'], item_dict['played_time'], item_dict['played_percent']) context.get_ui().refresh_container() return True def on_root(self, context, re_match): """ Support old YouTube url calls, but also log a deprecation warnings. """ old_action = context.get_param('action', '') if old_action: return yt_old_actions.process_old_action(self, context, re_match) settings = context.get_settings() _ = self.get_client(context) # required for self.is_logged_in() self.set_content_type(context, kodion.constants.content_type.FILES) result = [] # sign in if not self.is_logged_in() and settings.get_bool('youtube.folder.sign.in.show', True): sign_in_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.sign.in'])), context.create_uri(['sign', 'in']), image=context.create_resource_path('media', 'sign_in.png')) sign_in_item.set_action(True) sign_in_item.set_fanart(self.get_fanart(context)) result.append(sign_in_item) if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions.show', True): # my subscription my_subscriptions_item = DirectoryItem( context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.my_subscriptions'])), context.create_uri(['special', 'new_uploaded_videos_tv']), context.create_resource_path('media', 'new_uploads.png')) my_subscriptions_item.set_fanart(self.get_fanart(context)) result.append(my_subscriptions_item) if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions_filtered.show', True): # my subscriptions filtered my_subscriptions_filtered_item = DirectoryItem( context.localize(self.LOCAL_MAP['youtube.my_subscriptions_filtered']), context.create_uri(['special', 'new_uploaded_videos_tv_filtered']), context.create_resource_path('media', 'new_uploads.png')) my_subscriptions_filtered_item.set_fanart(self.get_fanart(context)) result.append(my_subscriptions_filtered_item) # Recommendations if self.is_logged_in() and settings.get_bool('youtube.folder.recommendations.show', True): recommendations_item = DirectoryItem( context.localize(self.LOCAL_MAP['youtube.recommendations']), context.create_uri(['special', 'recommendations']), context.create_resource_path('media', 'popular.png')) recommendations_item.set_fanart(self.get_fanart(context)) result.append(recommendations_item) # what to watch if settings.get_bool('youtube.folder.popular_right_now.show', True): what_to_watch_item = DirectoryItem( context.localize(self.LOCAL_MAP['youtube.popular_right_now']), context.create_uri(['special', 'popular_right_now']), context.create_resource_path('media', 'popular.png')) what_to_watch_item.set_fanart(self.get_fanart(context)) result.append(what_to_watch_item) # search if settings.get_bool('youtube.folder.search.show', True): search_item = kodion.items.SearchItem(context, image=context.create_resource_path('media', 'search.png'), fanart=self.get_fanart(context)) result.append(search_item) if settings.get_bool('youtube.folder.quick_search.show', True): quick_search_item = kodion.items.NewSearchItem(context, alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search']), fanart=self.get_fanart(context)) result.append(quick_search_item) if settings.get_bool('youtube.folder.quick_search_incognito.show', True): quick_search_incognito_item = kodion.items.NewSearchItem(context, alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search.incognito']), image=context.create_resource_path('media', 'search.png'), fanart=self.get_fanart(context), incognito=True) result.append(quick_search_incognito_item) # my location if settings.get_bool('youtube.folder.my_location.show', True) and settings.get_location(): my_location_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.my_location']), context.create_uri(['location', 'mine']), image=context.create_resource_path('media', 'channel.png')) my_location_item.set_fanart(self.get_fanart(context)) result.append(my_location_item) # subscriptions if self.is_logged_in(): access_manager = context.get_access_manager() # my channel if settings.get_bool('youtube.folder.my_channel.show', True): my_channel_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.my_channel']), context.create_uri(['channel', 'mine']), image=context.create_resource_path('media', 'channel.png')) my_channel_item.set_fanart(self.get_fanart(context)) result.append(my_channel_item) # purchases if settings.get_bool('youtube.folder.purchases.show', False) and \ settings.use_dash() and \ settings.use_dash_videos() and \ 'drm' in context.inputstream_adaptive_capabilities(): purchases_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.purchases']), context.create_uri(['special', 'purchases']), image=context.create_resource_path('media', 'popular.png')) purchases_item.set_fanart(self.get_fanart(context)) result.append(purchases_item) # watch later if settings.get_bool('youtube.folder.watch_later.show', True): watch_later_playlist_id = access_manager.get_watch_later_id() watch_later_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.watch_later']), context.create_uri( ['channel', 'mine', 'playlist', watch_later_playlist_id]), context.create_resource_path('media', 'watch_later.png')) watch_later_item.set_fanart(self.get_fanart(context)) context_menu = [] yt_context_menu.append_play_all_from_playlist(context_menu, self, context, watch_later_playlist_id) watch_later_item.set_context_menu(context_menu) result.append(watch_later_item) # liked videos if settings.get_bool('youtube.folder.liked_videos.show', True): resource_manager = self.get_resource_manager(context) playlists = resource_manager.get_related_playlists(channel_id='mine') if 'likes' in playlists: liked_videos_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.video.liked']), context.create_uri( ['channel', 'mine', 'playlist', playlists['likes']]), context.create_resource_path('media', 'likes.png')) liked_videos_item.set_fanart(self.get_fanart(context)) context_menu = [] yt_context_menu.append_play_all_from_playlist(context_menu, self, context, playlists['likes']) liked_videos_item.set_context_menu(context_menu) result.append(liked_videos_item) # disliked videos if settings.get_bool('youtube.folder.disliked_videos.show', True): disliked_videos_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.video.disliked']), context.create_uri(['special', 'disliked_videos']), context.create_resource_path('media', 'dislikes.png')) disliked_videos_item.set_fanart(self.get_fanart(context)) result.append(disliked_videos_item) # history if settings.get_bool('youtube.folder.history.show', False): watch_history_playlist_id = access_manager.get_watch_history_id() if watch_history_playlist_id == 'HL': watch_history_item = DirectoryItem( context.localize(self.LOCAL_MAP['youtube.history']), context.create_uri(['special', 'watch_history_tv']), context.create_resource_path('media', 'history.png')) watch_history_item.set_fanart(self.get_fanart(context)) else: watch_history_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.history']), context.create_uri( ['channel', 'mine', 'playlist', watch_history_playlist_id]), context.create_resource_path('media', 'history.png')) watch_history_item.set_fanart(self.get_fanart(context)) context_menu = [] yt_context_menu.append_play_all_from_playlist(context_menu, self, context, watch_history_playlist_id) watch_history_item.set_context_menu(context_menu) result.append(watch_history_item) # (my) playlists if settings.get_bool('youtube.folder.playlists.show', True): playlists_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.playlists']), context.create_uri(['channel', 'mine', 'playlists']), context.create_resource_path('media', 'playlist.png')) playlists_item.set_fanart(self.get_fanart(context)) result.append(playlists_item) # saved playlists if settings.get_bool('youtube.folder.saved.playlists.show', True): playlists_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.saved.playlists']), context.create_uri(['special', 'saved_playlists']), context.create_resource_path('media', 'playlist.png')) playlists_item.set_fanart(self.get_fanart(context)) result.append(playlists_item) # subscriptions if settings.get_bool('youtube.folder.subscriptions.show', True): subscriptions_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.subscriptions']), context.create_uri(['subscriptions', 'list']), image=context.create_resource_path('media', 'channels.png')) subscriptions_item.set_fanart(self.get_fanart(context)) result.append(subscriptions_item) # browse channels if settings.get_bool('youtube.folder.browse_channels.show', True): browse_channels_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.browse_channels']), context.create_uri(['special', 'browse_channels']), image=context.create_resource_path('media', 'browse_channels.png')) browse_channels_item.set_fanart(self.get_fanart(context)) result.append(browse_channels_item) # completed live events if settings.get_bool('youtube.folder.completed.live.show', True): live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.completed.live']), context.create_uri(['special', 'completed_live']), image=context.create_resource_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # upcoming live events if settings.get_bool('youtube.folder.upcoming.live.show', True): live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.upcoming.live']), context.create_uri(['special', 'upcoming_live']), image=context.create_resource_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # live events if settings.get_bool('youtube.folder.live.show', True): live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.live']), context.create_uri(['special', 'live']), image=context.create_resource_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # switch user if settings.get_bool('youtube.folder.switch.user.show', True): switch_user_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.switch.user']), context.create_uri(['users', 'switch']), image=context.create_resource_path('media', 'channel.png')) switch_user_item.set_action(True) switch_user_item.set_fanart(self.get_fanart(context)) result.append(switch_user_item) # sign out if self.is_logged_in() and settings.get_bool('youtube.folder.sign.out.show', True): sign_out_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.sign.out']), context.create_uri(['sign', 'out']), image=context.create_resource_path('media', 'sign_out.png')) sign_out_item.set_action(True) sign_out_item.set_fanart(self.get_fanart(context)) result.append(sign_out_item) if settings.get_bool('youtube.folder.settings.show', True): settings_menu_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.settings']), context.create_uri(['config', 'youtube']), image=context.create_resource_path('media', 'settings.png')) settings_menu_item.set_action(True) settings_menu_item.set_fanart(self.get_fanart(context)) result.append(settings_menu_item) return result @staticmethod def set_content_type(context, content_type): context.set_content_type(content_type) if content_type == kodion.constants.content_type.VIDEOS: context.add_sort_method(kodion.constants.sort_method.UNSORTED, kodion.constants.sort_method.VIDEO_RUNTIME, kodion.constants.sort_method.DATE_ADDED, kodion.constants.sort_method.TRACK_NUMBER, kodion.constants.sort_method.VIDEO_TITLE, kodion.constants.sort_method.DATE) def handle_exception(self, context, exception_to_handle): if (isinstance(exception_to_handle, InvalidGrant) or isinstance(exception_to_handle, LoginException)): ok_dialog = False message_timeout = 5000 message = exception_to_handle.get_message() msg = exception_to_handle.get_message() log_message = exception_to_handle.get_message() error = '' code = '' if isinstance(msg, dict): if 'error_description' in msg: message = strip_html_from_text(msg['error_description']) log_message = strip_html_from_text(msg['error_description']) elif 'message' in msg: message = strip_html_from_text(msg['message']) log_message = strip_html_from_text(msg['message']) else: message = 'No error message' log_message = 'No error message' if 'error' in msg: error = msg['error'] if 'code' in msg: code = msg['code'] if error and code: title = '%s: [%s] %s' % ('LoginException', code, error) elif error: title = '%s: %s' % ('LoginException', error) else: title = 'LoginException' context.log_error('%s: %s' % (title, log_message)) if error == 'deleted_client': message = context.localize(self.LOCAL_MAP['youtube.key.requirement.notification']) context.get_access_manager().update_access_token(access_token='', refresh_token='') ok_dialog = True if error == 'invalid_client': if message == 'The OAuth client was not found.': message = context.localize(self.LOCAL_MAP['youtube.client.id.incorrect']) message_timeout = 7000 elif message == 'Unauthorized': message = context.localize(self.LOCAL_MAP['youtube.client.secret.incorrect']) message_timeout = 7000 if ok_dialog: context.get_ui().on_ok(title, message) else: context.get_ui().show_notification(message, title, time_milliseconds=message_timeout) return False return True