fix(anilist): paginate large libraries to avoid CPU budget kills (#184)

* fix(anilist): paginate large libraries to avoid CPU budget kills

Chunk GraphQL fetches and defer merge, sort, and cover work to update()
ticks so 1000+ anime and manga entries load without blocking callbacks.

* fix(anilist): handle OAuth callback immediately and simplify service helpers

Process browser login tokens synchronously from runStream and file poll, keep a 500ms update interval during OAuth, and avoid duplicate OAuth result emissions from Python.
This commit is contained in:
Cleboost
2026-07-31 17:12:24 -04:00
committed by GitHub
parent dffd46ab4d
commit be208b42c5
7 changed files with 682 additions and 198 deletions
+24 -7
View File
@@ -15,6 +15,7 @@ local visibleRowLimit = INITIAL_VISIBLE_ROWS
local snapshot = noctalia.state.get("anilist_snapshot") or {
revision = 0,
oauthLoading = false,
loading = false,
refreshing = false,
busy = false,
@@ -22,6 +23,7 @@ local snapshot = noctalia.state.get("anilist_snapshot") or {
viewer = nil,
anime = {},
manga = {},
loadProgress = nil,
}
local mediaTab = "ANIME"
@@ -229,6 +231,17 @@ local function settingsButton()
})
end
local function loadProgressText()
local progress = snapshot.loadProgress
if type(progress) ~= "table" then
return tr("loading")
end
return tr("loading_progress", {
anime = progress.animeLoaded or 0,
manga = progress.mangaLoaded or 0,
})
end
local function renderLogin()
local children = {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
@@ -239,8 +252,10 @@ local function renderLogin()
ui.label({ text = tr("login_help"), fontSize = 12, color = "on_surface_variant", maxLines = 8 }),
}
if snapshot.loading then
if snapshot.oauthLoading then
table.insert(children, ui.label({ text = tr("login_waiting"), color = "primary", fontSize = 13 }))
elseif snapshot.loading then
table.insert(children, ui.label({ text = loadProgressText(), color = "primary", fontSize = 13 }))
else
table.insert(children, ui.button({
text = tr("connect"),
@@ -578,17 +593,17 @@ local function renderLibrary()
}),
}))
if snapshot.loading and not snapshot.viewer then
table.insert(children, ui.label({ text = tr("loading"), color = "on_surface_variant", padding = { top = 12 } }))
if snapshot.loading then
table.insert(children, ui.label({ text = loadProgressText(), color = "on_surface_variant", padding = { top = 12 } }))
elseif snapshot.error ~= "" then
table.insert(children, ui.label({ text = tr("error", { message = snapshot.error }), color = "error", padding = { top = 12 } }))
elseif snapshot.refreshing then
elseif snapshot.refreshing and #rows == 0 then
table.insert(children, ui.label({ text = tr("refreshing"), color = "on_surface_variant", padding = { top = 8 } }))
elseif snapshot.busy then
table.insert(children, ui.label({ text = tr("updating"), color = "on_surface_variant", padding = { top = 8 } }))
end
if (not snapshot.loading or snapshot.viewer) and #rows == 0 then
if not snapshot.loading and #rows == 0 then
table.insert(children, ui.label({ text = tr("empty"), color = "on_surface_variant", padding = { top = 12 } }))
else
local listChildren = {}
@@ -647,7 +662,7 @@ function onOpen(_context)
noctalia.state.set("anilist_open", true)
resetListView()
if snapshot.viewer and snapshot.viewer.id then
sendCommand("refresh", { mediaType = mediaTab })
sendCommand("refresh", { mediaType = mediaTab, silent = true })
end
dirty = true
render()
@@ -656,7 +671,9 @@ end
function onClose()
coverPreview = nil
noctalia.state.set("anilist_open", false)
noctalia.setUpdateInterval(LIST_IDLE_INTERVAL_MS)
if not snapshot.oauthLoading then
noctalia.setUpdateInterval(LIST_IDLE_INTERVAL_MS)
end
end
function update()
+1 -1
View File
@@ -1,6 +1,6 @@
id = "cleboost/anilist"
name = "AniList (UNOFFICIAL)"
version = "1.1.0"
version = "1.1.1"
plugin_api = 15
author = "Cleboost"
license = "MIT"
+17 -2
View File
@@ -113,9 +113,15 @@ def main() -> int:
emit({"ok": False, "error": str(exc)}, result_path)
return 1
result: dict[str, str] = {"status": "pending"}
result: dict[str, str | bool] = {"status": "pending"}
emitted = False
done = threading.Event()
def report(payload: dict) -> None:
nonlocal emitted
emit(payload, result_path)
emitted = True
class CallbackHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802
if done.is_set():
@@ -132,6 +138,7 @@ def main() -> int:
if error:
result["status"] = "error"
result["error"] = str(error)
report({"ok": False, "error": str(error)})
self._success_page("Login failed", "Return to Noctalia and try again.")
done.set()
return
@@ -140,6 +147,7 @@ def main() -> int:
if not code:
result["status"] = "error"
result["error"] = "missing authorization code"
report({"ok": False, "error": "missing authorization code"})
self._success_page("Login failed", "No authorization code was received.")
done.set()
return
@@ -147,20 +155,24 @@ def main() -> int:
try:
token = exchange_code(client_id, client_secret, code)
except urllib.error.HTTPError as exc:
message = format_http_error(exc)
result["status"] = "error"
result["error"] = format_http_error(exc)
result["error"] = message
report({"ok": False, "error": message})
self._success_page("Login failed", "Could not finish login. Return to Noctalia and try again.")
done.set()
return
except Exception as exc: # noqa: BLE001
result["status"] = "error"
result["error"] = str(exc)
report({"ok": False, "error": str(exc)})
self._success_page("Login failed", "Could not finish login. Return to Noctalia and try again.")
done.set()
return
result["status"] = "ok"
result["access_token"] = token
report({"ok": True, "access_token": token})
self._success_page(
"Connected to AniList",
"You can close this tab and return to Noctalia.",
@@ -233,6 +245,9 @@ def main() -> int:
server.server_close()
if emitted:
return 0 if result.get("status") == "ok" else 1
if result.get("status") == "ok" and result.get("access_token"):
emit({"ok": True, "access_token": result["access_token"]}, result_path)
return 0
+630 -186
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -23,6 +23,7 @@
"increment": "Next episode/chapter",
"loading": "Loading your lists…",
"loading_more": "Loading more entries…",
"loading_progress": "Loading lists… {anime} anime, {manga} manga",
"logged_in_as": "Signed in as {name}",
"login_help": "Set your client ID and client secret in plugin settings, then click Connect. Your browser opens, you approve AniList, and the plugin finishes login automatically.",
"login_title": "Connect to AniList",
@@ -44,6 +45,8 @@
"updating": "Updating…"
},
"service": {
"empty_response": "AniList returned an empty response.",
"invalid_response": "AniList returned an unreadable response.",
"invalid_token": "Invalid or expired access token.",
"mutation_failed": "Update failed.",
"network_error": "Could not reach AniList.",
@@ -87,6 +90,7 @@
"tooltip_error": "AniList — {error}",
"tooltip_in_progress": "{count} in progress (anime + manga)",
"tooltip_loading": "AniList — loading…",
"tooltip_oauth": "AniList — waiting for browser login…",
"tooltip_planning": "{count} anime planned",
"tooltip_total": "{count} anime on your list"
}
+4
View File
@@ -23,6 +23,7 @@
"increment": "Épisode ou chapitre suivant",
"loading": "Chargement de vos listes…",
"loading_more": "Chargement de la liste…",
"loading_progress": "Chargement… {anime} anime, {manga} manga",
"logged_in_as": "Connecté en tant que {name}",
"login_help": "Renseignez votre ID client et votre secret client dans les paramètres du plugin, puis cliquez sur Se connecter. Votre navigateur s'ouvre, vous autorisez AniList, et le plugin termine la connexion automatiquement.",
"login_title": "Se connecter à AniList",
@@ -44,6 +45,8 @@
"updating": "Mise à jour…"
},
"service": {
"empty_response": "AniList a renvoyé une réponse vide.",
"invalid_response": "AniList a renvoyé une réponse illisible.",
"invalid_token": "Jeton d'accès invalide ou expiré.",
"mutation_failed": "La mise à jour a échoué.",
"network_error": "Impossible de joindre AniList.",
@@ -87,6 +90,7 @@
"tooltip_error": "AniList — {error}",
"tooltip_in_progress": "{count} en cours (anime + manga)",
"tooltip_loading": "AniList — chargement…",
"tooltip_oauth": "AniList — en attente de la connexion navigateur…",
"tooltip_planning": "{count} anime prévus",
"tooltip_total": "{count} anime sur votre liste"
}
+2 -2
View File
@@ -76,9 +76,9 @@ local function render()
barWidget.setGlyph(glyph)
barWidget.setGlyphColor(if open then "primary" else "on_surface")
if snapshot.loading then
if snapshot.oauthLoading or snapshot.loading then
barWidget.setText("")
barWidget.setTooltip(tr("tooltip_loading"))
barWidget.setTooltip(if snapshot.oauthLoading then tr("tooltip_oauth") else tr("tooltip_loading"))
return
end