added chapter index json
This commit is contained in:
+92
-2
@@ -43,6 +43,7 @@ Dependencies
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
@@ -64,6 +65,62 @@ from MangaBakaRateLimit import apply_to_session as _apply_mangabaka_rate_limit
|
|||||||
_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".avif"}
|
_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".avif"}
|
||||||
_CHAPTER_RE = re.compile(r'[Cc]hapter\s+(\d+(?:\.\d+)?)')
|
_CHAPTER_RE = re.compile(r'[Cc]hapter\s+(\d+(?:\.\d+)?)')
|
||||||
|
|
||||||
|
# JSON file written into each Kavita series folder, listing every chapter
|
||||||
|
# already moved. Avoids opening CBZ archives to determine what is present.
|
||||||
|
# Absence is interpreted as "folder empty" (per spec), not "scan the folder".
|
||||||
|
_CHAPTER_INDEX_FILENAME = "chapter_index.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_volume_value(value):
|
||||||
|
"""
|
||||||
|
Normalises a volume identifier for storage in chapter_index.json.
|
||||||
|
|
||||||
|
Returns int when the value is a whole number, float for fractional
|
||||||
|
volumes, None when missing. Mirrors how the user wants volumes
|
||||||
|
rendered (``"volume": 1`` rather than ``"volume": "1"``).
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
f = float(text)
|
||||||
|
return int(f) if f.is_integer() else f
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _load_chapter_index(dest_series: Path) -> dict:
|
||||||
|
"""
|
||||||
|
Reads chapter_index.json from a Kavita series folder.
|
||||||
|
|
||||||
|
Returns ``{"chapter": {}}`` when the file is missing or unreadable —
|
||||||
|
per the project spec, absence means "no chapters are present yet".
|
||||||
|
"""
|
||||||
|
path = dest_series / _CHAPTER_INDEX_FILENAME
|
||||||
|
if not path.is_file():
|
||||||
|
return {"chapter": {}}
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
print(f" [warn] chapter_index unreadable ({path.name}): {exc} — "
|
||||||
|
f"treating folder as empty")
|
||||||
|
return {"chapter": {}}
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("chapter"), dict):
|
||||||
|
return {"chapter": {}}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _save_chapter_index(dest_series: Path, index: dict) -> None:
|
||||||
|
"""Writes chapter_index.json atomically into a Kavita series folder."""
|
||||||
|
path = dest_series / _CHAPTER_INDEX_FILENAME
|
||||||
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
with tmp.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump(index, f, ensure_ascii=False, indent=2)
|
||||||
|
tmp.replace(path)
|
||||||
|
|
||||||
# Parenthetical source labels that Suwayomi appends to series names.
|
# Parenthetical source labels that Suwayomi appends to series names.
|
||||||
# These are not part of the actual title and confuse MangaBaka searches.
|
# These are not part of the actual title and confuse MangaBaka searches.
|
||||||
_SOURCE_LABEL_RE = re.compile(
|
_SOURCE_LABEL_RE = re.compile(
|
||||||
@@ -509,13 +566,38 @@ class SuwayomiMover:
|
|||||||
dest_series = self._dst / _sanitize_dirname(mangabaka_title)
|
dest_series = self._dst / _sanitize_dirname(mangabaka_title)
|
||||||
dest_series.mkdir(parents=True, exist_ok=True)
|
dest_series.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Skip chapters that have already been moved to Kavita. The index
|
||||||
|
# file in the destination folder is the authoritative source — we
|
||||||
|
# never open CBZ archives or stat them individually.
|
||||||
|
chapter_index = _load_chapter_index(dest_series)
|
||||||
|
already_moved = chapter_index["chapter"]
|
||||||
|
skipped: list[tuple[Path, str]] = []
|
||||||
|
pending: list[tuple[Path, dict, str]] = []
|
||||||
|
for item in chapter_items:
|
||||||
|
chapter_dir, _fields, chapter_num = item
|
||||||
|
if chapter_num in already_moved:
|
||||||
|
skipped.append((chapter_dir, chapter_num))
|
||||||
|
else:
|
||||||
|
pending.append(item)
|
||||||
|
|
||||||
|
for chapter_dir, chapter_num in skipped:
|
||||||
|
print(f" Chapter {chapter_num}: skip (already in Kavita)")
|
||||||
|
if self._delete_source:
|
||||||
|
shutil.rmtree(chapter_dir, ignore_errors=True)
|
||||||
|
|
||||||
chapter_results: list[dict] = []
|
chapter_results: list[dict] = []
|
||||||
for chapter_dir, _fields, chapter_num in chapter_items:
|
for chapter_dir, _fields, chapter_num in pending:
|
||||||
result = self._process_chapter(
|
result = self._process_chapter(
|
||||||
builder, chapter_num, chapter_dir, dest_series)
|
builder, chapter_num, chapter_dir, dest_series)
|
||||||
chapter_results.append(result)
|
chapter_results.append(result)
|
||||||
status = "ok" if result["ok"] else f"ERROR: {result.get('error')}"
|
status = "ok" if result["ok"] else f"ERROR: {result.get('error')}"
|
||||||
print(f" Chapter {chapter_num}: {status}")
|
print(f" Chapter {chapter_num}: {status}")
|
||||||
|
if result["ok"]:
|
||||||
|
already_moved[chapter_num] = {
|
||||||
|
"volume": _normalise_volume_value(result.get("volume")),
|
||||||
|
"archiveName": Path(result["cbz"]).name,
|
||||||
|
}
|
||||||
|
_save_chapter_index(dest_series, chapter_index)
|
||||||
|
|
||||||
# Sync Kavita persons once per series.
|
# Sync Kavita persons once per series.
|
||||||
# Both MAL and AniList IDs come from MangaBaka's source map;
|
# Both MAL and AniList IDs come from MangaBaka's source map;
|
||||||
@@ -557,11 +639,19 @@ class SuwayomiMover:
|
|||||||
try:
|
try:
|
||||||
builder.chapter = chapter_num
|
builder.chapter = chapter_num
|
||||||
builder.add_pages_from_folder(chapter_dir, cover_filename="000")
|
builder.add_pages_from_folder(chapter_dir, cover_filename="000")
|
||||||
|
# Resolving the volume here piggy-backs on caches already warmed
|
||||||
|
# by add_pages_from_folder, so it's effectively free. Used by
|
||||||
|
# the chapter index in the Kavita destination folder.
|
||||||
|
try:
|
||||||
|
volume = builder._determine_volume()
|
||||||
|
except Exception:
|
||||||
|
volume = None
|
||||||
builder.save_xml(chapter_dir)
|
builder.save_xml(chapter_dir)
|
||||||
_pack_to_cbz(chapter_dir, cbz_path)
|
_pack_to_cbz(chapter_dir, cbz_path)
|
||||||
if self._delete_source:
|
if self._delete_source:
|
||||||
shutil.rmtree(chapter_dir)
|
shutil.rmtree(chapter_dir)
|
||||||
return {"chapter": chapter_num, "cbz": str(cbz_path), "ok": True}
|
return {"chapter": chapter_num, "cbz": str(cbz_path),
|
||||||
|
"ok": True, "volume": volume}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"chapter": chapter_num, "cbz": str(cbz_path),
|
return {"chapter": chapter_num, "cbz": str(cbz_path),
|
||||||
"ok": False, "error": str(exc)}
|
"ok": False, "error": str(exc)}
|
||||||
|
|||||||
Reference in New Issue
Block a user