Use AniDB for watchlist alternative titles
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.38.0 - 2026-05-25
|
||||||
|
|
||||||
|
- Switched watchlist `Alternative titles` from AniList lookups to AniDB title-dump matching and now keep only English alternate titles.
|
||||||
|
- Reused the cached AniDB title metadata already present in the app, which makes alternate-title matching more predictable for watchlist refreshes.
|
||||||
|
|
||||||
## 0.37.0 - 2026-05-25
|
## 0.37.0 - 2026-05-25
|
||||||
|
|
||||||
- Added an `Alternative titles` toggle to every watchlist card so AniList title variants can be expanded inline.
|
- Added an `Alternative titles` toggle to every watchlist card so AniList title variants can be expanded inline.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Small local web UI for a system-wide `ani-cli` install.
|
Small local web UI for a system-wide `ani-cli` install.
|
||||||
|
|
||||||
Current version: `0.37.0`
|
Current version: `0.38.0`
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ Main behavior:
|
|||||||
6. Each card has an inline category selector.
|
6. Each card has an inline category selector.
|
||||||
7. Summary cards show total tracked shows plus `sub` and `dub` counts.
|
7. Summary cards show total tracked shows plus `sub` and `dub` counts.
|
||||||
8. Pagination shows 30 cards per page.
|
8. Pagination shows 30 cards per page.
|
||||||
9. `Alternative titles` expands AniList Romaji, English, Native, and synonym titles when AniList can match the show.
|
9. `Alternative titles` expands English alternate titles from AniDB when the watchlist refresh can match the show to an AniDB entry.
|
||||||
|
|
||||||
Status behavior:
|
Status behavior:
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from app_support import (
|
|||||||
ALLANIME_API,
|
ALLANIME_API,
|
||||||
ALLANIME_REFERER,
|
ALLANIME_REFERER,
|
||||||
ALLMANGA_SITE_BASE,
|
ALLMANGA_SITE_BASE,
|
||||||
ANILIST_API,
|
|
||||||
ANIDB_ANIME_PAGE,
|
ANIDB_ANIME_PAGE,
|
||||||
ANIDB_TITLES_CACHE,
|
ANIDB_TITLES_CACHE,
|
||||||
ANIDB_TITLES_PATH,
|
ANIDB_TITLES_PATH,
|
||||||
@@ -231,123 +230,6 @@ def animeschedule_request(path, params=None, timeout=25):
|
|||||||
raise RuntimeError("AnimeSchedule returned unreadable JSON") from exc
|
raise RuntimeError("AnimeSchedule returned unreadable JSON") from exc
|
||||||
|
|
||||||
|
|
||||||
def anilist_request(query, variables, timeout=25):
|
|
||||||
payload = json.dumps({"variables": variables, "query": query}).encode("utf-8")
|
|
||||||
request = urllib.request.Request(
|
|
||||||
ANILIST_API,
|
|
||||||
data=payload,
|
|
||||||
headers={
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": AGENT,
|
|
||||||
},
|
|
||||||
method="POST",
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
||||||
raw = response.read().decode("utf-8")
|
|
||||||
except urllib.error.URLError as exc:
|
|
||||||
raise RuntimeError(f"Could not reach AniList: {exc}") from exc
|
|
||||||
except TimeoutError as exc:
|
|
||||||
raise RuntimeError("AniList request timed out") from exc
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(raw)
|
|
||||||
except json.JSONDecodeError as exc:
|
|
||||||
raise RuntimeError("AniList returned unreadable JSON") from exc
|
|
||||||
|
|
||||||
if data.get("errors"):
|
|
||||||
message = str(((data.get("errors") or [{}])[0]).get("message") or "AniList returned an error")
|
|
||||||
raise RuntimeError(message)
|
|
||||||
return data.get("data", {})
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_anilist_entry(node):
|
|
||||||
if not isinstance(node, dict):
|
|
||||||
return None
|
|
||||||
media_id = node.get("id")
|
|
||||||
if media_id in {None, ""}:
|
|
||||||
return None
|
|
||||||
title = node.get("title") if isinstance(node.get("title"), dict) else {}
|
|
||||||
synonyms = node.get("synonyms") if isinstance(node.get("synonyms"), list) else []
|
|
||||||
return {
|
|
||||||
"id": int(media_id),
|
|
||||||
"title": {
|
|
||||||
"romaji": str(title.get("romaji") or "").strip(),
|
|
||||||
"english": str(title.get("english") or "").strip(),
|
|
||||||
"native": str(title.get("native") or "").strip(),
|
|
||||||
"userPreferred": str(title.get("userPreferred") or "").strip(),
|
|
||||||
},
|
|
||||||
"synonyms": [str(value or "").strip() for value in synonyms if str(value or "").strip()],
|
|
||||||
"siteUrl": str(node.get("siteUrl") or "").strip(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def anilist_title_candidates(entry):
|
|
||||||
if not isinstance(entry, dict):
|
|
||||||
return []
|
|
||||||
values = []
|
|
||||||
title = entry.get("title") if isinstance(entry.get("title"), dict) else {}
|
|
||||||
for key in ("romaji", "english", "native", "userPreferred"):
|
|
||||||
text = str(title.get(key) or "").strip()
|
|
||||||
if text:
|
|
||||||
values.append(text)
|
|
||||||
for value in entry.get("synonyms") or []:
|
|
||||||
text = str(value or "").strip()
|
|
||||||
if text:
|
|
||||||
values.append(text)
|
|
||||||
deduped = []
|
|
||||||
seen = set()
|
|
||||||
for value in values:
|
|
||||||
normalized = normalize_title_key(value)
|
|
||||||
if normalized and normalized not in seen:
|
|
||||||
seen.add(normalized)
|
|
||||||
deduped.append(value)
|
|
||||||
return deduped
|
|
||||||
|
|
||||||
|
|
||||||
def score_anilist_match(query_variants, entry):
|
|
||||||
best = None
|
|
||||||
for candidate in anilist_title_candidates(entry):
|
|
||||||
normalized = normalize_title_key(candidate)
|
|
||||||
if not normalized:
|
|
||||||
continue
|
|
||||||
score = None
|
|
||||||
if normalized in query_variants:
|
|
||||||
score = 110
|
|
||||||
else:
|
|
||||||
for variant in query_variants:
|
|
||||||
if variant and (normalized in variant or variant in normalized):
|
|
||||||
score = max(score or 0, 80 - abs(len(normalized) - len(variant)))
|
|
||||||
if score is not None:
|
|
||||||
best = max(best or score, score)
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
def extract_anilist_alternative_titles(entry, primary_title=""):
|
|
||||||
if not isinstance(entry, dict):
|
|
||||||
return []
|
|
||||||
primary_key = normalize_title_key(primary_title) or str(primary_title or "").strip().casefold()
|
|
||||||
results = []
|
|
||||||
seen = set()
|
|
||||||
title = entry.get("title") if isinstance(entry.get("title"), dict) else {}
|
|
||||||
for key, label in (("romaji", "Romaji"), ("english", "English"), ("native", "Native")):
|
|
||||||
text = str(title.get(key) or "").strip()
|
|
||||||
normalized = normalize_title_key(text) or text.casefold()
|
|
||||||
if not normalized or normalized == primary_key or normalized in seen:
|
|
||||||
continue
|
|
||||||
seen.add(normalized)
|
|
||||||
results.append({"label": label, "value": text})
|
|
||||||
for synonym in entry.get("synonyms") or []:
|
|
||||||
text = str(synonym or "").strip()
|
|
||||||
normalized = normalize_title_key(text) or text.casefold()
|
|
||||||
if not normalized or normalized == primary_key or normalized in seen:
|
|
||||||
continue
|
|
||||||
seen.add(normalized)
|
|
||||||
results.append({"label": "Synonym", "value": text})
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def encode_alternative_titles(alternative_titles):
|
def encode_alternative_titles(alternative_titles):
|
||||||
normalized = []
|
normalized = []
|
||||||
for item in alternative_titles or []:
|
for item in alternative_titles or []:
|
||||||
@@ -382,83 +264,32 @@ def decode_alternative_titles(payload):
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def fetch_anilist_media_by_id(media_id, timeout=25):
|
def extract_anidb_alternative_titles(aid, primary_title=""):
|
||||||
data = anilist_request(
|
normalized_aid = str(aid or "").strip()
|
||||||
"""
|
if not normalized_aid:
|
||||||
query ($id: Int) {
|
return []
|
||||||
Media(id: $id, type: ANIME) {
|
primary_key = normalize_title_key(primary_title) or str(primary_title or "").strip().casefold()
|
||||||
id
|
results = []
|
||||||
siteUrl
|
seen = set()
|
||||||
title {
|
type_labels = {
|
||||||
romaji
|
"official": "English",
|
||||||
english
|
"main": "English",
|
||||||
native
|
"short": "English",
|
||||||
userPreferred
|
"syn": "English",
|
||||||
}
|
}
|
||||||
synonyms
|
for entry in get_cached_anidb_titles():
|
||||||
}
|
if str(entry.get("aid") or "").strip() != normalized_aid:
|
||||||
}
|
continue
|
||||||
""",
|
if str(entry.get("lang") or "").strip().lower() != "en":
|
||||||
{"id": int(media_id)},
|
continue
|
||||||
timeout=timeout,
|
text = str(entry.get("title") or "").strip()
|
||||||
)
|
normalized = normalize_title_key(text) or text.casefold()
|
||||||
entry = normalize_anilist_entry(data.get("Media"))
|
if not normalized or normalized == primary_key or normalized in seen:
|
||||||
if not entry:
|
continue
|
||||||
raise RuntimeError("AniList media response was missing required fields")
|
seen.add(normalized)
|
||||||
return entry
|
label = type_labels.get(str(entry.get("type") or "").strip().lower(), "English")
|
||||||
|
results.append({"label": label, "value": text})
|
||||||
|
return results
|
||||||
def resolve_anilist_media(title, media_id=None, timeout=25):
|
|
||||||
stored_media_id = str(media_id or "").strip()
|
|
||||||
if stored_media_id:
|
|
||||||
try:
|
|
||||||
return fetch_anilist_media_by_id(int(stored_media_id), timeout=timeout)
|
|
||||||
except Exception:
|
|
||||||
debug_log("anilist.media_refresh_failed", media_id=stored_media_id, title=title)
|
|
||||||
|
|
||||||
query_variants = title_match_variants(title)
|
|
||||||
if not query_variants:
|
|
||||||
raise RuntimeError("Missing title for AniList lookup")
|
|
||||||
|
|
||||||
queries = title_lookup_queries(title)
|
|
||||||
best = None
|
|
||||||
for query in queries:
|
|
||||||
data = anilist_request(
|
|
||||||
"""
|
|
||||||
query ($search: String, $page: Int, $perPage: Int) {
|
|
||||||
Page(page: $page, perPage: $perPage) {
|
|
||||||
media(search: $search, type: ANIME) {
|
|
||||||
id
|
|
||||||
siteUrl
|
|
||||||
title {
|
|
||||||
romaji
|
|
||||||
english
|
|
||||||
native
|
|
||||||
userPreferred
|
|
||||||
}
|
|
||||||
synonyms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
{"search": query, "page": 1, "perPage": 8},
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
for node in ((data.get("Page") or {}).get("media") or []):
|
|
||||||
entry = normalize_anilist_entry(node)
|
|
||||||
if not entry:
|
|
||||||
continue
|
|
||||||
score = score_anilist_match(query_variants, entry)
|
|
||||||
if score is None:
|
|
||||||
continue
|
|
||||||
if best is None or score > best["score"]:
|
|
||||||
best = {"entry": entry, "score": score}
|
|
||||||
if best is not None:
|
|
||||||
break
|
|
||||||
|
|
||||||
if best is None:
|
|
||||||
raise RuntimeError("AniList lookup did not find a matching anime")
|
|
||||||
return best["entry"]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_nonnegative_int(value, default=0):
|
def parse_nonnegative_int(value, default=0):
|
||||||
@@ -1612,22 +1443,27 @@ class WatchlistStore:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
schedule = None
|
schedule = None
|
||||||
anilist = None
|
anidb_match = None
|
||||||
try:
|
try:
|
||||||
anilist = resolve_anilist_media(
|
if existing.get("anidb_aid"):
|
||||||
snapshot["title"] or existing["title"],
|
anidb_match = {
|
||||||
media_id=existing.get("anilist_id"),
|
"aid": str(existing.get("anidb_aid")).strip(),
|
||||||
timeout=request_timeout,
|
"title": str(existing.get("anidb_title") or snapshot["title"] or existing["title"]).strip(),
|
||||||
)
|
}
|
||||||
|
else:
|
||||||
|
anidb_match = resolve_anidb_aid(snapshot["title"] or existing["title"])
|
||||||
except Exception:
|
except Exception:
|
||||||
anilist = None
|
anidb_match = None
|
||||||
if self.refresh_stop_event.is_set():
|
if self.refresh_stop_event.is_set():
|
||||||
return None
|
return None
|
||||||
if not self.has_show(normalized_show_id):
|
if not self.has_show(normalized_show_id):
|
||||||
return None
|
return None
|
||||||
expected_count = parse_nonnegative_int((schedule or {}).get("episodes")) or int(existing.get("expected_count") or 0)
|
expected_count = parse_nonnegative_int((schedule or {}).get("episodes")) or int(existing.get("expected_count") or 0)
|
||||||
airing_status = normalize_animeschedule_status((schedule or {}).get("status") or existing.get("airing_status"))
|
airing_status = normalize_animeschedule_status((schedule or {}).get("status") or existing.get("airing_status"))
|
||||||
alternative_titles = extract_anilist_alternative_titles(anilist, primary_title=snapshot["title"] or existing["title"])
|
alternative_titles = extract_anidb_alternative_titles(
|
||||||
|
(anidb_match or {}).get("aid"),
|
||||||
|
primary_title=snapshot["title"] or existing["title"],
|
||||||
|
)
|
||||||
payload = (
|
payload = (
|
||||||
snapshot["title"] or existing["title"],
|
snapshot["title"] or existing["title"],
|
||||||
"updated",
|
"updated",
|
||||||
@@ -1647,15 +1483,12 @@ class WatchlistStore:
|
|||||||
dub_episodes[-1] if dub_episodes else None,
|
dub_episodes[-1] if dub_episodes else None,
|
||||||
(schedule or {}).get("route") or existing.get("animeschedule_route"),
|
(schedule or {}).get("route") or existing.get("animeschedule_route"),
|
||||||
(schedule or {}).get("title") or existing.get("animeschedule_title"),
|
(schedule or {}).get("title") or existing.get("animeschedule_title"),
|
||||||
(anilist or {}).get("id") or existing.get("anilist_id"),
|
existing.get("anilist_id"),
|
||||||
(
|
existing.get("anilist_title"),
|
||||||
((anilist or {}).get("title") or {}).get("userPreferred")
|
existing.get("anilist_site_url"),
|
||||||
or ((anilist or {}).get("title") or {}).get("romaji")
|
encode_alternative_titles(alternative_titles) if anidb_match is not None else existing.get("alternative_titles_json"),
|
||||||
or ((anilist or {}).get("title") or {}).get("english")
|
(anidb_match or {}).get("aid") or existing.get("anidb_aid"),
|
||||||
or existing.get("anilist_title")
|
(anidb_match or {}).get("title") or existing.get("anidb_title"),
|
||||||
),
|
|
||||||
(anilist or {}).get("siteUrl") or existing.get("anilist_site_url"),
|
|
||||||
encode_alternative_titles(alternative_titles) if anilist is not None else existing.get("alternative_titles_json"),
|
|
||||||
normalized_show_id,
|
normalized_show_id,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -1681,6 +1514,8 @@ class WatchlistStore:
|
|||||||
existing.get("anilist_title"),
|
existing.get("anilist_title"),
|
||||||
existing.get("anilist_site_url"),
|
existing.get("anilist_site_url"),
|
||||||
existing.get("alternative_titles_json"),
|
existing.get("alternative_titles_json"),
|
||||||
|
existing.get("anidb_aid"),
|
||||||
|
existing.get("anidb_title"),
|
||||||
normalized_show_id,
|
normalized_show_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1707,7 +1542,9 @@ class WatchlistStore:
|
|||||||
anilist_id = ?,
|
anilist_id = ?,
|
||||||
anilist_title = ?,
|
anilist_title = ?,
|
||||||
anilist_site_url = ?,
|
anilist_site_url = ?,
|
||||||
alternative_titles_json = ?
|
alternative_titles_json = ?,
|
||||||
|
anidb_aid = ?,
|
||||||
|
anidb_title = ?
|
||||||
WHERE show_id = ?
|
WHERE show_id = ?
|
||||||
""",
|
""",
|
||||||
payload,
|
payload,
|
||||||
|
|||||||
+1
-2
@@ -19,14 +19,13 @@ from uuid import uuid4
|
|||||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
||||||
APP_NAME = "ani-cli-web"
|
APP_NAME = "ani-cli-web"
|
||||||
VERSION = "0.37.0"
|
VERSION = "0.38.0"
|
||||||
ALLANIME_BASE = "allanime.day"
|
ALLANIME_BASE = "allanime.day"
|
||||||
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
||||||
ALLANIME_REFERER = "https://allmanga.to"
|
ALLANIME_REFERER = "https://allmanga.to"
|
||||||
ALLMANGA_SITE_BASE = "https://allmanga.to"
|
ALLMANGA_SITE_BASE = "https://allmanga.to"
|
||||||
ANIMESCHEDULE_API = "https://animeschedule.net/api/v3"
|
ANIMESCHEDULE_API = "https://animeschedule.net/api/v3"
|
||||||
ANIMESCHEDULE_IMAGE_BASE = "https://img.animeschedule.net/production/assets/public/img/"
|
ANIMESCHEDULE_IMAGE_BASE = "https://img.animeschedule.net/production/assets/public/img/"
|
||||||
ANILIST_API = "https://graphql.anilist.co"
|
|
||||||
ANIDB_TITLES_URL = "https://anidb.net/api/anime-titles.xml.gz"
|
ANIDB_TITLES_URL = "https://anidb.net/api/anime-titles.xml.gz"
|
||||||
ANIDB_ANIME_PAGE = "https://anidb.net/anime/{aid}"
|
ANIDB_ANIME_PAGE = "https://anidb.net/anime/{aid}"
|
||||||
AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0"
|
AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0"
|
||||||
|
|||||||
+18
-24
@@ -621,7 +621,7 @@ class WatchlistRefreshJobTests(unittest.TestCase):
|
|||||||
self.assertIn("restart", status["message"].lower())
|
self.assertIn("restart", status["message"].lower())
|
||||||
|
|
||||||
|
|
||||||
class WatchlistAniListTests(unittest.TestCase):
|
class WatchlistAlternativeTitleTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
APP.reset_runtime(wait=True)
|
APP.reset_runtime(wait=True)
|
||||||
APP.initialize_runtime(start_workers=False)
|
APP.initialize_runtime(start_workers=False)
|
||||||
@@ -629,16 +629,13 @@ class WatchlistAniListTests(unittest.TestCase):
|
|||||||
with self.watchlist._connect() as conn:
|
with self.watchlist._connect() as conn:
|
||||||
conn.execute("DELETE FROM watchlist")
|
conn.execute("DELETE FROM watchlist")
|
||||||
|
|
||||||
def test_watchlist_schema_includes_anilist_columns(self):
|
def test_watchlist_schema_includes_alternative_title_column(self):
|
||||||
with self.watchlist._connect() as conn:
|
with self.watchlist._connect() as conn:
|
||||||
columns = {row["name"] for row in conn.execute("PRAGMA table_info(watchlist)").fetchall()}
|
columns = {row["name"] for row in conn.execute("PRAGMA table_info(watchlist)").fetchall()}
|
||||||
|
|
||||||
self.assertIn("anilist_id", columns)
|
|
||||||
self.assertIn("anilist_title", columns)
|
|
||||||
self.assertIn("anilist_site_url", columns)
|
|
||||||
self.assertIn("alternative_titles_json", columns)
|
self.assertIn("alternative_titles_json", columns)
|
||||||
|
|
||||||
def test_refresh_caches_anilist_alternative_titles(self):
|
def test_refresh_caches_english_anidb_alternative_titles(self):
|
||||||
self.watchlist.add({"show_id": "show-1", "title": "Attack on Titan", "category": "watching"})
|
self.watchlist.add({"show_id": "show-1", "title": "Attack on Titan", "category": "watching"})
|
||||||
|
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
@@ -651,29 +648,26 @@ class WatchlistAniListTests(unittest.TestCase):
|
|||||||
return_value={"route": "attack-on-titan", "title": "Attack on Titan", "episodes": 2, "status": "Finished"},
|
return_value={"route": "attack-on-titan", "title": "Attack on Titan", "episodes": 2, "status": "Finished"},
|
||||||
), mock.patch.object(
|
), mock.patch.object(
|
||||||
APP,
|
APP,
|
||||||
"resolve_anilist_media",
|
"resolve_anidb_aid",
|
||||||
return_value={
|
return_value={"aid": "16498", "title": "Attack on Titan"},
|
||||||
"id": 16498,
|
), mock.patch.object(
|
||||||
"siteUrl": "https://anilist.co/anime/16498",
|
APP,
|
||||||
"title": {
|
"get_cached_anidb_titles",
|
||||||
"romaji": "Shingeki no Kyojin",
|
return_value=[
|
||||||
"english": "Attack on Titan",
|
{"aid": "16498", "title": "Attack on Titan", "type": "official", "lang": "en", "normalized_title": "attack on titan"},
|
||||||
"native": "進撃の巨人",
|
{"aid": "16498", "title": "AoT", "type": "syn", "lang": "en", "normalized_title": "aot"},
|
||||||
"userPreferred": "Attack on Titan",
|
{"aid": "16498", "title": "Shingeki no Kyojin", "type": "official", "lang": "x-jat", "normalized_title": "shingeki no kyojin"},
|
||||||
},
|
{"aid": "16498", "title": "Attack Titan", "type": "short", "lang": "en", "normalized_title": "attack titan"},
|
||||||
"synonyms": ["AoT"],
|
],
|
||||||
},
|
|
||||||
):
|
):
|
||||||
item = self.watchlist.refresh("show-1", include_thumbnail=False)
|
item = self.watchlist.refresh("show-1", include_thumbnail=False)
|
||||||
|
|
||||||
self.assertEqual(item["anilist_id"], 16498)
|
self.assertEqual(item["anidb_aid"], "16498")
|
||||||
self.assertEqual(item["anilist_site_url"], "https://anilist.co/anime/16498")
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
item["alternative_titles"],
|
item["alternative_titles"],
|
||||||
[
|
[
|
||||||
{"label": "Romaji", "value": "Shingeki no Kyojin"},
|
{"label": "English", "value": "AoT"},
|
||||||
{"label": "Native", "value": "進撃の巨人"},
|
{"label": "English", "value": "Attack Titan"},
|
||||||
{"label": "Synonym", "value": "AoT"},
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.assertTrue(item["has_alternative_titles"])
|
self.assertTrue(item["has_alternative_titles"])
|
||||||
@@ -1841,7 +1835,7 @@ class TemplateHelperTests(unittest.TestCase):
|
|||||||
self.assertIn("Alternative titles", APP.WATCHLIST_HTML)
|
self.assertIn("Alternative titles", APP.WATCHLIST_HTML)
|
||||||
self.assertIn('class="ghost alt-titles-toggle"', APP.WATCHLIST_HTML)
|
self.assertIn('class="ghost alt-titles-toggle"', APP.WATCHLIST_HTML)
|
||||||
self.assertIn('altTitlesPanel.classList.toggle("open")', APP.WATCHLIST_HTML)
|
self.assertIn('altTitlesPanel.classList.toggle("open")', APP.WATCHLIST_HTML)
|
||||||
self.assertIn("AniList alternative titles were not available for this show.", APP.WATCHLIST_HTML)
|
self.assertIn("AniDB English alternative titles were not available for this show.", APP.WATCHLIST_HTML)
|
||||||
|
|
||||||
def test_watchlist_thumbnail_warmup_skips_repeated_attempts(self):
|
def test_watchlist_thumbnail_warmup_skips_repeated_attempts(self):
|
||||||
self.assertIn("thumbnailWarmupPending: new Set()", APP.WATCHLIST_HTML)
|
self.assertIn("thumbnailWarmupPending: new Set()", APP.WATCHLIST_HTML)
|
||||||
|
|||||||
+2
-2
@@ -1118,9 +1118,9 @@ WATCHLIST_HTML = r"""<!doctype html>
|
|||||||
pill.append(label, value);
|
pill.append(label, value);
|
||||||
altTitlesList.appendChild(pill);
|
altTitlesList.appendChild(pill);
|
||||||
}
|
}
|
||||||
altTitlesEmpty.textContent = "Fetched from AniList.";
|
altTitlesEmpty.textContent = "Fetched from AniDB English titles.";
|
||||||
} else {
|
} else {
|
||||||
altTitlesEmpty.textContent = "AniList alternative titles were not available for this show.";
|
altTitlesEmpty.textContent = "AniDB English alternative titles were not available for this show.";
|
||||||
}
|
}
|
||||||
altTitlesToggle.addEventListener("click", () => {
|
altTitlesToggle.addEventListener("click", () => {
|
||||||
const open = altTitlesPanel.classList.toggle("open");
|
const open = altTitlesPanel.classList.toggle("open");
|
||||||
|
|||||||
Reference in New Issue
Block a user