Simplify search mode and prefer provider artwork

This commit is contained in:
Codex
2026-08-09 12:14:47 +02:00
parent f79cbd9b7d
commit 15a9ec1948
5 changed files with 32 additions and 19 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 0.51.3 - 2026-08-09
- Removed the Episode mode selector from the Search page and now choose the selected result's episode language automatically from the saved Config default and available `SUB`/`DUB` flags.
- Search result thumbnails now prefer provider-supplied artwork from the site that returned the search result, then fall back to the local title-based thumbnail route if provider artwork is missing or fails to load.
## 0.51.2 - 2026-08-09
- Changed Search to inspect both subbed and dubbed episode availability for each result.
+1
View File
@@ -5,6 +5,7 @@ Kaizoku is a local web app for searching, tracking, and downloading anime from A
## Features
- Search anime through the configured provider and tag results with available `SUB` and `DUB` episode languages.
- Prefer provider-supplied search artwork, with a local title-based thumbnail fallback when provider artwork is missing or broken.
- 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.
- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`.
+1 -1
View File
@@ -1 +1 @@
0.51.2
0.51.3
+1
View File
@@ -939,6 +939,7 @@ def search_anime(query, mode):
"has_dub": language_summary["dub"],
"sub_episodes": language_summary["sub_count"],
"dub_episodes": language_summary["dub_count"],
"image": show.get("image") or "",
"index": index,
"query": query,
"mode": mode,
+24 -18
View File
@@ -423,12 +423,6 @@ INDEX_HTML = r"""<!doctype html>
<label for="search-query">Anime Title / Keyword</label>
<input type="text" id="search-query" placeholder="e.g., Attack on Titan">
<label for="search-mode">Episode mode</label>
<select id="search-mode">
<option value="sub">Subtitled (Sub)</option>
<option value="dub">Dubbed (Dub)</option>
</select>
<label for="search-quality">Quality</label>
<select id="search-quality">
<option value="best">Best</option>
@@ -522,11 +516,6 @@ INDEX_HTML = r"""<!doctype html>
if (notice) notice.textContent = text || "";
}
function setMode(mode) {
state.config.mode = mode;
$("search-mode").value = mode;
}
function coverInitials(title) {
const parts = String(title || "")
.trim()
@@ -537,7 +526,12 @@ INDEX_HTML = r"""<!doctype html>
return parts.map((part) => part[0]).join("").slice(0, 2);
}
function resultThumbnailUrl(title) {
function resultThumbnailUrl(item) {
const providerImage = String(item && item.image || "").trim();
return providerImage || fallbackThumbnailUrl(item && item.title);
}
function fallbackThumbnailUrl(title) {
return `/api/search/thumb?title=${encodeURIComponent(title || "")}`;
}
@@ -593,7 +587,7 @@ INDEX_HTML = r"""<!doctype html>
const img = btn.querySelector("img");
const tile = btn.querySelector(".cover-tile");
img.alt = item.title;
img.src = resultThumbnailUrl(item.title);
img.src = resultThumbnailUrl(item);
if (img.complete && img.naturalWidth > 0) {
tile.classList.add("has-image");
}
@@ -601,6 +595,11 @@ INDEX_HTML = r"""<!doctype html>
if (img.naturalWidth > 0) tile.classList.add("has-image");
});
img.addEventListener("error", () => {
const fallbackUrl = fallbackThumbnailUrl(item.title);
if (img.src !== new URL(fallbackUrl, window.location.href).href) {
img.src = fallbackUrl;
return;
}
tile.classList.remove("has-image");
});
btn.addEventListener("click", () => selectResult(item, btn));
@@ -615,6 +614,15 @@ INDEX_HTML = r"""<!doctype html>
button.classList.add("active");
state.selected = item;
state.episodes = [];
const preferredMode = state.config.mode === "dub" ? "dub" : "sub";
const selectedMode = item[`has_${preferredMode}`]
? preferredMode
: item.has_sub
? "sub"
: item.has_dub
? "dub"
: preferredMode;
state.config.mode = selectedMode;
$("selectedTitle").textContent = item.title;
$("animeNameInput").value = item.title;
$("seasonInput").value = $("seasonInput").value || "1";
@@ -623,13 +631,13 @@ INDEX_HTML = r"""<!doctype html>
$("addBtn").disabled = true;
$("trackBtn").disabled = false;
try {
const data = await api(`/api/anime/${encodeURIComponent(item.id)}/episodes?mode=${state.config.mode}`);
const data = await api(`/api/anime/${encodeURIComponent(item.id)}/episodes?mode=${selectedMode}`);
if (requestId !== state.episodeRequestId || state.selected !== item) return;
state.episodes = data.episodes;
const first = state.episodes[0] || "";
const last = state.episodes[state.episodes.length - 1] || "";
$("episodesInput").value = first && last ? `${first}-${last}` : "";
$("selectedMeta").textContent = `${state.episodes.length} available episodes`;
$("selectedMeta").textContent = `${state.episodes.length} ${selectedMode.toUpperCase()} episodes`;
renderEpisodeChips();
$("addBtn").disabled = !state.episodes.length;
} catch (error) {
@@ -655,7 +663,6 @@ INDEX_HTML = r"""<!doctype html>
const requestId = ++state.searchRequestId;
const query = $("search-query").value.trim();
if (!query) return setNotice("Type a title first.");
setMode($("search-mode").value);
setNotice("Searching...");
$("resultsMeta").textContent = "Searching...";
state.selected = null;
@@ -726,14 +733,13 @@ INDEX_HTML = r"""<!doctype html>
state.config = await api("/api/config");
$("jobFolder").value = state.config.download_dir;
$("search-quality").value = state.config.quality;
setMode(state.config.mode);
state.config.mode = state.config.mode === "dub" ? "dub" : "sub";
}
$("searchBtn").addEventListener("click", search);
$("search-query").addEventListener("keydown", (event) => {
if (event.key === "Enter") search();
});
$("search-mode").addEventListener("change", (event) => setMode(event.target.value));
$("trackBtn").addEventListener("click", addSelectedToWatchlist);
$("addBtn").addEventListener("click", addSelected);
$("search-quality").addEventListener("change", () => { state.config.quality = $("search-quality").value; });