@@ -153,6 +153,16 @@ def _do_refresh(self) -> bool:
153153 # Scrobble POST (with offline queue fallback)
154154 # ------------------------------------------------------------------
155155
156+ def _should_drop_client_error (self , status_code : int ) -> bool :
157+ """
158+ Return True when a client error is permanent and retrying will not help.
159+
160+ 401 is handled specially: if a queued replay still gets a 401 after the
161+ built-in refresh attempt, keep the event so it can be retried after the
162+ user logs in again.
163+ """
164+ return 400 <= status_code < 500 and status_code != 401
165+
156166 def post (self , path : str , payload : dict [str , Any ]) -> dict [str , Any ] | None :
157167 """
158168 POST *payload* to *path*. On network error, writes the event to the
@@ -177,6 +187,13 @@ def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any] | None:
177187 )
178188 if self ._cache is not None :
179189 self ._cache .enqueue_scrobble (path , payload )
190+ elif not self ._should_drop_client_error (exc .code ):
191+ xbmc .log (
192+ f"[PunchPlay] HTTP { exc .code } on { path } — preserving for retry" ,
193+ xbmc .LOGWARNING ,
194+ )
195+ if self ._cache is not None :
196+ self ._cache .enqueue_scrobble (path , payload )
180197 else :
181198 # Permanent client error (4xx) — drop, retrying won't help.
182199 xbmc .log (
@@ -211,13 +228,19 @@ def flush_queue(self) -> None:
211228 xbmc .log ("[PunchPlay] Still offline — stopping queue flush" , xbmc .LOGDEBUG )
212229 break # remain offline; try again later
213230 except urllib .error .HTTPError as exc :
231+ if self ._should_drop_client_error (exc .code ):
232+ xbmc .log (
233+ f"[PunchPlay] HTTP { exc .code } replaying id={ scrobble_id } — dropping" ,
234+ xbmc .LOGWARNING ,
235+ )
236+ self ._cache .delete_pending_scrobble (scrobble_id )
237+ continue
238+
214239 xbmc .log (
215- f"[PunchPlay] HTTP { exc .code } replaying id={ scrobble_id } — dropping " ,
240+ f"[PunchPlay] HTTP { exc .code } replaying id={ scrobble_id } — keeping queued " ,
216241 xbmc .LOGWARNING ,
217242 )
218- # Drop unrecoverable server errors (4xx) so they don't block the queue.
219- if 400 <= exc .code < 500 :
220- self ._cache .delete_pending_scrobble (scrobble_id )
243+ break
221244
222245 # ------------------------------------------------------------------
223246 # Device-code login
@@ -228,6 +251,8 @@ def device_code_login(self) -> bool:
228251 Run the full device-code OAuth flow with Kodi dialogs.
229252 Returns True on success, False on failure/cancellation.
230253 """
254+ addon = xbmcaddon .Addon (_ADDON_ID )
255+ _s = addon .getLocalizedString
231256 dialog = xbmcgui .Dialog ()
232257
233258 # Step 1 — request a device code.
@@ -236,7 +261,7 @@ def device_code_login(self) -> bool:
236261 "POST" , "/api/auth/device/code" , {}, retry_on_401 = False
237262 )
238263 except Exception as exc :
239- dialog .ok ("PunchPlay — Login" , f"Could not reach the server: \n { exc } " )
264+ dialog .ok (_s ( 32000 ) , f"{ _s ( 32001 ) } \n { exc } " )
240265 return False
241266
242267 user_code = resp .get ("user_code" , "" )
@@ -245,27 +270,26 @@ def device_code_login(self) -> bool:
245270 expires_in : int = int (resp .get ("expires_in" , 600 ))
246271
247272 if not user_code or not device_code :
248- dialog .ok ("PunchPlay — Login" , "Invalid response from server. Try again." )
273+ dialog .ok (_s ( 32000 ), _s ( 32002 ) )
249274 return False
250275
251276 # Step 2 — show the code to the user.
252277 dialog .ok (
253- "PunchPlay — Login" ,
278+ _s ( 32000 ) ,
254279 (
255- f"Open your browser and visit: \n "
280+ f"{ _s ( 32003 ) } \n "
256281 f"[B]{ verification_uri } [/B]\n \n "
257- f"Enter this code: \n "
282+ f"{ _s ( 32004 ) } \n "
258283 f"[B]{ user_code } [/B]\n \n "
259- f"The code expires in [B]{ expires_in // 60 } [/B] minutes.\n "
260- f"Press OK — then this dialog will wait for approval."
284+ + _s (32005 ).format (expires_in // 60 )
261285 ),
262286 )
263287
264288 # Step 3 — poll for the token with a cancellable progress dialog.
265289 monitor = xbmc .Monitor ()
266290 deadline = time .monotonic () + expires_in
267291 progress = xbmcgui .DialogProgress ()
268- progress .create ("PunchPlay — Waiting for Login" , "Waiting for approval…" )
292+ progress .create (_s ( 32006 ), _s ( 32007 ) )
269293
270294 try :
271295 while time .monotonic () < deadline and not monitor .abortRequested ():
@@ -275,7 +299,7 @@ def device_code_login(self) -> bool:
275299
276300 remaining = max (0 , int (deadline - time .monotonic ()))
277301 pct = int (100 * (1 - remaining / expires_in ))
278- progress .update (pct , f"Waiting for approval… ( { remaining } s remaining)" )
302+ progress .update (pct , _s ( 32008 ). format ( remaining ) )
279303
280304 try :
281305 token_resp = self ._request (
@@ -293,17 +317,14 @@ def device_code_login(self) -> bool:
293317 self ._save_tokens (token_resp )
294318 xbmc .log ("[PunchPlay] Device-code login succeeded" , xbmc .LOGINFO )
295319 xbmcgui .Dialog ().notification (
296- "PunchPlay" ,
297- "Login successful!" ,
298- xbmcgui .NOTIFICATION_INFO ,
299- 4000 ,
320+ "PunchPlay" , _s (32011 ), xbmcgui .NOTIFICATION_INFO , 4000
300321 )
301322 return True
302323
303324 error = token_resp .get ("error" , "" )
304325 if error in ("expired" , "access_denied" ):
305326 progress .close ()
306- dialog .ok ("PunchPlay — Login" , f"Login failed ( { error } ). Please try again." )
327+ dialog .ok (_s ( 32000 ), _s ( 32009 ). format ( error ) )
307328 return False
308329 # 'authorization_pending' or 'slow_down' → keep polling.
309330
@@ -319,7 +340,7 @@ def device_code_login(self) -> bool:
319340 except Exception :
320341 pass
321342
322- dialog .ok ("PunchPlay — Login" , "Login timed out. Please try again." )
343+ dialog .ok (_s ( 32000 ), _s ( 32010 ) )
323344 return False
324345
325346 # ------------------------------------------------------------------
@@ -333,7 +354,8 @@ def logout(self) -> None:
333354 self ._tokens = {}
334355 xbmc .log ("[PunchPlay] Tokens cleared (logged out)" , xbmc .LOGINFO )
335356 xbmcgui .Dialog ().notification (
336- "PunchPlay" , "Logged out." , xbmcgui .NOTIFICATION_INFO , 3000
357+ "PunchPlay" , xbmcaddon .Addon (_ADDON_ID ).getLocalizedString (32012 ),
358+ xbmcgui .NOTIFICATION_INFO , 3000
337359 )
338360
339361 # ------------------------------------------------------------------
0 commit comments