Simplify search mode and prefer provider artwork
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# 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
|
## 0.51.2 - 2026-08-09
|
||||||
|
|
||||||
- Changed Search to inspect both subbed and dubbed episode availability for each result.
|
- 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
|
## Features
|
||||||
|
|
||||||
- Search anime through the configured provider and tag results with available `SUB` and `DUB` episode languages.
|
- 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.
|
- 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`.
|
||||||
|
|||||||
@@ -939,6 +939,7 @@ def search_anime(query, mode):
|
|||||||
"has_dub": language_summary["dub"],
|
"has_dub": language_summary["dub"],
|
||||||
"sub_episodes": language_summary["sub_count"],
|
"sub_episodes": language_summary["sub_count"],
|
||||||
"dub_episodes": language_summary["dub_count"],
|
"dub_episodes": language_summary["dub_count"],
|
||||||
|
"image": show.get("image") or "",
|
||||||
"index": index,
|
"index": index,
|
||||||
"query": query,
|
"query": query,
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
|
|||||||
+24
-18
@@ -423,12 +423,6 @@ 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">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>
|
<label for="search-quality">Quality</label>
|
||||||
<select id="search-quality">
|
<select id="search-quality">
|
||||||
<option value="best">Best</option>
|
<option value="best">Best</option>
|
||||||
@@ -522,11 +516,6 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
if (notice) notice.textContent = text || "";
|
if (notice) notice.textContent = text || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMode(mode) {
|
|
||||||
state.config.mode = mode;
|
|
||||||
$("search-mode").value = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function coverInitials(title) {
|
function coverInitials(title) {
|
||||||
const parts = String(title || "")
|
const parts = String(title || "")
|
||||||
.trim()
|
.trim()
|
||||||
@@ -537,7 +526,12 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
return parts.map((part) => part[0]).join("").slice(0, 2);
|
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 || "")}`;
|
return `/api/search/thumb?title=${encodeURIComponent(title || "")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,7 +587,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
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;
|
||||||
img.src = resultThumbnailUrl(item.title);
|
img.src = resultThumbnailUrl(item);
|
||||||
if (img.complete && img.naturalWidth > 0) {
|
if (img.complete && img.naturalWidth > 0) {
|
||||||
tile.classList.add("has-image");
|
tile.classList.add("has-image");
|
||||||
}
|
}
|
||||||
@@ -601,6 +595,11 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
if (img.naturalWidth > 0) tile.classList.add("has-image");
|
if (img.naturalWidth > 0) tile.classList.add("has-image");
|
||||||
});
|
});
|
||||||
img.addEventListener("error", () => {
|
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");
|
tile.classList.remove("has-image");
|
||||||
});
|
});
|
||||||
btn.addEventListener("click", () => selectResult(item, btn));
|
btn.addEventListener("click", () => selectResult(item, btn));
|
||||||
@@ -615,6 +614,15 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
button.classList.add("active");
|
button.classList.add("active");
|
||||||
state.selected = item;
|
state.selected = item;
|
||||||
state.episodes = [];
|
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;
|
$("selectedTitle").textContent = item.title;
|
||||||
$("animeNameInput").value = item.title;
|
$("animeNameInput").value = item.title;
|
||||||
$("seasonInput").value = $("seasonInput").value || "1";
|
$("seasonInput").value = $("seasonInput").value || "1";
|
||||||
@@ -623,13 +631,13 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
$("addBtn").disabled = true;
|
$("addBtn").disabled = true;
|
||||||
$("trackBtn").disabled = false;
|
$("trackBtn").disabled = false;
|
||||||
try {
|
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;
|
if (requestId !== state.episodeRequestId || state.selected !== item) return;
|
||||||
state.episodes = data.episodes;
|
state.episodes = data.episodes;
|
||||||
const first = state.episodes[0] || "";
|
const first = state.episodes[0] || "";
|
||||||
const last = state.episodes[state.episodes.length - 1] || "";
|
const last = state.episodes[state.episodes.length - 1] || "";
|
||||||
$("episodesInput").value = first && last ? `${first}-${last}` : "";
|
$("episodesInput").value = first && last ? `${first}-${last}` : "";
|
||||||
$("selectedMeta").textContent = `${state.episodes.length} available episodes`;
|
$("selectedMeta").textContent = `${state.episodes.length} ${selectedMode.toUpperCase()} episodes`;
|
||||||
renderEpisodeChips();
|
renderEpisodeChips();
|
||||||
$("addBtn").disabled = !state.episodes.length;
|
$("addBtn").disabled = !state.episodes.length;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -655,7 +663,6 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
const requestId = ++state.searchRequestId;
|
const requestId = ++state.searchRequestId;
|
||||||
const query = $("search-query").value.trim();
|
const query = $("search-query").value.trim();
|
||||||
if (!query) return setNotice("Type a title first.");
|
if (!query) return setNotice("Type a title first.");
|
||||||
setMode($("search-mode").value);
|
|
||||||
setNotice("Searching...");
|
setNotice("Searching...");
|
||||||
$("resultsMeta").textContent = "Searching...";
|
$("resultsMeta").textContent = "Searching...";
|
||||||
state.selected = null;
|
state.selected = null;
|
||||||
@@ -726,14 +733,13 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
state.config = await api("/api/config");
|
state.config = await api("/api/config");
|
||||||
$("jobFolder").value = state.config.download_dir;
|
$("jobFolder").value = state.config.download_dir;
|
||||||
$("search-quality").value = state.config.quality;
|
$("search-quality").value = state.config.quality;
|
||||||
setMode(state.config.mode);
|
state.config.mode = state.config.mode === "dub" ? "dub" : "sub";
|
||||||
}
|
}
|
||||||
|
|
||||||
$("searchBtn").addEventListener("click", search);
|
$("searchBtn").addEventListener("click", search);
|
||||||
$("search-query").addEventListener("keydown", (event) => {
|
$("search-query").addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Enter") search();
|
if (event.key === "Enter") search();
|
||||||
});
|
});
|
||||||
$("search-mode").addEventListener("change", (event) => setMode(event.target.value));
|
|
||||||
$("trackBtn").addEventListener("click", addSelectedToWatchlist);
|
$("trackBtn").addEventListener("click", addSelectedToWatchlist);
|
||||||
$("addBtn").addEventListener("click", addSelected);
|
$("addBtn").addEventListener("click", addSelected);
|
||||||
$("search-quality").addEventListener("change", () => { state.config.quality = $("search-quality").value; });
|
$("search-quality").addEventListener("change", () => { state.config.quality = $("search-quality").value; });
|
||||||
|
|||||||
Reference in New Issue
Block a user