Skip to content

Commit d64fd6f

Browse files
committed
[script.punchplay] 1.0.0
1 parent 9ffbff4 commit d64fd6f

File tree

5 files changed

+118
-24
lines changed

5 files changed

+118
-24
lines changed

script.punchplay/api.py

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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
# ------------------------------------------------------------------

script.punchplay/fanart.jpg

-135 KB
Loading

script.punchplay/player.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,14 +317,18 @@ def _emit_stop(self, settings: dict[str, Any]) -> None:
317317
)
318318
self._api.post("/api/scrobble/stop", payload)
319319
if watched:
320-
title = self._metadata.get("title", "Unknown")
320+
_s = xbmcaddon.Addon(_ADDON_ID).getLocalizedString
321+
title = self._metadata.get("title", "")
321322
media_type = self._metadata.get("media_type", "movie")
322323
if media_type == "episode":
323324
season = self._metadata.get("season")
324325
episode = self._metadata.get("episode")
325-
msg = f"✓ {title} S{season:02d}E{episode:02d} scrobbled"
326+
if isinstance(season, int) and isinstance(episode, int):
327+
msg = _s(32014).format(title, f"{season:02d}", f"{episode:02d}")
328+
else:
329+
msg = _s(32013).format(title)
326330
else:
327-
msg = f"✓ {title} scrobbled"
331+
msg = _s(32013).format(title)
328332
self._notify(msg, settings)
329333
except Exception as exc:
330334
xbmc.log(f"[PunchPlay] Stop emit error: {exc}", xbmc.LOGDEBUG)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PunchPlay Scrobble
2+
msgid ""
3+
msgstr ""
4+
"Content-Type: text/plain; charset=UTF-8\n"
5+
"Content-Transfer-Encoding: 8bit\n"
6+
"Language: en_GB\n"
7+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
8+
9+
msgctxt "#32000"
10+
msgid "PunchPlay — Login"
11+
msgstr ""
12+
13+
msgctxt "#32001"
14+
msgid "Could not reach the server:"
15+
msgstr ""
16+
17+
msgctxt "#32002"
18+
msgid "Invalid response from server. Try again."
19+
msgstr ""
20+
21+
msgctxt "#32003"
22+
msgid "Open your browser and visit:"
23+
msgstr ""
24+
25+
msgctxt "#32004"
26+
msgid "Enter this code:"
27+
msgstr ""
28+
29+
msgctxt "#32005"
30+
msgid "The code expires in {0} minutes. Press OK — then this dialog will wait for approval."
31+
msgstr ""
32+
33+
msgctxt "#32006"
34+
msgid "PunchPlay — Waiting for Login"
35+
msgstr ""
36+
37+
msgctxt "#32007"
38+
msgid "Waiting for approval…"
39+
msgstr ""
40+
41+
msgctxt "#32008"
42+
msgid "Waiting for approval… ({0}s remaining)"
43+
msgstr ""
44+
45+
msgctxt "#32009"
46+
msgid "Login failed ({0}). Please try again."
47+
msgstr ""
48+
49+
msgctxt "#32010"
50+
msgid "Login timed out. Please try again."
51+
msgstr ""
52+
53+
msgctxt "#32011"
54+
msgid "Login successful!"
55+
msgstr ""
56+
57+
msgctxt "#32012"
58+
msgid "Logged out."
59+
msgstr ""
60+
61+
msgctxt "#32013"
62+
msgid "{0} scrobbled"
63+
msgstr ""
64+
65+
msgctxt "#32014"
66+
msgid "{0} S{1}E{2} scrobbled"
67+
msgstr ""

script.punchplay/service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ def run(self) -> None:
8181
self._api.flush_queue()
8282
except Exception as exc:
8383
xbmc.log(f"[PunchPlay] Queue flush error: {exc}", xbmc.LOGDEBUG)
84-
self._last_flush = now
84+
else:
85+
self._last_flush = now
8586

8687
# Prune stale identifier cache entries once a day.
8788
if now - self._last_prune >= _PRUNE_INTERVAL:

0 commit comments

Comments
 (0)