manga matching and WebApp
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
matches_cache.py
|
||||
================
|
||||
|
||||
Persistent JSON cache that maps a Suwayomi/series search title to the
|
||||
MangaBaka series it was matched against.
|
||||
|
||||
Structure on disk::
|
||||
|
||||
{
|
||||
"matches": {
|
||||
"<search title>": {
|
||||
"mangabakaId": "12345",
|
||||
"mangabakaName": "One-Punch Man",
|
||||
"imageUrl": "https://.../cover.jpg",
|
||||
"firstMatchTime": 1700000000
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
The cache is consulted by ComicInfoBuilder before issuing a MangaBaka
|
||||
search request, and is written back to disk on every mutation so a crash
|
||||
does not lose matches that were resolved in the current run.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class MatchesCache:
|
||||
def __init__(self, path):
|
||||
self._path = Path(path)
|
||||
self._lock = threading.RLock()
|
||||
self._data: dict = {"matches": {}}
|
||||
self._load()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public lookup / mutation API
|
||||
# ------------------------------------------------------------------
|
||||
def get(self, title: str) -> "dict | None":
|
||||
with self._lock:
|
||||
entry = self._data["matches"].get(title)
|
||||
return dict(entry) if entry else None
|
||||
|
||||
def add(self, title: str, *,
|
||||
mangabaka_id,
|
||||
mangabaka_name: str,
|
||||
image_url: "str | None") -> dict:
|
||||
entry = {
|
||||
"mangabakaId": str(mangabaka_id) if mangabaka_id is not None else "",
|
||||
"mangabakaName": mangabaka_name or "",
|
||||
"imageUrl": image_url or "",
|
||||
"firstMatchTime": int(time.time()),
|
||||
}
|
||||
with self._lock:
|
||||
self._data["matches"][title] = entry
|
||||
self._save_unlocked()
|
||||
return dict(entry)
|
||||
|
||||
def upsert(self, title: str, *,
|
||||
mangabaka_id=None,
|
||||
mangabaka_name=None,
|
||||
image_url=None,
|
||||
first_match_time=None) -> dict:
|
||||
with self._lock:
|
||||
entry = self._data["matches"].get(title)
|
||||
if entry is None:
|
||||
entry = {
|
||||
"mangabakaId": "",
|
||||
"mangabakaName": "",
|
||||
"imageUrl": "",
|
||||
"firstMatchTime": int(time.time()),
|
||||
}
|
||||
self._data["matches"][title] = entry
|
||||
if mangabaka_id is not None:
|
||||
entry["mangabakaId"] = str(mangabaka_id)
|
||||
if mangabaka_name is not None:
|
||||
entry["mangabakaName"] = mangabaka_name
|
||||
if image_url is not None:
|
||||
entry["imageUrl"] = image_url
|
||||
if first_match_time is not None:
|
||||
try:
|
||||
entry["firstMatchTime"] = int(first_match_time)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
self._save_unlocked()
|
||||
return dict(entry)
|
||||
|
||||
def rename(self, old_title: str, new_title: str) -> bool:
|
||||
if not new_title or old_title == new_title:
|
||||
return False
|
||||
with self._lock:
|
||||
entry = self._data["matches"].pop(old_title, None)
|
||||
if entry is None:
|
||||
return False
|
||||
self._data["matches"][new_title] = entry
|
||||
self._save_unlocked()
|
||||
return True
|
||||
|
||||
def remove(self, title: str) -> bool:
|
||||
with self._lock:
|
||||
existed = title in self._data["matches"]
|
||||
if existed:
|
||||
del self._data["matches"][title]
|
||||
self._save_unlocked()
|
||||
return existed
|
||||
|
||||
def all(self) -> dict:
|
||||
with self._lock:
|
||||
return {"matches": {k: dict(v)
|
||||
for k, v in self._data["matches"].items()}}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal IO
|
||||
# ------------------------------------------------------------------
|
||||
def _load(self) -> None:
|
||||
if not self._path.is_file():
|
||||
return
|
||||
try:
|
||||
with self._path.open("r", encoding="utf-8") as f:
|
||||
loaded = json.load(f)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
print(f"[MatchesCache] failed to load {self._path}: {exc}",
|
||||
flush=True)
|
||||
return
|
||||
if isinstance(loaded, dict) and isinstance(loaded.get("matches"), dict):
|
||||
self._data = loaded
|
||||
|
||||
def _save_unlocked(self) -> None:
|
||||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = self._path.with_suffix(self._path.suffix + ".tmp")
|
||||
with tmp.open("w", encoding="utf-8") as f:
|
||||
json.dump(self._data, f, ensure_ascii=False, indent=2)
|
||||
tmp.replace(self._path)
|
||||
Reference in New Issue
Block a user