Add search language availability badges
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.51.2 - 2026-08-09
|
||||||
|
|
||||||
|
- Changed Search to inspect both subbed and dubbed episode availability for each result.
|
||||||
|
- Added `SUB` and `DUB` language badges to Search result cards when those episode versions are present.
|
||||||
|
|
||||||
## 0.51.1 - 2026-08-09
|
## 0.51.1 - 2026-08-09
|
||||||
|
|
||||||
- Removed the manual add form from the Watchlist page so entries are added through Search.
|
- Removed the manual add form from the Watchlist page so entries are added through Search.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Kaizoku is a local web app for searching, tracking, and downloading anime from A
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Search anime through the configured provider: `anikoto`, `anineko`, or `pahe`.
|
- Search anime through the configured provider and tag results with available `SUB` and `DUB` episode languages.
|
||||||
- Queue single episodes or batches in subbed or dubbed mode.
|
- Queue single episodes or batches in subbed or dubbed mode.
|
||||||
- Fall back across the other configured providers when an episode or stream cannot be resolved on the primary provider.
|
- Fall back across the other configured providers when an episode or stream cannot be resolved on the primary provider.
|
||||||
- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`.
|
- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`.
|
||||||
|
|||||||
@@ -879,6 +879,43 @@ def cache_thumbnail_image(show_id, image_url):
|
|||||||
return final_path.name
|
return final_path.name
|
||||||
|
|
||||||
|
|
||||||
|
def episode_language_summary(show_id, provider):
|
||||||
|
summary = {
|
||||||
|
"sub": False,
|
||||||
|
"dub": False,
|
||||||
|
"sub_count": 0,
|
||||||
|
"dub_count": 0,
|
||||||
|
"total": 0,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
rows = provider_bridge.episodes(show_id, provider=provider)
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log("search.episode_language_summary_failed", show_id=show_id, provider=provider, error=exc)
|
||||||
|
return summary
|
||||||
|
sub_values = set()
|
||||||
|
dub_values = set()
|
||||||
|
all_values = set()
|
||||||
|
for row in rows:
|
||||||
|
value = str(row.get("number") or "").strip()
|
||||||
|
if value:
|
||||||
|
all_values.add(value)
|
||||||
|
langs = {str(lang or "").strip().lower() for lang in row.get("langs") or []}
|
||||||
|
if not langs:
|
||||||
|
langs.add("sub")
|
||||||
|
if "hsub" in langs:
|
||||||
|
langs.add("sub")
|
||||||
|
if "sub" in langs and value:
|
||||||
|
sub_values.add(value)
|
||||||
|
if "dub" in langs and value:
|
||||||
|
dub_values.add(value)
|
||||||
|
summary["sub"] = bool(sub_values)
|
||||||
|
summary["dub"] = bool(dub_values)
|
||||||
|
summary["sub_count"] = len(sub_values)
|
||||||
|
summary["dub_count"] = len(dub_values)
|
||||||
|
summary["total"] = len(all_values)
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def search_anime(query, mode):
|
def search_anime(query, mode):
|
||||||
runtime = ensure_runtime()
|
runtime = ensure_runtime()
|
||||||
provider = str((runtime.get("config") or {}).get("provider") or "anikoto").strip().lower()
|
provider = str((runtime.get("config") or {}).get("provider") or "anikoto").strip().lower()
|
||||||
@@ -890,13 +927,18 @@ def search_anime(query, mode):
|
|||||||
show_id = str(show.get("id") or "").strip()
|
show_id = str(show.get("id") or "").strip()
|
||||||
if not show_id or not title:
|
if not show_id or not title:
|
||||||
continue
|
continue
|
||||||
|
language_summary = episode_language_summary(show_id, show.get("provider") or provider)
|
||||||
alternative_titles = extract_anidb_alternative_titles_for_title(title, primary_title=title)
|
alternative_titles = extract_anidb_alternative_titles_for_title(title, primary_title=title)
|
||||||
index = len(results) + 1
|
index = len(results) + 1
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
"id": show_id,
|
"id": show_id,
|
||||||
"title": title,
|
"title": title,
|
||||||
"episodes": "",
|
"episodes": language_summary["total"],
|
||||||
|
"has_sub": language_summary["sub"],
|
||||||
|
"has_dub": language_summary["dub"],
|
||||||
|
"sub_episodes": language_summary["sub_count"],
|
||||||
|
"dub_episodes": language_summary["dub_count"],
|
||||||
"index": index,
|
"index": index,
|
||||||
"query": query,
|
"query": query,
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
|
|||||||
+13
-8
@@ -423,7 +423,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
<label for="search-query">Anime Title / Keyword</label>
|
<label for="search-query">Anime Title / Keyword</label>
|
||||||
<input type="text" id="search-query" placeholder="e.g., Attack on Titan">
|
<input type="text" id="search-query" placeholder="e.g., Attack on Titan">
|
||||||
|
|
||||||
<label for="search-mode">Mode</label>
|
<label for="search-mode">Episode mode</label>
|
||||||
<select id="search-mode">
|
<select id="search-mode">
|
||||||
<option value="sub">Subtitled (Sub)</option>
|
<option value="sub">Subtitled (Sub)</option>
|
||||||
<option value="dub">Dubbed (Dub)</option>
|
<option value="dub">Dubbed (Dub)</option>
|
||||||
@@ -569,10 +569,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
<div class="result-title"></div>
|
<div class="result-title"></div>
|
||||||
<div class="result-subtitle"></div>
|
<div class="result-subtitle"></div>
|
||||||
<div class="result-meta">
|
<div class="result-meta"></div>
|
||||||
<span class="result-pill"></span>
|
|
||||||
<span class="result-pill"></span>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
btn.querySelector(".result-title").textContent = item.title;
|
btn.querySelector(".result-title").textContent = item.title;
|
||||||
const subtitle = btn.querySelector(".result-subtitle");
|
const subtitle = btn.querySelector(".result-subtitle");
|
||||||
@@ -582,9 +579,17 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
subtitle.textContent = alternativeTitle ? `Also known as: ${alternativeTitle}` : "";
|
subtitle.textContent = alternativeTitle ? `Also known as: ${alternativeTitle}` : "";
|
||||||
subtitle.hidden = !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 meta = btn.querySelector(".result-meta");
|
||||||
pills[0].textContent = `Source: ${providerLabel(item.provider)}`;
|
const addPill = (text) => {
|
||||||
pills[1].textContent = item.episodes ? `${item.episodes} episodes` : `Result ${item.index}`;
|
const pill = document.createElement("span");
|
||||||
|
pill.className = "result-pill";
|
||||||
|
pill.textContent = text;
|
||||||
|
meta.appendChild(pill);
|
||||||
|
};
|
||||||
|
addPill(`Source: ${providerLabel(item.provider)}`);
|
||||||
|
if (item.has_sub) addPill("SUB");
|
||||||
|
if (item.has_dub) addPill("DUB");
|
||||||
|
addPill(item.episodes ? `${item.episodes} episodes` : `Result ${item.index}`);
|
||||||
const img = btn.querySelector("img");
|
const img = btn.querySelector("img");
|
||||||
const tile = btn.querySelector(".cover-tile");
|
const tile = btn.querySelector(".cover-tile");
|
||||||
img.alt = item.title;
|
img.alt = item.title;
|
||||||
|
|||||||
Reference in New Issue
Block a user