duplicate entry fix
Build and Deploy / build (push) Successful in 34s
Build and Deploy / deploy (push) Successful in 36s

This commit is contained in:
2026-07-05 09:06:51 +02:00
parent be2c227e1d
commit 33d7bd9590
2 changed files with 73 additions and 21 deletions
+36 -8
View File
@@ -196,11 +196,20 @@ class ComicInfoBuilder:
mal_resolver: "MALResolver | None" = None,
al_resolver: "AniListResolver | None" = None,
matches_cache: "MatchesCache | None" = None,
cover_cache: "CoverCache | None" = None):
cover_cache: "CoverCache | None" = None,
match_title: "str | None" = None):
if not manga_title or not str(manga_title).strip():
raise ValueError("manga_title must not be empty.")
self._manga_title = str(manga_title).strip()
# matches.json identity key. The mover passes the *folder name*
# here so the cache key stays stable across runs — manga_title (the
# ComicInfo <Series> title) is only used as the search query and can
# differ from the folder (Windows-illegal chars are substituted in
# folder names, e.g. "Foo" vs [Foo]).
self._match_title = (str(match_title).strip()
if match_title and str(match_title).strip()
else None)
self._chapter = chapter
self.api_base_url = api_base_url.rstrip("/")
@@ -258,6 +267,11 @@ class ComicInfoBuilder:
def manga_title(self) -> str:
return self._manga_title
@property
def match_title(self) -> str:
"""matches.json key; falls back to manga_title when not set."""
return self._match_title or self._manga_title
@manga_title.setter
def manga_title(self, value):
value = str(value).strip()
@@ -412,19 +426,33 @@ class ComicInfoBuilder:
Resolves `title` to a MangaBaka series.
Lookup order:
1. matches.json cache (if attached) — uses the stored series ID
to fetch the full series, skipping the search step entirely.
2. Fresh MangaBaka search — top hit. The match is persisted to
matches.json before being returned so it survives a crash.
1. matches.json cache (if attached) — keyed by `match_title` (the
stable folder name); uses the stored series ID to fetch the
full series, skipping the search step entirely.
2. Fresh MangaBaka search with `title` (the cleaned ComicInfo
series title — better search hits than folder names with
substituted characters). A genuinely new match is persisted
under `match_title` so it survives a crash.
"""
cache_key = self.match_title
if self._matches_cache is not None:
cached = self._matches_cache.get(title)
cached = self._matches_cache.get(cache_key)
if cached is None and cache_key != title:
# Migration: older versions keyed entries by the (cleaned)
# ComicInfo series title, which drifts from the folder name
# when it contains Windows-illegal characters. Move such an
# entry onto the stable folder-name key.
if self._matches_cache.get(title):
self._matches_cache.rename(title, cache_key)
cached = self._matches_cache.get(cache_key)
print(f"[ComicInfoBuilder] migrated match key "
f"{title!r} -> {cache_key!r}", flush=True)
if cached and cached.get("mangabakaId"):
try:
return self._fetch_series_by_id(cached["mangabakaId"])
except Exception as exc:
print(f"[ComicInfoBuilder] cached id "
f"{cached['mangabakaId']} for {title!r} failed "
f"{cached['mangabakaId']} for {cache_key!r} failed "
f"({exc}); falling back to fresh search",
flush=True)
@@ -443,7 +471,7 @@ class ComicInfoBuilder:
# for uncached titles, or when a cached id failed to resolve; in
# the latter case the manual entry must be preserved.)
self._matches_cache.add_if_absent(
title,
cache_key,
mangabaka_id=series.get("id"),
mangabaka_name=series.get("title") or "",
image_url=_pick_thumbnail_url(series.get("cover")),
+37 -13
View File
@@ -445,7 +445,13 @@ class SuwayomiMover:
if not manga_dir.is_dir():
continue
raw_series = manga_dir.name
# The FOLDER NAME is the stable matches.json key — it is
# what exists on disk and never drifts between runs. The
# ComicInfo <Series> title is only the search query: it has
# the real characters (e.g. quotes) that folder names can't
# carry, so it produces better MangaBaka hits.
folder_title = manga_dir.name
raw_series = folder_title
for chapter_dir in sorted(manga_dir.iterdir(),
key=lambda p: _chapter_sort_key(p.name)):
if chapter_dir.is_dir():
@@ -454,36 +460,50 @@ class SuwayomiMover:
raw_series = fields["Series"]
break
builder_title = _clean_suwayomi_title(raw_series)
search_title = _clean_suwayomi_title(raw_series)
if self._matches_cache.get(builder_title):
print(f"[matches] {builder_title} — cached")
if self._matches_cache.get(folder_title):
print(f"[matches] {folder_title} — cached")
continue
print(f"[matches] {builder_title} — searching")
# Migration: older versions keyed entries by the cleaned
# ComicInfo/folder title. Move such an entry onto the
# stable folder-name key instead of creating a duplicate.
for legacy_key in {search_title,
_clean_suwayomi_title(folder_title)}:
if (legacy_key != folder_title
and self._matches_cache.get(legacy_key)):
self._matches_cache.rename(legacy_key, folder_title)
print(f"[matches] {folder_title} — migrated key "
f"from {legacy_key!r}")
break
if self._matches_cache.get(folder_title):
continue
print(f"[matches] {folder_title} — searching "
f"(query: {search_title!r})")
try:
resp = self._session.get(
search_url,
params={"q": builder_title, "type": _SEARCH_TYPES,
params={"q": search_title, "type": _SEARCH_TYPES,
"page": 1, "limit": 1},
timeout=self._timeout)
resp.raise_for_status()
data = resp.json().get("data") or []
if not data:
print(f" [warn] no MangaBaka match for {builder_title!r}")
print(f" [warn] no MangaBaka match for {search_title!r}")
continue
series = data[0]
# add_if_absent (never overwrite) — the get() above
# already skips cached titles; this is a safety net so a
# match run can only ever append, never replace.
# add_if_absent (never overwrite) — a match run can only
# ever append, never replace manual corrections.
self._matches_cache.add_if_absent(
builder_title,
folder_title,
mangabaka_id=series.get("id"),
mangabaka_name=series.get("title") or "",
image_url=_pick_thumbnail_url(series.get("cover")),
)
except Exception as exc:
print(f" [warn] search failed for {builder_title!r}: {exc}")
print(f" [warn] search failed for {search_title!r}: {exc}")
return self._matches_cache.all()
@@ -524,7 +544,10 @@ class SuwayomiMover:
raw_series = xml_series
builder_title = _clean_suwayomi_title(raw_series)
# One builder per series — metadata fetched once, reused for all chapters.
# One builder per series — metadata fetched once, reused for all
# chapters. match_title = the folder name: the stable matches.json
# key (the XML series title is only the search query and can drift
# from the folder when it contains Windows-illegal characters).
builder = ComicInfoBuilder(
builder_title, chapter=1,
api_base_url=self._api_base_url,
@@ -537,6 +560,7 @@ class SuwayomiMover:
al_resolver=self._al,
matches_cache=self._matches_cache,
cover_cache=self._cover_cache,
match_title=manga_title,
)
# Fetch MangaBaka metadata now to get the canonical title and MAL ID.