Skip to content

Commit 71479b5

Browse files
feat: Refactor token management to use a dedicated token model, remove token salt, and streamline catalog refresh logic.
1 parent 88bf2a9 commit 71479b5

File tree

12 files changed

+123
-348
lines changed

12 files changed

+123
-348
lines changed

.env.example

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ PORT=8000
33
ADDON_ID=com.bimal.watchly
44
ADDON_NAME=Watchly
55
REDIS_URL=redis://redis:6379/0
6-
TOKEN_SALT=replace-with-a-long-random-string
76
TOKEN_TTL_SECONDS=0
87
ANNOUNCEMENT_HTML=
98
HOST_NAME=<your_addon_url>
109
RECOMMENDATION_SOURCE_ITEMS_LIMIT=10 # fetches recent watched/loved 10 movies and series to recommend based on those
1110

1211
# UPDATER
13-
CATALOG_UPDATE_MODE=cron
12+
CATALOG_UPDATE_MODE=cron # Available options: cron, interval
13+
# cron updates catalogs at specified times
14+
# interval updates in specific intervals
1415
CATALOG_UPDATE_CRON_SCHEDULES=[{"hour": 12, "minute": 0, "id": "catalog_refresh_noon"},{"hour": 0, "minute": 0, "id": "catalog_refresh_midnight"}]
1516
CATALOG_REFRESH_INTERVAL_SECONDS=6*60*60

app/api/endpoints/catalogs.py

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,26 @@
33
from fastapi import APIRouter, HTTPException, Response
44
from loguru import logger
55

6-
from app.core.settings import decode_settings
6+
from app.core.settings import UserSettings, get_default_settings
77
from app.services.catalog_updater import refresh_catalogs_for_credentials
88
from app.services.recommendation_service import RecommendationService
99
from app.services.stremio_service import StremioService
10-
from app.utils import redact_token, resolve_user_credentials
10+
from app.services.token_store import token_store
11+
12+
MAX_RESULTS = 50
13+
SOURCE_ITEMS_LIMIT = 10
1114

1215
router = APIRouter()
1316

1417

15-
@router.get("/catalog/{type}/{id}.json")
1618
@router.get("/{token}/catalog/{type}/{id}.json")
17-
@router.get("/{settings_str}/{token}/catalog/{type}/{id}.json")
18-
async def get_catalog(
19-
type: str,
20-
id: str,
21-
response: Response,
22-
token: str | None = None,
23-
settings_str: str | None = None,
24-
):
25-
"""
26-
Stremio catalog endpoint for movies and series.
27-
"""
19+
async def get_catalog(type: str, id: str, response: Response, token: str):
2820
if not token:
2921
raise HTTPException(
3022
status_code=400,
3123
detail="Missing credentials token. Please open Watchly from a configured manifest URL.",
3224
)
3325

34-
logger.info(f"[{redact_token(token)}] Fetching catalog for {type} with id {id}")
35-
36-
credentials = await resolve_user_credentials(token)
37-
3826
if type not in ["movie", "series"]:
3927
logger.warning(f"Invalid type: {type}")
4028
raise HTTPException(status_code=400, detail="Invalid type. Use 'movie' or 'series'")
@@ -51,22 +39,23 @@ async def get_catalog(
5139
" specific item IDs."
5240
),
5341
)
42+
43+
logger.info(f"[{token}] Fetching catalog for {type} with id {id}")
44+
45+
credentials = await token_store.get_user_data(token)
5446
try:
55-
# Decode settings to get language
56-
user_settings = decode_settings(settings_str) if settings_str else None
47+
# Extract settings from credentials
48+
settings_dict = credentials.get("settings", {})
49+
user_settings = UserSettings(**settings_dict) if settings_dict else get_default_settings()
5750
language = user_settings.language if user_settings else "en-US"
5851

5952
# Create services with credentials
60-
stremio_service = StremioService(
61-
username=credentials.get("username") or "",
62-
password=credentials.get("password") or "",
63-
auth_key=credentials.get("authKey"),
64-
)
53+
stremio_service = StremioService(auth_key=credentials.get("authKey"))
6554
recommendation_service = RecommendationService(
6655
stremio_service=stremio_service, language=language, user_settings=user_settings
6756
)
6857

69-
# Handle item-based recommendations (legacy or explicit link)
58+
# Handle item-based recommendations
7059
if id.startswith("tt"):
7160
recommendations = await recommendation_service.get_recommendations_for_item(item_id=id)
7261
logger.info(f"Found {len(recommendations)} recommendations for {id}")
@@ -84,12 +73,8 @@ async def get_catalog(
8473
logger.info(f"Found {len(recommendations)} recommendations for theme {id}")
8574

8675
else:
87-
# Top Picks (watchly.rec)
88-
8976
recommendations = await recommendation_service.get_recommendations(
90-
content_type=type,
91-
source_items_limit=10,
92-
max_results=50,
77+
content_type=type, source_items_limit=SOURCE_ITEMS_LIMIT, max_results=MAX_RESULTS
9378
)
9479
logger.info(f"Found {len(recommendations)} recommendations for {type}")
9580

@@ -101,7 +86,7 @@ async def get_catalog(
10186
except HTTPException:
10287
raise
10388
except Exception as e:
104-
logger.error(f"[{redact_token(token)}] Error fetching catalog for {type}/{id}: {e}", exc_info=True)
89+
logger.exception(f"[{token}] Error fetching catalog for {type}/{id}: {e}")
10590
raise HTTPException(status_code=500, detail=str(e))
10691

10792

@@ -111,9 +96,9 @@ async def update_catalogs(token: str):
11196
Update the catalogs for the addon. This is a manual endpoint to update the catalogs.
11297
"""
11398
# Decode credentials from path
114-
credentials = await resolve_user_credentials(token)
99+
credentials = await token_store.get_user_data(token)
115100

116-
logger.info(f"[{redact_token(token)}] Updating catalogs in response to manual request")
117-
updated = await refresh_catalogs_for_credentials(credentials)
101+
logger.info(f"[{token}] Updating catalogs in response to manual request")
102+
updated = await refresh_catalogs_for_credentials(token, credentials)
118103
logger.info(f"Manual catalog update completed: {updated}")
119104
return {"success": updated}

app/api/endpoints/manifest.py

Lines changed: 43 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from async_lru import alru_cache
2-
from fastapi import Response
2+
from fastapi import HTTPException, Response
33
from fastapi.routing import APIRouter
44

55
from app.core.config import settings
6-
from app.core.settings import UserSettings, decode_settings
6+
from app.core.settings import UserSettings, get_default_settings
77
from app.core.version import __version__
88
from app.services.catalog import DynamicCatalogService
99
from app.services.stremio_service import StremioService
10+
from app.services.token_store import token_store
1011
from app.services.translation import translation_service
11-
from app.utils import resolve_user_credentials
1212

1313
router = APIRouter()
1414

@@ -55,27 +55,17 @@ def get_base_manifest(user_settings: UserSettings | None = None):
5555
}
5656

5757

58-
# Cache catalog definitions for 1 hour (3600s)
5958
# Cache catalog definitions for 1 hour (3600s)
6059
@alru_cache(maxsize=1000, ttl=3600)
61-
async def fetch_catalogs(token: str | None = None, settings_str: str | None = None):
62-
if not token:
63-
return []
60+
async def fetch_catalogs(token: str):
61+
credentials = await token_store.get_user_data(token)
6462

65-
credentials = await resolve_user_credentials(token)
66-
67-
if settings_str:
68-
user_settings = decode_settings(settings_str)
69-
elif credentials.get("settings"):
63+
if credentials.get("settings"):
7064
user_settings = UserSettings(**credentials["settings"])
7165
else:
72-
user_settings = None
66+
user_settings = get_default_settings()
7367

74-
stremio_service = StremioService(
75-
username=credentials.get("username") or "",
76-
password=credentials.get("password") or "",
77-
auth_key=credentials.get("authKey"),
78-
)
68+
stremio_service = StremioService(auth_key=credentials.get("authKey"))
7969

8070
# Note: get_library_items is expensive, but we need it to determine *which* genre catalogs to show.
8171
library_items = await stremio_service.get_library_items()
@@ -88,75 +78,56 @@ async def fetch_catalogs(token: str | None = None, settings_str: str | None = No
8878
return catalogs
8979

9080

91-
async def _manifest_handler(response: Response, token: str | None, settings_str: str | None):
92-
"""Stremio manifest handler."""
93-
# Cache manifest for 1 day (86400 seconds)
81+
def get_config_id(catalog) -> str | None:
82+
catalog_id = catalog.get("id", "")
83+
if catalog_id.startswith("watchly.theme."):
84+
return "watchly.theme"
85+
if catalog_id.startswith("watchly.loved."):
86+
return "watchly.loved"
87+
if catalog_id.startswith("watchly.watched."):
88+
return "watchly.watched"
89+
if catalog_id.startswith("watchly.item."):
90+
return "watchly.item"
91+
if catalog_id.startswith("watchly.rec"):
92+
return "watchly.rec"
93+
return catalog_id
94+
95+
96+
async def _manifest_handler(response: Response, token: str):
9497
response.headers["Cache-Control"] = "public, max-age=86400"
9598

99+
if not token:
100+
raise HTTPException(status_code=401, detail="Missing token. Please reconfigure the addon.")
101+
96102
user_settings = None
97-
if settings_str:
98-
user_settings = decode_settings(settings_str)
99-
elif token:
100-
try:
101-
creds = await resolve_user_credentials(token)
102-
if creds.get("settings"):
103-
user_settings = UserSettings(**creds["settings"])
104-
except Exception:
105-
# Fallback to defaults if token resolution fails (or let it fail later in fetch_catalogs)
106-
pass
103+
try:
104+
creds = await token_store.get_user_data(token)
105+
if creds.get("settings"):
106+
user_settings = UserSettings(**creds["settings"])
107+
except Exception:
108+
raise HTTPException(status_code=401, detail="Invalid or expired token. Please reconfigure the addon.")
107109

108110
base_manifest = get_base_manifest(user_settings)
109111

112+
# translate to target language
110113
if user_settings and user_settings.language:
111114
for cat in base_manifest.get("catalogs", []):
112115
if cat.get("name"):
113116
cat["name"] = await translation_service.translate(cat["name"], user_settings.language)
114117

115-
if token:
116-
# We pass settings_str to fetch_catalogs so it can cache different versions
117-
# We COPY the lists to avoid modifying cached objects or base_manifest defaults
118-
fetched_catalogs = await fetch_catalogs(token, settings_str)
119-
120-
# Create a new list with copies of all catalogs
121-
all_catalogs = [c.copy() for c in base_manifest["catalogs"]] + [c.copy() for c in fetched_catalogs]
122-
123-
if user_settings:
124-
# Create a lookup for order index
125-
order_map = {c.id: i for i, c in enumerate(user_settings.catalogs)}
126-
127-
# Sort. Items not in map go to end.
128-
# Extract config id from catalog id for matching with user settings
129-
def get_config_id(catalog):
130-
catalog_id = catalog.get("id", "")
131-
if catalog_id.startswith("watchly.theme."):
132-
return "watchly.theme"
133-
if catalog_id.startswith("watchly.loved."):
134-
return "watchly.loved"
135-
if catalog_id.startswith("watchly.watched."):
136-
return "watchly.watched"
137-
if catalog_id.startswith("watchly.item."):
138-
return "watchly.item"
139-
if catalog_id.startswith("watchly.rec"):
140-
return "watchly.rec"
141-
return catalog_id
142-
143-
all_catalogs.sort(key=lambda x: order_map.get(get_config_id(x), 999))
144-
145-
base_manifest["catalogs"] = all_catalogs
118+
fetched_catalogs = await fetch_catalogs(token)
146119

147-
return base_manifest
120+
all_catalogs = [c.copy() for c in base_manifest["catalogs"]] + [c.copy() for c in fetched_catalogs]
121+
122+
if user_settings:
123+
order_map = {c.id: i for i, c in enumerate(user_settings.catalogs)}
124+
all_catalogs.sort(key=lambda x: order_map.get(get_config_id(x), 999))
148125

126+
base_manifest["catalogs"] = all_catalogs
149127

150-
@router.get("/manifest.json")
151-
async def manifest_root(response: Response):
152-
return await _manifest_handler(response, None, None)
128+
return base_manifest
153129

154130

155131
@router.get("/{token}/manifest.json")
156132
async def manifest_token(response: Response, token: str):
157-
return await _manifest_handler(response, token, None)
158-
159-
160-
@router.get("/{settings_str}/{token}/manifest.json")
161-
async def manifest_settings(response: Response, settings_str: str, token: str):
162-
return await _manifest_handler(response, token, settings_str)
133+
return await _manifest_handler(response, token)

0 commit comments

Comments
 (0)