merged ln metadata into manga mover
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
relationship_sync.py
|
||||
====================
|
||||
|
||||
Mirrors MangaBaka's ``relationships_v2`` graph into Kavita:
|
||||
|
||||
1. Every related MangaBaka series that is *also* present in Kavita
|
||||
(resolved via MatchesCache) is added to a shared Kavita collection
|
||||
so the whole franchise can be browsed in one place.
|
||||
2. Series-level relationships (prequel / sequel / spin-off / …) are
|
||||
written via ``POST /api/Series/update-related`` so navigating
|
||||
between entries surfaces the right neighbours.
|
||||
|
||||
Only relationships where both endpoints exist in Kavita are written.
|
||||
Relationships pointing to series that have not been imported yet are
|
||||
silently skipped (the next match run picks them up).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from KavitaClient import KavitaClient
|
||||
from MatchesCache import MatchesCache
|
||||
|
||||
|
||||
# MangaBaka relation_type -> Kavita UpdateRelatedSeriesDto bucket
|
||||
_RELATION_MAP = {
|
||||
"prequel": "prequels",
|
||||
"sequel": "sequels",
|
||||
"side_story": "sideStories",
|
||||
"spin_off": "spinOffs",
|
||||
"spinoff": "spinOffs",
|
||||
"alternative_version": "alternativeVersions",
|
||||
"alternative_story": "alternativeVersions",
|
||||
"alternative_setting": "alternativeSettings",
|
||||
"adapted_from": "adaptations",
|
||||
"adaptation": "adaptations",
|
||||
"doujinshi": "doujinshis",
|
||||
"parent": "contains", # the parent "contains" the child
|
||||
}
|
||||
|
||||
_ALL_BUCKETS = (
|
||||
"adaptations", "characters", "contains", "others",
|
||||
"prequels", "sequels", "sideStories", "spinOffs",
|
||||
"alternativeSettings", "alternativeVersions", "doujinshis",
|
||||
"editions", "annuals",
|
||||
)
|
||||
|
||||
|
||||
class RelationshipSync:
|
||||
def __init__(self, client: KavitaClient, cache: MatchesCache, *,
|
||||
builder=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
client : KavitaClient for collection / relation writes.
|
||||
cache : MatchesCache to resolve mangabakaId -> kavitaSeriesId.
|
||||
builder : optional LightNovelMetadataBuilder used to fetch parent
|
||||
series titles when picking the collection name.
|
||||
"""
|
||||
self._client = client
|
||||
self._cache = cache
|
||||
self._builder = builder
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public
|
||||
# ------------------------------------------------------------------
|
||||
def sync(self, kavita_series_id: int, built: dict) -> dict:
|
||||
"""
|
||||
Applies the relationship and collection links described by
|
||||
`built["relationships"]` (raw MangaBaka relationships_v2 list)
|
||||
for the given Kavita series. Returns a small status dict.
|
||||
"""
|
||||
report: dict = {"relations": {}, "collection": None,
|
||||
"missing_series": []}
|
||||
|
||||
relationships = built.get("relationships") or []
|
||||
if not relationships:
|
||||
return report
|
||||
|
||||
# Resolve mangabakaId -> kavitaSeriesId for every related entry.
|
||||
related: dict[str, list[int]] = {b: [] for b in _ALL_BUCKETS}
|
||||
all_kavita_ids: set[int] = set()
|
||||
for rel in relationships:
|
||||
mb_id = rel.get("to_series_id")
|
||||
if mb_id is None:
|
||||
continue
|
||||
hit = self._cache.get_by_mangabaka_id(mb_id)
|
||||
if not hit:
|
||||
report["missing_series"].append(int(mb_id))
|
||||
continue
|
||||
_title, entry = hit
|
||||
ksid = int(entry.get("kavitaSeriesId") or 0)
|
||||
if not ksid:
|
||||
report["missing_series"].append(int(mb_id))
|
||||
continue
|
||||
bucket = _RELATION_MAP.get((rel.get("relation_type") or "").lower(),
|
||||
"others")
|
||||
if ksid not in related[bucket]:
|
||||
related[bucket].append(ksid)
|
||||
all_kavita_ids.add(ksid)
|
||||
|
||||
# ----- Relationships ------------------------------------------
|
||||
if any(related.values()):
|
||||
payload = {"seriesId": int(kavita_series_id)}
|
||||
for bucket in _ALL_BUCKETS:
|
||||
payload[bucket] = related[bucket]
|
||||
try:
|
||||
self._client.update_related(payload)
|
||||
report["relations"] = {k: v for k, v in related.items() if v}
|
||||
except Exception as exc:
|
||||
report["relations"] = {"error": str(exc)}
|
||||
|
||||
# ----- Collection ---------------------------------------------
|
||||
# Include the current series in the collection so it shows up too.
|
||||
all_kavita_ids.add(int(kavita_series_id))
|
||||
if len(all_kavita_ids) >= 2:
|
||||
collection_name = self._collection_name(built, relationships)
|
||||
collection_id = self._find_collection_id(collection_name)
|
||||
try:
|
||||
self._client.add_series_to_collection(
|
||||
collection_id=collection_id,
|
||||
title=collection_name,
|
||||
series_ids=sorted(all_kavita_ids),
|
||||
)
|
||||
report["collection"] = collection_name
|
||||
except Exception as exc:
|
||||
report["collection"] = f"error: {exc}"
|
||||
|
||||
return report
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal
|
||||
# ------------------------------------------------------------------
|
||||
def _find_collection_id(self, name: str) -> int:
|
||||
"""Returns the id of an existing collection by title, or 0 to create."""
|
||||
if not name:
|
||||
return 0
|
||||
target = name.strip().lower()
|
||||
try:
|
||||
for col in self._client.list_collections():
|
||||
if (col.get("title") or "").strip().lower() == target:
|
||||
try:
|
||||
return int(col.get("id") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def _collection_name(self, built: dict,
|
||||
relationships: list[dict]) -> str:
|
||||
"""
|
||||
Picks the collection name. Uses the parent series title from
|
||||
MangaBaka if the current series has one; otherwise falls back to
|
||||
the current series' own title.
|
||||
"""
|
||||
for rel in relationships:
|
||||
if (rel.get("relation_type") or "").lower() == "parent":
|
||||
parent_id = rel.get("to_series_id")
|
||||
if parent_id is not None and self._builder is not None:
|
||||
try:
|
||||
parent_md = self._builder.fetch_series(parent_id)
|
||||
if parent_md and parent_md.get("title"):
|
||||
return parent_md["title"]
|
||||
except Exception:
|
||||
pass
|
||||
# Even without a builder, the cache may know the parent.
|
||||
hit = self._cache.get_by_mangabaka_id(parent_id)
|
||||
if hit:
|
||||
_title, entry = hit
|
||||
name = entry.get("mangabakaName")
|
||||
if name:
|
||||
return name
|
||||
return built.get("mangabakaTitle") or ""
|
||||
Reference in New Issue
Block a user