From 1547cbe55ad6593bf2dc1fa6ca821b071bbe205a Mon Sep 17 00:00:00 2001 From: pax Date: Thu, 9 Apr 2026 19:40:09 -0500 Subject: [PATCH] fix: remove early-exit on non-empty tag_categories in ensure path Two places checked `if post.tag_categories: return` before doing a full cache-coverage check, causing posts with partial cache composes (e.g. 5/40 tags from the background prefetch) to get stuck at low coverage forever: ensure_categories: removed the post.tag_categories early exit. Now ALWAYS runs try_compose_from_cache first. Only the 100% coverage return (True) is trusted as "done." Partial composes return False and fall through to the fetch path. _ensure_post_categories_async: removed the post.tag_categories guard. Danbooru/e621 are filtered by the client.category_fetcher is None check instead (they categorize inline, no fetcher). For Gelbooru-style sites, always schedules ensure_categories regardless of current post state. Root cause: the partial-compose fix (try_compose_from_cache populates tag_categories even when cache coverage is <100%) conflicted with the early-exit guards that assumed non-empty tag_categories = fully categorized. Now the only "fully done" signal is try_compose_from_cache returning True (100% coverage). --- booru_viewer/core/api/category_fetcher.py | 16 +++++++++------- booru_viewer/gui/main_window.py | 13 ++++++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/booru_viewer/core/api/category_fetcher.py b/booru_viewer/core/api/category_fetcher.py index 48700a1..a2013eb 100644 --- a/booru_viewer/core/api/category_fetcher.py +++ b/booru_viewer/core/api/category_fetcher.py @@ -289,18 +289,20 @@ class CategoryFetcher: # ----- dispatch: ensure (single post) ----- async def ensure_categories(self, post: "Post") -> None: - """Idempotent. Guarantee ``post.tag_categories`` is populated. + """Guarantee ``post.tag_categories`` is fully populated. Dispatch: - 1. Already populated → return. - 2. Cache compose → return if complete. - 3. Batch tag API (if available + probe passed) → return. - 4. Per-post HTML scrape → return. + 1. Cache compose with 100% coverage → return. + 2. Batch tag API (if available + probe passed) → return. + 3. Per-post HTML scrape → return. + + Does NOT short-circuit on non-empty ``post.tag_categories`` + because partial cache composes can leave the post at e.g. + 5/40 coverage. Only the 100%-coverage return from + ``try_compose_from_cache`` is trusted as "done." Coalesces concurrent calls for the same ``post.id``. """ - if post.tag_categories: - return if self.try_compose_from_cache(post): return diff --git a/booru_viewer/gui/main_window.py b/booru_viewer/gui/main_window.py index e2ce488..d15f278 100644 --- a/booru_viewer/gui/main_window.py +++ b/booru_viewer/gui/main_window.py @@ -161,13 +161,16 @@ class BooruApp(QMainWindow): return client.category_fetcher if client else None def _ensure_post_categories_async(self, post) -> None: - """Schedule an async ensure_categories if the post needs it. + """Schedule an async ensure_categories for the post. - No-op if the post already has categories, or if the active - client doesn't have a CategoryFetcher attached. + No-op if the active client doesn't have a CategoryFetcher + (Danbooru/e621 categorize inline, no fetcher needed). + + Does NOT check post.tag_categories — partial cache composes + from the background prefetch can leave the post at e.g. + 5/40 coverage. ensure_categories checks 100% cache coverage + internally and fetches the remainder if needed. """ - if post.tag_categories: - return client = self._make_client() if client is None or client.category_fetcher is None: return