1- """
2- Catalog Service - Facade for catalog generation.
3-
4- Encapsulates all catalog logic: auth, profile building, routing, and recommendations.
5- """
6-
71import re
82from typing import Any
93
4+ from fastapi import HTTPException
105from loguru import logger
116
127from app .api .endpoints .manifest import get_config_id
138from app .core .config import settings
9+ from app .core .constants import DEFAULT_MAX_ITEMS , DEFAULT_MIN_ITEMS
1410from app .core .settings import UserSettings , get_default_settings
1511from app .models .taste_profile import TasteProfile
1612from app .services .catalog_updater import catalog_updater
2420from app .services .tmdb .service import get_tmdb_service
2521from app .services .token_store import token_store
2622
27- DEFAULT_MIN_ITEMS = 20
28- DEFAULT_MAX_ITEMS = 32
29-
3023
3124class CatalogService :
32- """
33- Facade for catalog generation.
34-
35- Handles all catalog logic: validation, auth, profile building, routing, and recommendations.
36- """
37-
3825 def __init__ (self ):
39- """Initialize catalog service."""
4026 pass
4127
4228 async def get_catalog (
@@ -61,13 +47,18 @@ async def get_catalog(
6147 # Get credentials
6248 credentials = await token_store .get_user_data (token )
6349 if not credentials :
64- from fastapi import HTTPException
65-
50+ logger .error ("No credentials found for token" )
6651 raise HTTPException (status_code = 401 , detail = "Invalid or expired token. Please reconfigure the addon." )
6752
6853 # Trigger lazy update if needed
6954 if settings .AUTO_UPDATE_CATALOGS :
70- await catalog_updater .trigger_update (token , credentials )
55+ logger .info (f"[{ token [:8 ]} ...] Triggering auto update for token" )
56+ try :
57+ await catalog_updater .trigger_update (token , credentials )
58+ except Exception as e :
59+ logger .error (f"[{ token [:8 ]} ...] Failed to trigger auto update: { e } " )
60+ # continue with the request even if the auto update fails
61+ pass
7162
7263 bundle = StremioBundle ()
7364 try :
@@ -82,11 +73,13 @@ async def get_catalog(
8273 # Initialize services
8374 services = self ._initialize_services (language , user_settings )
8475
76+ integration_service : ProfileIntegration = services ["integration" ]
77+
8578 # Build profile and watched sets (once, reused)
86- profile , watched_tmdb , watched_imdb = await services [ "integration" ] .build_profile_from_library (
79+ profile , watched_tmdb , watched_imdb = await integration_service .build_profile_from_library (
8780 library_items , content_type , bundle , auth_key
8881 )
89- whitelist = await services [ "integration" ] .get_genre_whitelist (profile , content_type ) if profile else set ()
82+ whitelist = await integration_service .get_genre_whitelist (profile , content_type ) if profile else set ()
9083
9184 # Get catalog limits
9285 min_items , max_items = self ._get_catalog_limits (catalog_id , user_settings )
@@ -127,9 +120,6 @@ async def get_catalog(
127120 await bundle .close ()
128121
129122 def _validate_inputs (self , token : str , content_type : str , catalog_id : str ) -> None :
130- """Validate input parameters."""
131- from fastapi import HTTPException
132-
133123 if not token :
134124 raise HTTPException (
135125 status_code = 400 ,
@@ -144,26 +134,18 @@ def _validate_inputs(self, token: str, content_type: str, catalog_id: str) -> No
144134 if catalog_id not in ["watchly.rec" , "watchly.creators" ] and not any (
145135 catalog_id .startswith (p )
146136 for p in (
147- "tt" ,
148137 "watchly.theme." ,
149- "watchly.item." ,
150138 "watchly.loved." ,
151139 "watchly.watched." ,
152140 )
153141 ):
154142 logger .warning (f"Invalid id: { catalog_id } " )
155143 raise HTTPException (
156144 status_code = 400 ,
157- detail = (
158- "Invalid id. Supported: 'watchly.rec', 'watchly.creators', 'watchly.theme.<params>',"
159- "'watchly.item.<id>', or specific item IDs."
160- ),
145+ detail = ("Invalid id. Supported: 'watchly.rec', 'watchly.creators', 'watchly.theme.<params>'" ),
161146 )
162147
163148 async def _resolve_auth (self , bundle : StremioBundle , credentials : dict , token : str ) -> str :
164- """Resolve and validate auth key."""
165- from fastapi import HTTPException
166-
167149 auth_key = credentials .get ("authKey" )
168150 email = credentials .get ("email" )
169151 password = credentials .get ("password" )
@@ -174,7 +156,8 @@ async def _resolve_auth(self, bundle: StremioBundle, credentials: dict, token: s
174156 try :
175157 await bundle .auth .get_user_info (auth_key )
176158 is_valid = True
177- except Exception :
159+ except Exception as e :
160+ logger .error (f"Failed to validate auth key during catalog fetch: { e } " )
178161 pass
179162
180163 # Try to refresh if invalid
@@ -188,31 +171,27 @@ async def _resolve_auth(self, bundle: StremioBundle, credentials: dict, token: s
188171 logger .error (f"Failed to refresh auth key during catalog fetch: { e } " )
189172
190173 if not auth_key :
174+ logger .error ("No auth key found during catalog fetch" )
191175 raise HTTPException (status_code = 401 , detail = "Stremio session expired. Please reconfigure." )
192176
193177 return auth_key
194178
195179 def _extract_settings (self , credentials : dict ) -> UserSettings :
196- """Extract user settings from credentials."""
197180 settings_dict = credentials .get ("settings" , {})
198181 return UserSettings (** settings_dict ) if settings_dict else get_default_settings ()
199182
200183 def _initialize_services (self , language : str , user_settings : UserSettings ) -> dict [str , Any ]:
201- """Initialize all recommendation services."""
202184 tmdb_service = get_tmdb_service (language = language )
203- integration = ProfileIntegration (language = language )
204-
205185 return {
206186 "tmdb" : tmdb_service ,
207- "integration" : integration ,
187+ "integration" : ProfileIntegration ( language = language ) ,
208188 "item" : ItemBasedService (tmdb_service , user_settings ),
209189 "theme" : ThemeBasedService (tmdb_service , user_settings ),
210190 "top_picks" : TopPicksService (tmdb_service , user_settings ),
211191 "creators" : CreatorsService (tmdb_service , user_settings ),
212192 }
213193
214194 def _get_catalog_limits (self , catalog_id : str , user_settings : UserSettings ) -> tuple [int , int ]:
215- """Get min/max items for catalog."""
216195 try :
217196 cfg_id = get_config_id ({"id" : catalog_id })
218197 except Exception :
@@ -255,21 +234,19 @@ async def _get_recommendations(
255234 ) -> list [dict [str , Any ]]:
256235 """Route to appropriate recommendation service based on catalog ID."""
257236 # Item-based recommendations
258- if catalog_id . startswith ( "tt" ) or any (
237+ if any (
259238 catalog_id .startswith (p )
260239 for p in (
261- "watchly.item." ,
262240 "watchly.loved." ,
263241 "watchly.watched." ,
264242 )
265243 ):
266244 # Extract item ID
267- if catalog_id .startswith ("tt" ):
268- item_id = catalog_id
269- else :
270- item_id = re .sub (r"^watchly\.(item|loved|watched)\." , "" , catalog_id )
245+ item_id = re .sub (r"^watchly\.(loved|watched)\." , "" , catalog_id )
271246
272- recommendations = await services ["item" ].get_recommendations_for_item (
247+ item_service : ItemBasedService = services ["item" ]
248+
249+ recommendations = await item_service .get_recommendations_for_item (
273250 item_id = item_id ,
274251 content_type = content_type ,
275252 watched_tmdb = watched_tmdb ,
@@ -281,7 +258,9 @@ async def _get_recommendations(
281258
282259 # Theme-based recommendations
283260 elif catalog_id .startswith ("watchly.theme." ):
284- recommendations = await services ["theme" ].get_recommendations_for_theme (
261+ theme_service : ThemeBasedService = services ["theme" ]
262+
263+ recommendations = await theme_service .get_recommendations_for_theme (
285264 theme_id = catalog_id ,
286265 content_type = content_type ,
287266 profile = profile ,
@@ -294,8 +273,10 @@ async def _get_recommendations(
294273
295274 # Creators-based recommendations
296275 elif catalog_id == "watchly.creators" :
276+ creators_service : CreatorsService = services ["creators" ]
277+
297278 if profile :
298- recommendations = await services [ "creators" ] .get_recommendations_from_creators (
279+ recommendations = await creators_service .get_recommendations_from_creators (
299280 profile = profile ,
300281 content_type = content_type ,
301282 library_items = library_items ,
@@ -311,7 +292,9 @@ async def _get_recommendations(
311292 # Top picks
312293 elif catalog_id == "watchly.rec" :
313294 if profile :
314- recommendations = await services ["top_picks" ].get_top_picks (
295+ top_picks_service : TopPicksService = services ["top_picks" ]
296+
297+ recommendations = await top_picks_service .get_top_picks (
315298 profile = profile ,
316299 content_type = content_type ,
317300 library_items = library_items ,
0 commit comments