merged ln metadata into manga mover
Build and Deploy / build (push) Successful in 59s
Build and Deploy / deploy (push) Successful in 24s

This commit is contained in:
2026-06-14 10:47:47 +02:00
parent 8a44b85a48
commit 216771f709
27 changed files with 3040 additions and 280 deletions
+32 -17
View File
@@ -32,27 +32,35 @@ Dependencies
from __future__ import annotations
import datetime
import difflib
import time
import requests
from MediaResolver import MediaResolver
from TextUtils import best_similarity
# --------------------------------------------------------------------------
# GraphQL query strings
# --------------------------------------------------------------------------
_SEARCH_MANGA = """
# AniList models both manga and light novels as type MANGA; the format
# clause decides which of the two a search returns. The placeholder is
# substituted at construction time (see `media_format`).
_SEARCH_MANGA_TEMPLATE = """
query ($search: String) {
Page(page: 1, perPage: 5) {
media(search: $search, type: MANGA, format_not_in: [NOVEL]) {
media(search: $search, type: MANGA, __FORMAT_CLAUSE__) {
id title { romaji english native } siteUrl
}
}
}
"""
_FORMAT_CLAUSES = {
"manga": "format_not_in: [NOVEL]",
"novel": "format_in: [NOVEL]",
}
_MANGA_STATS = """
query ($id: Int) {
Media(id: $id, type: MANGA) {
@@ -131,10 +139,24 @@ class AniListResolver(MediaResolver):
cls._instance._initialized = False
return cls._instance
def __init__(self, *, request_timeout: int = 30):
def __init__(self, *, request_timeout: int = 30,
media_format: str = "manga"):
"""
media_format : "manga" (excludes novels) or "novel" (novels only).
Only the FIRST construction in the process sets it
(singleton); construct the resolver with the correct
format in the entry point / orchestrator.
"""
if self._initialized:
return
if media_format not in _FORMAT_CLAUSES:
raise ValueError(f"media_format must be one of "
f"{sorted(_FORMAT_CLAUSES)}, got {media_format!r}")
self.media_format = media_format
self._search_query = _SEARCH_MANGA_TEMPLATE.replace(
"__FORMAT_CLAUSE__", _FORMAT_CLAUSES[media_format])
self.request_timeout = request_timeout
self._session = requests.Session()
@@ -178,7 +200,7 @@ class AniListResolver(MediaResolver):
return self._id_cache[key]
try:
data = self._gql(_SEARCH_MANGA, {"search": title})
data = self._gql(self._search_query, {"search": title})
results = ((data.get("data") or {})
.get("Page", {})
.get("media") or [])
@@ -469,18 +491,11 @@ class AniListResolver(MediaResolver):
def _score_title(query: str, entry: dict) -> float:
"""Returns the best title-similarity score for an AniList media entry."""
title_obj = entry.get("title") or {}
candidates = [
title_obj.get("romaji") or "",
title_obj.get("english") or "",
title_obj.get("native") or "",
]
best = 0.0
q = query.lower()
for t in candidates:
if t:
ratio = difflib.SequenceMatcher(None, q, t.lower()).ratio()
best = max(best, ratio)
return best
return best_similarity(query, (
title_obj.get("romaji"),
title_obj.get("english"),
title_obj.get("native"),
))
# --------------------------------------------------------------------------