resources/lib/services/nfsession/directorybuilder/dir_builder_items.py
# -*- coding: utf-8 -*-
"""
Copyright (C) 2017 Sebastian Golasch (plugin.video.netflix)
Copyright (C) 2020 Stefano Gottardo (original implementation module)
Generate the data to build a directory of xbmcgui ListItem's
SPDX-License-Identifier: MIT
See LICENSES/MIT.md for more information.
"""
import resources.lib.common as common
from resources.lib.common.cache_utils import CACHE_BOOKMARKS
from resources.lib.common.exceptions import CacheMiss
from resources.lib.common.kodi_wrappers import ListItemW
from resources.lib.database.db_utils import (TABLE_MENU_DATA)
from resources.lib.globals import G
from resources.lib.kodi.context_menu import (generate_context_menu_items, generate_context_menu_profile,
generate_context_menu_remind_me)
from resources.lib.kodi.infolabels import get_color_name, set_watched_status, add_info_list_item, get_video_codec_hint
from resources.lib.services.nfsession.directorybuilder.dir_builder_utils import (get_param_watched_status_by_profile,
add_items_previous_next_page,
get_availability_message)
from resources.lib.utils.logging import measure_exec_time_decorator
# This module convert a DataType object like VideoListSorted (that contains a list of items videos, items, etc)
# in a list of ListItemW items (a wrapper of the real xbmcgui.ListItem).
# All build methods should return same tuple data ('directory items', 'extra data dict'),
# all the 'directory_items' variables stand for the items to put in to xbmcplugin.addDirectoryItems
# 'common_data' dict is used to avoid cpu overload for multiple accesses to other resources improve a lot the execution
def get_common_data():
"""Temporarily stores common data for faster access"""
return {
'supplemental_info_color': get_color_name(G.ADDON.getSettingInt('supplemental_info_color')),
'profile_language_code': G.LOCAL_DB.get_profile_config('language', ''),
'video_codec_hint': get_video_codec_hint()
}
@measure_exec_time_decorator(is_immediate=True)
def build_mainmenu_listing(loco_list):
"""Builds the main menu listing (my list, continue watching, etc.)"""
from resources.lib.kodi.context_menu import generate_context_menu_mainmenu
directory_items = []
common_data = get_common_data()
for menu_id, data in G.MAIN_MENU_ITEMS.items():
if data.get('has_show_setting', True) and not G.ADDON.getSettingBool('_'.join(('show_menu', menu_id))):
continue
if data['loco_known']:
list_id, video_list = loco_list.find_by_context(data['loco_contexts'][0])
if not list_id:
continue
menu_title = video_list['displayName']
directory_item = _create_videolist_item(list_id, video_list, data, common_data, static_lists=True)
directory_item[1].addContextMenuItems(generate_context_menu_mainmenu(menu_id))
directory_items.append(directory_item)
else:
menu_title = common.get_local_string(data['label_id']) if data.get('label_id') else 'Missing menu title'
menu_description = (common.get_local_string(data['description_id'])
if data['description_id'] is not None
else '')
list_item = ListItemW(label=menu_title)
list_item.setArt({'icon': data['icon']})
list_item.setInfo('video', {'Plot': menu_description})
list_item.addContextMenuItems(generate_context_menu_mainmenu(menu_id))
directory_items.append((common.build_url(data['path'], mode=G.MODE_DIRECTORY), list_item, True))
# Save the menu titles, to reuse it when will be open the content of menus
G.LOCAL_DB.set_value(menu_id, {'title': menu_title}, TABLE_MENU_DATA)
# Add "Profiles" menu
pfl_list_item = ListItemW(label=common.get_local_string(13200))
pfl_list_item.setArt({'icon': 'DefaultUser.png'})
directory_items.append((common.build_url(['profiles'], mode=G.MODE_DIRECTORY), pfl_list_item, True))
G.CACHE_MANAGEMENT.execute_pending_db_ops()
return directory_items, {}
def build_profiles_listing(preselect_guid=None, detailed_info=True):
"""Builds the profiles listing"""
directory_items = []
preselect_guid = preselect_guid or G.LOCAL_DB.get_active_profile_guid()
autoselect_guid = G.LOCAL_DB.get_value('autoselect_profile_guid')
library_playback_guid = G.LOCAL_DB.get_value('library_playback_profile_guid')
for guid in G.LOCAL_DB.get_guid_profiles():
directory_items.append(_create_profile_item(guid,
(guid == preselect_guid),
(guid == autoselect_guid),
(guid == library_playback_guid),
detailed_info))
return directory_items, {}
def _create_profile_item(profile_guid, is_selected, is_autoselect, is_autoselect_library, detailed_info):
profile_name = G.LOCAL_DB.get_profile_config('profileName', '???', guid=profile_guid)
profile_attributes = []
is_pin_locked = G.LOCAL_DB.get_profile_config('isPinLocked', False, guid=profile_guid)
if is_pin_locked:
profile_attributes.append(f'[COLOR red]{common.get_local_string(20068)}[/COLOR]')
if G.LOCAL_DB.get_profile_config('isAccountOwner', False, guid=profile_guid):
profile_attributes.append(common.get_local_string(30221))
if G.LOCAL_DB.get_profile_config('isKids', False, guid=profile_guid):
profile_attributes.append(common.get_local_string(30222))
if is_autoselect and detailed_info:
profile_attributes.append(common.get_local_string(30055).format('●'))
if is_autoselect_library and detailed_info:
profile_attributes.append(common.get_local_string(30052).format('●'))
if detailed_info and G.LOCAL_DB.get_profile_config('addon_remember_pin', False, guid=profile_guid):
profile_attributes.append(common.get_local_string(30057).format('●'))
attributes_desc = '[CR]'.join(profile_attributes) + '[CR]' if profile_attributes else ''
description = f'{attributes_desc}[{G.LOCAL_DB.get_profile_config("language_desc", "", guid=profile_guid)}]'
if detailed_info:
menu_items = generate_context_menu_profile(profile_guid, is_autoselect, is_autoselect_library, is_pin_locked)
else:
menu_items = []
list_item = ListItemW(label=profile_name)
list_item.setProperties({
'nf_guid': profile_guid,
'nf_description': description.replace('[CR]', ' - ')
})
list_item.setArt({'icon': G.LOCAL_DB.get_profile_config('avatar', '', guid=profile_guid)})
list_item.setInfo('video', {'Plot': description})
list_item.select(is_selected)
list_item.addContextMenuItems(menu_items)
return (common.build_url(pathitems=['home'], params={'switch_profile_guid': profile_guid}, mode=G.MODE_DIRECTORY),
list_item,
True)
@measure_exec_time_decorator(is_immediate=True)
def build_season_listing(season_list, tvshowid, pathitems=None):
"""Build a season listing"""
common_data = get_common_data()
directory_items = [_create_season_item(tvshowid, seasonid_value, season, season_list, common_data)
for seasonid_value, season in season_list.seasons.items()]
# add_items_previous_next_page use the new value of perpetual_range_selector
add_items_previous_next_page(directory_items, pathitems, season_list.perpetual_range_selector, tvshowid)
G.CACHE_MANAGEMENT.execute_pending_db_ops()
return directory_items, {'title': f'{season_list.tvshow["title"]["value"]} - {common.get_local_string(20366)[2:]}'}
def _create_season_item(tvshowid, seasonid_value, season, season_list, common_data):
seasonid = tvshowid.derive_season(seasonid_value)
list_item = ListItemW(label=season['summary']['value']['name'])
list_item.setProperty('nf_videoid', seasonid.to_string())
add_info_list_item(list_item, seasonid, season, season_list.data, False, common_data,
art_item=season_list.artitem)
list_item.addContextMenuItems(generate_context_menu_items(seasonid, False, None))
return common.build_url(videoid=seasonid, mode=G.MODE_DIRECTORY), list_item, True
@measure_exec_time_decorator(is_immediate=True)
def build_episode_listing(episodes_list, seasonid, pathitems=None):
"""Build a episodes listing of a season"""
common_data = get_common_data()
common_data['params'] = get_param_watched_status_by_profile()
common_data['set_watched_status'] = G.ADDON.getSettingBool('sync_watched_status')
common_data['active_profile_guid'] = G.LOCAL_DB.get_active_profile_guid()
directory_items = [_create_episode_item(seasonid, episodeid_value, episode, episodes_list, common_data)
for episodeid_value, episode
in episodes_list.episodes.items()]
# add_items_previous_next_page use the new value of perpetual_range_selector
add_items_previous_next_page(directory_items, pathitems, episodes_list.perpetual_range_selector)
G.CACHE_MANAGEMENT.execute_pending_db_ops()
return directory_items, {
'title': f'{episodes_list.tvshow["title"]["value"]} - {episodes_list.season["summary"]["value"]["name"]}'}
def _create_episode_item(seasonid, episodeid_value, episode, episodes_list, common_data):
is_playable = episode['availability'].get('value', {}).get('isPlayable', False)
episodeid = seasonid.derive_episode(episodeid_value)
list_item = ListItemW(label=episode['title']['value'])
list_item.setProperties({
'isPlayable': str(is_playable).lower(),
'nf_videoid': episodeid.to_string()
})
add_info_list_item(list_item, episodeid, episode, episodes_list.data, False, common_data)
set_watched_status(list_item, episode, common_data)
if is_playable:
url = common.build_url(videoid=episodeid, mode=G.MODE_PLAY, params=common_data['params'])
list_item.addContextMenuItems(generate_context_menu_items(episodeid, False, None))
else:
# The video is not playable, try check if there is a date
list_item.setProperty('nf_availability_message', get_availability_message(episode))
url = common.build_url(['show_availability_message'], videoid=episodeid, mode=G.MODE_ACTION)
return url, list_item, False
@measure_exec_time_decorator(is_immediate=True)
def build_loco_listing(loco_list, menu_data, force_use_videolist_id=False):
"""Build a listing of video lists (LoCo)"""
# If contexts are specified (loco_contexts in the menu_data), then the loco_list data will be filtered by
# the specified contexts, otherwise all LoCo items will be added
common_data = get_common_data()
common_data['menu_data'] = menu_data
contexts = menu_data.get('loco_contexts')
items_list = loco_list.lists_by_context(contexts) if contexts else loco_list.lists.items()
directory_items = []
for video_list_id, video_list in items_list: # pylint: disable=unused-variable
# Create dynamic sub-menu info in MAIN_MENU_ITEMS
if video_list['context'] == 'genre':
menu_func_name = menu_data['path'][0]
list_id = str(video_list['genreId'])
else:
menu_func_name = 'video_list'
list_id = video_list['id']
sub_menu_data = menu_data.copy()
sub_menu_data['path'] = [menu_func_name, list_id, list_id]
sub_menu_data['loco_known'] = False
sub_menu_data['content_type'] = menu_data.get('content_type', G.CONTENT_SHOW)
sub_menu_data['force_use_videolist_id'] = force_use_videolist_id
sub_menu_data['title'] = video_list['displayName']
sub_menu_data['initial_menu_id'] = menu_data.get('initial_menu_id', menu_data['path'][1])
# Do not use the cache with 'Top 10' menus, so that you always get up-to-date data.
sub_menu_data['no_use_cache'] = video_list['context'] == 'mostWatched'
G.LOCAL_DB.set_value(list_id, sub_menu_data, TABLE_MENU_DATA)
directory_items.append(_create_videolist_item(list_id, video_list, sub_menu_data, common_data))
G.CACHE_MANAGEMENT.execute_pending_db_ops()
return directory_items, {}
def _create_videolist_item(list_id, video_list, menu_data, common_data, static_lists=False):
if static_lists and G.is_known_menu_context(video_list['context']):
pathitems = list(menu_data['path']) # Make a copy
pathitems.append(video_list['context'])
else:
# It is a dynamic video list / menu context
if menu_data.get('force_use_videolist_id', False):
path = 'video_list'
else:
path = 'video_list_sorted'
pathitems = [path, menu_data['path'][1], list_id]
list_item = ListItemW(label=video_list['displayName'])
add_info_list_item(list_item, video_list.videoid, video_list, video_list.data, False, common_data,
art_item=video_list.artitem)
# Add possibility to browse the sub-genres (see build_video_listing)
sub_genre_id = video_list.get('genreId')
params = {'sub_genre_id': str(sub_genre_id)} if sub_genre_id else None
return common.build_url(pathitems, params=params, mode=G.MODE_DIRECTORY), list_item, True
@measure_exec_time_decorator(is_immediate=True)
def build_video_listing(video_list, menu_data, sub_genre_id=None, pathitems=None, perpetual_range_start=None,
mylist_items=None, path_params=None):
"""Build a video listing"""
trackid = None
if hasattr(video_list, 'component_summary'):
trackid = video_list.component_summary.get('trackIds', {}).get('trackId', 'None')
common_data = get_common_data()
common_data.update({
'params': get_param_watched_status_by_profile(),
'mylist_items': mylist_items,
'set_watched_status': G.ADDON.getSettingBool('sync_watched_status'),
'mylist_titles_color': (get_color_name(G.ADDON.getSettingInt('mylist_titles_color'))
if menu_data['path'][1] != 'myList'
else None),
'rememberme_titles_color': get_color_name(G.ADDON.getSettingInt('rememberme_titles_color')),
'ctxmenu_remove_watched_status': menu_data['path'][1] == 'continueWatching',
'active_profile_guid': G.LOCAL_DB.get_active_profile_guid(),
'marks_tvshow_started': G.ADDON.getSettingBool('marks_tvshow_started'),
'trackid': trackid,
'is_supplemental_type': video_list.__class__.__name__ == 'VideoListSupplemental'
})
directory_items = [_create_video_item(videoid_value, video, video_list, perpetual_range_start, common_data)
for videoid_value, video
in video_list.videos.items()]
# If genre_id exists add possibility to browse LoCo sub-genres
# With checking if 'previous_start' is existing, we know that it is the first page
if sub_genre_id and sub_genre_id != 'None' and (not video_list.perpetual_range_selector or 'previous_start' not in video_list.perpetual_range_selector):
# Create dynamic sub-menu info in MAIN_MENU_ITEMS
menu_id = f'subgenre_{sub_genre_id}'
sub_menu_data = menu_data.copy()
sub_menu_data['path'] = [menu_data['path'][0], menu_id, sub_genre_id]
sub_menu_data['loco_known'] = False
sub_menu_data['content_type'] = menu_data.get('content_type', G.CONTENT_SHOW)
sub_menu_data.update({'title': common.get_local_string(30089)})
sub_menu_data['initial_menu_id'] = menu_data.get('initial_menu_id', menu_data['path'][1])
G.LOCAL_DB.set_value(menu_id, sub_menu_data, TABLE_MENU_DATA)
# Create the folder for the access to sub-genre
folder_list_item = ListItemW(label=common.get_local_string(30089))
folder_list_item.setArt({'icon': 'DefaultVideoPlaylists.png'})
folder_list_item.setInfo('video', {'Plot': common.get_local_string(30088)}) # The description
directory_items.insert(0, (common.build_url(['genres', menu_id, sub_genre_id], mode=G.MODE_DIRECTORY),
folder_list_item,
True))
# add_items_previous_next_page use the new value of perpetual_range_selector
add_items_previous_next_page(directory_items, pathitems, video_list.perpetual_range_selector, sub_genre_id,
path_params)
G.CACHE_MANAGEMENT.execute_pending_db_ops()
return directory_items, {}
def _create_video_item(videoid_value, video, video_list, perpetual_range_start, common_data): # pylint: disable=unused-argument
if common_data['is_supplemental_type']:
# 10/10/2022 Broken api? the video trailers are not more identified as supplemental type but as movie type
# as workaround we check the data type
videoid = common.VideoId(supplementalid=videoid_value)
else:
videoid = common.VideoId.from_videolist_item(video)
is_folder = videoid.mediatype == common.VideoId.SHOW
is_playable = video['availability'].get('value', {}).get('isPlayable', False)
is_video_playable = not is_folder and is_playable
is_in_mylist = videoid in common_data['mylist_items']
is_in_remind_me = False
list_item = ListItemW(label=video['title']['value'])
list_item.setProperties({
'isPlayable': str(is_video_playable).lower(),
'nf_videoid': videoid.to_string(),
'nf_is_in_mylist': str(is_in_mylist),
'nf_perpetual_range_start': str(perpetual_range_start)
})
if is_playable:
# The movie or tvshow (episodes) is playable
url = common.build_url(videoid=videoid,
mode=G.MODE_DIRECTORY if is_folder else G.MODE_PLAY,
params=None if is_folder else common_data['params'])
list_item.addContextMenuItems(generate_context_menu_items(videoid, is_in_mylist, perpetual_range_start,
common_data['ctxmenu_remove_watched_status'],
common_data['trackid']))
else:
# The movie or tvshow (episodes) is not available
# Try check if there is a availability date
list_item.setProperty('nf_availability_message', get_availability_message(video))
# Check if the user has set "Remind Me" feature,
try:
# Due to the add-on cache we can not change in easy way the value stored in database cache,
# then we temporary override the value (see 'remind_me' in navigation/actions.py)
is_in_remind_me = G.CACHE.get(CACHE_BOOKMARKS, f'is_in_remind_me_{videoid}')
except CacheMiss:
# The website check the "Remind Me" value on key "inRemindMeList" and also "queue"/"inQueue"
is_in_remind_me = video['inRemindMeList']['value'] or video['queue']['value']['inQueue']
list_item.addContextMenuItems(generate_context_menu_remind_me(videoid, is_in_remind_me, common_data['trackid']))
url = common.build_url(['show_availability_message'], videoid=videoid, mode=G.MODE_ACTION)
add_info_list_item(list_item, videoid, video, video_list.data, is_in_mylist, common_data,
is_in_remind_me=is_in_remind_me)
if not is_folder:
set_watched_status(list_item, video, common_data)
return url, list_item, is_folder and is_playable
@measure_exec_time_decorator(is_immediate=True)
def build_subgenres_listing(subgenre_list, menu_data):
"""Build a listing of sub-genres list"""
directory_items = []
for index, subgenre_data in subgenre_list.lists: # pylint: disable=unused-variable
# Create dynamic sub-menu info in MAIN_MENU_ITEMS
subgenre_id = subgenre_data['id'].get('value')
if subgenre_id is None: # if there is no id the list is ended
break
sel_video_list_id = str(subgenre_id)
sub_menu_data = menu_data.copy()
sub_menu_data['path'] = [menu_data['path'][0], sel_video_list_id, sel_video_list_id]
sub_menu_data['loco_known'] = False
sub_menu_data['loco_contexts'] = None
sub_menu_data['content_type'] = menu_data.get('content_type', G.CONTENT_SHOW)
sub_menu_data['title'] = subgenre_data['name']['value']
sub_menu_data['initial_menu_id'] = menu_data.get('initial_menu_id', menu_data['path'][1])
G.LOCAL_DB.set_value(sel_video_list_id, sub_menu_data, TABLE_MENU_DATA)
directory_items.append(_create_subgenre_item(sel_video_list_id,
subgenre_data,
sub_menu_data))
return directory_items, {}
def _create_subgenre_item(video_list_id, subgenre_data, menu_data):
pathitems = ['video_list_sorted', menu_data['path'][1], video_list_id]
list_item = ListItemW(label=subgenre_data['name']['value'])
return common.build_url(pathitems, mode=G.MODE_DIRECTORY), list_item, True
def build_lolomo_category_listing(lolomo_cat_list, menu_data):
"""Build a folders listing of a LoLoMo category"""
common_data = get_common_data()
directory_items = []
for list_id, summary_data, video_list in lolomo_cat_list.lists():
if summary_data['length'] == 0: # Do not show empty lists
continue
# Create dynamic sub-menu info in MAIN_MENU_ITEMS
sub_menu_data = menu_data.copy()
sub_menu_data['path'] = [menu_data['path'][0], list_id, list_id]
sub_menu_data['loco_known'] = False
sub_menu_data['loco_contexts'] = None
sub_menu_data['content_type'] = menu_data.get('content_type', G.CONTENT_SHOW)
sub_menu_data['title'] = summary_data['displayName']
sub_menu_data['initial_menu_id'] = menu_data.get('initial_menu_id', menu_data['path'][1])
# Do not use the cache with 'Top 10' menus, so that you always get up-to-date data.
sub_menu_data['no_use_cache'] = video_list['context'] == 'mostWatched'
G.LOCAL_DB.set_value(list_id, sub_menu_data, TABLE_MENU_DATA)
directory_item = _create_category_item(list_id, video_list, sub_menu_data, common_data, summary_data)
directory_items.append(directory_item)
G.CACHE_MANAGEMENT.execute_pending_db_ops()
return directory_items, {}
def _create_category_item(list_id, video_list, menu_data, common_data, summary_data):
pathitems = ['video_list', menu_data['path'][1], list_id]
list_item = ListItemW(label=summary_data['displayName'])
add_info_list_item(list_item, video_list.videoid, video_list, video_list.data, False, common_data,
art_item=video_list.artitem)
return common.build_url(pathitems, mode=G.MODE_DIRECTORY), list_item, True