2 Commits

Author SHA256 Message Date
johannesbot e0f0778972 Person updater change from name -> name (mal-character-Id)
Build and Deploy / build (push) Successful in 19s
Build and Deploy / deploy (push) Successful in 39s
Release / build (push) Successful in 13s
2026-06-12 10:55:37 +02:00
johannesbot 1e6285528c release.yml
Build and Deploy / build (push) Successful in 13s
Build and Deploy / deploy (push) Successful in 23s
2026-06-12 09:57:07 +02:00
4 changed files with 75 additions and 14 deletions
+1 -4
View File
@@ -23,12 +23,9 @@ jobs:
run: | run: |
VERSION="${GITHUB_REF_NAME#v}" VERSION="${GITHUB_REF_NAME#v}"
docker build \ docker build \
-t gitea.johannesbot.de/johannesbot/kavita-lightnovel-metadata-fetcher:${VERSION} \ -t gitea.johannesbot.de/johannesbot/kavita-lightnovel-metadata-fetcher:${VERSION} .
-t gitea.johannesbot.de/johannesbot/kavita-lightnovel-metadata-fetcher:${GITHUB_REF_NAME} \
.
- name: Push Image - name: Push Image
run: | run: |
VERSION="${GITHUB_REF_NAME#v}" VERSION="${GITHUB_REF_NAME#v}"
docker push gitea.johannesbot.de/johannesbot/kavita-lightnovel-metadata-fetcher:${VERSION} docker push gitea.johannesbot.de/johannesbot/kavita-lightnovel-metadata-fetcher:${VERSION}
docker push gitea.johannesbot.de/johannesbot/kavita-lightnovel-metadata-fetcher:${GITHUB_REF_NAME}
+36 -5
View File
@@ -30,7 +30,7 @@ import requests
from KavitaClient import KavitaClient from KavitaClient import KavitaClient
from MALResolver import MALResolver from MALResolver import MALResolver
from AniListResolver import AniListResolver 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: class KavitaPersonUpdater:
@@ -152,11 +152,28 @@ class KavitaPersonUpdater:
if not name and not raw_name: if not name and not raw_name:
continue continue
# Search by the cleaned (XML-safe) name first; if Kavita stores 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. # the legacy comma form, retry with the raw MAL name.
matches = self._find_kavita_person(name) if name else [] search_names = [name]
if not matches and raw_name and raw_name != name: if raw_name and raw_name != name:
matches = self._find_kavita_person(raw_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: if not matches:
result["not_found"] += 1 result["not_found"] += 1
@@ -234,6 +251,20 @@ class KavitaPersonUpdater:
current_mal_id: int = person.get("malId") or 0 current_mal_id: int = person.get("malId") or 0
current_al_id: int = person.get("aniListId") 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_mal_id = bool(mal_id and current_mal_id != mal_id)
needs_al_id = bool(al_id and current_al_id != al_id) needs_al_id = bool(al_id and current_al_id != al_id)
+10 -4
View File
@@ -26,7 +26,7 @@ from MangaBakaRateLimit import apply_to_session as _apply_mangabaka_rate_limit
from MALResolver import MALResolver from MALResolver import MALResolver
from AniListResolver import AniListResolver from AniListResolver import AniListResolver
from MatchesCache import MatchesCache 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. # MangaBaka series type for the search endpoint.
@@ -311,9 +311,15 @@ class LightNovelMetadataBuilder:
if not staff_detailed and al_id: if not staff_detailed and al_id:
staff_detailed = self._al.get_staff_detailed(al_id) staff_detailed = self._al.get_staff_detailed(al_id)
# Character / writer name lists for SeriesMetadata # Character names for SeriesMetadata, disambiguated with the
character_names = [c["name"] for c in characters_detailed # tracker character id ("Rem (MAL 118737)") because Kavita person
if c.get("name")] # 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 come from MangaBaka first (authoritative for novels)
writers = list(md.get("authors") or []) writers = list(md.get("authors") or [])
# Illustrators / artists -> CoverArtists (Kavita has no dedicated # Illustrators / artists -> CoverArtists (Kavita has no dedicated
+27
View File
@@ -43,3 +43,30 @@ def best_similarity(query: str, candidates: Iterable[str]) -> float:
None, q, str(candidate).lower()).ratio() None, q, str(candidate).lower()).ratio()
best = max(best, ratio) best = max(best, ratio)
return best 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