diff --git a/CHANGELOG.md b/CHANGELOG.md index fd90081..78a8ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.45.16 - 2026-06-02 + +- Show the first English AniDB alternative title beneath each Search result when one exists, reusing the watchlist-backed alternative-title extraction path instead of duplicating title matching in the browser. +- Included alternative titles in the Search API payload so the result cards can render the subtitle without doing extra per-card network requests. + ## 0.45.15 - 2026-06-02 - Moved Search page results out of the sidebar and into a poster-style grid below the selection panel so hits now read like watchlist cards instead of a compact list. diff --git a/README.md b/README.md index 3d15d3b..301ce7d 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ Local web UI for a system-wide `ani-cli` install. -Current version: `0.45.15` +Current version: `0.45.16` ## What it does -- Search anime in `sub` or `dub`, with poster-style results shown below the selection panel +- Search anime in `sub` or `dub`, with poster-style results shown below the selection panel and the first English alternate title when available - Queue downloads with folder, quality, media type, season, and episode controls - Track shows in a watchlist with `Watching`, `Planned`, `Finished`, and `Dropped` categories - Refresh watchlist episode counts manually or on a schedule @@ -16,7 +16,7 @@ Current version: `0.45.15` ## Pages -- `/`: Search and queue downloads, with poster-card search results below the selection box +- `/`: Search and queue downloads, with poster-card search results below the selection box and alternate English titles under the main name when available - `/queue`: Monitor jobs, inspect logs, retry failed jobs, remove finished jobs - `/watchlist`: Track shows, refresh them, download all available episodes, manage auto-download settings - `/config`: Save defaults, schedule refreshes, configure Jellyfin and Discord, view runtime info and the changelog diff --git a/VERSION b/VERSION index 9e97f91..eaae82d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.45.15 +0.45.16 diff --git a/app.py b/app.py index 171f576..8c41c00 100644 --- a/app.py +++ b/app.py @@ -721,6 +721,22 @@ def fetch_anidb_cover(title): return fetch_anidb_cover_by_aid(match["aid"], fallback_title=match["title"]) +def extract_anidb_alternative_titles_for_title(title, primary_title=""): + clean_title = str(title or "").strip() + if not clean_title: + return [] + try: + match = resolve_anidb_aid(clean_title) + except Exception as exc: + debug_log("anidb.alt_titles.resolve_failed", title=clean_title, error=exc) + return [] + try: + return extract_anidb_alternative_titles(match["aid"], primary_title=primary_title or clean_title) + except Exception as exc: + debug_log("anidb.alt_titles.extract_failed", title=clean_title, aid=match.get("aid"), error=exc) + return [] + + def fetch_anidb_cover_by_aid(aid, fallback_title=""): page_url = ANIDB_ANIME_PAGE.format(aid=aid) debug_log("thumbnail.source.route_lookup", source="AniDB", aid=aid, fallback_title=fallback_title, page_url=page_url) @@ -882,6 +898,7 @@ def search_anime(query, mode): title = str(show.get("name") or "").replace('\\"', '"').strip() if not show.get("_id") or not title: continue + alternative_titles = extract_anidb_alternative_titles_for_title(title, primary_title=title) index = len(results) + 1 results.append( { @@ -891,6 +908,7 @@ def search_anime(query, mode): "index": index, "query": query, "mode": mode, + "alternative_titles": alternative_titles, } ) return results diff --git a/search_page.py b/search_page.py index 3cc8aa5..c02f4e5 100644 --- a/search_page.py +++ b/search_page.py @@ -323,6 +323,14 @@ INDEX_HTML = r""" line-height: 1.35; min-height: 2.7em; } + .result-subtitle { + text-align: left; + color: var(--muted); + font-size: 12px; + line-height: 1.3; + min-height: 1.3em; + overflow-wrap: anywhere; + } .result-meta { display: flex; gap: 6px; @@ -550,12 +558,19 @@ INDEX_HTML = r"""
+ `; btn.querySelector(".result-title").textContent = item.title; + const subtitle = btn.querySelector(".result-subtitle"); + const alternativeTitle = Array.isArray(item.alternative_titles) && item.alternative_titles.length + ? String(item.alternative_titles[0].value || "").trim() + : ""; + subtitle.textContent = alternativeTitle ? `Also known as: ${alternativeTitle}` : ""; + subtitle.hidden = !alternativeTitle; btn.querySelector(".cover-fallback").textContent = coverInitials(item.title); const pills = btn.querySelectorAll(".result-pill"); pills[0].textContent = `${item.episodes} episodes`; diff --git a/test_app.py b/test_app.py index cf426ee..7c86634 100644 --- a/test_app.py +++ b/test_app.py @@ -3237,6 +3237,28 @@ class TemplateHelperTests(unittest.TestCase): self.assertIn("Select results", APP.INDEX_HTML) self.assertIn("resultThumbnailUrl(title)", APP.INDEX_HTML) self.assertIn('/api/search/thumb?title=', APP.INDEX_HTML) + self.assertIn("result-subtitle", APP.INDEX_HTML) + self.assertIn("Also known as:", APP.INDEX_HTML) + + def test_search_anime_includes_alternative_titles(self): + payload = { + "shows": { + "edges": [ + { + "_id": "show-1", + "name": "Example Show", + "availableEpisodes": {"sub": 12}, + } + ] + } + } + with mock.patch.object(APP, "graph_request", return_value=payload), mock.patch.object( + APP, "extract_anidb_alternative_titles_for_title", + return_value=[{"label": "English", "value": "Example Show English"}], + ): + results = APP.search_anime("Example Show", "sub") + + self.assertEqual(results[0]["alternative_titles"], [{"label": "English", "value": "Example Show English"}]) def test_pages_share_serial_polling_helper(self): self.assertIn("function startSerialPoll(callback, intervalMs)", APP.QUEUE_HTML)