From fd362380fe108c6c9bb6f05c951f362eb0c56bb8 Mon Sep 17 00:00:00 2001 From: pizzamx Date: Mon, 13 Jan 2025 23:18:25 +0800 Subject: [PATCH] Add script.service.latestrating plugin --- script.service.latestrating/LICENSE.txt | 8 + script.service.latestrating/README.md | 26 + script.service.latestrating/addon.xml | 21 + script.service.latestrating/default.py | 63 +++ .../resource.language.en_gb/strings.po | 7 + .../resources/lib/logger.py | 29 ++ .../resources/lib/rate_limiter.py | 33 ++ .../resources/lib/rating_updater.py | 456 ++++++++++++++++++ .../resources/settings.xml | 14 + script.service.latestrating/service.py | 55 +++ 10 files changed, 712 insertions(+) create mode 100644 script.service.latestrating/LICENSE.txt create mode 100644 script.service.latestrating/README.md create mode 100644 script.service.latestrating/addon.xml create mode 100644 script.service.latestrating/default.py create mode 100644 script.service.latestrating/resources/language/resource.language.en_gb/strings.po create mode 100644 script.service.latestrating/resources/lib/logger.py create mode 100644 script.service.latestrating/resources/lib/rate_limiter.py create mode 100644 script.service.latestrating/resources/lib/rating_updater.py create mode 100644 script.service.latestrating/resources/settings.xml create mode 100644 script.service.latestrating/service.py diff --git a/script.service.latestrating/LICENSE.txt b/script.service.latestrating/LICENSE.txt new file mode 100644 index 000000000..f1de2dc40 --- /dev/null +++ b/script.service.latestrating/LICENSE.txt @@ -0,0 +1,8 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +This program is derived in part from the Kodi TV Show Scraper +(https://github.com/xbmc/metadata.tvshows.themoviedb.org.python) +which is licensed under GPL-3.0. + +For the full license text, see https://www.gnu.org/licenses/gpl-3.0.txt \ No newline at end of file diff --git a/script.service.latestrating/README.md b/script.service.latestrating/README.md new file mode 100644 index 000000000..068338069 --- /dev/null +++ b/script.service.latestrating/README.md @@ -0,0 +1,26 @@ +# Latest Rating Service for Kodi + +A Kodi service add-on that automatically updates movie and TV show ratings in your library. + +## Features +- Automatically updates ratings on a configurable schedule +- Supports IMDb and Trakt as rating sources +- Weighted average calculation when multiple sources are enabled +- Configurable date range filters to limit which movies and TV shows get updated +- Built-in log viewer through "Run" function of the add-on (for now you can only see logs in current session) + +## Installation +1. Download the zip file +2. In Kodi, go to Settings > Add-ons > Install from zip file +3. Select the downloaded zip file + +## Configuration +1. Enable desired rating sources (IMDb and/or Trakt) +2. Set update interval and content filters +3. The service will start automatically + +## Credits +Rating fetch functionality and Trakt API key from [Kodi's official TV Show scraper](https://github.com/xbmc/metadata.tvshows.themoviedb.org.python). + +## License +GPL-3.0 \ No newline at end of file diff --git a/script.service.latestrating/addon.xml b/script.service.latestrating/addon.xml new file mode 100644 index 000000000..e6c5433bd --- /dev/null +++ b/script.service.latestrating/addon.xml @@ -0,0 +1,21 @@ + + + + + + + + + executable + + + service + + + Latest Rating Service + Automatically updates ratings for movies and TV shows in your library. Enalbe in settings first after install. + all + GPL-3.0 + https://github.com/pizzamx/script.service.latestrating + + \ No newline at end of file diff --git a/script.service.latestrating/default.py b/script.service.latestrating/default.py new file mode 100644 index 000000000..911d834e0 --- /dev/null +++ b/script.service.latestrating/default.py @@ -0,0 +1,63 @@ +import xbmcaddon +import xbmcgui +import xbmc +import xbmcvfs +import os +from datetime import datetime + +ADDON = xbmcaddon.Addon() +ADDON_NAME = ADDON.getAddonInfo('name') + +def get_kodi_log_path(): + if xbmc.getCondVisibility('system.platform.windows'): + return xbmcvfs.translatePath('special://home/kodi.log') + else: + return xbmcvfs.translatePath('special://home/temp/kodi.log') + +def parse_log_file(): + log_path = get_kodi_log_path() + if not os.path.exists(log_path): + return ["Log file not found"] + + addon_logs = [] + try: + with open(log_path, 'r', encoding='utf-8', errors='replace') as f: + for line in f: + if f'[{ADDON_NAME}]' in line and '[UPDATE_RESULT]' in line: + try: + # Format: [Latest Rating Service] [2024-01-06 14:30:01] [UPDATE_RESULT] Movie: The Matrix - Rating: 7.9 → 8.5 + # Split by ']' and remove leading '[' + parts = [p.strip(' []') for p in line.split(']')] + if len(parts) >= 3: + timestamp = parts[1] # The timestamp part + # Find the message after [UPDATE_RESULT] + for i, part in enumerate(parts): + if 'UPDATE_RESULT' in part: + message = '] '.join(parts[i+1:]).strip() + formatted_line = f"{timestamp} - {message}" + addon_logs.append(formatted_line) + break + except Exception as e: + print(f"Error parsing line: {line}, Error: {str(e)}") + continue + except Exception as e: + return [f"Error reading log file: {str(e)}"] + + return list(reversed(addon_logs[-1000:])) # Return last 1000 lines in reverse chronological order + +def show_log_viewer(): + logs = parse_log_file() + if not logs: + xbmcgui.Dialog().ok(ADDON_NAME, "No rating updates found") + return + + # Create a list dialog + dialog = xbmcgui.Dialog() + while True: + # Show the logs in a select dialog + idx = dialog.select("Rating Update History", logs) + if idx == -1: # User pressed back/cancel + break + +if __name__ == '__main__': + show_log_viewer() \ No newline at end of file diff --git a/script.service.latestrating/resources/language/resource.language.en_gb/strings.po b/script.service.latestrating/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 000000000..d06159a5f --- /dev/null +++ b/script.service.latestrating/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,7 @@ +# Kodi Media Center language file +msgid "" +msgstr "" + +msgctxt "#32001" +msgid "View Update Log" +msgstr "View Update Log" \ No newline at end of file diff --git a/script.service.latestrating/resources/lib/logger.py b/script.service.latestrating/resources/lib/logger.py new file mode 100644 index 000000000..c934f5404 --- /dev/null +++ b/script.service.latestrating/resources/lib/logger.py @@ -0,0 +1,29 @@ +import xbmc +import xbmcaddon +from datetime import datetime + +class Logger: + def __init__(self): + self.addon = xbmcaddon.Addon() + self.addon_name = self.addon.getAddonInfo('name') + + def log(self, message, level=xbmc.LOGINFO): + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + log_message = f'[{self.addon_name}] [{timestamp}] {message}' + xbmc.log(log_message, level) + + def update_result(self, message): + """Log an update result with a special prefix""" + self.log(f'[UPDATE_RESULT] {message}', xbmc.LOGINFO) + + def error(self, message): + self.log(message, xbmc.LOGERROR) + + def info(self, message): + self.log(message, xbmc.LOGINFO) + + def debug(self, message): + self.log(message, xbmc.LOGDEBUG) + + def warning(self, message): + self.log(message, xbmc.LOGWARNING) \ No newline at end of file diff --git a/script.service.latestrating/resources/lib/rate_limiter.py b/script.service.latestrating/resources/lib/rate_limiter.py new file mode 100644 index 000000000..433575c2b --- /dev/null +++ b/script.service.latestrating/resources/lib/rate_limiter.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta +import xbmc +from collections import deque + +class RateLimiter: + def __init__(self, calls_per_second=1): + self.calls_per_second = calls_per_second + self.calls = {} # Dictionary to track calls for different sources + + def wait_for_token(self, source): + """Wait until we can make another API call for the given source""" + if source not in self.calls: + self.calls[source] = deque(maxlen=self.calls_per_second) + self.calls[source].append(datetime.now()) + return + + # Remove old timestamps + now = datetime.now() + while self.calls[source] and (now - self.calls[source][0]) > timedelta(seconds=1): + self.calls[source].popleft() + + # If we've made too many calls in the last second, wait + if len(self.calls[source]) >= self.calls_per_second: + oldest_call = self.calls[source][0] + wait_time = 1 - (now - oldest_call).total_seconds() + if wait_time > 0: + xbmc.sleep(int(wait_time * 1000)) + + def add_call(self, source): + """Record that we made an API call""" + if source not in self.calls: + self.calls[source] = deque(maxlen=self.calls_per_second) + self.calls[source].append(datetime.now()) \ No newline at end of file diff --git a/script.service.latestrating/resources/lib/rating_updater.py b/script.service.latestrating/resources/lib/rating_updater.py new file mode 100644 index 000000000..7b25d466c --- /dev/null +++ b/script.service.latestrating/resources/lib/rating_updater.py @@ -0,0 +1,456 @@ +# -------------------------------------------------------------------------------- +# Latest Rating Service for Kodi +# Copyright (C) 2024 +# -------------------------------------------------------------------------------- +# This program is derived in part from the Kodi TV Show Scraper +# (https://github.com/xbmc/metadata.tvshows.themoviedb.org.python) +# which is licensed under GPL-3.0. +# -------------------------------------------------------------------------------- +import xbmc +import xbmcaddon +import json +import requests +import re +from datetime import datetime, timedelta +from resources.lib.logger import Logger +from resources.lib.rate_limiter import RateLimiter + +# Rating fetch functionality derived from Kodi's official TV Show scraper: +# https://github.com/xbmc/metadata.tvshows.themoviedb.org.python +# Specifically: +# - IMDb rating logic from libs/imdbratings.py +# - Trakt rating logic from libs/traktratings.py + +IMDB_RATINGS_URL = 'https://www.imdb.com/title/{}/' +IMDB_JSON_REGEX = re.compile(r'') +IMDB_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', + 'Accept': 'application/json' +} + +# Rate limits (calls per second) +IMDB_RATE_LIMIT = 2 # 2 request per second +TRAKT_RATE_LIMIT = 2 # 2 request per second + +TRAKT_HEADERS = { + # Not sure if I can keep this info intact + 'User-Agent': 'Kodi TV Show scraper by Team Kodi; contact pkscout@kodi.tv', + 'Accept': 'application/json', + # The Trakt API key here came from offical scraper's code mentioned above too + 'trakt-api-key': '90901c6be3b2de5a4fa0edf9ab5c75e9a5a0fef2b4ee7373d8b63dcf61f95697', + 'trakt-api-version': '2', + 'Content-Type': 'application/json' +} + +TRAKT_SHOW_URL = 'https://api.trakt.tv/shows/{}' +TRAKT_MOVIE_URL = 'https://api.trakt.tv/movies/{}' +TRAKT_EP_URL = TRAKT_SHOW_URL + '/seasons/{}/episodes/{}/ratings' + +class RatingUpdater: + def __init__(self): + self.addon = xbmcaddon.Addon() + self.logger = Logger() + self.rating_sources = self._get_enabled_sources() + self.rate_limiters = { + 'imdb': RateLimiter(IMDB_RATE_LIMIT), + 'trakt': RateLimiter(TRAKT_RATE_LIMIT) + } + + def _get_enabled_sources(self): + sources = [] + if self.addon.getSettingBool('use_imdb'): + sources.append('imdb') + if self.addon.getSettingBool('use_trakt'): + sources.append('trakt') + + if not sources: + # Ensure at least one source is enabled + self.addon.setSettingBool('use_imdb', True) + sources.append('imdb') + self.logger.warning("No rating source selected, defaulting to IMDb") + + return sources + + def update_library_ratings(self): + if self.addon.getSettingBool('update_movies'): + self.update_movies() + + #TV Shows + if self.addon.getSettingBool('update_tvshows'): + self.update_tvshow_episodes() + + def update_movies(self): + self.logger.info("Starting movie ratings update") + movies = self._get_movies() + for movie in movies: + try: + old_rating = round(float(movie.get('rating', 0)), 1) + new_rating = self._fetch_rating(movie['imdbnumber'], is_movie=True) + if new_rating and new_rating != old_rating: + self._update_movie_rating(movie['movieid'], new_rating) + self.logger.update_result(f"Movie: {movie['title']} - Rating: {old_rating} → {new_rating}") + except Exception as e: + self.logger.error(f"Error updating {movie['title']}: {str(e)}") + + def update_tvshow_episodes(self): + """Update ratings for TV show episodes""" + self.logger.info("Starting TV show episode ratings update") + episodes = self._get_tvshow_episodes() + for episode in episodes: + try: + old_rating = round(float(episode.get('rating', 0)), 1) + show_title = episode.get('showtitle', 'Unknown Show') + season = episode.get('season') + episode_num = episode.get('episode') + + self.logger.info(f"Processing {show_title} S{season:02d}E{episode_num:02d}") + + new_rating = self._fetch_rating( + episode, # Pass entire episode object + is_movie=False, + season=season, + episode=episode_num + ) + + if new_rating and new_rating != old_rating: + self._update_episode_rating(episode['episodeid'], new_rating) + self.logger.update_result(f"TV: {show_title} S{season:02d}E{episode_num:02d} - Rating: {old_rating} → {new_rating}") + except Exception as e: + self.logger.error(f"Error updating {show_title} S{season:02d}E{episode_num:02d}: {str(e)}") + + def _get_movies(self): + try: + years_back = self.addon.getSettingInt('movie_years_back') + current_year = datetime.now().year + cutoff_year = current_year - years_back + command = { + 'jsonrpc': '2.0', + 'method': 'VideoLibrary.GetMovies', + 'params': { + 'properties': [ + 'uniqueid', + 'rating', + 'year', + 'title' + ] + }, + 'id': 1 + } + + command_str = json.dumps(command) + self.logger.debug(f"Sending detailed GetMovies command: {command_str}") + result = xbmc.executeJSONRPC(command_str) + self.logger.debug(f"Detailed GetMovies raw response: {result}") + + if not result: + self.logger.error("Empty response from JSON-RPC GetMovies call") + return [] + + response = json.loads(result) + movies = response.get('result', {}).get('movies', []) + self.logger.debug(f"Found {len(movies)} movies, cutoff year: {cutoff_year}") + + # Filter movies by year and extract IMDb ID + recent_movies = [] + for movie in movies: + if movie.get('year', 0) >= cutoff_year: + # uniqueid contains IMDb ID in format {'imdb': 'tt1234567', ...} + unique_ids = movie.get('uniqueid') + if not isinstance(unique_ids, dict): + continue + + imdb_id = unique_ids.get('imdb', '') + if not imdb_id: + continue + + movie['imdbnumber'] = imdb_id + recent_movies.append(movie) + + self.logger.debug(f"After year filter: {len(recent_movies)} movies") + return recent_movies + except Exception as e: + self.logger.error(f"Error in _get_movies: {str(e)}") + return [] + + def _get_tvshow_episodes(self): + """Get TV show episodes that need rating updates""" + months_back = self.addon.getSettingInt('tvshow_months_back') + cutoff_date = datetime.now() - timedelta(days=months_back * 30) + + # First get all TV shows to get their IMDb IDs + shows_command = { + 'jsonrpc': '2.0', + 'method': 'VideoLibrary.GetTVShows', + 'params': { + 'properties': [ + 'uniqueid' + ] + }, + 'id': 1 + } + + try: + command_str = json.dumps(shows_command) + self.logger.debug(f"Sending GetTVShows command: {command_str}") + result = xbmc.executeJSONRPC(command_str) + + if not result: + self.logger.error("Empty response from JSON-RPC GetTVShows call") + return [] + + response = json.loads(result) + shows = response.get('result', {}).get('tvshows', []) + + # Create a mapping of tvshowid to show IMDb ID + show_imdb_map = {} + for show in shows: + unique_ids = show.get('uniqueid') + if isinstance(unique_ids, dict): + imdb_id = unique_ids.get('imdb', '') + if imdb_id: + show_imdb_map[show['tvshowid']] = imdb_id + + # Now get all episodes + episodes_command = { + 'jsonrpc': '2.0', + 'method': 'VideoLibrary.GetEpisodes', + 'params': { + 'properties': [ + 'season', + 'episode', + 'firstaired', + 'rating', + 'showtitle', + 'tvshowid', + 'uniqueid' + ] + }, + 'id': 1 + } + + command_str = json.dumps(episodes_command) + self.logger.debug(f"Sending GetEpisodes command: {command_str}") + result = xbmc.executeJSONRPC(command_str) + + if not result: + self.logger.error("Empty response from JSON-RPC GetEpisodes call") + return [] + + response = json.loads(result) + episodes = response.get('result', {}).get('episodes', []) + self.logger.debug(f"Found {len(episodes)} total episodes") + + # Filter episodes by air date and add both IMDb IDs + recent_episodes = [] + for episode in episodes: + if self._is_recent_episode(episode, cutoff_date): + # Get show's IMDb ID from our mapping + show_imdb = show_imdb_map.get(episode['tvshowid']) + if not show_imdb: + continue + + # Get episode's IMDb ID + unique_ids = episode.get('uniqueid') + if not isinstance(unique_ids, dict): + continue + + episode_imdb = unique_ids.get('imdb', '') + if not episode_imdb: + continue + + # Store both IDs + episode['imdbnumber'] = episode_imdb # For IMDb API + episode['show_imdbnumber'] = show_imdb # For Trakt API + recent_episodes.append(episode) + + self.logger.debug(f"Found {len(recent_episodes)} recent episodes with IMDb IDs") + return recent_episodes + + except Exception as e: + self.logger.error(f"Error in _get_tvshow_episodes: {str(e)}") + return [] + + def _is_recent_episode(self, episode, cutoff_date): + """Check if episode aired after the cutoff date""" + date_str = episode.get('firstaired', '') + if not date_str: + return False + + try: + # Air date format: YYYY-MM-DD + air_date = datetime.strptime(date_str, '%Y-%m-%d') + return air_date >= cutoff_date + except ValueError: + self.logger.error(f"Invalid air date format: {date_str}") + return False + + def _fetch_rating(self, imdb_id, is_movie=True, season=None, episode=None): + """ + Fetch rating from external API + Args: + imdb_id: The IMDB ID of the movie or show + is_movie: True if fetching movie rating, False for TV show + season: Season number for TV shows (optional) + episode: Episode number for TV shows (optional) + Returns: + float: Weighted average rating or None if no ratings found + """ + total_rating_sum = 0 + total_vote_count = 0 + + for source in self.rating_sources: + try: + rating = self._fetch_rating_from_source( + source, imdb_id, is_movie, season, episode + ) + if rating is not None and isinstance(rating, tuple): + self.logger.info(f"New rating from {source}: {rating}") + rating_value, vote_count = rating + if rating_value > 0 and vote_count > 0: # Only include valid ratings + total_rating_sum += rating_value * vote_count + total_vote_count += vote_count + except Exception as e: + self.logger.error(f"Error fetching {source} rating: {str(e)}") + + if total_vote_count == 0: + return None + + # Calculate weighted average and round to 1 decimal place + weighted_average = round(total_rating_sum / total_vote_count, 1) + self.logger.info(f"New weighted average rating: {weighted_average}") + return weighted_average + + def _fetch_rating_from_source(self, source, imdb_id, is_movie, season=None, episode=None): + """ + Fetch rating from a specific source + Args: + source: 'imdb' or 'trakt' + imdb_id: For movies: IMDb ID + For TV: Dictionary containing both episode and show IMDb IDs + is_movie: True if fetching movie rating, False for TV show + season: Season number for TV shows (optional) + episode: Episode number for TV shows (optional) + Returns: tuple(rating, vote_count) or None + rating: average rating from the source + vote_count: number of votes for this rating + """ + if source == 'imdb': + # For IMDb, use episode ID for TV shows + episode_id = imdb_id if is_movie else imdb_id['imdbnumber'] + return self._fetch_imdb_rating(episode_id, is_movie, season, episode) + elif source == 'trakt': + # For Trakt, use show ID for TV shows + show_id = imdb_id if is_movie else imdb_id['show_imdbnumber'] + return self._fetch_trakt_rating(show_id, is_movie, season, episode) + + return -1, -1 + + def _fetch_imdb_rating(self, imdb_id, is_movie, season=None, episode=None): + """Get IMDb rating and vote count""" + try: + # Wait for rate limit + self.rate_limiters['imdb'].wait_for_token('imdb') + + # Make request to IMDb + response = requests.get(IMDB_RATINGS_URL.format(imdb_id), headers=IMDB_HEADERS) + response.raise_for_status() + + # Find and parse JSON data + match = re.search(IMDB_JSON_REGEX, response.text) + if not match: + self.logger.error(f"No IMDb rating data found for {imdb_id}") + return -1, -1 + + imdb_json = json.loads(match.group(1)) + imdb_ratings = imdb_json.get("aggregateRating", {}) + + rating = imdb_ratings.get("ratingValue") + votes = imdb_ratings.get("ratingCount") + + if rating is not None and votes is not None: + self.rate_limiters['imdb'].add_call('imdb') + return float(rating), int(votes) + + return -1, -1 + + except (requests.RequestException, json.JSONDecodeError, ValueError) as e: + self.logger.error(f"Error fetching IMDb rating for {imdb_id}: {str(e)}") + return -1, -1 + + def _fetch_trakt_rating(self, imdb_id, is_movie, season=None, episode=None): + # Hardcode a default client ID or return None if Trakt is disabled + try: + # Wait for rate limit + self.rate_limiters['trakt'].wait_for_token('trakt') + + # Determine URL based on content type and parameters + if is_movie: + url = TRAKT_MOVIE_URL.format(imdb_id) + params = {'extended': 'full'} + elif season and episode: + url = TRAKT_EP_URL.format(imdb_id, season, episode) + params = None + else: + url = TRAKT_SHOW_URL.format(imdb_id) + params = {'extended': 'full'} + + # Make request to Trakt + response = requests.get(url, headers=TRAKT_HEADERS, params=params) + response.raise_for_status() + + data = response.json() + rating = data.get('rating') + votes = data.get('votes') + + if rating is not None and votes is not None: + self.rate_limiters['trakt'].add_call('trakt') + return float(rating), int(votes) + + return -1, -1 + + except (requests.RequestException, json.JSONDecodeError, ValueError) as e: + self.logger.error(f"Error fetching Trakt rating for {imdb_id}: {str(e)}") + return -1, -1 + + def _update_movie_rating(self, movie_id, rating): + command = { + 'jsonrpc': '2.0', + 'method': 'VideoLibrary.SetMovieDetails', + 'params': {'movieid': movie_id, 'rating': rating}, + 'id': 1 + } + try: + command_str = json.dumps(command) + self.logger.debug(f"Sending SetMovieDetails command: {command_str}") + result = xbmc.executeJSONRPC(command_str) + self.logger.debug(f"SetMovieDetails raw response: {result}") + + if not result: + self.logger.error("Empty response from JSON-RPC SetMovieDetails call") + else: + response = json.loads(result) + self.logger.debug(f"SetMovieDetails parsed response: {response}") + except Exception as e: + self.logger.error(f"Error in _update_movie_rating: {str(e)}") + + def _update_episode_rating(self, episode_id, rating): + command = { + 'jsonrpc': '2.0', + 'method': 'VideoLibrary.SetEpisodeDetails', + 'params': {'episodeid': episode_id, 'rating': rating}, + 'id': 1 + } + try: + command_str = json.dumps(command) + self.logger.debug(f"Sending SetEpisodeDetails command: {command_str}") + result = xbmc.executeJSONRPC(command_str) + self.logger.debug(f"SetEpisodeDetails raw response: {result}") + + if not result: + self.logger.error("Empty response from JSON-RPC SetEpisodeDetails call") + else: + response = json.loads(result) + self.logger.debug(f"SetEpisodeDetails parsed response: {response}") + except Exception as e: + self.logger.error(f"Error in _update_episode_rating: {str(e)}") + diff --git a/script.service.latestrating/resources/settings.xml b/script.service.latestrating/resources/settings.xml new file mode 100644 index 000000000..3fc8c46ee --- /dev/null +++ b/script.service.latestrating/resources/settings.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/script.service.latestrating/service.py b/script.service.latestrating/service.py new file mode 100644 index 000000000..1c551cc38 --- /dev/null +++ b/script.service.latestrating/service.py @@ -0,0 +1,55 @@ +import xbmc +import xbmcaddon +from datetime import datetime, timedelta +from resources.lib.rating_updater import RatingUpdater +from resources.lib.logger import Logger + +class RatingUpdaterService(xbmc.Monitor): + def __init__(self): + super(RatingUpdaterService, self).__init__() + self.addon = xbmcaddon.Addon() + self.logger = Logger() + self.updater = RatingUpdater() + + def is_first_run(self): + """Check if this is the first time the script is running""" + last_completion = self.addon.getSetting('last_completion') + return last_completion == '' + + def should_run_update(self): + if self.is_first_run(): + return True + + try: + last_completion = self.addon.getSetting('last_completion') + last_completion_date = datetime.fromisoformat(last_completion) + interval_days = self.addon.getSettingInt('update_interval') + next_run = last_completion_date + timedelta(days=interval_days) + + return datetime.now() >= next_run + except ValueError: + return True + + def save_completion_time(self): + completion_time = datetime.now().isoformat() + self.addon.setSetting('last_completion', completion_time) + + def run(self): + self.logger.info("Get Latest Rating Service Started") + + if self.is_first_run(): + self.logger.info("First time running - performing initial update") + + if self.should_run_update(): + self.logger.info("Starting scheduled update") + self.updater.update_library_ratings() + self.save_completion_time() + self.logger.info("Update completed successfully") + else: + self.logger.debug("Skipping update - next update not due yet") + + self.logger.info("Rating Updater Service Stopped") + +if __name__ == '__main__': + service = RatingUpdaterService() + service.run() \ No newline at end of file