1515
1616
1717class TokenRequest (BaseModel ):
18- watchly_username : str | None = Field (default = None , description = "Watchly account (user/id)" )
19- watchly_password : str | None = Field (default = None , description = "Watchly account password" )
20- username : str | None = Field (default = None , description = "Stremio username or email" )
21- password : str | None = Field (default = None , description = "Stremio password" )
22- authKey : str | None = Field (default = None , description = "Existing Stremio auth key" )
18+ authKey : str | None = Field (default = None , description = "Stremio auth key" )
2319 catalogs : list [CatalogConfig ] | None = Field (default = None , description = "Optional catalog configuration" )
2420 language : str = Field (default = "en-US" , description = "Language for TMDB API" )
2521 rpdb_key : str | None = Field (default = None , description = "Optional RPDB API Key" )
@@ -38,18 +34,13 @@ class TokenResponse(BaseModel):
3834
3935async def _verify_credentials_or_raise (payload : dict ) -> str :
4036 """Ensure the supplied credentials/auth key are valid before issuing tokens."""
41- stremio_service = StremioService (
42- username = payload .get ("username" ) or "" ,
43- password = payload .get ("password" ) or "" ,
44- auth_key = payload .get ("authKey" ),
45- )
37+ stremio_service = StremioService (auth_key = payload .get ("authKey" ))
4638
4739 try :
48- if payload .get ("authKey" ) and not payload . get ( "username" ) :
40+ if payload .get ("authKey" ):
4941 await stremio_service .get_addons (auth_key = payload ["authKey" ])
5042 return payload ["authKey" ]
51- auth_key = await stremio_service .get_auth_key ()
52- return auth_key
43+ raise ValueError ("Please Login using stremio account to continue!" )
5344 except ValueError as exc :
5445 raise HTTPException (
5546 status_code = 400 ,
@@ -79,21 +70,33 @@ async def _verify_credentials_or_raise(payload: dict) -> str:
7970
8071@router .post ("/" , response_model = TokenResponse )
8172async def create_token (payload : TokenRequest , request : Request ) -> TokenResponse :
82- # Stremio Credentials
83- stremio_username = payload .username .strip () if payload .username else None
84- stremio_password = payload .password
8573 stremio_auth_key = payload .authKey .strip () if payload .authKey else None
8674
87- # Watchly Credentials (The new flow)
88- watchly_username = payload .watchly_username .strip () if payload .watchly_username else None
89- watchly_password = payload .watchly_password
75+ if not stremio_auth_key :
76+ raise HTTPException (status_code = 400 , detail = "Stremio auth key is required." )
77+
78+ # Remove quotes if present
79+ if stremio_auth_key .startswith ('"' ) and stremio_auth_key .endswith ('"' ):
80+ stremio_auth_key = stremio_auth_key [1 :- 1 ].strip ()
9081
9182 rpdb_key = payload .rpdb_key .strip () if payload .rpdb_key else None
9283
93- if stremio_auth_key and stremio_auth_key .startswith ('"' ) and stremio_auth_key .endswith ('"' ):
94- stremio_auth_key = stremio_auth_key [1 :- 1 ].strip ()
84+ # 1. Fetch user info from Stremio (user_id and email)
85+ stremio_service = StremioService (auth_key = stremio_auth_key )
86+ try :
87+ user_info = await stremio_service .get_user_info ()
88+ user_id = user_info ["user_id" ]
89+ email = user_info .get ("email" , "" )
90+ except Exception as e :
91+ raise HTTPException (status_code = 400 , detail = f"Failed to verify Stremio identity: { e } " )
92+ finally :
93+ await stremio_service .close ()
9594
96- # Construct Settings
95+ # 2. Check if user already exists
96+ token = token_store .get_token_from_user_id (user_id )
97+ existing_data = await token_store .get_user_data (token )
98+
99+ # 3. Construct Settings
97100 default_settings = get_default_settings ()
98101
99102 user_settings = UserSettings (
@@ -104,107 +107,44 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
104107 excluded_series_genres = payload .excluded_series_genres ,
105108 )
106109
107- # Logic to handle "Update Mode" (Watchly credentials only)
108- is_update_mode = (watchly_username and watchly_password ) and not (
109- stremio_username or stremio_password or stremio_auth_key
110- )
111-
112- if is_update_mode :
113- # User is trying to update settings using only Watchly credentials
114- # We must retrieve their existing Stremio credentials from the store
115- temp_payload_for_derivation = {
116- "watchly_username" : watchly_username ,
117- "watchly_password" : watchly_password ,
118- "username" : None ,
119- "password" : None ,
120- "authKey" : None ,
121- }
122- derived_token = token_store .derive_token (temp_payload_for_derivation )
123- existing_data = await token_store .get_payload (derived_token )
124-
125- if not existing_data :
126- raise HTTPException (
127- status_code = 404 ,
128- detail = "Account not found. Please start as a New User to connect Stremio." ,
129- )
130-
131- # Hydrate Stremio credentials from existing data
132- stremio_username = existing_data .get ("username" )
133- stremio_password = existing_data .get ("password" )
134- stremio_auth_key = existing_data .get ("authKey" )
135-
136- # Regular Validation Logic
137- if stremio_username and not stremio_password :
138- raise HTTPException (status_code = 400 , detail = "Stremio password is required when username is provided." )
139-
140- if stremio_password and not stremio_username :
141- raise HTTPException (
142- status_code = 400 ,
143- detail = "Stremio username/email is required when password is provided." ,
144- )
145-
146- if not stremio_auth_key and not (stremio_username and stremio_password ):
147- raise HTTPException (
148- status_code = 400 ,
149- detail = "Provide either a Stremio auth key or both Stremio username and password." ,
150- )
151-
152- # if creating a new account, check if the Watchly ID is already taken.
153- if watchly_username and not is_update_mode :
154- derived_token = token_store .derive_token (
155- {"watchly_username" : watchly_username , "watchly_password" : watchly_password }
156- )
157- if await token_store .get_payload (derived_token ):
158- raise HTTPException (
159- status_code = 409 ,
160- detail = "This Watchly ID is already in use. Please choose a different one or log in as an Existing User." , # noqa: E501
161- )
162-
163- # Payload to store includes BOTH Watchly and Stremio credentials + User Settings
110+ # 4. Prepare payload to store
164111 payload_to_store = {
165- "watchly_username" : watchly_username ,
166- "watchly_password" : watchly_password ,
167- "username" : stremio_username ,
168- "password" : stremio_password ,
169112 "authKey" : stremio_auth_key ,
113+ "email" : email ,
170114 "settings" : user_settings .model_dump (),
171115 }
172116
173- verified_auth_key = await _verify_credentials_or_raise ( payload_to_store )
117+ is_new_account = not existing_data
174118
119+ # 5. Verify Stremio connection
120+ verified_auth_key = await _verify_credentials_or_raise ({"authKey" : stremio_auth_key })
121+
122+ # 6. Store user data
175123 try :
176- token , created = await token_store .store_payload ( payload_to_store )
177- logger .info (f"[{ redact_token (token )} ] Token { 'created' if created else 'updated' } " )
124+ token = await token_store .store_user_data ( user_id , payload_to_store )
125+ logger .info (f"[{ redact_token (token )} ] Account { 'created' if is_new_account else 'updated' } for user { user_id } " )
178126 except RuntimeError as exc :
179- logger .error ("Token storage failed: {}" , exc )
180- raise HTTPException (
181- status_code = 500 ,
182- detail = "Server configuration error: TOKEN_SALT must be set to a secure value." ,
183- ) from exc
127+ raise HTTPException (status_code = 500 , detail = "Server configuration error." ) from exc
184128 except (redis_exceptions .RedisError , OSError ) as exc :
185- logger .error ("Token storage unavailable: {}" , exc )
186- raise HTTPException (
187- status_code = 503 ,
188- detail = "Token storage is temporarily unavailable. Please try again once Redis is reachable." ,
189- ) from exc
129+ raise HTTPException (status_code = 503 , detail = "Storage temporarily unavailable." ) from exc
190130
191- if created :
192- try :
193- await refresh_catalogs_for_credentials (
194- payload_to_store , user_settings = user_settings , auth_key = verified_auth_key
195- )
196- except Exception as exc : # pragma: no cover - remote dependency
197- logger .error (f"[{ redact_token (token )} ] Initial catalog refresh failed: {{}}" , exc , exc_info = True )
198- await token_store .delete_token (token = token )
131+ # 7. Refresh Catalogs
132+ try :
133+ await refresh_catalogs_for_credentials (
134+ payload_to_store , user_settings = user_settings , auth_key = verified_auth_key
135+ )
136+ except Exception as exc :
137+ logger .error (f"Catalog refresh failed: { exc } " )
138+ if is_new_account :
139+ # Rollback on new account creation failure
140+ await token_store .delete_token (token )
199141 raise HTTPException (
200142 status_code = 502 ,
201- detail = "Credentials verified, but Watchly couldn't refresh your catalogs yet . Please try again." ,
143+ detail = "Credentials verified, but catalog refresh failed . Please try again." ,
202144 ) from exc
203145
204146 base_url = settings .HOST_NAME
205- # New URL structure (Settings stored in Token)
206147 manifest_url = f"{ base_url } /{ token } /manifest.json"
207-
208148 expires_in = settings .TOKEN_TTL_SECONDS if settings .TOKEN_TTL_SECONDS > 0 else None
209149
210150 return TokenResponse (
@@ -214,78 +154,85 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
214154 )
215155
216156
217- @router .post ("/verify" , status_code = 200 )
218- async def verify_user (payload : TokenRequest ):
219- """Verify if a Watchly user exists."""
220- watchly_username = payload .watchly_username .strip () if payload .watchly_username else None
221- watchly_password = payload .watchly_password
157+ @router .post ("/stremio-identity" , status_code = 200 )
158+ async def check_stremio_identity (payload : TokenRequest ):
159+ """Fetch user info from Stremio and check if account exists."""
160+ auth_key = payload .authKey .strip () if payload .authKey else None
222161
223- if not watchly_username or not watchly_password :
224- raise HTTPException (status_code = 400 , detail = "Watchly username and password required." )
162+ if not auth_key :
163+ raise HTTPException (status_code = 400 , detail = "Auth Key required." )
225164
226- payload_to_derive = {
227- "watchly_username" : watchly_username ,
228- "watchly_password" : watchly_password ,
229- "username" : None ,
230- "password" : None ,
231- "authKey" : None ,
232- }
165+ if auth_key .startswith ('"' ) and auth_key .endswith ('"' ):
166+ auth_key = auth_key [1 :- 1 ].strip ()
233167
234- token = token_store .derive_token (payload_to_derive )
235- exists = await token_store .get_payload (token )
168+ stremio_service = StremioService (auth_key = auth_key )
169+ try :
170+ user_info = await stremio_service .get_user_info ()
171+ user_id = user_info ["user_id" ]
172+ email = user_info .get ("email" , "" )
173+ except Exception as e :
174+ logger .error (f"Stremio identity check failed: { e } " )
175+ raise HTTPException (
176+ status_code = 400 , detail = "Failed to verify Stremio identity. Your auth key might be invalid or expired."
177+ )
178+ finally :
179+ await stremio_service .close ()
180+
181+ # Check existence
182+ try :
183+ token = token_store .get_token_from_user_id (user_id )
184+ user_data = await token_store .get_user_data (token )
185+ exists = bool (user_data )
186+ except ValueError :
187+ exists = False
188+ user_data = None
236189
237- if not exists :
238- raise HTTPException (status_code = 404 , detail = "Account not found." )
190+ response = {"user_id" : user_id , "email" : email , "exists" : exists }
191+ if exists and user_data :
192+ response ["settings" ] = user_data .get ("settings" )
239193
240- return { "found" : True , "token" : token , "settings" : exists . get ( "settings" )}
194+ return response
241195
242196
243197@router .delete ("/" , status_code = 200 )
244198async def delete_token (payload : TokenRequest ):
245- """Delete a token based on provided credentials."""
246- # Stremio Credentials
247- stremio_username = payload .username .strip () if payload .username else None
248- stremio_password = payload .password
199+ """Delete a token based on Stremio auth key."""
249200 stremio_auth_key = payload .authKey .strip () if payload .authKey else None
250201
251- # Watchly Credentials
252- watchly_username = payload .watchly_username .strip () if payload .watchly_username else None
253- watchly_password = payload .watchly_password
254-
255- if stremio_auth_key and stremio_auth_key .startswith ('"' ) and stremio_auth_key .endswith ('"' ):
256- stremio_auth_key = stremio_auth_key [1 :- 1 ].strip ()
257-
258- # Need either Watchly creds OR Stremio creds (for legacy)
259- if (
260- not (watchly_username and watchly_password )
261- and not stremio_auth_key
262- and not (stremio_username and stremio_password )
263- ):
202+ if not stremio_auth_key :
264203 raise HTTPException (
265204 status_code = 400 ,
266- detail = "Provide Watchly credentials (or Stremio credentials for legacy accounts) to delete account." ,
205+ detail = "Stremio auth key is required to delete account." ,
267206 )
268207
269- payload_to_derive = {
270- "watchly_username" : watchly_username ,
271- "watchly_password" : watchly_password ,
272- "username" : stremio_username ,
273- "password" : stremio_password ,
274- "authKey" : stremio_auth_key ,
275- }
208+ if stremio_auth_key .startswith ('"' ) and stremio_auth_key .endswith ('"' ):
209+ stremio_auth_key = stremio_auth_key [1 :- 1 ].strip ()
276210
277211 try :
278- # We don't verify credentials with Stremio here, we just check if we have a token for them.
279- # If the user provides wrong credentials, we'll derive a wrong token, which won't exist in Redis.
280- # That's fine, we can just say "deleted" or "not found".
281- # However, to be nice, we might want to say "Settings deleted" even if they didn't exist.
282- # But if we want to be strict, we could check existence.
283- # Let's just try to delete.
284-
285- token = token_store .derive_token (payload_to_derive )
212+ # Fetch user info from Stremio
213+ stremio_service = StremioService (auth_key = stremio_auth_key )
214+ try :
215+ user_info = await stremio_service .get_user_info ()
216+ user_id = user_info ["user_id" ]
217+ except Exception as e :
218+ raise HTTPException (status_code = 400 , detail = f"Failed to verify Stremio identity: { e } " )
219+ finally :
220+ await stremio_service .close ()
221+
222+ # Get token from user_id
223+ token = token_store .get_token_from_user_id (user_id )
224+
225+ # Verify account exists
226+ existing_data = await token_store .get_user_data (token )
227+ if not existing_data :
228+ raise HTTPException (status_code = 404 , detail = "Account not found." )
229+
230+ # Delete the token
286231 await token_store .delete_token (token )
287- logger .info (f"[{ redact_token (token )} ] Token deleted (if existed) " )
232+ logger .info (f"[{ redact_token (token )} ] Token deleted for user { user_id } " )
288233 return {"detail" : "Settings deleted successfully" }
234+ except HTTPException :
235+ raise
289236 except (redis_exceptions .RedisError , OSError ) as exc :
290237 logger .error ("Token deletion failed: {}" , exc )
291238 raise HTTPException (
0 commit comments