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:
+23
-6
@@ -15,6 +15,7 @@ local visibleRowLimit = INITIAL_VISIBLE_ROWS
|
|||||||
|
|
||||||
local snapshot = noctalia.state.get("anilist_snapshot") or {
|
local snapshot = noctalia.state.get("anilist_snapshot") or {
|
||||||
revision = 0,
|
revision = 0,
|
||||||
|
oauthLoading = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
refreshing = false,
|
refreshing = false,
|
||||||
busy = false,
|
busy = false,
|
||||||
@@ -22,6 +23,7 @@ local snapshot = noctalia.state.get("anilist_snapshot") or {
|
|||||||
viewer = nil,
|
viewer = nil,
|
||||||
anime = {},
|
anime = {},
|
||||||
manga = {},
|
manga = {},
|
||||||
|
loadProgress = nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
local mediaTab = "ANIME"
|
local mediaTab = "ANIME"
|
||||||
@@ -229,6 +231,17 @@ local function settingsButton()
|
|||||||
})
|
})
|
||||||
end
|
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 function renderLogin()
|
||||||
local children = {
|
local children = {
|
||||||
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
|
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 }),
|
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 }))
|
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
|
else
|
||||||
table.insert(children, ui.button({
|
table.insert(children, ui.button({
|
||||||
text = tr("connect"),
|
text = tr("connect"),
|
||||||
@@ -578,17 +593,17 @@ local function renderLibrary()
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if snapshot.loading and not snapshot.viewer then
|
if snapshot.loading then
|
||||||
table.insert(children, ui.label({ text = tr("loading"), color = "on_surface_variant", padding = { top = 12 } }))
|
table.insert(children, ui.label({ text = loadProgressText(), color = "on_surface_variant", padding = { top = 12 } }))
|
||||||
elseif snapshot.error ~= "" then
|
elseif snapshot.error ~= "" then
|
||||||
table.insert(children, ui.label({ text = tr("error", { message = snapshot.error }), color = "error", padding = { top = 12 } }))
|
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 } }))
|
table.insert(children, ui.label({ text = tr("refreshing"), color = "on_surface_variant", padding = { top = 8 } }))
|
||||||
elseif snapshot.busy then
|
elseif snapshot.busy then
|
||||||
table.insert(children, ui.label({ text = tr("updating"), color = "on_surface_variant", padding = { top = 8 } }))
|
table.insert(children, ui.label({ text = tr("updating"), color = "on_surface_variant", padding = { top = 8 } }))
|
||||||
end
|
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 } }))
|
table.insert(children, ui.label({ text = tr("empty"), color = "on_surface_variant", padding = { top = 12 } }))
|
||||||
else
|
else
|
||||||
local listChildren = {}
|
local listChildren = {}
|
||||||
@@ -647,7 +662,7 @@ function onOpen(_context)
|
|||||||
noctalia.state.set("anilist_open", true)
|
noctalia.state.set("anilist_open", true)
|
||||||
resetListView()
|
resetListView()
|
||||||
if snapshot.viewer and snapshot.viewer.id then
|
if snapshot.viewer and snapshot.viewer.id then
|
||||||
sendCommand("refresh", { mediaType = mediaTab })
|
sendCommand("refresh", { mediaType = mediaTab, silent = true })
|
||||||
end
|
end
|
||||||
dirty = true
|
dirty = true
|
||||||
render()
|
render()
|
||||||
@@ -656,8 +671,10 @@ end
|
|||||||
function onClose()
|
function onClose()
|
||||||
coverPreview = nil
|
coverPreview = nil
|
||||||
noctalia.state.set("anilist_open", false)
|
noctalia.state.set("anilist_open", false)
|
||||||
|
if not snapshot.oauthLoading then
|
||||||
noctalia.setUpdateInterval(LIST_IDLE_INTERVAL_MS)
|
noctalia.setUpdateInterval(LIST_IDLE_INTERVAL_MS)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function update()
|
function update()
|
||||||
if snapshot.viewer and snapshot.viewer.id then
|
if snapshot.viewer and snapshot.viewer.id then
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
id = "cleboost/anilist"
|
id = "cleboost/anilist"
|
||||||
name = "AniList (UNOFFICIAL)"
|
name = "AniList (UNOFFICIAL)"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
plugin_api = 15
|
plugin_api = 15
|
||||||
author = "Cleboost"
|
author = "Cleboost"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -113,9 +113,15 @@ def main() -> int:
|
|||||||
emit({"ok": False, "error": str(exc)}, result_path)
|
emit({"ok": False, "error": str(exc)}, result_path)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
result: dict[str, str] = {"status": "pending"}
|
result: dict[str, str | bool] = {"status": "pending"}
|
||||||
|
emitted = False
|
||||||
done = threading.Event()
|
done = threading.Event()
|
||||||
|
|
||||||
|
def report(payload: dict) -> None:
|
||||||
|
nonlocal emitted
|
||||||
|
emit(payload, result_path)
|
||||||
|
emitted = True
|
||||||
|
|
||||||
class CallbackHandler(BaseHTTPRequestHandler):
|
class CallbackHandler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self) -> None: # noqa: N802
|
def do_GET(self) -> None: # noqa: N802
|
||||||
if done.is_set():
|
if done.is_set():
|
||||||
@@ -132,6 +138,7 @@ def main() -> int:
|
|||||||
if error:
|
if error:
|
||||||
result["status"] = "error"
|
result["status"] = "error"
|
||||||
result["error"] = str(error)
|
result["error"] = str(error)
|
||||||
|
report({"ok": False, "error": str(error)})
|
||||||
self._success_page("Login failed", "Return to Noctalia and try again.")
|
self._success_page("Login failed", "Return to Noctalia and try again.")
|
||||||
done.set()
|
done.set()
|
||||||
return
|
return
|
||||||
@@ -140,6 +147,7 @@ def main() -> int:
|
|||||||
if not code:
|
if not code:
|
||||||
result["status"] = "error"
|
result["status"] = "error"
|
||||||
result["error"] = "missing authorization code"
|
result["error"] = "missing authorization code"
|
||||||
|
report({"ok": False, "error": "missing authorization code"})
|
||||||
self._success_page("Login failed", "No authorization code was received.")
|
self._success_page("Login failed", "No authorization code was received.")
|
||||||
done.set()
|
done.set()
|
||||||
return
|
return
|
||||||
@@ -147,20 +155,24 @@ def main() -> int:
|
|||||||
try:
|
try:
|
||||||
token = exchange_code(client_id, client_secret, code)
|
token = exchange_code(client_id, client_secret, code)
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
|
message = format_http_error(exc)
|
||||||
result["status"] = "error"
|
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.")
|
self._success_page("Login failed", "Could not finish login. Return to Noctalia and try again.")
|
||||||
done.set()
|
done.set()
|
||||||
return
|
return
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
result["status"] = "error"
|
result["status"] = "error"
|
||||||
result["error"] = str(exc)
|
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.")
|
self._success_page("Login failed", "Could not finish login. Return to Noctalia and try again.")
|
||||||
done.set()
|
done.set()
|
||||||
return
|
return
|
||||||
|
|
||||||
result["status"] = "ok"
|
result["status"] = "ok"
|
||||||
result["access_token"] = token
|
result["access_token"] = token
|
||||||
|
report({"ok": True, "access_token": token})
|
||||||
self._success_page(
|
self._success_page(
|
||||||
"Connected to AniList",
|
"Connected to AniList",
|
||||||
"You can close this tab and return to Noctalia.",
|
"You can close this tab and return to Noctalia.",
|
||||||
@@ -233,6 +245,9 @@ def main() -> int:
|
|||||||
|
|
||||||
server.server_close()
|
server.server_close()
|
||||||
|
|
||||||
|
if emitted:
|
||||||
|
return 0 if result.get("status") == "ok" else 1
|
||||||
|
|
||||||
if result.get("status") == "ok" and result.get("access_token"):
|
if result.get("status") == "ok" and result.get("access_token"):
|
||||||
emit({"ok": True, "access_token": result["access_token"]}, result_path)
|
emit({"ok": True, "access_token": result["access_token"]}, result_path)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+630
-186
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
"increment": "Next episode/chapter",
|
"increment": "Next episode/chapter",
|
||||||
"loading": "Loading your lists…",
|
"loading": "Loading your lists…",
|
||||||
"loading_more": "Loading more entries…",
|
"loading_more": "Loading more entries…",
|
||||||
|
"loading_progress": "Loading lists… {anime} anime, {manga} manga",
|
||||||
"logged_in_as": "Signed in as {name}",
|
"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_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",
|
"login_title": "Connect to AniList",
|
||||||
@@ -44,6 +45,8 @@
|
|||||||
"updating": "Updating…"
|
"updating": "Updating…"
|
||||||
},
|
},
|
||||||
"service": {
|
"service": {
|
||||||
|
"empty_response": "AniList returned an empty response.",
|
||||||
|
"invalid_response": "AniList returned an unreadable response.",
|
||||||
"invalid_token": "Invalid or expired access token.",
|
"invalid_token": "Invalid or expired access token.",
|
||||||
"mutation_failed": "Update failed.",
|
"mutation_failed": "Update failed.",
|
||||||
"network_error": "Could not reach AniList.",
|
"network_error": "Could not reach AniList.",
|
||||||
@@ -87,6 +90,7 @@
|
|||||||
"tooltip_error": "AniList — {error}",
|
"tooltip_error": "AniList — {error}",
|
||||||
"tooltip_in_progress": "{count} in progress (anime + manga)",
|
"tooltip_in_progress": "{count} in progress (anime + manga)",
|
||||||
"tooltip_loading": "AniList — loading…",
|
"tooltip_loading": "AniList — loading…",
|
||||||
|
"tooltip_oauth": "AniList — waiting for browser login…",
|
||||||
"tooltip_planning": "{count} anime planned",
|
"tooltip_planning": "{count} anime planned",
|
||||||
"tooltip_total": "{count} anime on your list"
|
"tooltip_total": "{count} anime on your list"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"increment": "Épisode ou chapitre suivant",
|
"increment": "Épisode ou chapitre suivant",
|
||||||
"loading": "Chargement de vos listes…",
|
"loading": "Chargement de vos listes…",
|
||||||
"loading_more": "Chargement de la liste…",
|
"loading_more": "Chargement de la liste…",
|
||||||
|
"loading_progress": "Chargement… {anime} anime, {manga} manga",
|
||||||
"logged_in_as": "Connecté en tant que {name}",
|
"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_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",
|
"login_title": "Se connecter à AniList",
|
||||||
@@ -44,6 +45,8 @@
|
|||||||
"updating": "Mise à jour…"
|
"updating": "Mise à jour…"
|
||||||
},
|
},
|
||||||
"service": {
|
"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é.",
|
"invalid_token": "Jeton d'accès invalide ou expiré.",
|
||||||
"mutation_failed": "La mise à jour a échoué.",
|
"mutation_failed": "La mise à jour a échoué.",
|
||||||
"network_error": "Impossible de joindre AniList.",
|
"network_error": "Impossible de joindre AniList.",
|
||||||
@@ -87,6 +90,7 @@
|
|||||||
"tooltip_error": "AniList — {error}",
|
"tooltip_error": "AniList — {error}",
|
||||||
"tooltip_in_progress": "{count} en cours (anime + manga)",
|
"tooltip_in_progress": "{count} en cours (anime + manga)",
|
||||||
"tooltip_loading": "AniList — chargement…",
|
"tooltip_loading": "AniList — chargement…",
|
||||||
|
"tooltip_oauth": "AniList — en attente de la connexion navigateur…",
|
||||||
"tooltip_planning": "{count} anime prévus",
|
"tooltip_planning": "{count} anime prévus",
|
||||||
"tooltip_total": "{count} anime sur votre liste"
|
"tooltip_total": "{count} anime sur votre liste"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -76,9 +76,9 @@ local function render()
|
|||||||
barWidget.setGlyph(glyph)
|
barWidget.setGlyph(glyph)
|
||||||
barWidget.setGlyphColor(if open then "primary" else "on_surface")
|
barWidget.setGlyphColor(if open then "primary" else "on_surface")
|
||||||
|
|
||||||
if snapshot.loading then
|
if snapshot.oauthLoading or snapshot.loading then
|
||||||
barWidget.setText("")
|
barWidget.setText("")
|
||||||
barWidget.setTooltip(tr("tooltip_loading"))
|
barWidget.setTooltip(if snapshot.oauthLoading then tr("tooltip_oauth") else tr("tooltip_loading"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user