# -*- 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 re from six.moves.urllib_parse import quote from six.moves.urllib_parse import unquote from .exceptions import KodionException from . import items from . import constants from . import utils class AbstractProvider(object): RESULT_CACHE_TO_DISC = 'cache_to_disc' # (bool) RESULT_UPDATE_LISTING = 'update_listing' def __init__(self): self._local_map = { 'kodion.wizard.view.default': 30027, 'kodion.wizard.view.episodes': 30028, 'kodion.wizard.view.movies': 30029, 'kodion.wizard.view.tvshows': 30032, 'kodion.wizard.view.songs': 30033, 'kodion.wizard.view.artists': 30034, 'kodion.wizard.view.albums': 30035 } # map for regular expression (path) to method (names) self._dict_path = {} self._data_cache = None # register some default paths self.register_path(r'^/$', '_internal_root') self.register_path(r''.join(['^/', constants.paths.WATCH_LATER, '/(?Padd|remove|list)/?$']), '_internal_watch_later') self.register_path(r''.join(['^/', constants.paths.FAVORITES, '/(?Padd|remove|list)/?$']), '_internal_favorite') self.register_path(r''.join(['^/', constants.paths.SEARCH, '/(?Pinput|query|list|remove|clear|rename)/?$']), '_internal_search') self.register_path(r'(?P.*\/)extrafanart\/([\?#].+)?$', '_internal_on_extra_fanart') """ Test each method of this class for the appended attribute '_re_match' by the decorator (RegisterProviderPath). The '_re_match' attributes describes the path which must match for the decorated method. """ for method_name in dir(self): method = getattr(self, method_name) if hasattr(method, 'kodion_re_path'): self.register_path(method.kodion_re_path, method_name) def get_alternative_fanart(self, context): return context.get_fanart() def register_path(self, re_path, method_name): """ Registers a new method by name (string) for the given regular expression :param re_path: regular expression of the path :param method_name: name of the method :return: """ self._dict_path[re_path] = method_name def _process_wizard(self, context): # start the setup wizard wizard_steps = [] if context.get_settings().is_setup_wizard_enabled(): context.get_settings().set_bool(constants.setting.SETUP_WIZARD, False) wizard_steps.extend(self.get_wizard_steps(context)) if wizard_steps and context.get_ui().on_yes_no_input(context.get_name(), context.localize(constants.localize.SETUP_WIZARD_EXECUTE)): for wizard_step in wizard_steps: wizard_step[0](*wizard_step[1]) def get_wizard_supported_views(self): return ['default'] def get_wizard_steps(self, context): # can be overridden by the derived class return [] def navigate(self, context): self._process_wizard(context) path = context.get_path() for key in self._dict_path: re_match = re.search(key, path, re.UNICODE) if re_match is not None: method_name = self._dict_path.get(key, '') method = getattr(self, method_name) if method is not None: result = method(context, re_match) if not isinstance(result, tuple): result = result, {} return result raise KodionException("Mapping for path '%s' not found" % path) # noinspection PyUnusedLocal @staticmethod def on_extra_fanart(context, re_match): """ The implementation of the provider can override this behavior. :param context: :param re_match: :return: """ return None def _internal_on_extra_fanart(self, context, re_match): path = re_match.group('path') new_context = context.clone(new_path=path) return self.on_extra_fanart(new_context, re_match) def on_search(self, search_text, context, re_match): raise NotImplementedError() def on_root(self, context, re_match): raise NotImplementedError() def on_watch_later(self, context, re_match): pass def _internal_root(self, context, re_match): return self.on_root(context, re_match) @staticmethod def _internal_favorite(context, re_match): context.add_sort_method(constants.sort_method.LABEL_IGNORE_THE) params = context.get_params() command = re_match.group('command') if command == 'add': fav_item = items.from_json(params['item']) context.get_favorite_list().add(fav_item) elif command == 'remove': fav_item = items.from_json(params['item']) context.get_favorite_list().remove(fav_item) context.get_ui().refresh_container() elif command == 'list': directory_items = context.get_favorite_list().list() for directory_item in directory_items: context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), 'RunPlugin(%s)' % context.create_uri([constants.paths.FAVORITES, 'remove'], params={'item': items.to_jsons(directory_item)}))] directory_item.set_context_menu(context_menu) return directory_items else: pass def _internal_watch_later(self, context, re_match): self.on_watch_later(context, re_match) params = context.get_params() command = re_match.group('command') if command == 'add': item = items.from_json(params['item']) context.get_watch_later_list().add(item) elif command == 'remove': item = items.from_json(params['item']) context.get_watch_later_list().remove(item) context.get_ui().refresh_container() elif command == 'list': video_items = context.get_watch_later_list().list() for video_item in video_items: context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), 'RunPlugin(%s)' % context.create_uri([constants.paths.WATCH_LATER, 'remove'], params={'item': items.to_jsons(video_item)}))] video_item.set_context_menu(context_menu) return video_items else: # do something pass @property def data_cache(self): return self._data_cache @data_cache.setter def data_cache(self, context): if not self._data_cache: self._data_cache = context.get_data_cache() def _internal_search(self, context, re_match): params = context.get_params() command = re_match.group('command') search_history = context.get_search_history() if command == 'remove': query = params['q'] search_history.remove(query) context.get_ui().refresh_container() return True elif command == 'rename': query = params['q'] result, new_query = context.get_ui().on_keyboard_input(context.localize(constants.localize.SEARCH_RENAME), query) if result: search_history.rename(query, new_query) context.get_ui().refresh_container() return True elif command == 'clear': search_history.clear() context.get_ui().refresh_container() return True elif command == 'input': self.data_cache = context folder_path = context.get_ui().get_info_label('Container.FolderPath') query = None if (folder_path.startswith('plugin://%s' % context.get_id()) and re.match('.+/(?:query|input)/.*', folder_path)): cached_query = self.data_cache.get_item(self.data_cache.ONE_DAY, 'search_query') # came from page 1 of search query by '..'/back, user doesn't want to input on this path if cached_query and cached_query.get('search_query', {}).get('query'): query = cached_query.get('search_query', {}).get('query') query = utils.to_unicode(query) query = unquote(query) else: result, input_query = context.get_ui().on_keyboard_input(context.localize(constants.localize.SEARCH_TITLE)) if result: query = input_query if not query: return False incognito = str(context.get_param('incognito', False)).lower() == 'true' channel_id = context.get_param('channel_id', '') query = utils.to_utf8(query) try: self._data_cache.set('search_query', json.dumps({'query': quote(query)})) except KeyError: encoded = json.dumps({'query': quote(query.encode('utf8'))}) self._data_cache.set('search_query', encoded) if not incognito and not channel_id: try: search_history.update(query) except: pass context.set_path('/kodion/search/query/') if isinstance(query, bytes): query = query.decode('utf-8') return self.on_search(query, context, re_match) elif command == 'query': incognito = str(context.get_param('incognito', False)).lower() == 'true' channel_id = context.get_param('channel_id', '') query = params['q'] query = utils.to_unicode(query) if not incognito and not channel_id: try: search_history.update(query) except: pass if isinstance(query, bytes): query = query.decode('utf-8') return self.on_search(query, context, re_match) else: context.set_content_type(constants.content_type.FILES) result = [] location = str(context.get_param('location', False)).lower() == 'true' # 'New Search...' new_search_item = items.NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) result.append(new_search_item) for search in search_history.list(): # little fallback for old history entries if isinstance(search, items.DirectoryItem): search = search.get_name() # we create a new instance of the SearchItem search_history_item = items.SearchHistoryItem(context, search, fanart=self.get_alternative_fanart(context), location=location) result.append(search_history_item) if search_history.is_empty(): # context.execute('RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'input'])) pass return result, {self.RESULT_CACHE_TO_DISC: False} def handle_exception(self, context, exception_to_handle): return True def tear_down(self, context): pass