duplicate entry fix
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user