Person updater change from name -> name (mal-character-Id)
This commit is contained in:
@@ -30,7 +30,7 @@ import requests
|
||||
from KavitaClient import KavitaClient
|
||||
from MALResolver import MALResolver
|
||||
from AniListResolver import AniListResolver
|
||||
from TextUtils import best_similarity, paragraphs_to_html
|
||||
from TextUtils import best_similarity, paragraphs_to_html, person_name_with_id
|
||||
|
||||
|
||||
class KavitaPersonUpdater:
|
||||
@@ -152,11 +152,28 @@ class KavitaPersonUpdater:
|
||||
if not name and not raw_name:
|
||||
continue
|
||||
|
||||
# Search by the cleaned (XML-safe) name first; if Kavita stores
|
||||
# the legacy comma form, retry with the raw MAL name.
|
||||
matches = self._find_kavita_person(name) if name else []
|
||||
if not matches and raw_name and raw_name != name:
|
||||
matches = self._find_kavita_person(raw_name)
|
||||
if kind == "character":
|
||||
# Characters are stored under their disambiguated name
|
||||
# ("Rem (MAL 118737)") — see person_name_with_id. The
|
||||
# series metadata write creates the person under exactly
|
||||
# this name, so only that form is searched.
|
||||
search_names = [person_name_with_id(
|
||||
name, mal_id=entry.get("mal_id"),
|
||||
al_id=entry.get("al_id"))]
|
||||
else:
|
||||
# Staff: cleaned (XML-safe) name first; if Kavita stores
|
||||
# the legacy comma form, retry with the raw MAL name.
|
||||
search_names = [name]
|
||||
if raw_name and raw_name != name:
|
||||
search_names.append(raw_name)
|
||||
|
||||
matches: list[dict] = []
|
||||
for search_name in search_names:
|
||||
if not search_name:
|
||||
continue
|
||||
matches = self._find_kavita_person(search_name)
|
||||
if matches:
|
||||
break
|
||||
|
||||
if not matches:
|
||||
result["not_found"] += 1
|
||||
@@ -234,6 +251,20 @@ class KavitaPersonUpdater:
|
||||
|
||||
current_mal_id: int = person.get("malId") or 0
|
||||
current_al_id: int = person.get("aniListId") or 0
|
||||
|
||||
# Collision guard: the Kavita person is already linked to a
|
||||
# *different* tracker entity — same display name, different
|
||||
# character/person. Never overwrite; first writer wins.
|
||||
if ((mal_id and current_mal_id and current_mal_id != mal_id)
|
||||
or (al_id and current_al_id and current_al_id != al_id)):
|
||||
if errors is not None:
|
||||
errors.append(
|
||||
f"conflict: '{person_name}' (#{person_id}) is linked to "
|
||||
f"malId={current_mal_id or '-'}/aniListId={current_al_id or '-'} "
|
||||
f"but this entry has malId={mal_id or '-'}/aniListId={al_id or '-'} "
|
||||
f"— skipped")
|
||||
return False
|
||||
|
||||
needs_mal_id = bool(mal_id and current_mal_id != mal_id)
|
||||
needs_al_id = bool(al_id and current_al_id != al_id)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from MangaBakaRateLimit import apply_to_session as _apply_mangabaka_rate_limit
|
||||
from MALResolver import MALResolver
|
||||
from AniListResolver import AniListResolver
|
||||
from MatchesCache import MatchesCache
|
||||
from TextUtils import paragraphs_to_html
|
||||
from TextUtils import paragraphs_to_html, person_name_with_id
|
||||
|
||||
|
||||
# MangaBaka series type for the search endpoint.
|
||||
@@ -311,9 +311,15 @@ class LightNovelMetadataBuilder:
|
||||
if not staff_detailed and al_id:
|
||||
staff_detailed = self._al.get_staff_detailed(al_id)
|
||||
|
||||
# Character / writer name lists for SeriesMetadata
|
||||
character_names = [c["name"] for c in characters_detailed
|
||||
if c.get("name")]
|
||||
# Character names for SeriesMetadata, disambiguated with the
|
||||
# tracker character id ("Rem (MAL 118737)") because Kavita person
|
||||
# records are global and keyed by name only.
|
||||
character_names = [
|
||||
person_name_with_id(c["name"],
|
||||
mal_id=c.get("mal_id"),
|
||||
al_id=c.get("al_id"))
|
||||
for c in characters_detailed if c.get("name")
|
||||
]
|
||||
# Writers come from MangaBaka first (authoritative for novels)
|
||||
writers = list(md.get("authors") or [])
|
||||
# Illustrators / artists -> CoverArtists (Kavita has no dedicated
|
||||
|
||||
@@ -43,3 +43,30 @@ def best_similarity(query: str, candidates: Iterable[str]) -> float:
|
||||
None, q, str(candidate).lower()).ratio()
|
||||
best = max(best, ratio)
|
||||
return best
|
||||
|
||||
|
||||
def person_name_with_id(name: str, *,
|
||||
mal_id: "int | None" = None,
|
||||
al_id: "int | None" = None) -> str:
|
||||
"""
|
||||
Disambiguates a character name with its tracker id: "Rem (MAL 118737)".
|
||||
|
||||
Kavita Person records are global and keyed by name only, so two
|
||||
different characters who share a name would collapse into one record.
|
||||
Suffixing the tracker *character* id keeps them apart while still
|
||||
sharing the record across the manga and light-novel version of the
|
||||
same series (MAL/AniList character ids are per character, not per
|
||||
medium). MAL is preferred; AniList ids get an "AL" marker so the two
|
||||
id spaces cannot collide. Without any id the name is returned as-is.
|
||||
|
||||
The format must stay in sync with the manga project so both tools
|
||||
address the same Kavita person records.
|
||||
"""
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return name
|
||||
if mal_id:
|
||||
return f"{name} (MAL {mal_id})"
|
||||
if al_id:
|
||||
return f"{name} (AL {al_id})"
|
||||
return name
|
||||
|
||||
Reference in New Issue
Block a user