diff --git a/src/controllers/webhooks.py b/src/controllers/webhooks.py index 16db67e1..a46eca21 100644 --- a/src/controllers/webhooks.py +++ b/src/controllers/webhooks.py @@ -7,6 +7,7 @@ from program.media.item import MediaItem from requests import RequestException from utils.logger import logger +from program.db.db_functions import _ensure_item_exists_in_db from .models.overseerr import OverseerrWebhook @@ -19,53 +20,57 @@ @router.post("/overseerr") async def overseerr(request: Request) -> Dict[str, Any]: """Webhook for Overseerr""" - response = await request.json() - - if response.get("subject") == "Test Notification": - logger.log("API", "Received test notification, Overseerr configured properly") - return {"success": True} - try: + response = await request.json() + if response.get("subject") == "Test Notification": + logger.log("API", "Received test notification, Overseerr configured properly") + return {"status": "success"} req = OverseerrWebhook.model_validate(response) - except pydantic.ValidationError: - return {"success": False, "message": "Invalid request"} - except Exception as e: + except (Exception, pydantic.ValidationError) as e: logger.error(f"Failed to process request: {e}") - return {"success": False, "message": "Failed to process request"} - + return {"status": "error", "message": str(e)} - imdb_id = req.media.imdbId + imdb_id = get_imdbid_from_overseerr(req) if not imdb_id: - try: - _type = req.media.media_type - if _type == "tv": - _type = "show" - imdb_id = get_imdbid_from_tmdb(req.media.tmdbId, type=_type) - if not imdb_id or not imdb_id.startswith("tt"): - imdb_id = get_imdbid_from_tvdb(req.media.tvdbId, type=_type) - if not imdb_id or not imdb_id.startswith("tt"): - logger.error(f"Failed to get imdb_id from Overseerr: {req.media.tmdbId}") - return {"success": False, "message": "Failed to get imdb_id from Overseerr", "title": req.subject} - except RequestException: - logger.error(f"Failed to get imdb_id from Overseerr: {req.media.tmdbId}") - return {"success": False, "message": "Failed to get imdb_id from Overseerr", "title": req.subject} + logger.error(f"Failed to get imdb_id from Overseerr: {req.media.tmdbId}") + return {"status": "error", "message": "Failed to get imdb_id from Overseerr"} overseerr: Overseerr = request.app.program.all_services[Overseerr] if not overseerr.initialized: logger.error("Overseerr not initialized") - return {"success": False, "message": "Overseerr not initialized", "title": req.subject} + return {"status": "error", "message": "Overseerr not initialized"} + + try: + new_item = MediaItem({"imdb_id": imdb_id, "requested_by": "overseerr", "requested_id": req.request.request_id}) + except Exception as e: + logger.error(f"Failed to create item for {imdb_id}: {e}") + return {"status": "error", "message": str(e)} - if imdb_id in overseerr.recurring_items: - logger.log("API", "Request already in queue", {"imdb_id": imdb_id}) - return {"success": True, "message": "Request already in queue", "title": req.subject} + if _ensure_item_exists_in_db(new_item) or imdb_id in overseerr.recurring_items: + logger.log("API", "Request already in queue or already exists in the database") + return {"status": "success"} else: overseerr.recurring_items.add(imdb_id) try: - new_item = MediaItem({"imdb_id": imdb_id, "requested_by": "overseerr"}) request.app.program.em.add_item(new_item) - except Exception: - logger.error(f"Failed to create item from imdb_id: {imdb_id}") - return {"success": False, "message": "Failed to create item from imdb_id", "title": req.subject} + except Exception as e: + logger.error(f"Failed to add item for {imdb_id}: {e}") + + return {"status": "success"} - return {"success": True, "message": f"Added {imdb_id} to queue"} + +def get_imdbid_from_overseerr(req: OverseerrWebhook) -> str: + """Get the imdb_id from the Overseerr webhook""" + imdb_id = req.media.imdbId + if not imdb_id: + try: + _type = req.media.media_type + if _type == "tv": + _type = "show" + imdb_id = get_imdbid_from_tmdb(req.media.tmdbId, type=_type) + if not imdb_id or not imdb_id.startswith("tt"): + imdb_id = get_imdbid_from_tvdb(req.media.tvdbId, type=_type) + except RequestException: + pass + return imdb_id \ No newline at end of file diff --git a/src/program/content/listrr.py b/src/program/content/listrr.py index a9b152c9..39da0eeb 100644 --- a/src/program/content/listrr.py +++ b/src/program/content/listrr.py @@ -1,5 +1,4 @@ """Listrr content module""" - from typing import Generator from program.indexers.trakt import get_imdbid_from_tmdb @@ -8,6 +7,7 @@ from requests.exceptions import HTTPError from utils.logger import logger from utils.request import get, ping +from program.db.db_functions import _filter_existing_items class Listrr: @@ -21,14 +21,12 @@ def __init__(self): self.initialized = self.validate() if not self.initialized: return - self.not_found_ids = [] - self.recurring_items = set() + self.recurring_items: set[str] = set() logger.success("Listrr initialized!") def validate(self) -> bool: """Validate Listrr settings.""" if not self.settings.enabled: - logger.debug("Listrr is set to disabled.") return False if self.settings.api_key == "" or len(self.settings.api_key) != 64: logger.error("Listrr api key is not set or invalid.") @@ -60,15 +58,22 @@ def validate(self) -> bool: def run(self) -> Generator[MediaItem, None, None]: """Fetch new media from `Listrr`""" - items_to_yield = [] - self.not_found_ids.clear() - movie_items = self._get_items_from_Listrr("Movies", self.settings.movie_lists) - show_items = self._get_items_from_Listrr("Shows", self.settings.show_lists) - for imdb_id in movie_items + show_items: - if imdb_id not in self.recurring_items: - self.recurring_items.add(imdb_id) - items_to_yield.append(MediaItem({"imdb_id": imdb_id, "requested_by": self.key})) - yield items_to_yield + try: + movie_items = self._get_items_from_Listrr("Movies", self.settings.movie_lists) + show_items = self._get_items_from_Listrr("Shows", self.settings.show_lists) + except Exception as e: + logger.error(f"Failed to fetch items from Listrr: {e}") + return + + listrr_items = movie_items + show_items + non_existing_items = _filter_existing_items(listrr_items) + new_non_recurring_items = [item for item in non_existing_items if item.imdb_id not in self.recurring_items] + self.recurring_items.update([item.imdb_id for item in new_non_recurring_items]) + + if new_non_recurring_items: + logger.info(f"Fetched {len(new_non_recurring_items)} new items from Listrr") + + yield new_non_recurring_items def _get_items_from_Listrr(self, content_type, content_lists) -> list[MediaItem]: # noqa: C901, PLR0912 """Fetch unique IMDb IDs from Listrr for a given type and list of content.""" @@ -95,8 +100,6 @@ def _get_items_from_Listrr(self, content_type, content_lists) -> list[MediaItem] imdb_id = get_imdbid_from_tmdb(item["tmDbId"]) if imdb_id: unique_ids.add(imdb_id) - else: - self.not_found_ids.append(item["id"]) except HTTPError as e: if e.response.status_code in [400, 404, 429, 500]: break diff --git a/src/program/content/mdblist.py b/src/program/content/mdblist.py index 43fc19cb..2695d042 100644 --- a/src/program/content/mdblist.py +++ b/src/program/content/mdblist.py @@ -7,6 +7,7 @@ from utils.logger import logger from utils.ratelimiter import RateLimiter, RateLimitExceeded from utils.request import get, ping +from program.db.db_functions import _filter_existing_items class Mdblist: @@ -18,14 +19,12 @@ def __init__(self): self.initialized = self.validate() if not self.initialized: return - self.recurring_items = set() self.requests_per_2_minutes = self._calculate_request_time() self.rate_limiter = RateLimiter(self.requests_per_2_minutes, 120, True) logger.success("mdblist initialized") def validate(self): if not self.settings.enabled: - logger.debug("Mdblist is set to disabled.") return False if self.settings.api_key == "" or len(self.settings.api_key) != 25: logger.error("Mdblist api key is not set.") @@ -55,15 +54,17 @@ def run(self) -> Generator[MediaItem, None, None]: items = list_items_by_url(list, self.settings.api_key) for item in items: # Check if the item is already completed in the media container - if item.imdb_id and item.imdb_id not in self.recurring_items: - self.recurring_items.add(item.imdb_id) + if item.imdb_id: items_to_yield.append(MediaItem( {"imdb_id": item.imdb_id, "requested_by": self.key} )) - except RateLimitExceeded: pass - yield items_to_yield + + non_existing_items = _filter_existing_items(items_to_yield) + if len(non_existing_items) > 0: + logger.info(f"Found {len(non_existing_items)} new items to fetch") + yield non_existing_items def _calculate_request_time(self): limits = my_limits(self.settings.api_key).limits diff --git a/src/program/content/overseerr.py b/src/program/content/overseerr.py index 3d017fb5..67d46549 100644 --- a/src/program/content/overseerr.py +++ b/src/program/content/overseerr.py @@ -9,6 +9,7 @@ from urllib3.exceptions import MaxRetryError, NewConnectionError from utils.logger import logger from utils.request import delete, get, ping, post +from program.db.db_functions import _filter_existing_items class Overseerr: @@ -22,12 +23,11 @@ def __init__(self): self.run_once = False if not self.initialized: return - self.recurring_items = set() + self.recurring_items: set[str] = set() logger.success("Overseerr initialized!") def validate(self) -> bool: if not self.settings.enabled: - logger.debug("Overseerr is set to disabled.") return False if self.settings.api_key == "" or len(self.settings.api_key) != 68: logger.error("Overseerr api key is not set.") @@ -53,57 +53,56 @@ def validate(self) -> bool: def run(self): """Fetch new media from `Overseerr`""" - if self.settings.use_webhook and not self.run_once: - logger.info("Webhook is enabled, but running Overseerr once before switching to webhook.") - self.run_once = True - elif self.run_once: + if self.settings.use_webhook and self.run_once: return + overseerr_items: list[MediaItem] = self.get_media_requests() + non_existing_items = _filter_existing_items(overseerr_items) + new_non_recurring_items = [item for item in non_existing_items if item.imdb_id not in self.recurring_items] + self.recurring_items.update([item.imdb_id for item in new_non_recurring_items]) + + if self.settings.use_webhook: + logger.debug("Webhook is enabled. Running Overseerr once before switching to webhook.") + self.run_once = True + + if new_non_recurring_items: + logger.info(f"Fetched {len(new_non_recurring_items)} new items from Overseerr") + + yield new_non_recurring_items + + def get_media_requests(self) -> list[MediaItem]: + """Get media requests from `Overseerr`""" try: - response = get( - self.settings.url + f"/api/v1/request?take={10000}&filter=approved&sort=added", - additional_headers=self.headers, - ) + response = get(self.settings.url + f"/api/v1/request?take={10000}&filter=approved&sort=added", additional_headers=self.headers) + if not response.is_ok: + logger.error(f"Failed to fetch requests from overseerr: {response.data}") + return [] except (ConnectionError, RetryError, MaxRetryError) as e: logger.error(f"Failed to fetch requests from overseerr: {str(e)}") - return + return [] except Exception as e: logger.error(f"Unexpected error during fetching requests: {str(e)}") - return + return [] - if not response.is_ok or not hasattr(response.data, "pageInfo") or getattr(response.data.pageInfo, "results", 0) == 0: - return + if not hasattr(response.data, "pageInfo") or getattr(response.data.pageInfo, "results", 0) == 0: + return [] # Lets look at approved items only that are only in the pending state pending_items = [ - item - for item in response.data.results + item for item in response.data.results if item.status == 2 and item.media.status == 3 ] - items_to_yield = [] - for item in pending_items: - try: - mediaId: int = int(item.media.id) - if not item.media.imdbId: - imdb_id = self.get_imdb_id(item.media) - else: - imdb_id = item.media.imdbId - if not imdb_id or imdb_id in self.recurring_items: - continue - self.recurring_items.add(imdb_id) - media_item = MediaItem({"imdb_id": imdb_id, "requested_by": self.key, "overseerr_id": mediaId, "requested_id": item.id}) - if media_item: - items_to_yield.append(media_item) - else: - logger.log("NOT_FOUND", f"Failed to create media item for {imdb_id}") - except Exception as e: - logger.error(f"Error processing item {item}: {str(e)}") - continue - if self.settings.use_webhook: - self.run_once = True + return [ + MediaItem({ + "imdb_id": self.get_imdb_id(item.media), + "requested_by": self.key, + "overseerr_id": item.media.id, + "requested_id": item.id + }) + for item in pending_items + ] - yield items_to_yield def get_imdb_id(self, data) -> str: """Get imdbId for item from overseerr""" diff --git a/src/program/content/plex_watchlist.py b/src/program/content/plex_watchlist.py index f75f22f3..e7dd4f9d 100644 --- a/src/program/content/plex_watchlist.py +++ b/src/program/content/plex_watchlist.py @@ -1,18 +1,14 @@ """Plex Watchlist Module""" -from types import SimpleNamespace -import xml.etree.ElementTree as ET - from typing import Generator, Union from program.media.item import Episode, MediaItem, Movie, Season, Show from program.settings.manager import settings_manager -from program.indexers.trakt import get_imdbid_from_tvdb from requests import HTTPError from utils.logger import logger from utils.request import get, ping from plexapi.myplex import MyPlexAccount from requests import Session -import xmltodict +from program.db.db_functions import _filter_existing_items class PlexWatchlist: @@ -28,12 +24,11 @@ def __init__(self): self.initialized = self.validate() if not self.initialized: return - self.recurring_items = set() + self.recurring_items: set[str] = set() # set of imdb ids logger.success("Plex Watchlist initialized!") def validate(self): if not self.settings.enabled: - logger.debug("Plex Watchlists is set to disabled.") return False if not self.token: logger.error("Plex token is not set!") @@ -63,95 +58,80 @@ def validate(self): return False return True - def run(self) -> Generator[Union[Movie, Show, Season, Episode], None, None]: + def run(self) -> Generator[MediaItem, None, None]: """Fetch new media from `Plex Watchlist` and RSS feed if enabled.""" - items_to_yield = [] try: - watchlist_items = set(self._get_items_from_watchlist()) - rss_items = set(self._get_items_from_rss()) if self.rss_enabled else set() + watchlist_items: list[str] = self._get_items_from_watchlist() + rss_items: list[str] = self._get_items_from_rss() if self.rss_enabled else [] except Exception as e: logger.error(f"Error fetching items: {e}") return - new_items = watchlist_items | rss_items - - for imdb_id in new_items: - if not imdb_id or imdb_id in self.recurring_items: - continue - self.recurring_items.add(imdb_id) - items_to_yield.append(MediaItem({"imdb_id": imdb_id, "requested_by": self.key})) + plex_items: set[str] = set(watchlist_items) | set(rss_items) + items_to_yield: list[MediaItem] = [MediaItem({"imdb_id": imdb_id, "requested_by": self.key}) for imdb_id in plex_items if imdb_id.startswith("tt")] + non_existing_items = _filter_existing_items(items_to_yield) + new_non_recurring_items = [item for item in non_existing_items if item.imdb_id not in self.recurring_items] + self.recurring_items.update([item.imdb_id for item in new_non_recurring_items]) - yield items_to_yield + if new_non_recurring_items: + logger.info(f"Found {len(new_non_recurring_items)} new items to fetch") + yield new_non_recurring_items - def _get_items_from_rss(self) -> Generator[MediaItem, None, None]: + def _get_items_from_rss(self) -> list[str]: """Fetch media from Plex RSS Feeds.""" + rss_items: list[str] = [] for rss_url in self.settings.rss: try: - response = self.session.get(rss_url, timeout=60) - if not response.ok: - logger.error(f"Failed to fetch Plex RSS feed from {rss_url}: HTTP {response.status_code}") - continue - - xmltodict_data = xmltodict.parse(response.content) - items = xmltodict_data.get("rss", {}).get("channel", {}).get("item", []) - for item in items: - guid_text = item.get("guid", {}).get("#text", "") - guid_id = guid_text.split("//")[-1] if guid_text else "" - if not guid_id or guid_id in self.recurring_items: - continue - if guid_id and guid_id.startswith("tt") and guid_id not in self.recurring_items: - yield guid_id - elif guid_id: - imdb_id: str = get_imdbid_from_tvdb(guid_id) - if not imdb_id or not imdb_id.startswith("tt") or imdb_id in self.recurring_items: - continue - yield imdb_id + response = self.session.get(rss_url + "?format=json", timeout=60) + for _item in response.json().get("items", []): + imdb_id = self._extract_imdb_ids(_item.get("guids", [])) + if imdb_id.startswith("tt"): + rss_items.append(imdb_id) else: - logger.log("NOT_FOUND", f"Failed to extract IMDb ID from {item['title']}") - continue + logger.log("NOT_FOUND", f"Failed to extract IMDb ID from {_item['title']}") except Exception as e: logger.error(f"An unexpected error occurred while fetching Plex RSS feed from {rss_url}: {e}") - continue + return rss_items - def _get_items_from_watchlist(self) -> Generator[MediaItem, None, None]: + def _get_items_from_watchlist(self) -> list[str]: """Fetch media from Plex watchlist""" - # filter_params = "includeFields=title,year,ratingkey&includeElements=Guid&sort=watchlistedAt:desc" - # url = f"https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token={self.token}&{filter_params}" - # response = get(url) items = self.account.watchlist() + watchlist_items: list[str] = [] for item in items: try: if hasattr(item, "guids") and item.guids: imdb_id: str = next((guid.id.split("//")[-1] for guid in item.guids if guid.id.startswith("imdb://")), "") - if not imdb_id or imdb_id in self.recurring_items: - continue - elif imdb_id.startswith("tt"): - yield imdb_id + if imdb_id.startswith("tt"): + watchlist_items.append(imdb_id) else: logger.log("NOT_FOUND", f"Unable to extract IMDb ID from {item.title} ({item.year}) with data id: {imdb_id}") else: logger.log("NOT_FOUND", f"{item.title} ({item.year}) is missing guids attribute from Plex") except Exception as e: logger.error(f"An unexpected error occurred while fetching Plex watchlist item {item.log_string}: {e}") + return watchlist_items - @staticmethod - def _ratingkey_to_imdbid(ratingKey: str) -> str: - """Convert Plex rating key to IMDb ID""" - token = settings_manager.settings.updaters.plex.token - filter_params = "includeGuids=1&includeFields=guid,title,year&includeElements=Guid" - url = f"https://metadata.provider.plex.tv/library/metadata/{ratingKey}?X-Plex-Token={token}&{filter_params}" - response = get(url) - if response.is_ok and hasattr(response.data, "MediaContainer"): - metadata = response.data.MediaContainer.Metadata[0] - return next((guid.id.split("//")[-1] for guid in metadata.Guid if "imdb://" in guid.id), None) - logger.debug(f"Failed to fetch IMDb ID for ratingKey: {ratingKey}") - return None - - def _extract_imdb_ids(self, guids): + def _extract_imdb_ids(self, guids: list) -> str | None: """Helper method to extract IMDb IDs from guids""" for guid in guids: if guid.startswith("imdb://"): imdb_id = guid.split("//")[-1] if imdb_id: - return imdb_id \ No newline at end of file + return imdb_id + return None + + +# api + +def _ratingkey_to_imdbid(ratingKey: str) -> str | None: + """Convert Plex rating key to IMDb ID""" + token = settings_manager.settings.updaters.plex.token + filter_params = "includeGuids=1&includeFields=guid,title,year&includeElements=Guid" + url = f"https://metadata.provider.plex.tv/library/metadata/{ratingKey}?X-Plex-Token={token}&{filter_params}" + response = get(url) + if response.is_ok and hasattr(response.data, "MediaContainer"): + metadata = response.data.MediaContainer.Metadata[0] + return next((guid.id.split("//")[-1] for guid in metadata.Guid if "imdb://" in guid.id), None) + logger.debug(f"Failed to fetch IMDb ID for ratingKey: {ratingKey}") + return None \ No newline at end of file diff --git a/src/program/content/trakt.py b/src/program/content/trakt.py index 2706b7cb..fad9f5ac 100644 --- a/src/program/content/trakt.py +++ b/src/program/content/trakt.py @@ -1,14 +1,16 @@ """Trakt content module""" import re import time +from typing import Union from urllib.parse import urlencode -from program.media.item import MediaItem, Movie, Show +from program.media.item import MediaItem from program.settings.manager import settings_manager from requests import RequestException from utils.logger import logger from utils.ratelimiter import RateLimiter from utils.request import get, post +from program.db.db_functions import _filter_existing_items class TraktContent: @@ -35,7 +37,6 @@ def validate(self) -> bool: """Validate Trakt settings.""" try: if not self.settings.enabled: - logger.debug("Trakt is set to disabled.") return False if not self.settings.api_key: logger.error("Trakt API key is not set.") @@ -115,7 +116,11 @@ def run(self): if total_new_items > 0: logger.log("TRAKT", f"Total new items fetched: {total_new_items}") - yield items_to_yield + non_existing_items = _filter_existing_items(items_to_yield) + if len(non_existing_items) > 0: + logger.info(f"Found {len(non_existing_items)} new items to fetch") + + yield non_existing_items def _get_watchlist(self, watchlist_users: list) -> list: """Get IMDb IDs from Trakt watchlist""" @@ -331,7 +336,7 @@ def match_full_url(url: str) -> tuple: return None, None -def _resolve_short_url(short_url) -> str or None: +def _resolve_short_url(short_url) -> Union[str, None]: """Resolve short URL to full URL""" try: response = get(short_url, additional_headers={"Content-Type": "application/json", "Accept": "text/html"}) diff --git a/src/program/db/db_functions.py b/src/program/db/db_functions.py index a8114cd0..4613d75e 100644 --- a/src/program/db/db_functions.py +++ b/src/program/db/db_functions.py @@ -239,6 +239,18 @@ def _ensure_item_exists_in_db(item: "MediaItem") -> bool: return session.execute(select(func.count(MediaItem._id)).where(MediaItem._id == item._id)).scalar_one() != 0 return bool(item and item._id) +def _filter_existing_items(items: list["MediaItem"]) -> list["MediaItem"]: + """Return a list of MediaItems that do not exist in the database.""" + from program.media.item import MediaItem + with db.Session() as session: + existing_items = set( + session.execute( + select(MediaItem.imdb_id) + .where(MediaItem.imdb_id.in_([item.imdb_id for item in items])) + ).scalars().all() + ) + return [item for item in items if item.imdb_id not in existing_items] + def _get_item_type_from_db(item: "MediaItem") -> str: from program.media.item import MediaItem with db.Session() as session: diff --git a/src/program/downloaders/alldebrid.py b/src/program/downloaders/alldebrid.py index ec247163..4c7bd3fd 100644 --- a/src/program/downloaders/alldebrid.py +++ b/src/program/downloaders/alldebrid.py @@ -47,7 +47,6 @@ def __init__(self): def validate(self) -> bool: """Validate All-Debrid settings and API key""" if not self.settings.enabled: - logger.warning("All-Debrid is set to disabled") return False if not self.settings.api_key: logger.warning("All-Debrid API key is not set") diff --git a/src/program/downloaders/realdebrid.py b/src/program/downloaders/realdebrid.py index a7fa6505..307f8a77 100644 --- a/src/program/downloaders/realdebrid.py +++ b/src/program/downloaders/realdebrid.py @@ -24,7 +24,6 @@ def __init__(self): def validate(self) -> bool: """Validate Real-Debrid settings and API key""" if not self.settings.enabled: - logger.debug("Real-Debrid is set to disabled") return False if not self.settings.api_key: logger.warning("Real-Debrid API key is not set") diff --git a/src/program/downloaders/shared.py b/src/program/downloaders/shared.py index e53147c1..6b4dcb93 100644 --- a/src/program/downloaders/shared.py +++ b/src/program/downloaders/shared.py @@ -74,7 +74,7 @@ def cache_matches(self, cached_files: dict, needed_media: dict[int, list[int]], return {1: {1: biggest_file}} def get_needed_media(item: MediaItem) -> dict: - acceptable_states = [States.Indexed, States.Scraped, States.Unknown, States.Failed, States.PartiallyCompleted] + acceptable_states = [States.Indexed, States.Scraped, States.Unknown, States.Failed, States.PartiallyCompleted, States.Ongoing] if item.type == "movie": needed_media = None elif item.type == "show": diff --git a/src/program/downloaders/torbox.py b/src/program/downloaders/torbox.py index a4d24f00..5bfbda0b 100644 --- a/src/program/downloaders/torbox.py +++ b/src/program/downloaders/torbox.py @@ -37,7 +37,6 @@ def __init__(self): def validate(self) -> bool: """Validate the TorBox Downloader as a service""" if not self.settings.enabled: - logger.info("Torbox downloader is not enabled") return False if not self.settings.api_key: logger.error("Torbox API key is not set") diff --git a/src/program/indexers/trakt.py b/src/program/indexers/trakt.py index 17c43c56..4790a83a 100644 --- a/src/program/indexers/trakt.py +++ b/src/program/indexers/trakt.py @@ -99,10 +99,6 @@ def should_submit(item: MediaItem) -> bool: @staticmethod def _add_seasons_to_show(show: Show, imdb_id: str): """Add seasons to the given show using Trakt API.""" - if not isinstance(show, Show): - logger.error(f"Item {show.log_string} is not a show") - return - if not imdb_id or not imdb_id.startswith("tt"): logger.error(f"Item {show.log_string} does not have an imdb_id, cannot index it") return @@ -179,14 +175,14 @@ def _get_formatted_date(data, item_type: str) -> Optional[datetime]: def get_show(imdb_id: str) -> dict: """Wrapper for trakt.tv API show method.""" url = f"https://api.trakt.tv/shows/{imdb_id}/seasons?extended=episodes,full" - response = get(url, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) + response = get(url, timeout=30, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) return response.data if response.is_ok and response.data else {} def create_item_from_imdb_id(imdb_id: str, type: str = None) -> Optional[MediaItem]: """Wrapper for trakt.tv API search method.""" url = f"https://api.trakt.tv/search/imdb/{imdb_id}?extended=full" - response = get(url, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) + response = get(url, timeout=30, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) if not response.is_ok or not response.data: logger.error(f"Failed to create item using imdb id: {imdb_id}") # This returns an empty list for response.data return None @@ -202,7 +198,7 @@ def create_item_from_imdb_id(imdb_id: str, type: str = None) -> Optional[MediaIt def get_imdbid_from_tmdb(tmdb_id: str, type: str = "movie") -> Optional[str]: """Wrapper for trakt.tv API search method.""" url = f"https://api.trakt.tv/search/tmdb/{tmdb_id}" # ?extended=full - response = get(url, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) + response = get(url, timeout=30, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) if not response.is_ok or not response.data: return None imdb_id = get_imdb_id_from_list(response.data, id_type="tmdb", _id=tmdb_id, type=type) @@ -215,7 +211,7 @@ def get_imdbid_from_tmdb(tmdb_id: str, type: str = "movie") -> Optional[str]: def get_imdbid_from_tvdb(tvdb_id: str, type: str = "show") -> Optional[str]: """Wrapper for trakt.tv API search method.""" url = f"https://api.trakt.tv/search/tvdb/{tvdb_id}" - response = get(url, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) + response = get(url, timeout=30, additional_headers={"trakt-api-version": "2", "trakt-api-key": CLIENT_ID}) if not response.is_ok or not response.data: return None imdb_id = get_imdb_id_from_list(response.data, id_type="tvdb", _id=tvdb_id, type=type) diff --git a/src/program/media/item.py b/src/program/media/item.py index e5bfc1d4..d3bf4fb4 100644 --- a/src/program/media/item.py +++ b/src/program/media/item.py @@ -398,7 +398,7 @@ class Show(MediaItem): """Show class""" __tablename__ = "Show" _id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("MediaItem._id"), primary_key=True) - seasons: Mapped[List["Season"]] = relationship(back_populates="parent", foreign_keys="Season.parent_id", lazy="joined", cascade="all, delete-orphan") + seasons: Mapped[List["Season"]] = relationship(back_populates="parent", foreign_keys="Season.parent_id", lazy="joined", cascade="all, delete-orphan", order_by="Season.number") __mapper_args__ = { "polymorphic_identity": "show", @@ -513,7 +513,7 @@ class Season(MediaItem): _id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("MediaItem._id"), primary_key=True) parent_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("Show._id"), use_existing_column=True) parent: Mapped["Show"] = relationship(lazy=False, back_populates="seasons", foreign_keys="Season.parent_id") - episodes: Mapped[List["Episode"]] = relationship(back_populates="parent", foreign_keys="Episode.parent_id", lazy="joined", cascade="all, delete-orphan") + episodes: Mapped[List["Episode"]] = relationship(back_populates="parent", foreign_keys="Episode.parent_id", lazy="joined", cascade="all, delete-orphan", order_by="Episode.number") __mapper_args__ = { "polymorphic_identity": "season", "polymorphic_load": "inline", diff --git a/src/program/scrapers/annatar.py b/src/program/scrapers/annatar.py index 66ee4ca7..6ed98221 100644 --- a/src/program/scrapers/annatar.py +++ b/src/program/scrapers/annatar.py @@ -28,7 +28,6 @@ def __init__(self): def validate(self) -> bool: """Validate the Annatar settings.""" if not self.settings.enabled: - logger.debug("Annatar is set to disabled.") return False if not isinstance(self.settings.url, str) or not self.settings.url: logger.error("Annatar URL is not configured and will not be used.") diff --git a/src/program/scrapers/comet.py b/src/program/scrapers/comet.py index 7a9905db..9770d2c9 100644 --- a/src/program/scrapers/comet.py +++ b/src/program/scrapers/comet.py @@ -38,7 +38,6 @@ def __init__(self): def validate(self) -> bool: """Validate the Comet settings.""" if not self.settings.enabled: - logger.debug("Comet is set to disabled.") return False if not self.settings.url: logger.error("Comet URL is not configured and will not be used.") diff --git a/src/program/scrapers/jackett.py b/src/program/scrapers/jackett.py index fb3fb009..19a54565 100644 --- a/src/program/scrapers/jackett.py +++ b/src/program/scrapers/jackett.py @@ -45,7 +45,6 @@ def __init__(self): def validate(self) -> bool: """Validate Jackett settings.""" if not self.settings.enabled: - logger.debug("Jackett is set to disabled.") return False if self.settings.url and self.settings.api_key: self.api_key = self.settings.api_key diff --git a/src/program/scrapers/knightcrawler.py b/src/program/scrapers/knightcrawler.py index 118dd006..9b83f67a 100644 --- a/src/program/scrapers/knightcrawler.py +++ b/src/program/scrapers/knightcrawler.py @@ -27,7 +27,6 @@ def __init__(self): def validate(self) -> bool: """Validate the Knightcrawler settings.""" if not self.settings.enabled: - logger.debug("Knightcrawler is set to disabled.") return False if not self.settings.url: logger.error("Knightcrawler URL is not configured and will not be used.") diff --git a/src/program/scrapers/mediafusion.py b/src/program/scrapers/mediafusion.py index f3b9bc2a..a3186e36 100644 --- a/src/program/scrapers/mediafusion.py +++ b/src/program/scrapers/mediafusion.py @@ -34,7 +34,6 @@ def __init__(self): def validate(self) -> bool: """Validate the Mediafusion settings.""" if not self.settings.enabled: - logger.debug("Mediafusion is set to disabled.") return False if not self.settings.url: logger.error("Mediafusion URL is not configured and will not be used.") diff --git a/src/program/scrapers/orionoid.py b/src/program/scrapers/orionoid.py index 153b62bf..c48b81b9 100644 --- a/src/program/scrapers/orionoid.py +++ b/src/program/scrapers/orionoid.py @@ -34,7 +34,6 @@ def __init__(self): def validate(self) -> bool: """Validate the Orionoid class_settings.""" if not self.settings.enabled: - logger.warning("Orionoid is set to disabled.") return False if len(self.settings.api_key) != 32 or self.settings.api_key == "": logger.error("Orionoid API Key is not valid or not set. Please check your settings.") diff --git a/src/program/scrapers/prowlarr.py b/src/program/scrapers/prowlarr.py index 477f8249..f71bfc81 100644 --- a/src/program/scrapers/prowlarr.py +++ b/src/program/scrapers/prowlarr.py @@ -46,7 +46,6 @@ def __init__(self): def validate(self) -> bool: """Validate Prowlarr settings.""" if not self.settings.enabled: - logger.debug("Prowlarr is set to disabled.") return False if self.settings.url and self.settings.api_key: self.api_key = self.settings.api_key diff --git a/src/program/scrapers/torbox.py b/src/program/scrapers/torbox.py index ba465b3f..5b937443 100644 --- a/src/program/scrapers/torbox.py +++ b/src/program/scrapers/torbox.py @@ -25,7 +25,6 @@ def __init__(self): def validate(self) -> bool: """Validate the TorBox Scraper as a service""" if not self.settings.enabled: - logger.debug("TorBox Scraper is set to disabled") return False if not isinstance(self.timeout, int) or self.timeout <= 0: logger.error("TorBox timeout is not set or invalid.") diff --git a/src/program/scrapers/torrentio.py b/src/program/scrapers/torrentio.py index deec8392..fff7bef0 100644 --- a/src/program/scrapers/torrentio.py +++ b/src/program/scrapers/torrentio.py @@ -23,13 +23,11 @@ def __init__(self): if not self.initialized: return self.hour_limiter: RateLimiter | None = RateLimiter(max_calls=1, period=5) if self.ratelimit else None - self.running: bool = True logger.success("Torrentio initialized!") def validate(self) -> bool: """Validate the Torrentio settings.""" if not self.settings.enabled: - logger.debug("Torrentio is set to disabled.") return False if not self.settings.url: logger.error("Torrentio URL is not configured and will not be used.") diff --git a/src/program/scrapers/zilean.py b/src/program/scrapers/zilean.py index 02da2b09..d34540ff 100644 --- a/src/program/scrapers/zilean.py +++ b/src/program/scrapers/zilean.py @@ -33,7 +33,6 @@ def __init__(self): def validate(self) -> bool: """Validate the Zilean settings.""" if not self.settings.enabled: - logger.debug("Zilean is set to disabled.") return False if not self.settings.url: logger.error("Zilean URL is not configured and will not be used.") diff --git a/src/program/state_transition.py b/src/program/state_transition.py index 8ce9f9e8..6ba6891a 100644 --- a/src/program/state_transition.py +++ b/src/program/state_transition.py @@ -25,9 +25,9 @@ def process_event(existing_item: MediaItem | None, emitted_by: Service, item: Me source_services = (Overseerr, PlexWatchlist, Listrr, Mdblist, SymlinkLibrary, TraktContent) if emitted_by in source_services or item.last_state in [States.Requested]: next_service = TraktIndexer - # if _imdb_exists_in_db(item.imdb_id) and item.last_state == States.Completed: - # logger.debug(f"Item {item.log_string} already exists in the database.") - # return no_further_processing + if _imdb_exists_in_db(item.imdb_id) and item.last_state == States.Completed: + logger.debug(f"Item {item.log_string} already exists in the database.") + return no_further_processing if isinstance(item, Season): item = item.parent existing_item = existing_item.parent if existing_item else None diff --git a/src/program/updaters/__init__.py b/src/program/updaters/__init__.py index 1443e32c..7606fb77 100644 --- a/src/program/updaters/__init__.py +++ b/src/program/updaters/__init__.py @@ -1,6 +1,4 @@ """Updater module""" -from typing import Dict - from program.media.item import MediaItem from program.updaters.plex import PlexUpdater from utils.logger import logger