diff --git a/app/api/endpoints/catalogs.py b/app/api/endpoints/catalogs.py index 15f262a..8e3c46f 100644 --- a/app/api/endpoints/catalogs.py +++ b/app/api/endpoints/catalogs.py @@ -76,9 +76,7 @@ async def get_catalog(type: str, id: str, response: Response, token: str): logger.info(f"Found {len(recommendations)} recommendations for item {item_id}") elif id.startswith("watchly.theme."): - recommendations = await recommendation_service.get_recommendations_for_theme( - theme_id=id, content_type=type - ) + recommendations = await recommendation_service.get_recommendations_for_theme(theme_id=id, content_type=type) logger.info(f"Found {len(recommendations)} recommendations for theme {id}") else: diff --git a/app/api/endpoints/manifest.py b/app/api/endpoints/manifest.py index a715015..0eb42e6 100644 --- a/app/api/endpoints/manifest.py +++ b/app/api/endpoints/manifest.py @@ -80,7 +80,7 @@ def get_config_id(catalog) -> str | None: async def _manifest_handler(response: Response, token: str): - response.headers["Cache-Control"] = "public, max-age=7200" + response.headers["Cache-Control"] = "no-cache" if not token: raise HTTPException(status_code=401, detail="Missing token. Please reconfigure the addon.") diff --git a/app/core/version.py b/app/core/version.py index 72f26f5..c72e379 100644 --- a/app/core/version.py +++ b/app/core/version.py @@ -1 +1 @@ -__version__ = "1.1.2" +__version__ = "1.1.4" diff --git a/app/services/scoring.py b/app/services/scoring.py index c13b80b..a01770c 100644 --- a/app/services/scoring.py +++ b/app/services/scoring.py @@ -37,9 +37,7 @@ def process_item(self, raw_item: dict) -> ScoredItem: source_type="loved" if item.is_loved else ("liked" if item.is_liked else "watched"), ) - def calculate_score( - self, item: dict | StremioLibraryItem, is_loved: bool = False, is_liked: bool = False - ) -> float: + def calculate_score(self, item: dict | StremioLibraryItem, is_loved: bool = False, is_liked: bool = False) -> float: """ Backwards compatible method to just get the float score. Accepts either a raw dict or a StremioLibraryItem. diff --git a/app/services/stremio_service.py b/app/services/stremio_service.py index f1f39c6..fe7bf99 100644 --- a/app/services/stremio_service.py +++ b/app/services/stremio_service.py @@ -422,9 +422,7 @@ async def _post_with_retries(self, client: httpx.AsyncClient, url: str, json: di except httpx.RequestError as e: attempts += 1 backoff = (2 ** (attempts - 1)) + random.uniform(0, 0.25) - logger.warning( - f"Stremio POST {url} request error: {e}; retry {attempts}/{max_tries} in {backoff:.2f}s" - ) + logger.warning(f"Stremio POST {url} request error: {e}; retry {attempts}/{max_tries} in {backoff:.2f}s") await asyncio.sleep(backoff) last_exc = e continue diff --git a/app/services/tmdb_service.py b/app/services/tmdb_service.py index fed117d..3f6836e 100644 --- a/app/services/tmdb_service.py +++ b/app/services/tmdb_service.py @@ -67,9 +67,7 @@ async def _make_request(self, endpoint: str, params: dict | None = None) -> dict try: return response.json() except ValueError as e: - logger.error( - f"TMDB API returned invalid JSON for {endpoint}: {e}. Response: {response.text[:200]}" - ) + logger.error(f"TMDB API returned invalid JSON for {endpoint}: {e}. Response: {response.text[:200]}") return {} except httpx.HTTPStatusError as e: status = e.response.status_code diff --git a/app/services/token_store.py b/app/services/token_store.py index 77946fb..e632383 100644 --- a/app/services/token_store.py +++ b/app/services/token_store.py @@ -37,7 +37,7 @@ def _ensure_secure_salt(self) -> None: if not settings.TOKEN_SALT or settings.TOKEN_SALT == "change-me": logger.error("Refusing to store credentials because TOKEN_SALT is unset or using the insecure default.") raise RuntimeError( - "Server misconfiguration: TOKEN_SALT must be set to a non-default value before storing credentials." + "Server misconfiguration: TOKEN_SALT must be set to a non-default value before storing" " credentials." ) def _get_cipher(self) -> Fernet: @@ -145,6 +145,20 @@ async def store_user_data(self, user_id: str, payload: dict[str, Any]) -> str: else: await client.set(key, json_str) + # Invalidate async LRU cache for fresh reads on subsequent requests + try: + # bound method supports targeted invalidation by argument(s) + self.get_user_data.cache_invalidate(token) + except KeyError: + # The token was not in the cache, no action needed. + pass + except Exception as e: + logger.warning(f"Targeted cache invalidation failed: {e}. Falling back to clearing cache.") + try: + self.get_user_data.cache_clear() + except Exception as e_clear: + logger.error(f"Error while clearing cache: {e_clear}") + # Ensure we remove from negative cache so new value is read next time try: if token in self._missing_tokens: @@ -194,6 +208,19 @@ async def delete_token(self, token: str = None, key: str = None) -> None: client = await self._get_client() await client.delete(key) + # Invalidate async LRU cache so future reads reflect deletion + try: + if token: + self.get_user_data.cache_invalidate(token) + else: + # If only key is provided, clear cache entirely to be safe + self.get_user_data.cache_clear() + except KeyError: + # The token was not in the cache, no action needed. + pass + except Exception as e: + logger.warning(f"Failed to invalidate user data cache during token deletion: {e}") + # Remove from negative cache as token is deleted try: if token and token in self._missing_tokens: diff --git a/app/static/index.html b/app/static/index.html index bb7f844..2d4b4f1 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -19,7 +19,8 @@ slate: { 850: '#151f32', 900: '#0f172a', - 950: '#020617', + 950: '#020617' + }, }, stremio: { DEFAULT: '#3b2667', @@ -42,8 +43,16 @@