Show alt titles in search results
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user