resources/lib/navigation/directory_search.py
# -*- coding: utf-8 -*-
"""
Copyright (C) 2017 Sebastian Golasch (plugin.video.netflix)
Copyright (C) 2020 Stefano Gottardo (original implementation module)
Navigation for search menu
SPDX-License-Identifier: MIT
See LICENSES/MIT.md for more information.
"""
from copy import deepcopy
import xbmcgui
import xbmcplugin
import resources.lib.utils.api_requests as api
from resources.lib import common
from resources.lib.globals import G
from resources.lib.kodi import ui
from resources.lib.kodi.context_menu import generate_context_menu_searchitem
from resources.lib.navigation.directory_utils import (finalize_directory, end_of_directory,
custom_viewmode, get_title)
from resources.lib.utils.logging import LOG, measure_exec_time_decorator
# The search types allows you to provide a modular structure to the search feature,
# in this way you can add new/remove types of search in a simple way.
# To add a new type: add the new type name to SEARCH_TYPES, then implement the new type to search_add/search_query.
SEARCH_TYPES = ['text', 'audio_lang', 'subtitles_lang', 'genre_id']
SEARCH_TYPES_DESC = {
'text': common.get_local_string(30410),
'audio_lang': common.get_local_string(30411),
'subtitles_lang': common.get_local_string(30412),
'genre_id': common.get_local_string(30413)
}
def route_search_nav(pathitems, perpetual_range_start, dir_update_listing, params):
if 'query' in params:
path = 'query'
else:
path = pathitems[2] if len(pathitems) > 2 else 'list'
LOG.debug('Routing "search" navigation to: {}', path)
ret = True
if path == 'list':
search_list()
elif path == 'add':
ret = search_add()
elif path == 'edit':
search_edit(params['row_id'])
elif path == 'remove':
search_remove(params['row_id'])
elif path == 'clear':
ret = search_clear()
elif path == 'query':
# Used to make a search by text from a JSON-RPC request
# without save the item to the add-on database
# Endpoint: plugin://plugin.video.netflix/directory/search/search/?query=something
ret = exec_query(None, 'text', None, params['query'], perpetual_range_start, dir_update_listing,
{'query': params['query']})
else:
ret = search_query(path, perpetual_range_start, dir_update_listing)
if not ret:
xbmcplugin.endOfDirectory(G.PLUGIN_HANDLE, succeeded=False)
def search_list(dir_update_listing=False):
"""Show the list of search item (main directory)"""
dir_items = [_create_diritem_from_row(row) for row in G.LOCAL_DB.get_search_list()]
dir_items.insert(0, _get_diritem_add())
dir_items.append(_get_diritem_clear())
sort_type = 'sort_nothing'
if G.ADDON.getSettingInt('menu_sortorder_search_history') == 1:
sort_type = 'sort_label_ignore_folders'
finalize_directory(dir_items, G.CONTENT_FOLDER, sort_type,
common.get_local_string(30400))
end_of_directory(dir_update_listing)
def search_add():
"""Perform actions to add and execute a new research"""
# Ask to user the type of research
search_types_desc = [SEARCH_TYPES_DESC.get(stype, 'Unknown') for stype in SEARCH_TYPES]
type_index = ui.show_dlg_select(common.get_local_string(30401), search_types_desc)
if type_index == -1: # Cancelled
return False
# If needed ask to user other info, then save the research to the database
search_type = SEARCH_TYPES[type_index]
row_id = None
if search_type == 'text':
search_term = ui.ask_for_search_term()
if search_term and search_term.strip():
row_id = G.LOCAL_DB.insert_search_item(SEARCH_TYPES[type_index], search_term.strip())
elif search_type == 'audio_lang':
row_id = _search_add_bylang(SEARCH_TYPES[type_index], api.get_available_audio_languages())
elif search_type == 'subtitles_lang':
row_id = _search_add_bylang(SEARCH_TYPES[type_index], api.get_available_subtitles_languages())
elif search_type == 'genre_id':
genre_id = ui.show_dlg_input_numeric(search_types_desc[type_index], mask_input=False)
if genre_id:
row_id = _search_add_bygenreid(SEARCH_TYPES[type_index], genre_id)
else:
raise NotImplementedError(f'Search type index {type_index} not implemented')
# Redirect to "search" endpoint (otherwise no results in JSON-RPC)
# Rewrite path history using dir_update_listing + container_update
# (otherwise will retrigger input dialog on Back or Container.Refresh)
if row_id is not None and search_query(row_id, 0, False):
url = common.build_url(['search', 'search', row_id], mode=G.MODE_DIRECTORY, params={'dir_update_listing': True})
from time import sleep
# The forced sleep its needed because seem that change the container path too fast
# make problems in Kodi core and the GUI fails to update, when this happens cause side effects to context menus
# like "add/remove from my list" that when used ask again to make a new search because re-open the initial path
sleep(1)
common.container_update(url, False)
return True
return False
def _search_add_bylang(search_type, dict_languages):
search_type_desc = SEARCH_TYPES_DESC.get(search_type, 'Unknown')
title = f'{search_type_desc} - {common.get_local_string(30405)}'
index = ui.show_dlg_select(title, list(dict_languages.values()))
if index == -1: # Cancelled
return None
lang_code = list(dict_languages.keys())[index]
lang_desc = list(dict_languages.values())[index]
# In this case the 'value' is used only as title for the ListItem and not for the query
value = f'{search_type_desc}: {lang_desc}'
row_id = G.LOCAL_DB.insert_search_item(search_type, value, {'lang_code': lang_code})
return row_id
def _search_add_bygenreid(search_type, genre_id):
# If the genre ID exists, the title of the list will be returned
title = api.get_genre_title(genre_id)
if not title:
ui.show_notification(common.get_local_string(30407))
return None
# In this case the 'value' is used only as title for the ListItem and not for the query
title += f' [{genre_id}]'
row_id = G.LOCAL_DB.insert_search_item(search_type, title, {'genre_id': genre_id})
return row_id
def search_edit(row_id):
"""Edit a search item"""
search_item = G.LOCAL_DB.get_search_item(row_id)
search_type = search_item['Type']
ret = False
if search_type == 'text':
search_term = ui.ask_for_search_term(search_item['Value'])
if search_term and search_term.strip():
G.LOCAL_DB.update_search_item_value(row_id, search_term.strip())
ret = True
if not ret:
return
common.container_update(common.build_url(['search', 'search', row_id], mode=G.MODE_DIRECTORY))
def search_remove(row_id):
"""Remove a search item"""
LOG.debug('Removing search item with ID {}', row_id)
G.LOCAL_DB.delete_search_item(row_id)
common.json_rpc('Input.Down') # Avoids selection back to the top
common.container_refresh()
def search_clear():
"""Clear all search items"""
if not ui.ask_for_confirmation(common.get_local_string(30404), common.get_local_string(30406)):
return False
G.LOCAL_DB.clear_search_items()
common.container_refresh()
return True
@measure_exec_time_decorator()
def search_query(row_id, perpetual_range_start, dir_update_listing):
"""Perform the research"""
# Get item from database
search_item = G.LOCAL_DB.get_search_item(row_id)
if not search_item:
ui.show_error_info('Search error', 'Item not found in the database.')
return False
# Update the last access data (move on top last used items)
if not perpetual_range_start:
G.LOCAL_DB.update_search_item_last_access(row_id)
return exec_query(row_id, search_item['Type'], search_item['Parameters'], search_item['Value'],
perpetual_range_start, dir_update_listing)
def exec_query(row_id, search_type, search_params, search_value, perpetual_range_start, dir_update_listing,
path_params=None):
menu_data = deepcopy(G.MAIN_MENU_ITEMS['search'])
if search_type == 'text':
call_args = {
'menu_data': menu_data,
'search_term': search_value,
'pathitems': ['search', 'search', row_id] if row_id else ['search', 'search'],
'path_params': path_params,
'perpetual_range_start': perpetual_range_start
}
dir_items, extra_data = common.make_call('get_video_list_search', call_args)
elif search_type == 'audio_lang':
call_args = {
'menu_data': menu_data,
'pathitems': ['search', 'search', row_id],
'perpetual_range_start': perpetual_range_start,
'context_name': 'genres',
'context_id': common.convert_from_string(search_params, dict)['lang_code']
}
dir_items, extra_data = common.make_call('get_video_list_sorted_sp', call_args)
elif search_type == 'subtitles_lang':
call_args = {
'menu_data': menu_data,
'pathitems': ['search', 'search', row_id],
'perpetual_range_start': perpetual_range_start,
'context_name': 'genres',
'context_id': common.convert_from_string(search_params, dict)['lang_code']
}
dir_items, extra_data = common.make_call('get_video_list_sorted_sp', call_args)
elif search_type == 'genre_id':
call_args = {
'menu_data': menu_data,
'pathitems': ['search', 'search', row_id],
'perpetual_range_start': perpetual_range_start,
'context_name': 'genres',
'context_id': common.convert_from_string(search_params, dict)['genre_id']
}
dir_items, extra_data = common.make_call('get_video_list_sorted_sp', call_args)
else:
raise NotImplementedError(f'Search type {search_type} not implemented')
# Show the results
if not dir_items:
ui.show_notification(common.get_local_string(30407))
return False
_search_results_directory(search_value, menu_data, dir_items, extra_data, dir_update_listing)
return True
@custom_viewmode(G.VIEW_SHOW)
def _search_results_directory(search_value, menu_data, dir_items, extra_data, dir_update_listing):
extra_data['title'] = f'{common.get_local_string(30400)} - {search_value}'
finalize_directory(dir_items, menu_data.get('content_type', G.CONTENT_SHOW),
title=get_title(menu_data, extra_data))
end_of_directory(dir_update_listing)
return menu_data.get('view')
def _get_diritem_add():
"""Generate the "add" menu item"""
list_item = xbmcgui.ListItem(label=common.get_local_string(30403), offscreen=True)
list_item.setArt({'icon': 'DefaultAddSource.png'})
list_item.setProperty('specialsort', 'top') # Force an item to stay on top
return common.build_url(['search', 'search', 'add'], mode=G.MODE_DIRECTORY), list_item, True
def _get_diritem_clear():
"""Generate the "clear" menu item"""
list_item = xbmcgui.ListItem(label=common.get_local_string(30404), offscreen=True)
list_item.setArt({'icon': 'icons\\infodialogs\\uninstall.png'})
list_item.setProperty('specialsort', 'bottom') # Force an item to stay on bottom
# This ListItem is not set as folder so that the executed command is not added to the history
return common.build_url(['search', 'search', 'clear'], mode=G.MODE_DIRECTORY), list_item, False
def _create_diritem_from_row(row):
row_id = str(row['ID'])
search_desc = common.get_local_string(30401) + ': ' + SEARCH_TYPES_DESC.get(row['Type'], 'Unknown')
list_item = xbmcgui.ListItem(label=row['Value'], offscreen=True)
list_item.setInfo('video', {'Plot': search_desc})
list_item.addContextMenuItems(generate_context_menu_searchitem(row_id, row['Type']))
return common.build_url(['search', 'search', row_id], mode=G.MODE_DIRECTORY), list_item, True