import json
import os
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from urllib.parse import quote
from lib.api.tmdbv3api.as_obj import AsObj
from lib.api.tmdbv3api.objs.anime import TmdbAnime
from lib.api.tmdbv3api.objs.search import Search
from lib.api.tmdbv3api.tmdb import TMDb
from lib.clients.tmdb.anime import TmdbAnimeClient
from lib.clients.tmdb.base import BaseTmdbClient
from lib.clients.tmdb.people_client import PeopleClient
from lib.clients.tmdb.utils.utils import (
add_kodi_dir_item,
filter_excluded_languages,
tmdb_get,
)
from lib.db.cached import cache
from lib.db.pickle_db import PickleDatabase
from lib.utils.general.utils import (
Anime,
add_next_button,
execute_thread_pool,
set_content_type,
set_media_infoTag,
set_pluging_category,
translate_weekday,
)
from lib.utils.kodi.utils import (
ADDON_PATH,
add_directory_items_batch,
apply_section_view,
build_url,
close_busy_dialog,
end_of_directory,
kodilog,
make_list_item,
notification,
show_busy_dialog,
show_keyboard,
translation,
)
from lib.utils.views.shows import show_episode_info, show_season_info
from lib.utils.views.weekly_calendar import (
get_episodes_for_show,
is_this_week,
parse_date_str,
)
def _apply_tmdb_view(mode, content_type=""):
if mode == "movies":
return apply_section_view("view.movies", content_type or "movies")
if mode == "tv":
return apply_section_view("view.tvshows", content_type or "tvshows")
if mode == "season":
return apply_section_view("view.seasons", content_type or "seasons")
if mode == "episode":
return apply_section_view("view.episodes", content_type or "episodes")
return apply_section_view("view.main", content_type=content_type)
import xbmc
import xbmcgui
class TmdbClient(BaseTmdbClient):
@staticmethod
def _get_result_value(item, key, default=None):
if isinstance(item, dict):
return item.get(key, default)
return getattr(item, key, default)
@staticmethod
def _result_to_metadata(item):
keys = (
"id",
"title",
"name",
"original_title",
"original_name",
"overview",
"poster_path",
"backdrop_path",
"still_path",
"profile_path",
"release_date",
"first_air_date",
"vote_average",
"vote_count",
"popularity",
"genre_ids",
"genres",
"origin_country",
"media_type",
"imdb_id",
"tvdb_id",
)
metadata = {
key: TmdbClient._get_result_value(item, key)
for key in keys
if TmdbClient._get_result_value(item, key) is not None
}
images = TmdbClient._get_result_value(item, "images")
if images:
metadata["images"] = images
return metadata
@staticmethod
def _get_cached_tmdb_item_metadata(item, mode):
tmdb_id = TmdbClient._get_result_value(item, "id")
if not tmdb_id:
return TmdbClient._result_to_metadata(item)
media_type = TmdbClient._get_result_value(item, "media_type", "") or mode
normalized_mode = "movies" if media_type in ("movie", "movies") else "tv"
current_lang = TMDb().language
cache_key = f"tmdb_ui_meta|{normalized_mode}|{tmdb_id}|{current_lang}"
cached_metadata = cache.get(cache_key)
if cached_metadata:
merged = TmdbClient._result_to_metadata(item)
merged.update({k: v for k, v in cached_metadata.items() if v not in (None, "", [], {})})
return merged
metadata = TmdbClient._result_to_metadata(item)
# Fallback to English if localized title/name or overview is missing
has_title = bool(metadata.get("title") or metadata.get("name"))
has_overview = bool(metadata.get("overview"))
if not has_title or not has_overview:
try:
TMDb().language = "en-US"
path = "movie_details" if normalized_mode == "movies" else "tv_details"
en_data = tmdb_get(path, {"id": tmdb_id})
if en_data:
en_metadata = TmdbClient._result_to_metadata(en_data)
if not has_title:
for key in ("title", "name"):
if not metadata.get(key) and en_metadata.get(key):
metadata[key] = en_metadata[key]
if not has_overview and en_metadata.get("overview"):
metadata["overview"] = en_metadata["overview"]
finally:
TMDb().language = current_lang
path = "movie_images" if normalized_mode == "movies" else "tv_images"
images = tmdb_get(path, {"id": tmdb_id})
if images:
metadata["images"] = images
cache.set(cache_key, metadata, timedelta(hours=24))
return metadata
@staticmethod
def handle_tmdb_query(params):
mode = params.get("mode")
submode = params.get("submode")
category = params.get("category")
page = int(params.get("page", 1))
handlers = {
"movies": lambda: TmdbClient.handle_tmdb_movie_query(params),
"tv": lambda: TmdbClient.handle_tmdb_show_query(params),
}
anime_modes = {"anime", "cartoon", "animation"}
if mode in anime_modes:
handlers[mode] = lambda: TmdbClient.handle_tmdb_anime_query(
category, mode, submode, page
)
handler = handlers.get(mode)
if handler:
return handler()
else:
notification(translation(90518))
@staticmethod
def handle_tmdb_movie_query(params):
query = params.get("query", "")
subquery = params.get("subquery", "")
mode = params.get("mode")
page = int(params.get("page", 1))
query_handlers = {
"tmdb_trending": lambda: TmdbClient.show_trending_movies(mode, page),
"tmdb_genres": lambda: TmdbClient.show_genres_items(mode, page),
"tmdb_popular": lambda: TmdbClient.show_popular_items(mode, page),
"tmdb_years": lambda: BaseTmdbClient.show_years_items(mode, page),
"tmdb_lang": lambda: TmdbClient.show_languages(mode, page),
"tmdb_collections": lambda: TmdbClient.show_collections_menu(mode),
"tmdb_keywords": lambda: TmdbClient.show_keywords_items(query, page, mode),
"tmdb_people": lambda: TmdbClient.handle_tmdb_people(subquery, mode, page),
}
handler = query_handlers.get(query)
if handler:
handler()
else:
notification(translation(90390))
@staticmethod
def handle_tmdb_people(subquery="", mode="", page=1):
set_pluging_category("People")
query_handlers = {
"search_people": lambda: PeopleClient().search_people(mode, page),
"popular_people": lambda: PeopleClient().show_popular_people(mode, page),
"latest_people": lambda: PeopleClient().show_trending_people(mode, page),
}
handler = query_handlers.get(subquery)
if handler:
handler()
else:
notification("Invalid query")
@staticmethod
def handle_tmdb_show_query(params):
query = params.get("query", "")
subquery = params.get("subquery", "")
mode = params.get("mode")
page = int(params.get("page", 1))
query_handlers = {
"tmdb_trending": lambda: TmdbClient.show_trending_shows(query, mode, page),
"tmdb_popular": lambda: TmdbClient.show_popular_items(mode, page),
"tmdb_airing_today": lambda: TmdbClient.show_airing_today_items(mode, page),
"tmdb_lang": lambda: TmdbClient.show_languages(mode, page),
"tmdb_genres": lambda: TmdbClient.show_genres_items(mode, page),
"tmdb_calendar": lambda: TmdbClient.show_calendar_items(query, page, mode),
"tmdb_years": lambda: TmdbClient.show_years_items(mode, page),
"tmdb_networks": lambda: TmdbClient.show_networks(mode, page),
"tmdb_people": lambda: TmdbClient.handle_tmdb_people(subquery, mode, page),
}
handler = query_handlers.get(query)
if handler:
return handler()
else:
notification("Invalid query")
@staticmethod
def handle_tmdb_anime_query(category, mode, submode, page):
set_content_type(mode)
def handle_search():
return TmdbAnime().anime_search(
TmdbAnimeClient().handle_anime_search_query(page), submode, page
)
def handle_category():
return TmdbAnimeClient().handle_anime_category_query(
TmdbAnime(), category, submode, page
)
def handle_years_or_genres():
return TmdbAnimeClient().handle_anime_years_or_genres(category, mode, page, submode)
handlers = {
Anime.SEARCH: handle_search,
Anime.AIRING: handle_category,
Anime.POPULAR: handle_category,
Anime.POPULAR_RECENT: handle_category,
Anime.TOP_RATED: handle_category,
Anime.YEARS: handle_years_or_genres,
Anime.GENRES: handle_years_or_genres,
}
handler = handlers.get(category)
if handler:
data = handler()
if data:
TmdbAnimeClient().process_anime_results(data, submode, page, mode, category)
@staticmethod
def handle_tmdb_search(params):
set_pluging_category(translation(90006))
mode = params.get("mode")
set_content_type(mode)
page = int(params.get("page", 1))
query = params.get("query")
if not query:
query = (
show_keyboard(id=30241) if page == 1 else PickleDatabase().get_key("search_query")
)
if not query:
end_of_directory(cache=False)
return
show_busy_dialog()
if page == 1:
PickleDatabase().set_key("search_query", query)
from datetime import timedelta
from lib.db.cached import cache
from lib.utils.kodi.settings import get_cache_expiration
cache.add_to_list(
key="multi",
item=("multi", query),
expires=timedelta(hours=get_cache_expiration()),
)
data = tmdb_get("search_multi", {"query": query, "page": page})
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90389))
close_busy_dialog()
end_of_directory(cache=False)
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
if results:
directory_items = []
for item in results:
tmdb_id = item.get("id", "")
media_type = item.get("media_type", "")
if media_type not in ("movie", "tv"):
continue
metadata = TmdbClient._get_cached_tmdb_item_metadata(item, media_type)
title = (
metadata.get("title")
or metadata.get("name")
or item.get("title", "")
or item.get("name", "")
)
if media_type == "movie":
label_title = f"[B]{translation(90008)} -[/B] {title}"
elif media_type == "tv":
label_title = f"[B]{translation(90007)} -[/B] {title}"
list_item = make_list_item(label=label_title)
# Use item directly as tmdb_obj for basic info
set_media_infoTag(list_item, data=metadata, mode=media_type)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button("handle_tmdb_search", page=page + 1, mode=mode)
end_of_directory()
_apply_tmdb_view("main")
close_busy_dialog()
@staticmethod
def tmdb_search_genres(mode, genre_id, page, submode=None):
path_map = {
"movies": "discover_movie",
"tv": "discover_tv",
"anime": "anime_genres",
}
params_map = {
"movies": {
"with_genres": genre_id,
"append_to_response": "external_ids",
"page": page,
},
"tv": {"with_genres": genre_id, "page": page},
"anime": {
"mode": submode,
"genre_id": genre_id,
"page": page,
},
}
path = path_map.get(mode)
params = params_map.get(mode)
if not path or not params:
notification(translation(90390))
return
data = tmdb_get(path=path, params=params)
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90389))
return
results = getattr(data, "results", [])
if mode != "anime":
results = filter_excluded_languages(results)
if results:
display_mode = submode if mode == "anime" and submode else mode
directory_items = []
for item in results:
tmdb_id = item.get("id", "")
title = item.get("title", "") or item.get("name", "")
media_type = item.get("media_type", "")
list_item = make_list_item(label=title)
display_mode = submode if mode == "anime" and submode else mode
metadata = TmdbClient._get_cached_tmdb_item_metadata(item, display_mode)
set_media_infoTag(list_item, data=metadata, mode=display_mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=display_mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button(
"search_tmdb_genres",
mode=mode,
submode=submode,
genre_id=genre_id,
page=page,
)
end_of_directory()
_apply_tmdb_view(display_mode)
@staticmethod
def tmdb_search_year(mode, submode, year, page):
set_pluging_category(str(year))
path_map = {
"movies": "discover_movie",
"tv": "discover_tv",
"anime": "anime_year",
}
params_map = {
"movies": {"primary_release_year": year, "page": page},
"tv": {"first_air_date_year": year, "page": page},
"anime": {"mode": submode, "year": year, "page": page},
}
path = path_map.get(mode)
params = params_map.get(mode)
if not path or not params:
notification("Invalid mode")
return
data = tmdb_get(path=path, params=params)
if not data:
return
if getattr(data, "total_results", 0) == 0:
notification("No results found")
return
results = getattr(data, "results", [])
if mode != "anime":
results = filter_excluded_languages(results)
if results:
display_mode = submode if mode == "anime" and submode else mode
directory_items = []
for item in results:
tmdb_id = item.get("id", "")
title = item.get("title", "") or item.get("name", "")
media_type = item.get("media_type", "")
list_item = make_list_item(label=title)
display_mode = submode if mode == "anime" and submode else mode
metadata = TmdbClient._get_cached_tmdb_item_metadata(item, display_mode)
set_media_infoTag(list_item, data=metadata, mode=display_mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=display_mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button("search_tmdb_year", page=page, mode=mode, submode=submode, year=year)
end_of_directory()
_apply_tmdb_view(display_mode)
@staticmethod
def _get_tmdb_metadata(mode, media_type, tmdb_id):
from lib.clients.tmdb.utils.utils import (
get_tmdb_movie_details,
get_tmdb_show_details,
)
tmdb_obj = None
if mode == "movies":
tmdb_obj = get_tmdb_movie_details(tmdb_id)
elif mode == "tv":
tmdb_obj = get_tmdb_show_details(tmdb_id)
elif mode == "multi":
if media_type == "movie":
tmdb_obj = get_tmdb_movie_details(tmdb_id)
elif media_type == "tv":
tmdb_obj = get_tmdb_show_details(tmdb_id)
else:
kodilog(f"Invalid media type: {media_type}", level=xbmc.LOGERROR)
return None
return tmdb_obj
@staticmethod
def show_trending_shows(query, mode, page):
set_pluging_category(translation(90028))
set_content_type(mode)
data = tmdb_get("trending_tv", page)
if not data or getattr(data, "total_results", 0) == 0:
notification("No results found")
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
result_media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
title = (
metadata.get("title")
or metadata.get("name")
or getattr(res, "title", "")
or getattr(res, "name", "")
)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=result_media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button("handle_tmdb_query", query=query, page=page, mode=mode)
end_of_directory()
_apply_tmdb_view("tv")
@staticmethod
def show_trending_movies(mode, page):
set_pluging_category(translation(90028))
set_content_type(mode)
data = tmdb_get("trending_movie", page)
if not data or getattr(data, "total_results", 0) == 0:
notification("No results found")
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
result_media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
title = (
metadata.get("title")
or metadata.get("name")
or getattr(res, "title", "")
or getattr(res, "name", "")
)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=result_media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button("handle_tmdb_query", query="tmdb_trending", page=page, mode=mode)
end_of_directory()
_apply_tmdb_view("movies")
@staticmethod
def show_tmdb_item(params):
mode = params.get("mode", "")
submode = params.get("submode", "")
tmdb_id = params.get("id", "")
media_type = params.get("media_type", "")
title = params.get("title", "") or params.get("name", "")
# Adjust mode for anime
if mode == "anime":
mode = submode
tmdb_obj = TmdbClient._get_tmdb_metadata(mode, media_type, tmdb_id)
if not tmdb_obj:
return
if mode == "movies" or (mode == "multi" and media_type == "movie"):
TmdbClient._show_tmdb_movie(tmdb_obj, mode, title, tmdb_id)
elif mode == "tv" or (mode == "multi" and media_type == "tv"):
TmdbClient._show_tmdb_shows(tmdb_obj, mode, title, tmdb_id, media_type)
else:
notification(translation(90401))
@staticmethod
def _show_tmdb_movie(tmdb_obj, mode, title, tmdb_id):
mode = "movies" if mode == "multi" else mode
ids = {"tmdb_id": tmdb_id}
ids["imdb_id"] = tmdb_obj.get("external_ids", {}).get("imdb_id", "")
from lib.search import run_search_entry
run_search_entry({"query": title, "mode": mode, "ids": json.dumps(ids)})
@staticmethod
def _show_tmdb_shows(tmdb_obj, mode, title, tmdb_id, media_type):
mode = "tv" if mode == "multi" else mode
ids = {"tmdb_id": tmdb_id}
ids["imdb_id"] = tmdb_obj.get("external_ids", {}).get("imdb_id", "")
tvdb_id = tmdb_obj.get("external_ids", {}).get("tvdb_id", "") or ""
ids["tvdb_id"] = tvdb_id
number_of_seasons = tmdb_obj.get("number_of_seasons", 1) or 1
if number_of_seasons == 1:
TmdbClient.show_episodes_details(
tv_name=title,
ids=ids,
mode=mode,
media_type=media_type,
season=number_of_seasons,
)
else:
TmdbClient.show_seasons_details(ids=ids, mode=mode, media_type=media_type)
@staticmethod
def show_seasons_details(ids, mode, media_type):
set_content_type("season")
show_season_info(ids, mode, media_type)
end_of_directory()
_apply_tmdb_view("season")
@staticmethod
def show_episodes_details(tv_name, ids, mode, media_type, season):
set_content_type("episode")
show_episode_info(tv_name, season, ids, mode, media_type)
end_of_directory()
_apply_tmdb_view("episode")
@staticmethod
def show_popular_items(mode, page):
set_pluging_category(translation(90037))
set_content_type(mode)
path = "popular_shows" if mode == "tv" else "popular_movie"
data = tmdb_get(path, page)
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90389))
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
result_media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
title = (
metadata.get("title")
or metadata.get("name")
or getattr(res, "title", "")
or getattr(res, "name", "")
)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=result_media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button("handle_tmdb_query", query="tmdb_popular", page=page, mode=mode)
end_of_directory()
_apply_tmdb_view(mode)
@staticmethod
def show_airing_today_items(mode, page):
set_pluging_category(translation(90086))
set_content_type(mode)
today = datetime.now().strftime("%Y-%m-%d")
params = {
"air_date.gte": today,
"air_date.lte": today,
"without_genres": "10767,10763,10764",
"sort_by": "popularity.desc",
"page": page,
}
data = tmdb_get("discover_tv", params)
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90395))
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
title = getattr(res, "name", "")
media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button("handle_tmdb_query", query="tmdb_airing_today", page=page, mode=mode)
end_of_directory()
_apply_tmdb_view(mode)
@staticmethod
def show_languages(mode, page):
from lib.clients.tmdb.utils.utils import FULL_NAME_LANGUAGES
set_pluging_category(translation(90065))
directory_items = []
for lang in FULL_NAME_LANGUAGES:
list_item = make_list_item(label=lang["name"])
list_item.setArt(
{
"icon": os.path.join(ADDON_PATH, "resources", "img", "lang.png"),
}
)
item = add_kodi_dir_item(
list_item=list_item,
url=build_url(
"search_tmdb_lang",
mode=mode,
lang=lang["id"],
page=page,
),
batch=True,
)
directory_items.append(item)
add_directory_items_batch(directory_items)
end_of_directory()
_apply_tmdb_view("main")
@staticmethod
def show_lang_items(params):
lang = params.get("lang")
set_pluging_category(lang)
mode = params.get("mode")
set_content_type(mode)
page = int(params.get("page", 1))
route_map = {
"movies": "discover_movie",
"tv": "discover_tv",
}
path = route_map.get(mode)
if not path:
notification(translation(90390))
return
route_params = {"with_original_language": lang, "page": page}
data = tmdb_get(path=path, params=route_params)
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90389))
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results, force_allow_lang=lang)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
result_media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
title = (
metadata.get("title")
or metadata.get("name")
or getattr(res, "title", "")
or getattr(res, "name", "")
)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=result_media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button(
"search_tmdb_lang",
mode=mode,
lang=lang,
page=page,
)
end_of_directory()
_apply_tmdb_view(mode)
@staticmethod
def show_networks(mode, page):
from lib.clients.tmdb.utils.utils import NETWORKS
set_pluging_category(translation(90066))
directory_items = []
for network in NETWORKS:
list_item = make_list_item(label=network["name"])
list_item.setArt(
{
"icon": network["icon"],
"thumb": network["icon"],
"poster": network["icon"],
}
)
item = add_kodi_dir_item(
list_item=list_item,
url=build_url(
"search_tmbd_network",
mode=mode,
id=network["id"],
page=page,
),
batch=True,
)
directory_items.append(item)
add_directory_items_batch(directory_items)
end_of_directory()
_apply_tmdb_view("main")
@staticmethod
def show_network_items(params):
from lib.clients.tmdb.utils.utils import NETWORKS
network_id = int(params.get("id"))
network_name = next((net["name"] for net in NETWORKS if net["id"] == network_id), "")
set_pluging_category(network_name)
mode = params.get("mode")
set_content_type(mode)
page = int(params.get("page", 1))
route_map = {
"movies": "discover_movie",
"tv": "discover_tv",
}
path = route_map.get(mode)
if not path:
notification(translation(90390))
return
route_params = {"page": page}
if mode == "tv":
route_params["with_networks"] = network_id
elif mode == "movies":
route_params["with_companies"] = network_id
data = tmdb_get(path=path, params=route_params)
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90389))
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
result_media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
title = (
metadata.get("title")
or metadata.get("name")
or getattr(res, "title", "")
or getattr(res, "name", "")
)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=result_media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
add_next_button(
"search_tmbd_network",
mode=mode,
id=network_id,
page=page,
)
end_of_directory()
_apply_tmdb_view(mode)
@staticmethod
def show_calendar_items(query, page, mode):
set_pluging_category(translation(90021))
trending_data = tmdb_get("tv_week", page)
if not trending_data or trending_data.total_results == 0:
notification(translation(90394))
end_of_directory()
return
results = []
def fetch_episodes_for_trending_show(show):
tmdb_id = getattr(show, "id", None)
if not tmdb_id:
return
ids = {"tmdb_id": tmdb_id}
episodes, details = get_episodes_for_show(ids)
for ep in episodes:
air_date = ep.get("air_date")
if air_date and is_this_week(air_date):
results.append((getattr(show, "name", ""), show, ep, details))
trending_results = getattr(trending_data, "results", [])
trending_results = filter_excluded_languages(trending_results)
execute_thread_pool(trending_results, fetch_episodes_for_trending_show)
directory_items = []
# Add fixed item showing current date at the top
current_date = datetime.now().strftime("%A, %d %B %Y")
date_item = make_list_item(
label=f"[UPPERCASE][COLOR=orange]Today: {current_date}[/COLOR][/UPPERCASE]"
)
date_item.setArt({"icon": os.path.join(ADDON_PATH, "resources", "img", "history.png")})
item = add_kodi_dir_item(date_item, "", is_folder=False, batch=True)
directory_items.append(item)
today_str = datetime.now().strftime("%Y-%m-%d")
results_today = [r for r in results if r[2].get("air_date") == today_str]
results_other = [r for r in results if r[2].get("air_date") != today_str]
results = sorted(results_today, key=lambda x: x[2].get("air_date", "")) + sorted(
results_other, key=lambda x: x[2].get("air_date", "")
)
for title, show, ep, details in results:
tv_data = {"name": quote(title), "episode": ep["number"], "season": ep["season"]}
air_date_obj = parse_date_str(ep["air_date"])
weekday_name = air_date_obj.strftime("%A")
weekday_name_translated = translate_weekday(weekday_name)
# Mark if episode is released today
is_today = ep["air_date"] == today_str
mark = "[UPPERCASE][COLOR=orange]TODAY- [/COLOR][/UPPERCASE]" if is_today else ""
ep_title = f"{mark}{weekday_name_translated} - ({ep['air_date']}) - {title} - S{ep['season']:02}E{ep['number']:02}"
tmdb_id = show.id
if details is None:
kodilog(f"Show details not found for TMDB ID: {tmdb_id}")
continue
external_ids = details.external_ids
imdb_id = external_ids.get("imdb_id", "")
tvdb_id = external_ids.get("tvdb_id", "")
ids = {"tmdb_id": tmdb_id, "tvdb_id": tvdb_id, "imdb_id": imdb_id}
list_item = make_list_item(label=ep_title)
list_item.setProperty("IsPlayable", "true")
set_media_infoTag(list_item, data=details, mode="tv")
item = add_kodi_dir_item(
list_item=list_item,
url=build_url(
"search",
mode="tv",
media_type="tv",
query=title,
ids=ids,
tv_data=tv_data,
),
is_folder=False,
batch=True,
)
directory_items.append(item)
add_directory_items_batch(directory_items)
if getattr(trending_data, "total_pages", 0) > page:
add_next_button(
"handle_tmdb_query",
query=query,
page=page + 1,
mode=mode,
)
end_of_directory()
_apply_tmdb_view("tv")
@staticmethod
def show_collections_menu(mode):
set_pluging_category(translation(90067))
collections_menu = [
(translation(90376), "search", "search.png"),
(translation(90377), "popular", "tmdb.png"),
(translation(90378), "top_rated", "tmdb.png"),
]
directory_items = []
for label, submode, icon_path in collections_menu:
list_item = make_list_item(label=label)
item = add_kodi_dir_item(
list_item=list_item,
url=build_url(
"handle_collection_query",
mode=mode,
submode=submode,
page=1,
),
is_folder=True,
icon_path=icon_path,
batch=True,
)
directory_items.append(item)
add_directory_items_batch(directory_items)
end_of_directory()
_apply_tmdb_view("movies")
@staticmethod
def handle_collection_query(params):
mode = params.get("mode")
submode = params.get("submode")
page = int(params.get("page", 1))
set_content_type(mode)
from lib.clients.tmdb.collections import TmdbCollections
if submode == "popular":
TmdbCollections.get_popular_collections(mode, page)
elif submode == "top_rated":
TmdbCollections.get_top_rated_collections(mode, page)
elif submode == "search":
TmdbCollections.search_collections(mode, page)
else:
notification(translation(90396))
@staticmethod
def show_keywords_items(query, page, mode):
keywords_data = Search().keywords(query, page=page)
if not keywords_data or len(keywords_data) == 0:
notification(translation(90397))
end_of_directory()
return
directory_items = []
for keyword in keywords_data:
if isinstance(keyword, AsObj):
keyword_id = keyword.get("id")
keyword_name = keyword.get("name")
if not keyword_id or not keyword_name:
continue
list_item = make_list_item(label=keyword_name)
item = add_kodi_dir_item(
list_item=list_item,
url=build_url(
"show_keyword_results",
mode=mode,
keyword_id=keyword_id,
keyword_name=keyword_name,
page=1,
),
is_folder=True,
icon_path="tmdb.png",
batch=True,
)
directory_items.append(item)
add_directory_items_batch(directory_items)
add_next_button("handle_keyword_search", query=query, page=page + 1, mode=mode)
end_of_directory()
_apply_tmdb_view("main")
@staticmethod
def handle_keyword_search(params):
mode = params.get("mode", "multi")
query = params.get("query", "")
page = int(params.get("page", 1))
if not query:
query = show_keyboard(id=30243, default="")
if not query:
end_of_directory(cache=False)
return
TmdbClient.show_keywords_items(query, page, mode)
@staticmethod
def show_keyword_results(params):
from lib.clients.tmdb.utils.utils import tmdb_get
keyword_id = params.get("keyword_id")
keyword_name = params.get("keyword_name", "")
page = int(params.get("page", 1))
if not keyword_id:
notification(translation(90398))
return
set_pluging_category(translation(90381) % keyword_name)
set_content_type(
"movies"
) # Using movies content type ensures better poster rendering in many skins
def fetch_movies():
return tmdb_get("discover_movie", {"with_keywords": keyword_id, "page": page})
def fetch_tv():
return tmdb_get("discover_tv", {"with_keywords": keyword_id, "page": page})
results = []
total_pages = 0
with ThreadPoolExecutor(max_workers=2) as executor:
future_movies = executor.submit(fetch_movies)
future_tv = executor.submit(fetch_tv)
movie_data = future_movies.result()
if movie_data and getattr(movie_data, "results", None):
for item in movie_data.results:
if isinstance(item, dict):
item["media_type"] = "movie"
else:
item.media_type = "movie"
results.extend(movie_data.results)
total_pages = max(total_pages, getattr(movie_data, "total_pages", 0))
tv_data = future_tv.result()
if tv_data and getattr(tv_data, "results", None):
for item in tv_data.results:
if isinstance(item, dict):
item["media_type"] = "tv"
else:
item.media_type = "tv"
results.extend(tv_data.results)
total_pages = max(total_pages, getattr(tv_data, "total_pages", 0))
if not results:
notification(translation(90399))
end_of_directory()
return
# Sort combined results by popularity
results.sort(
key=lambda x: (
getattr(x, "popularity", x.get("popularity", 0))
if isinstance(x, dict)
else getattr(x, "popularity", 0)
),
reverse=True,
)
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "") if not isinstance(res, dict) else res.get("id", "")
title = (
getattr(res, "title", "") or getattr(res, "name", "")
if not isinstance(res, dict)
else res.get("title", "") or res.get("name", "")
)
media_type = (
getattr(res, "media_type", "")
if not isinstance(res, dict)
else res.get("media_type", "")
)
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, media_type or "multi")
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode="multi")
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode="multi",
title=title,
ids={"tmdb_id": tmdb_id},
media_type=media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
if total_pages > page:
add_next_button(
"show_keyword_results",
keyword_id=keyword_id,
keyword_name=keyword_name,
page=page + 1,
)
end_of_directory()
_apply_tmdb_view("main", content_type="movies")
@staticmethod
def search_tmdb_recommendations(params):
ids = json.loads(params.get("ids", "{}"))
mode = params.get("mode", "tv")
media_type = params.get("media_type", "")
tmdb_id = ids.get("tmdb_id")
page = int(params.get("page", 1))
if not tmdb_id:
notification(translation(90400))
return
effective_mode = mode
if mode == "multi":
if media_type == "movie":
effective_mode = "movies"
elif media_type == "tv":
effective_mode = "tv"
set_pluging_category(translation(90379))
set_content_type(effective_mode)
if effective_mode == "tv":
data = tmdb_get("tv_recommendations", {"id": tmdb_id, "page": page})
elif effective_mode == "movies":
data = tmdb_get("movie_recommendations", {"id": tmdb_id, "page": page})
else:
notification(translation(90390))
return
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90391))
end_of_directory()
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
result_media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
title = (
metadata.get("title")
or metadata.get("name")
or getattr(res, "title", "")
or getattr(res, "name", "")
)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=result_media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
if getattr(data, "total_pages", 0) > page:
add_next_button(
"search_tmdb_recommendations",
ids=ids,
mode=mode,
media_type=media_type,
page=page + 1,
)
end_of_directory()
_apply_tmdb_view(effective_mode)
@staticmethod
def search_tmdb_similar(params):
ids = json.loads(params.get("ids", "{}"))
mode = params.get("mode", "tv")
media_type = params.get("media_type", "")
tmdb_id = ids.get("tmdb_id")
page = int(params.get("page", 1))
if not tmdb_id:
notification(translation(90400))
return
if not tmdb_id:
notification(translation(90400))
return
effective_mode = mode
if mode == "multi":
if media_type == "movie":
effective_mode = "movies"
elif media_type == "tv":
effective_mode = "tv"
set_pluging_category(translation(90380))
set_content_type(effective_mode)
if effective_mode == "tv":
data = tmdb_get("tv_similar", {"id": tmdb_id, "page": page})
elif effective_mode == "movies":
data = tmdb_get("movie_similar", {"id": tmdb_id, "page": page})
else:
notification(translation(90390))
return
if not data or getattr(data, "total_results", 0) == 0:
notification(translation(90392))
end_of_directory()
return
results = getattr(data, "results", [])
results = filter_excluded_languages(results)
directory_items = []
for res in results:
tmdb_id = getattr(res, "id", "")
result_media_type = getattr(res, "media_type", "") or ""
metadata = TmdbClient._get_cached_tmdb_item_metadata(res, mode)
title = (
metadata.get("title")
or metadata.get("name")
or getattr(res, "title", "")
or getattr(res, "name", "")
)
list_item = make_list_item(label=title)
set_media_infoTag(list_item, data=metadata, mode=mode)
item_tuple = BaseTmdbClient.add_media_directory_item(
list_item=list_item,
mode=mode,
title=title,
ids={"tmdb_id": tmdb_id},
media_type=result_media_type,
batch=True,
)
directory_items.append(item_tuple)
add_directory_items_batch(directory_items)
if getattr(data, "total_pages", 0) > page:
add_next_button(
"search_tmdb_similar",
ids=ids,
mode=mode,
media_type=media_type,
page=page + 1,
)
end_of_directory()
_apply_tmdb_view(effective_mode)
@staticmethod
def rescrape_tmdb_media(params):
mode = params.get("mode", "")
media_type = params.get("media_type", "")
tmdb_id = params.get("tmdb_id", "")
query = params.get("query", "")
if mode == "anime":
mode = params.get("submode", mode)
if mode == "multi":
if media_type == "movie":
mode = "movies"
elif media_type == "tv":
mode = "tv"
tmdb_obj = TmdbClient._get_tmdb_metadata(mode, media_type, tmdb_id)
if not tmdb_obj:
notification(translation(90393))
return
external_ids = tmdb_obj.get("external_ids") or {}
ids = {
"tmdb_id": tmdb_id,
"imdb_id": external_ids.get("imdb_id", ""),
"tvdb_id": external_ids.get("tvdb_id", ""),
}
from lib.search import run_search_entry
run_search_entry(
{
"query": query,
"mode": mode,
"ids": json.dumps(ids),
"rescrape": True,
"force_select": params.get("force_select", False),
}
)
@staticmethod
def tmdb_search_modes(params):
mode = params.get("mode", "")
media_type = params.get("media_type", "")
tmdb_id = params.get("tmdb_id", "")
query = params.get("query", "")
if mode == "anime":
mode = params.get("submode", mode)
if mode == "multi":
if media_type == "movie":
mode = "movies"
elif media_type == "tv":
mode = "tv"
if mode != "movies":
return
tmdb_obj = TmdbClient._get_tmdb_metadata(mode, media_type, tmdb_id)
if not tmdb_obj:
notification(translation(90393))
return
selected = xbmcgui.Dialog().select(
translation(90733),
[translation(90730), translation(90731), translation(90732)],
)
if selected < 0:
return
variant_map = {
0: "title_year",
1: "original_title",
2: "original_title_year",
}
variant = variant_map.get(selected)
if not variant:
return
edited_query = show_keyboard(id=90736, default=query)
if not edited_query:
return
query = str(edited_query).strip()
if not query:
return
year = ""
if variant in ("title_year", "original_title_year"):
year_input = xbmcgui.Dialog().numeric(0, translation(90734), "")
if not year_input:
return
year_input = str(year_input).strip()
if not (year_input.isdigit() and len(year_input) == 4):
notification(translation(90735))
return
year = year_input
external_ids = tmdb_obj.get("external_ids") or {}
ids = {
"tmdb_id": tmdb_id,
"imdb_id": external_ids.get("imdb_id", ""),
"tvdb_id": external_ids.get("tvdb_id", ""),
}
kodilog(
f"TMDB search mode selected: query={query}, tmdb_id={tmdb_id}, variant={variant}, year={year}"
)
from lib.search import run_search_entry
run_search_entry(
{
"query": query,
"mode": mode,
"ids": json.dumps(ids),
"rescrape": True,
"force_select": True,
"search_variant": variant,
"year": year,
}
)
@staticmethod
def tmdb_episode_search_modes(params):
mode = params.get("mode", "")
media_type = params.get("media_type", "")
query = params.get("query", "")
ids = json.loads(params.get("ids", "{}"))
tv_data = json.loads(params.get("tv_data", "{}"))
tmdb_id = ids.get("tmdb_id", "")
if mode == "anime":
mode = params.get("submode", mode)
if mode == "multi":
if media_type == "movie":
mode = "movies"
elif media_type == "tv":
mode = "tv"
if mode != "tv":
return
tmdb_obj = TmdbClient._get_tmdb_metadata(mode, media_type, tmdb_id)
if not tmdb_obj:
notification(translation(90393))
return
original_name = getattr(tmdb_obj, "original_name", "") or ""
selected = xbmcgui.Dialog().select(
translation(90733),
[
translation(90740),
translation(90741),
translation(90773),
],
)
if selected < 0:
return
full_show_variant_map = {
2: "title_year",
}
full_show_variant = full_show_variant_map.get(selected)
if selected == 0 or full_show_variant == "title_year":
edited_query = show_keyboard(id=90736, default=query)
if not edited_query:
return
query = str(edited_query).strip()
elif selected == 1:
if original_name:
query = original_name
edited_query = show_keyboard(id=90736, default=query)
if not edited_query:
return
query = str(edited_query).strip()
else:
return
if not query:
return
if full_show_variant:
payload = {
"query": query,
"mode": mode,
"ids": json.dumps(ids),
"rescrape": True,
"force_select": True,
"search_variant": full_show_variant,
}
if full_show_variant == "title_year":
year_input = xbmcgui.Dialog().numeric(0, translation(90734), "")
if not year_input:
return
year_input = str(year_input).strip()
if not (year_input.isdigit() and len(year_input) == 4):
notification(translation(90735))
return
payload["year"] = year_input
kodilog(
f"TMDB full show search mode selected: query={query}, ids={ids}, variant={full_show_variant}, year={payload.get('year', '')}"
)
from lib.search import run_search_entry
run_search_entry(payload)
return
season_default = str(tv_data.get("season", ""))
season_input = xbmcgui.Dialog().numeric(0, translation(90512), season_default)
if not season_input:
return
season_input = str(season_input).strip()
if not season_input.isdigit():
notification(translation(90737))
return
episode_default = str(tv_data.get("episode", ""))
episode_input = xbmcgui.Dialog().numeric(0, translation(90738), episode_default)
if not episode_input:
return
episode_input = str(episode_input).strip()
if not episode_input.isdigit():
notification(translation(90739))
return
updated_tv_data = {
"name": tv_data.get("name", ""),
"season": int(season_input),
"episode": int(episode_input),
}
kodilog(
f"TMDB episode search mode selected: query={query}, season={season_input}, episode={episode_input}, ids={ids}"
)
from lib.search import run_search_entry
run_search_entry(
{
"query": query,
"mode": mode,
"ids": json.dumps(ids),
"tv_data": json.dumps(updated_tv_data),
"rescrape": True,
"force_select": True,
}
)