Revert "Show AllManga alternative watchlist titles"

This reverts commit 84c2e533d0.
This commit is contained in:
Dymas
2026-05-23 19:14:35 +02:00
parent bead7da60a
commit 2ff4e390c4
7 changed files with 7 additions and 204 deletions
-4
View File
@@ -1,9 +1,5 @@
# Changelog # Changelog
## 0.37.0 - 2026-05-23
- Added watchlist support for alternative English and synonym titles pulled from the AllManga show page and shown directly on each watchlist card.
## 0.36.1 - 2026-05-23 ## 0.36.1 - 2026-05-23
- Replaced the first-visit remote-access 403 dead end with a browser sign-in page and persistent auth cookie, so remote users no longer need to know the `?token=...` bootstrap URL format up front. - Replaced the first-visit remote-access 403 dead end with a browser sign-in page and persistent auth cookie, so remote users no longer need to know the `?token=...` bootstrap URL format up front.
+1 -6
View File
@@ -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.36.1`
## Project Layout ## Project Layout
@@ -238,11 +238,6 @@ Thumbnail behavior:
- Thumbnails are served from the local `/api/watchlist/thumb/...` route. - Thumbnails are served from the local `/api/watchlist/thumb/...` route.
- Cached thumbnails are reused. - Cached thumbnails are reused.
Title behavior:
- Watchlist refreshes also try to pull alternative English and synonym titles from the AllManga show page.
- Up to three alternate names are shown as a muted secondary line under the main watchlist title.
- Missing covers are warmed in small batches. - Missing covers are warmed in small batches.
- Failed lookups back off before retrying. - Failed lookups back off before retrying.
- You can force `Reload` or use `Upload` when no thumbnail exists. - You can force `Reload` or use `Upload` when no thumbnail exists.
+1 -1
View File
@@ -1 +1 @@
0.37.0 0.36.1
+3 -145
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import base64 import base64
import gzip import gzip
import html
import json import json
import mimetypes import mimetypes
import os import os
@@ -147,95 +146,6 @@ def anime_source_url(show_id, title):
return f"{ALLMANGA_SITE_BASE}/bangumi/{quote(normalized_show_id, safe='')}" return f"{ALLMANGA_SITE_BASE}/bangumi/{quote(normalized_show_id, safe='')}"
def unique_title_values(values, *exclude_values):
seen = {
normalize_title_key(value)
for value in exclude_values
if normalize_title_key(value)
}
deduped = []
for value in values:
text = str(value or "").strip()
normalized = normalize_title_key(text)
if not text or not normalized or normalized in seen:
continue
seen.add(normalized)
deduped.append(text)
return deduped
def parse_allmanga_alternative_titles(html_text, primary_title=""):
text = str(html_text or "")
primary = str(primary_title or "").strip()
candidates = []
preferred_english = ""
def add_candidate(value, english_preferred=False):
nonlocal preferred_english
cleaned = html.unescape(str(value or "")).replace('\\"', '"').strip()
if not cleaned:
return
candidates.append(cleaned)
if english_preferred and not preferred_english:
preferred_english = cleaned
quoted_key_patterns = (
(r'"english(?:Title|Name)?"\s*:\s*"([^"]+)"', True),
(r'"(?:altEnglish|alternativeEnglish(?:Title|Name)?)"\s*:\s*"([^"]+)"', True),
(r'"romaji(?:Title|Name)?"\s*:\s*"([^"]+)"', False),
(r'"native(?:Title|Name)?"\s*:\s*"([^"]+)"', False),
)
for pattern, english_preferred in quoted_key_patterns:
for match in re.findall(pattern, text, flags=re.IGNORECASE):
add_candidate(match, english_preferred=english_preferred)
array_key_patterns = (
(r'"synonyms"\s*:\s*\[(.*?)\]', False),
(r'"alternative(?:Names|Titles)?"\s*:\s*\[(.*?)\]', False),
(r'"alt(?:Titles|Names)?"\s*:\s*\[(.*?)\]', False),
)
for pattern, english_preferred in array_key_patterns:
for body in re.findall(pattern, text, flags=re.IGNORECASE | re.DOTALL):
for match in re.findall(r'"([^"]+)"', body):
add_candidate(match, english_preferred=english_preferred)
deduped = unique_title_values(candidates, primary)
english_title = ""
for candidate in unique_title_values([preferred_english], primary):
english_title = candidate
break
if not english_title and deduped:
english_title = deduped[0]
return {
"english_title": english_title,
"alt_titles": deduped,
}
def fetch_allmanga_alternative_titles(show_id, title="", timeout=25):
url = anime_source_url(show_id, title)
request = urllib.request.Request(
url,
headers={
"Accept": "text/html,application/xhtml+xml",
"Referer": ALLANIME_REFERER,
"User-Agent": AGENT,
},
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
raw = response.read().decode("utf-8", errors="replace")
except urllib.error.URLError as exc:
raise RuntimeError(f"Could not reach AllManga page: {exc}") from exc
except TimeoutError as exc:
raise RuntimeError("AllManga page request timed out") from exc
parsed = parse_allmanga_alternative_titles(raw, primary_title=title)
if not parsed["alt_titles"] and not parsed["english_title"]:
raise RuntimeError("AllManga page did not expose any alternative titles")
return parsed
def infer_image_extension(url, content_type): def infer_image_extension(url, content_type):
normalized_type = str(content_type or "").split(";", 1)[0].strip().lower() normalized_type = str(content_type or "").split(";", 1)[0].strip().lower()
extension = mimetypes.guess_extension(normalized_type, strict=False) extension = mimetypes.guess_extension(normalized_type, strict=False)
@@ -1118,8 +1028,6 @@ class WatchlistStore:
dub_latest_episode TEXT, dub_latest_episode TEXT,
animeschedule_route TEXT, animeschedule_route TEXT,
animeschedule_title TEXT, animeschedule_title TEXT,
english_title TEXT,
alt_titles_json TEXT NOT NULL DEFAULT '[]',
anidb_aid TEXT, anidb_aid TEXT,
anidb_title TEXT, anidb_title TEXT,
thumbnail_path TEXT, thumbnail_path TEXT,
@@ -1148,10 +1056,6 @@ class WatchlistStore:
conn.execute("ALTER TABLE watchlist ADD COLUMN animeschedule_route TEXT") conn.execute("ALTER TABLE watchlist ADD COLUMN animeschedule_route TEXT")
if "animeschedule_title" not in columns: if "animeschedule_title" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN animeschedule_title TEXT") conn.execute("ALTER TABLE watchlist ADD COLUMN animeschedule_title TEXT")
if "english_title" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN english_title TEXT")
if "alt_titles_json" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN alt_titles_json TEXT NOT NULL DEFAULT '[]'")
conn.execute( conn.execute(
"UPDATE watchlist SET category = 'watching' WHERE category IS NULL OR TRIM(category) = ''" "UPDATE watchlist SET category = 'watching' WHERE category IS NULL OR TRIM(category) = ''"
) )
@@ -1173,22 +1077,6 @@ class WatchlistStore:
item["dub_progress"] = format_episode_progress(item["dub_count"], item["expected_count"]) item["dub_progress"] = format_episode_progress(item["dub_count"], item["expected_count"])
item["sub_complete"] = mode_is_complete(item["sub_count"], item["expected_count"], item["airing_status"]) item["sub_complete"] = mode_is_complete(item["sub_count"], item["expected_count"], item["airing_status"])
item["dub_complete"] = mode_is_complete(item["dub_count"], item["expected_count"], item["airing_status"]) item["dub_complete"] = mode_is_complete(item["dub_count"], item["expected_count"], item["airing_status"])
try:
alt_titles = json.loads(item.get("alt_titles_json") or "[]")
except json.JSONDecodeError:
alt_titles = []
if not isinstance(alt_titles, list):
alt_titles = []
item["alt_titles"] = unique_title_values(alt_titles, item.get("title"))
item["english_title"] = next(iter(unique_title_values([item.get("english_title")], item.get("title"))), "")
display_alt_titles = []
if item["english_title"]:
display_alt_titles.append(item["english_title"])
for candidate in item["alt_titles"]:
if normalize_title_key(candidate) == normalize_title_key(item["english_title"]):
continue
display_alt_titles.append(candidate)
item["display_alt_titles"] = display_alt_titles[:3]
item["finished_badge"] = ( item["finished_badge"] = (
"downloaded" if item["category"] == "finished" and item["downloaded"] else "manual" if item["category"] == "finished" else "" "downloaded" if item["category"] == "finished" and item["downloaded"] else "manual" if item["category"] == "finished" else ""
) )
@@ -1209,19 +1097,15 @@ class WatchlistStore:
return item return item
def _upsert_conn(self, conn, item): def _upsert_conn(self, conn, item):
raw_alt_titles = item.get("alt_titles") if isinstance(item.get("alt_titles"), list) else []
if not raw_alt_titles and isinstance(item.get("alt_titles_json"), list):
raw_alt_titles = item.get("alt_titles_json")
normalized_alt_titles = unique_title_values(raw_alt_titles, item.get("title"))
conn.execute( conn.execute(
""" """
INSERT INTO watchlist ( INSERT INTO watchlist (
show_id, title, category, downloaded, status, status_message, created_at, updated_at, show_id, title, category, downloaded, status, status_message, created_at, updated_at,
last_checked, sub_count, dub_count, expected_count, airing_status, last_checked, sub_count, dub_count, expected_count, airing_status,
sub_latest_episode, dub_latest_episode, sub_latest_episode, dub_latest_episode,
animeschedule_route, animeschedule_title, english_title, alt_titles_json, anidb_aid, anidb_title, animeschedule_route, animeschedule_title, anidb_aid, anidb_title,
thumbnail_path, thumbnail_checked_at thumbnail_path, thumbnail_checked_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(show_id) DO UPDATE SET ON CONFLICT(show_id) DO UPDATE SET
title = excluded.title, title = excluded.title,
category = excluded.category, category = excluded.category,
@@ -1238,8 +1122,6 @@ class WatchlistStore:
dub_latest_episode = excluded.dub_latest_episode, dub_latest_episode = excluded.dub_latest_episode,
animeschedule_route = excluded.animeschedule_route, animeschedule_route = excluded.animeschedule_route,
animeschedule_title = excluded.animeschedule_title, animeschedule_title = excluded.animeschedule_title,
english_title = excluded.english_title,
alt_titles_json = excluded.alt_titles_json,
anidb_aid = excluded.anidb_aid, anidb_aid = excluded.anidb_aid,
anidb_title = excluded.anidb_title, anidb_title = excluded.anidb_title,
thumbnail_path = excluded.thumbnail_path, thumbnail_path = excluded.thumbnail_path,
@@ -1263,8 +1145,6 @@ class WatchlistStore:
item.get("dub_latest_episode"), item.get("dub_latest_episode"),
item.get("animeschedule_route"), item.get("animeschedule_route"),
item.get("animeschedule_title"), item.get("animeschedule_title"),
item.get("english_title"),
json.dumps(normalized_alt_titles),
item.get("anidb_aid"), item.get("anidb_aid"),
item.get("anidb_title"), item.get("anidb_title"),
item.get("thumbnail_path"), item.get("thumbnail_path"),
@@ -1310,8 +1190,6 @@ class WatchlistStore:
"dub_latest_episode": None, "dub_latest_episode": None,
"animeschedule_route": None, "animeschedule_route": None,
"animeschedule_title": None, "animeschedule_title": None,
"english_title": None,
"alt_titles": [],
"anidb_aid": None, "anidb_aid": None,
"anidb_title": None, "anidb_title": None,
"thumbnail_path": None, "thumbnail_path": None,
@@ -1437,8 +1315,6 @@ class WatchlistStore:
"dub_latest_episode": None, "dub_latest_episode": None,
"animeschedule_route": None, "animeschedule_route": None,
"animeschedule_title": None, "animeschedule_title": None,
"english_title": None,
"alt_titles": [],
"anidb_aid": None, "anidb_aid": None,
"anidb_title": None, "anidb_title": None,
"thumbnail_path": None, "thumbnail_path": None,
@@ -1465,10 +1341,6 @@ class WatchlistStore:
sub_episodes = snapshot["episodes"]["sub"] sub_episodes = snapshot["episodes"]["sub"]
dub_episodes = snapshot["episodes"]["dub"] dub_episodes = snapshot["episodes"]["dub"]
schedule = None schedule = None
allmanga_titles = {
"english_title": str(existing.get("english_title") or "").strip(),
"alt_titles": list(existing.get("alt_titles") or []),
}
try: try:
schedule = resolve_animeschedule_anime( schedule = resolve_animeschedule_anime(
snapshot["title"] or existing["title"], snapshot["title"] or existing["title"],
@@ -1477,14 +1349,6 @@ class WatchlistStore:
) )
except Exception: except Exception:
schedule = None schedule = None
try:
allmanga_titles = fetch_allmanga_alternative_titles(
normalized_show_id,
snapshot["title"] or existing["title"],
timeout=request_timeout,
)
except Exception as exc:
debug_log("watchlist.alt_titles.lookup_failed", show_id=normalized_show_id, error=exc)
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):
@@ -1510,8 +1374,6 @@ 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"),
allmanga_titles.get("english_title") or existing.get("english_title"),
json.dumps(unique_title_values(allmanga_titles.get("alt_titles") or [], snapshot["title"] or existing["title"])),
normalized_show_id, normalized_show_id,
) )
except Exception as exc: except Exception as exc:
@@ -1533,8 +1395,6 @@ class WatchlistStore:
existing.get("dub_latest_episode"), existing.get("dub_latest_episode"),
existing.get("animeschedule_route"), existing.get("animeschedule_route"),
existing.get("animeschedule_title"), existing.get("animeschedule_title"),
existing.get("english_title"),
existing.get("alt_titles_json") or "[]",
normalized_show_id, normalized_show_id,
) )
@@ -1557,9 +1417,7 @@ class WatchlistStore:
sub_latest_episode = ?, sub_latest_episode = ?,
dub_latest_episode = ?, dub_latest_episode = ?,
animeschedule_route = ?, animeschedule_route = ?,
animeschedule_title = ?, animeschedule_title = ?
english_title = ?,
alt_titles_json = ?
WHERE show_id = ? WHERE show_id = ?
""", """,
payload, payload,
+1 -1
View File
@@ -19,7 +19,7 @@ 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.36.1"
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"
-31
View File
@@ -849,23 +849,6 @@ class WatchlistCompletionTests(unittest.TestCase):
self.assertIn("Sub ready.", item["status_message"]) self.assertIn("Sub ready.", item["status_message"])
self.assertIn("Dub ready.", item["status_message"]) self.assertIn("Dub ready.", item["status_message"])
def test_refresh_persists_allmanga_alternative_titles(self):
self.seed_watchlist_item()
snapshot = {
"show_id": "show-1",
"title": "Example Show",
"episodes": {"sub": ["1"], "dub": []},
}
with mock.patch.object(APP, "fetch_show_episode_snapshot", return_value=snapshot), mock.patch.object(
APP, "resolve_animeschedule_anime", return_value=None
), mock.patch.object(
APP, "fetch_allmanga_alternative_titles", return_value={"english_title": "Example Show English", "alt_titles": ["Example Show English", "Sample Alias"]}
), mock.patch.object(APP.WatchlistStore, "ensure_thumbnail", return_value=None):
item = APP.WATCHLIST.refresh("show-1")
self.assertEqual(item["english_title"], "Example Show English")
self.assertEqual(item["display_alt_titles"], ["Example Show English", "Sample Alias"])
def test_list_filters_by_category_and_reports_category_counts(self): def test_list_filters_by_category_and_reports_category_counts(self):
self.seed_watchlist_item(show_id="show-1", title="Watching Show", category="watching", status="queued") self.seed_watchlist_item(show_id="show-1", title="Watching Show", category="watching", status="queued")
self.seed_watchlist_item(show_id="show-2", title="Finished Show", category="finished", downloaded=True) self.seed_watchlist_item(show_id="show-2", title="Finished Show", category="finished", downloaded=True)
@@ -1745,26 +1728,12 @@ class TemplateHelperTests(unittest.TestCase):
self.assertIn('api("/api/watchlist/download-all"', APP.WATCHLIST_HTML) self.assertIn('api("/api/watchlist/download-all"', APP.WATCHLIST_HTML)
self.assertIn("Enter sub or dub", APP.WATCHLIST_HTML) self.assertIn("Enter sub or dub", APP.WATCHLIST_HTML)
def test_watchlist_page_shows_alternative_titles(self):
self.assertIn('class="alt-title"', APP.WATCHLIST_HTML)
self.assertIn('Alt: ${item.display_alt_titles.join(" · ")}', 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)
self.assertIn("function thumbnailWarmupToken(item)", APP.WATCHLIST_HTML) self.assertIn("function thumbnailWarmupToken(item)", APP.WATCHLIST_HTML)
self.assertIn("if (state.thumbnailWarmupTokens[item.show_id] === token) continue;", APP.WATCHLIST_HTML) self.assertIn("if (state.thumbnailWarmupTokens[item.show_id] === token) continue;", APP.WATCHLIST_HTML)
self.assertIn("state.thumbnailWarmupPending.delete(showId);", APP.WATCHLIST_HTML) self.assertIn("state.thumbnailWarmupPending.delete(showId);", APP.WATCHLIST_HTML)
def test_parse_allmanga_alternative_titles_extracts_english_and_synonyms(self):
html_text = """
<script>
{"englishTitle":"Example Show English","synonyms":["Sample Alias","Example Show"]}
</script>
"""
parsed = APP.parse_allmanga_alternative_titles(html_text, primary_title="Example Show")
self.assertEqual(parsed["english_title"], "Example Show English")
self.assertEqual(parsed["alt_titles"], ["Example Show English", "Sample Alias"])
class AniDbCacheTests(unittest.TestCase): class AniDbCacheTests(unittest.TestCase):
def test_get_cached_anidb_titles_reuses_cached_entries(self): def test_get_cached_anidb_titles_reuses_cached_entries(self):
+1 -16
View File
@@ -413,14 +413,6 @@ WATCHLIST_HTML = r"""<!doctype html>
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.alt-title {
color: var(--muted);
font-size: 12px;
line-height: 1.35;
margin-top: 4px;
overflow-wrap: anywhere;
min-height: 1.35em;
}
.card-title a { .card-title a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
@@ -999,10 +991,7 @@ WATCHLIST_HTML = r"""<!doctype html>
<input class="thumb-upload-input" type="file" accept="image/*" hidden> <input class="thumb-upload-input" type="file" accept="image/*" hidden>
</div> </div>
<div class="title-row"> <div class="title-row">
<div> <div class="card-title"></div>
<div class="card-title"></div>
<div class="alt-title"></div>
</div>
<span class="finish-check ${item.finished_badge || ""}" ${item.finished_badge ? "" : "hidden"}>${item.finished_badge ? "&check;" : ""}</span> <span class="finish-check ${item.finished_badge || ""}" ${item.finished_badge ? "" : "hidden"}>${item.finished_badge ? "&check;" : ""}</span>
</div> </div>
<div class="count-row"> <div class="count-row">
@@ -1030,10 +1019,6 @@ WATCHLIST_HTML = r"""<!doctype html>
titleLink.rel = "noopener noreferrer"; titleLink.rel = "noopener noreferrer";
titleLink.textContent = item.title; titleLink.textContent = item.title;
titleNode.replaceChildren(titleLink); titleNode.replaceChildren(titleLink);
const altTitleNode = card.querySelector(".alt-title");
altTitleNode.textContent = (item.display_alt_titles || []).length
? `Alt: ${item.display_alt_titles.join(" · ")}`
: "";
const finishCheck = card.querySelector(".finish-check"); const finishCheck = card.querySelector(".finish-check");
if (finishCheck && item.finished_badge) { if (finishCheck && item.finished_badge) {
finishCheck.title = item.finished_badge === "downloaded" finishCheck.title = item.finished_badge === "downloaded"