Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
674 changes: 674 additions & 0 deletions script.skin.info.service/LICENSE.txt

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions script.skin.info.service/addon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.skin.info.service"
name="Skin Info Service"
version="2.0.0"
provider-name="MikeSiLVO">
<requires>
<import addon="xbmc.python" version="3.0.1" />
<import addon="xbmc.json" version="13.5.0" />
<import addon="script.module.pil" version="1.1.7" />
<import addon="script.module.requests" version="2.31.0" />
</requires>
<extension point="xbmc.python.pluginsource" library="plugin.py">
<provides>video</provides>
</extension>
<extension point="xbmc.python.library" library="script.py" />
<extension point="xbmc.service" library="service.py" />
<extension point="kodi.context.item">
<menu id="kodi.core.main">
<item library="context.py">
<label>$ADDON[script.skin.info.service 32102]</label>
<visible>!String.IsEmpty(ListItem.DBID) + [String.IsEqual(ListItem.DBType,movie) | String.IsEqual(ListItem.DBType,tvshow) | String.IsEqual(ListItem.DBType,season) | String.IsEqual(ListItem.DBType,episode) | String.IsEqual(ListItem.DBType,musicvideo) | String.IsEqual(ListItem.DBType,set) | String.IsEqual(ListItem.DBType,artist) | String.IsEqual(ListItem.DBType,album) | String.IsEqual(ListItem.DBType,song)]</visible>
</item>
</menu>
</extension>
<extension point="xbmc.addon.metadata">
<platform>all</platform>
<license>GPL-3.0-only</license>
<source>https://github.com/MikeSiLVO/script.skin.info.service</source>
<forum>https://forum.kodi.tv/showthread.php?tid=384016</forum>
<assets>
<icon>icon.png</icon>
<fanart>fanart.jpg</fanart>
</assets>
<news>
2.0.0
- Complete rewrite of the service
- Online metadata and multi-source ratings (TMDb, OMDb, MDBList, Trakt, IMDb, Rotten Tomatoes, Metacritic and more)
- Incremental IMDb rating updates and IMDb Top 250 rankings
- Bulk TV show metadata sync and a tool to repair missing IMDb, TMDB and TVDB IDs
- Smart caching that adapts to content age and TV show status, no manual settings
- Music and music video online data and artwork (TheAudioDB, Last.fm, Wikipedia, Fanart.tv)
- Artwork review tool plus single and bulk artwork download
- Metadata editor for movies, TV shows, episodes, music videos, artists, albums and songs, plus NFO export
- Plugin widgets: Next Up, Discovery, Recommendations, Cast, Crew, Similar, letter jump and more
- Info dialogs: Actor Info, Video Info and Image Viewer
- Stinger (mid/post-credits) notifications with skin override support
- Tools: image blur, color picker, fanart slideshow, texture cache cleanup and GIF poster scanner
- Skinner helpers: JSON-RPC wrapper (action=json) and skin setting actions
- Requires Kodi 21 (Omega) or newer; skins enable it with Skin.SetBool(SkinInfo.Service)
</news>
<summary lang="en_GB">Provides rich media info and artwork to skins.</summary>
<summary lang="fr_FR">Fournit aux habillages des informations enrichies sur les médias ainsi que leurs visuels.</summary>
<summary lang="pl_PL">Dostarcza bogatych informacji o mediach i grafikę dla skórek.</summary>
<description lang="en_GB">Skin Info Service provides skins with rich metadata and artwork beyond what Kodi exposes by default. It adds extra properties such as ratings from multiple sources, artwork, stream details, and cast and plot information for movies, TV shows, episodes, sets, music videos, artists, and albums, drawing on both your library and online data. It also includes a tool to find and add animated GIF posters to your library.</description>
<description lang="fr_FR">Skin Info Service fournit des informations supplémentaires telles que les notes, les illustrations et les détails de flux pour les films, les séries, les sagas, les artistes et les albums. Comprend un outil de recherche d&apos;affiches GIF pour trouver et ajouter automatiquement des affiches GIF animées à votre médiathèque.</description>
<description lang="pl_PL">Skin Info Service dostarcza skórkom bogate metadane i grafikę, wykraczając daleko poza to, co Kodi udostępnia domyślnie. Dodaje dodatkowe właściwości, takie jak oceny z wielu źródeł, grafikę, szczegóły transmisji oraz informacje o obsadzie i fabule filmów, seriali, odcinków, kolekcji filmowych, teledysków, wykonawców i albumów, korzystając zarówno z Twojej biblioteki, jak i danych online. Zawiera również narzędzie do wyszukiwania i dodawania animowanych plakatów GIF do Twojej biblioteki.</description>
</extension>
</addon>
3 changes: 3 additions & 0 deletions script.skin.info.service/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from lib.script.context import main

main()
Binary file added script.skin.info.service/fanart.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added script.skin.info.service/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions script.skin.info.service/lib/actor/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Actor image download configuration and constants."""
import sys

ILLEGAL_CHARS_ALL = "/\\?"
ILLEGAL_CHARS_WINDOWS = ':*"<>|'
DEFAULT_EXTENSION = ".jpg"


def sanitize_actor_filename(name: str, extension: str = DEFAULT_EXTENSION) -> str:
"""Convert actor name to Kodi-compatible filename, matching Kodi's GetSafeFile()."""
filename = name.replace(" ", "_")

for char in ILLEGAL_CHARS_ALL:
filename = filename.replace(char, "_")

if sys.platform == "win32":
for char in ILLEGAL_CHARS_WINDOWS:
filename = filename.replace(char, "_")
filename = filename.rstrip(". ")

return filename + extension
219 changes: 219 additions & 0 deletions script.skin.info.service/lib/actor/downloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""Actor image download logic using Kodi JSON-RPC and TMDB cache."""
from __future__ import annotations

import xbmc
import xbmcvfs
from typing import Dict, List, Optional, Tuple

from lib.kodi.client import log, request, extract_result, decode_image_url, get_item_details, ADDON
from lib.kodi.utilities import extract_media_ids
from lib.data.api.utilities import tmdb_image_url
from lib.download.artwork import DownloadArtwork
from lib.actor.config import sanitize_actor_filename
from lib.infrastructure.paths import vfs_join, vfs_ensure_dir_slash, build_actors_folder_path


def get_cast_with_ids(media_type: str, dbid: int) -> Tuple[List[Dict], Dict[str, Optional[str]]]:
"""Get cast list and media IDs for a movie or tvshow from Kodi JSON-RPC."""
if media_type not in ("movie", "tvshow"):
return [], {}

details = get_item_details(media_type, dbid, ["cast", "uniqueid"])
if not isinstance(details, dict):
return [], {}

return details.get("cast", []), extract_media_ids(details)


def get_episode_guest_stars(tvshowid: int) -> List[Dict]:
"""Get guest stars from every episode of a TV show (may contain duplicates across episodes)."""
response = request("VideoLibrary.GetEpisodes", {
"tvshowid": tvshowid,
"properties": ["cast"]
})
episodes = extract_result(response, "episodes")

if not episodes:
return []

guest_stars: List[Dict] = []
for episode in episodes:
cast = episode.get("cast", [])
guest_stars.extend(cast)

return guest_stars


def _get_tmdb_credits(media_type: str, tmdb_id: str) -> List[Dict]:
"""Get TMDB cast list from cache or API."""
from lib.data.api.tmdb import ApiTmdb

api = ApiTmdb()
data = api.get_complete_data(media_type, int(tmdb_id))

if not data:
return []

credits = data.get("credits", {})
return credits.get("cast", [])


def _match_actor_to_profile(
actor_name: str,
actor_role: str,
tmdb_credits: List[Dict]
) -> Optional[str]:
"""Match Kodi actor to TMDB cast member via 4-stage matching."""
from lib.data.api.person import (
exact_match,
fuzzy_role_match,
name_only_match,
fuzzy_name_match
)

match = exact_match(tmdb_credits, actor_name, actor_role)
if match and match.get("profile_path"):
return match["profile_path"]

match = fuzzy_role_match(tmdb_credits, actor_name, actor_role)
if match and match.get("profile_path"):
return match["profile_path"]

match = name_only_match(tmdb_credits, actor_name)
if match and match.get("profile_path"):
return match["profile_path"]

match = fuzzy_name_match(tmdb_credits, actor_name)
if match and match.get("profile_path"):
return match["profile_path"]

return None


def download_actor_images(
media_type: str,
dbid: int,
file_path: str,
show_path: Optional[str] = None,
existing_file_mode: str = "skip",
abort_flag=None
) -> Tuple[int, int, int]:
"""Download actor images for a single media item.

Falls back to Kodi thumbnail URLs when TMDB match fails.
Returns (downloaded, skipped, failed) counts.
"""
downloaded = 0
skipped = 0
failed = 0
monitor = xbmc.Monitor()

cast, media_ids = get_cast_with_ids(media_type, dbid)

if media_type == "tvshow" and ADDON.getSettingBool("download.include_guest_stars"):
guest_stars = get_episode_guest_stars(dbid)
if guest_stars:
log(
"Artwork",
f"Got {len(guest_stars)} guest star entries from episodes",
xbmc.LOGDEBUG,
)
cast = cast + guest_stars

if not cast:
log("Artwork", f"No cast found for {media_type} {dbid}", xbmc.LOGDEBUG)
return downloaded, skipped, failed

actors_folder = build_actors_folder_path(media_type, file_path, show_path)
if not actors_folder:
log(
"Artwork",
f"Could not determine .actors folder for {media_type} {dbid}",
xbmc.LOGWARNING,
)
return downloaded, skipped, failed

actors_folder_check = vfs_ensure_dir_slash(actors_folder)
if not xbmcvfs.exists(actors_folder_check):
xbmcvfs.mkdirs(actors_folder)
if not xbmcvfs.exists(actors_folder_check):
log("Artwork", f"Failed to create .actors folder: {actors_folder}", xbmc.LOGWARNING)
return downloaded, skipped, failed
log("Artwork", f"Created .actors folder: {actors_folder}", xbmc.LOGDEBUG)

tmdb_credits: List[Dict] = []
tmdb_id = media_ids.get("tmdb")
if tmdb_id:
tmdb_credits = _get_tmdb_credits(media_type, tmdb_id)
if tmdb_credits:
log("Artwork", f"Got {len(tmdb_credits)} cast members from TMDB", xbmc.LOGDEBUG)

downloader = DownloadArtwork()
seen_filenames: set = set()

for actor in cast:
if monitor.abortRequested():
break
if abort_flag and abort_flag.is_requested():
break

name = actor.get("name", "").strip()
if not name:
continue

role = actor.get("role", "").strip()

filename = sanitize_actor_filename(name, "")
if filename in seen_filenames:
log("Artwork", f"Duplicate actor filename '{filename}', skipping", xbmc.LOGDEBUG)
skipped += 1
continue
seen_filenames.add(filename)

local_path = vfs_join(actors_folder, filename)

profile_path = _match_actor_to_profile(name, role, tmdb_credits) if tmdb_credits else None
if profile_path:
url = tmdb_image_url(profile_path)
success, error, _, _ = downloader.download_artwork(
url=url,
local_path=local_path,
existing_file_mode=existing_file_mode,
)
if success:
downloaded += 1
log("Artwork", f"Downloaded actor image from TMDB: {name}", xbmc.LOGDEBUG)
continue
elif error is None:
skipped += 1
continue

thumbnail = actor.get("thumbnail", "").strip()
if thumbnail:
decoded_url = decode_image_url(thumbnail)
if decoded_url.startswith("http"):
success, error, _, _ = downloader.download_artwork(
url=decoded_url,
local_path=local_path,
existing_file_mode=existing_file_mode,
)
if success:
downloaded += 1
log("Artwork", f"Downloaded actor image from Kodi URL: {name}", xbmc.LOGDEBUG)
continue
elif error is None:
skipped += 1
continue
else:
failed += 1
log(
"Artwork",
f"Failed to download actor image for '{name}': {error}",
xbmc.LOGWARNING,
)
continue

log("Artwork", f"No image source for actor '{name}'", xbmc.LOGDEBUG)
skipped += 1

return downloaded, skipped, failed
Loading
Loading