Show alt titles in search results

This commit is contained in:
Dymas
2026-06-02 13:03:22 +02:00
parent 94740ebb76
commit 18d82a8678
6 changed files with 64 additions and 4 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog # 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 ## 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. - 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.
+3 -3
View File
@@ -2,11 +2,11 @@
Local web UI for a system-wide `ani-cli` install. Local web UI for a system-wide `ani-cli` install.
Current version: `0.45.15` Current version: `0.45.16`
## What it does ## 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 - Queue downloads with folder, quality, media type, season, and episode controls
- Track shows in a watchlist with `Watching`, `Planned`, `Finished`, and `Dropped` categories - Track shows in a watchlist with `Watching`, `Planned`, `Finished`, and `Dropped` categories
- Refresh watchlist episode counts manually or on a schedule - Refresh watchlist episode counts manually or on a schedule
@@ -16,7 +16,7 @@ Current version: `0.45.15`
## Pages ## 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 - `/queue`: Monitor jobs, inspect logs, retry failed jobs, remove finished jobs
- `/watchlist`: Track shows, refresh them, download all available episodes, manage auto-download settings - `/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 - `/config`: Save defaults, schedule refreshes, configure Jellyfin and Discord, view runtime info and the changelog
+1 -1
View File
@@ -1 +1 @@
0.45.15 0.45.16
+18
View File
@@ -721,6 +721,22 @@ def fetch_anidb_cover(title):
return fetch_anidb_cover_by_aid(match["aid"], fallback_title=match["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=""): def fetch_anidb_cover_by_aid(aid, fallback_title=""):
page_url = ANIDB_ANIME_PAGE.format(aid=aid) 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) 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() title = str(show.get("name") or "").replace('\\"', '"').strip()
if not show.get("_id") or not title: if not show.get("_id") or not title:
continue continue
alternative_titles = extract_anidb_alternative_titles_for_title(title, primary_title=title)
index = len(results) + 1 index = len(results) + 1
results.append( results.append(
{ {
@@ -891,6 +908,7 @@ def search_anime(query, mode):
"index": index, "index": index,
"query": query, "query": query,
"mode": mode, "mode": mode,
"alternative_titles": alternative_titles,
} }
) )
return results return results
+15
View File
@@ -323,6 +323,14 @@ INDEX_HTML = r"""<!doctype html>
line-height: 1.35; line-height: 1.35;
min-height: 2.7em; 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 { .result-meta {
display: flex; display: flex;
gap: 6px; gap: 6px;
@@ -550,12 +558,19 @@ INDEX_HTML = r"""<!doctype html>
<span class="cover-fallback"></span> <span class="cover-fallback"></span>
</div> </div>
<div class="result-title"></div> <div class="result-title"></div>
<div class="result-subtitle"></div>
<div class="result-meta"> <div class="result-meta">
<span class="result-pill"></span> <span class="result-pill"></span>
<span class="result-pill"></span> <span class="result-pill"></span>
</div> </div>
`; `;
btn.querySelector(".result-title").textContent = item.title; 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); btn.querySelector(".cover-fallback").textContent = coverInitials(item.title);
const pills = btn.querySelectorAll(".result-pill"); const pills = btn.querySelectorAll(".result-pill");
pills[0].textContent = `${item.episodes} episodes`; pills[0].textContent = `${item.episodes} episodes`;
+22
View File
@@ -3237,6 +3237,28 @@ class TemplateHelperTests(unittest.TestCase):
self.assertIn("Select results", APP.INDEX_HTML) self.assertIn("Select results", APP.INDEX_HTML)
self.assertIn("resultThumbnailUrl(title)", APP.INDEX_HTML) self.assertIn("resultThumbnailUrl(title)", APP.INDEX_HTML)
self.assertIn('/api/search/thumb?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): def test_pages_share_serial_polling_helper(self):
self.assertIn("function startSerialPoll(callback, intervalMs)", APP.QUEUE_HTML) self.assertIn("function startSerialPoll(callback, intervalMs)", APP.QUEUE_HTML)