Compare commits
2 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| e0f0778972 | |||
| 1e6285528c |
@@ -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}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user