Simplify search mode and prefer provider artwork
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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; });
|
||||
|
||||
Reference in New Issue
Block a user