Show alt titles in search results
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -323,6 +323,14 @@ INDEX_HTML = r"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
<span class="cover-fallback"></span>
|
||||
</div>
|
||||
<div class="result-title"></div>
|
||||
<div class="result-subtitle"></div>
|
||||
<div class="result-meta">
|
||||
<span class="result-pill"></span>
|
||||
<span class="result-pill"></span>
|
||||
</div>
|
||||
`;
|
||||
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`;
|
||||
|
||||
+22
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user