Skip to content

Commit 1d1dff4

Browse files
refactor: add centralized method to get auth token (#61)
1 parent 837d531 commit 1d1dff4

File tree

5 files changed

+106
-49
lines changed

5 files changed

+106
-49
lines changed

app/api/endpoints/catalogs.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,12 @@ async def get_catalog(type: str, id: str, response: Response, token: str):
8585
user_settings = UserSettings(**settings_dict) if settings_dict else get_default_settings()
8686
language = user_settings.language if user_settings else "en-US"
8787

88-
# Create services with credentials (prefer fresh login with email/password)
89-
if credentials.get("password") and credentials.get("email"):
90-
stremio_service = StremioService(
91-
username=credentials.get("email", ""), password=credentials.get("password", "")
92-
)
93-
try:
94-
await stremio_service._login_for_auth_key()
95-
except Exception:
96-
stremio_service = StremioService(auth_key=credentials.get("authKey"))
97-
else:
98-
stremio_service = StremioService(auth_key=credentials.get("authKey"))
88+
# Create a single service; get_auth_key() will validate/refresh as needed
89+
stremio_service = StremioService(
90+
username=credentials.get("email", ""),
91+
password=credentials.get("password", ""),
92+
auth_key=credentials.get("authKey"),
93+
)
9994
# Fetch library once per request and reuse across recommendation paths
10095
library_items = await stremio_service.get_library_items()
10196
recommendation_service = RecommendationService(

app/api/endpoints/manifest.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,12 @@ async def _manifest_handler(response: Response, token: str):
9696

9797
base_manifest = get_base_manifest(user_settings)
9898

99-
# Build dynamic catalogs using the already-fetched credentials
100-
# Build Stremio service with fresh auth when possible
101-
if creds.get("password") and creds.get("email"):
102-
stremio_service = StremioService(username=creds.get("email", ""), password=creds.get("password", ""))
103-
try:
104-
await stremio_service._login_for_auth_key()
105-
except Exception:
106-
# fallback to stored authKey if login fails
107-
stremio_service = StremioService(auth_key=creds.get("authKey"))
108-
else:
109-
stremio_service = StremioService(auth_key=creds.get("authKey"))
99+
# Build dynamic catalogs using a single service; get_auth_key() handles validation/refresh
100+
stremio_service = StremioService(
101+
username=creds.get("email", ""),
102+
password=creds.get("password", ""),
103+
auth_key=creds.get("authKey"),
104+
)
110105
try:
111106
fetched_catalogs = await build_dynamic_catalogs(
112107
stremio_service,

app/api/endpoints/tokens.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,9 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
8989
if email and password:
9090
stremio_service = StremioService(username=email, password=password)
9191
try:
92-
# Always get a fresh key
93-
fresh_key = await stremio_service._login_for_auth_key()
94-
stremio_auth_key = fresh_key
95-
user_info = await stremio_service.get_user_info()
92+
# Centralized key retrieval (validates/refreshes)
93+
stremio_auth_key = await stremio_service.get_auth_key()
94+
user_info = await stremio_service.get_user_info(stremio_auth_key)
9695
user_id = user_info["user_id"]
9796
resolved_email = user_info.get("email", email)
9897
except Exception as e:
@@ -102,7 +101,7 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
102101
else:
103102
stremio_service = StremioService(auth_key=stremio_auth_key)
104103
try:
105-
user_info = await stremio_service.get_user_info()
104+
user_info = await stremio_service.get_user_info(stremio_auth_key)
106105
user_id = user_info["user_id"]
107106
resolved_email = user_info.get("email", "")
108107
except Exception as e:
@@ -147,7 +146,12 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
147146
# 6. Store user data
148147
try:
149148
token = await token_store.store_user_data(user_id, payload_to_store)
150-
logger.info(f"[{redact_token(token)}] Account {'created' if is_new_account else 'updated'} for user {user_id}")
149+
logger.info(
150+
"[%s] Account %s for user %s",
151+
redact_token(token),
152+
"created" if is_new_account else "updated",
153+
user_id,
154+
)
151155
except RuntimeError as exc:
152156
# Surface a clear message when secure storage fails
153157
if "PASSWORD_ENCRYPT_FAILED" in str(exc):
@@ -175,20 +179,23 @@ async def get_stremio_user_data(payload: TokenRequest) -> tuple[str, str]:
175179
if email and password:
176180
svc = StremioService(username=email, password=password)
177181
try:
178-
await svc._login_for_auth_key()
179-
user_info = await svc.get_user_info()
182+
auth_key = await svc.get_auth_key()
183+
user_info = await svc.get_user_info(auth_key)
180184
return user_info["user_id"], user_info.get("email", email)
181185
except Exception as e:
182186
logger.error(f"Stremio identity check failed (email/password): {e}")
183-
raise HTTPException(status_code=400, detail="Failed to verify Stremio identity with email/password.")
187+
raise HTTPException(
188+
status_code=400,
189+
detail="Failed to verify Stremio identity with email/password.",
190+
)
184191
finally:
185192
await svc.close()
186193
elif auth_key:
187194
if auth_key.startswith('"') and auth_key.endswith('"'):
188195
auth_key = auth_key[1:-1].strip()
189196
svc = StremioService(auth_key=auth_key)
190197
try:
191-
user_info = await svc.get_user_info()
198+
user_info = await svc.get_user_info(auth_key)
192199
return user_info["user_id"], user_info.get("email", "")
193200
except Exception as e:
194201
logger.error(f"Stremio identity check failed: {e}")

app/core/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.3.0"
1+
__version__ = "1.3.1"

app/services/stremio_service.py

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,43 @@ async def _login_for_auth_key(self) -> str:
121121
raise
122122

123123
async def get_auth_key(self) -> str:
124-
"""Return the cached auth key."""
124+
"""Return a valid auth key; refresh transparently if expired and credentials exist."""
125+
# If we have a key, validate it via GetUser
126+
if self._auth_key:
127+
try:
128+
await self.get_user_info(self._auth_key)
129+
return self._auth_key
130+
except Exception:
131+
# fall through to attempt refresh
132+
pass
133+
134+
# Refresh using username/password if available
135+
if self.username and self.password:
136+
fresh_key = await self._login_for_auth_key()
137+
138+
# Persist refreshed key back to Redis, best-effort
139+
try:
140+
from app.services.token_store import token_store # lazy import to avoid cycles
141+
142+
try:
143+
info = await self.get_user_info(fresh_key)
144+
uid = info.get("user_id")
145+
except Exception:
146+
uid = None
147+
if uid:
148+
existing = await token_store.get_user_data(uid)
149+
if existing:
150+
updated = existing.copy()
151+
updated["authKey"] = fresh_key
152+
await token_store.store_user_data(uid, updated)
153+
except Exception:
154+
pass
155+
156+
return fresh_key
157+
158+
# No credentials to refresh; if we still have no key, error
125159
if not self._auth_key:
126-
raise ValueError("Stremio auth key is missing.")
160+
raise ValueError("Stremio auth key is missing and cannot be refreshed.")
127161
return self._auth_key
128162

129163
async def is_loved(self, auth_key: str, imdb_id: str, media_type: str) -> tuple[bool, bool]:
@@ -176,15 +210,16 @@ async def get_liked_items(self, auth_token: str, media_type: str) -> list[str]:
176210
logger.warning(f"Failed to fetch liked items: {e}")
177211
return []
178212

179-
async def get_user_info(self) -> dict[str, str]:
180-
"""Fetch user ID and email using the auth key."""
181-
if not self._auth_key:
213+
async def get_user_info(self, auth_key: str | None = None) -> dict[str, str]:
214+
"""Fetch user ID and email using the provided auth key (or self._auth_key)."""
215+
key = auth_key or self._auth_key
216+
if not key:
182217
raise ValueError("Stremio auth key is missing.")
183218

184219
url = f"{self.base_url}/api/getUser"
185220
payload = {
186221
"type": "GetUser",
187-
"authKey": self._auth_key,
222+
"authKey": key,
188223
}
189224

190225
try:
@@ -212,7 +247,8 @@ async def get_user_info(self) -> dict[str, str]:
212247

213248
async def get_user_email(self) -> str:
214249
"""Fetch user email using the auth key."""
215-
user_info = await self.get_user_info()
250+
auth_key = await self.get_auth_key()
251+
user_info = await self.get_user_info(auth_key)
216252
return user_info.get("email", "")
217253

218254
async def get_library_items(self) -> dict[str, list[dict]]:
@@ -221,10 +257,6 @@ async def get_library_items(self) -> dict[str, list[dict]]:
221257
Returns a dict with 'watched' and 'loved' keys.
222258
"""
223259

224-
if not self._auth_key:
225-
logger.warning("Stremio auth key not configured")
226-
return {"watched": [], "loved": []}
227-
228260
try:
229261
# Get auth token
230262
auth_key = await self.get_auth_key()
@@ -321,7 +353,11 @@ def _sort_key(x: dict):
321353

322354
added_items.append(item)
323355

324-
logger.info(f"Found {len(added_items)} added (unwatched) and {len(removed_items)} removed library items")
356+
logger.info(
357+
"Found %s added (unwatched) and %s removed library items",
358+
len(added_items),
359+
len(removed_items),
360+
)
325361
# Prepare result
326362
result = {
327363
"watched": watched_items,
@@ -413,7 +449,12 @@ async def _post_with_retries(self, client: httpx.AsyncClient, url: str, json: di
413449
attempts += 1
414450
backoff = (2 ** (attempts - 1)) + random.uniform(0, 0.25)
415451
logger.warning(
416-
f"Stremio POST {url} failed with {status}; retry {attempts}/{max_tries} in" f" {backoff:.2f}s"
452+
"Stremio POST %s failed with %s; retry %s/%s in %.2fs",
453+
url,
454+
status,
455+
attempts,
456+
max_tries,
457+
backoff,
417458
)
418459
await asyncio.sleep(backoff)
419460
last_exc = e
@@ -422,7 +463,14 @@ async def _post_with_retries(self, client: httpx.AsyncClient, url: str, json: di
422463
except httpx.RequestError as e:
423464
attempts += 1
424465
backoff = (2 ** (attempts - 1)) + random.uniform(0, 0.25)
425-
logger.warning(f"Stremio POST {url} request error: {e}; retry {attempts}/{max_tries} in {backoff:.2f}s")
466+
logger.warning(
467+
"Stremio POST %s request error: %s; retry %s/%s in %.2fs",
468+
url,
469+
e,
470+
attempts,
471+
max_tries,
472+
backoff,
473+
)
426474
await asyncio.sleep(backoff)
427475
last_exc = e
428476
continue
@@ -446,7 +494,12 @@ async def _get_with_retries(
446494
attempts += 1
447495
backoff = (2 ** (attempts - 1)) + random.uniform(0, 0.25)
448496
logger.warning(
449-
f"Stremio GET {url} failed with {status}; retry {attempts}/{max_tries} in" f" {backoff:.2f}s"
497+
"Stremio GET %s failed with %s; retry %s/%s in %.2fs",
498+
url,
499+
status,
500+
attempts,
501+
max_tries,
502+
backoff,
450503
)
451504
await asyncio.sleep(backoff)
452505
last_exc = e
@@ -455,7 +508,14 @@ async def _get_with_retries(
455508
except httpx.RequestError as e:
456509
attempts += 1
457510
backoff = (2 ** (attempts - 1)) + random.uniform(0, 0.25)
458-
logger.warning(f"Stremio GET {url} request error: {e}; retry {attempts}/{max_tries} in {backoff:.2f}s")
511+
logger.warning(
512+
"Stremio GET %s request error: %s; retry %s/%s in %.2fs",
513+
url,
514+
e,
515+
attempts,
516+
max_tries,
517+
backoff,
518+
)
459519
await asyncio.sleep(backoff)
460520
last_exc = e
461521
continue

0 commit comments

Comments
 (0)