feat (ui): toevoegen van banner en series info - 01
This commit is contained in:
+107
@@ -97,6 +97,113 @@ def _fetch_aired_episodes(client: TvdbClient, series_id: int) -> list[dict]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _status_name(value) -> str | None:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
name = value.get("name")
|
||||||
|
if isinstance(name, str) and name.strip():
|
||||||
|
return name.strip()
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
return value.strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_banner_url(artworks: list[dict]) -> str | None:
|
||||||
|
banner_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
|
||||||
|
for artwork in artworks:
|
||||||
|
if not isinstance(artwork, dict):
|
||||||
|
continue
|
||||||
|
image = artwork.get("image") or artwork.get("thumbnail")
|
||||||
|
if not isinstance(image, str) or not image.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
width = artwork.get("width")
|
||||||
|
height = artwork.get("height")
|
||||||
|
score = artwork.get("score") or 0
|
||||||
|
try:
|
||||||
|
width = int(width) if width is not None else 0
|
||||||
|
height = int(height) if height is not None else 0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
width, height = 0, 0
|
||||||
|
try:
|
||||||
|
score = float(score)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
score = 0.0
|
||||||
|
|
||||||
|
ratio = (width / height) if width > 0 and height > 0 else 0.0
|
||||||
|
record = (score, width, image.strip())
|
||||||
|
|
||||||
|
if ratio >= 1.4:
|
||||||
|
banner_candidates.append(record)
|
||||||
|
else:
|
||||||
|
fallback_candidates.append(record)
|
||||||
|
|
||||||
|
if banner_candidates:
|
||||||
|
banner_candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
||||||
|
return banner_candidates[0][2]
|
||||||
|
|
||||||
|
if fallback_candidates:
|
||||||
|
fallback_candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
||||||
|
return fallback_candidates[0][2]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/series/{series_id}/summary")
|
||||||
|
def get_series_summary(series_id: int):
|
||||||
|
client = TvdbClient()
|
||||||
|
try:
|
||||||
|
extended_payload = client.get(f"/series/{series_id}/extended")
|
||||||
|
base_payload = client.get(f"/series/{series_id}")
|
||||||
|
|
||||||
|
extended_data = extended_payload.get("data", {}) if isinstance(extended_payload, dict) else {}
|
||||||
|
base_data = base_payload.get("data", {}) if isinstance(base_payload, dict) else {}
|
||||||
|
|
||||||
|
artworks = extended_data.get("artworks")
|
||||||
|
artworks = artworks if isinstance(artworks, list) else []
|
||||||
|
|
||||||
|
banner_url = _pick_banner_url(artworks)
|
||||||
|
poster_url = (
|
||||||
|
base_data.get("image")
|
||||||
|
or extended_data.get("image")
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
|
||||||
|
first_aired = (
|
||||||
|
extended_data.get("firstAired")
|
||||||
|
or base_data.get("firstAired")
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
network = (
|
||||||
|
extended_data.get("latestNetwork", {}).get("name")
|
||||||
|
if isinstance(extended_data.get("latestNetwork"), dict)
|
||||||
|
else None
|
||||||
|
) or extended_data.get("network") or base_data.get("network")
|
||||||
|
status = _status_name(extended_data.get("status")) or _status_name(base_data.get("status"))
|
||||||
|
overview = (
|
||||||
|
extended_data.get("overview")
|
||||||
|
or base_data.get("overview")
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
slug = extended_data.get("slug") or base_data.get("slug")
|
||||||
|
tvdb_id = extended_data.get("id") or base_data.get("id") or series_id
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": tvdb_id,
|
||||||
|
"banner_url": banner_url,
|
||||||
|
"poster_url": poster_url,
|
||||||
|
"first_aired": first_aired,
|
||||||
|
"network": network,
|
||||||
|
"status": status,
|
||||||
|
"overview": overview,
|
||||||
|
"slug": slug,
|
||||||
|
"tvdb_url": f"https://www.thetvdb.com/series/{slug}" if slug else None,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/series/{series_id}/episodes")
|
@router.get("/series/{series_id}/episodes")
|
||||||
def get_series_episodes(
|
def get_series_episodes(
|
||||||
series_id: int,
|
series_id: int,
|
||||||
|
|||||||
+24
-6
@@ -4,6 +4,7 @@
|
|||||||
const state = {
|
const state = {
|
||||||
sessionId: initSessionId(),
|
sessionId: initSessionId(),
|
||||||
selectedSeries: null,
|
selectedSeries: null,
|
||||||
|
selectedSeriesSummary: null,
|
||||||
episodes: [],
|
episodes: [],
|
||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
selectedFiles: [],
|
selectedFiles: [],
|
||||||
@@ -117,8 +118,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildTvdbUrl(item) {
|
function buildTvdbUrl(item) {
|
||||||
|
const summary = state.selectedSeriesSummary || {};
|
||||||
|
const summaryUrl = (summary.tvdb_url || "").toString().trim();
|
||||||
|
if (summaryUrl) return summaryUrl;
|
||||||
|
|
||||||
const raw = item.raw || {};
|
const raw = item.raw || {};
|
||||||
const slug = (raw.slug || "").toString().trim();
|
const slug = (summary.slug || raw.slug || "").toString().trim();
|
||||||
if (slug) return `https://www.thetvdb.com/series/${encodeURIComponent(slug)}`;
|
if (slug) return `https://www.thetvdb.com/series/${encodeURIComponent(slug)}`;
|
||||||
|
|
||||||
const tvdbId = (raw.tvdb_id || item.id || "").toString().trim();
|
const tvdbId = (raw.tvdb_id || item.id || "").toString().trim();
|
||||||
@@ -133,8 +138,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const raw = item.raw || {};
|
const raw = item.raw || {};
|
||||||
const imageUrl = (raw.image_url || "").toString().trim();
|
const summary = state.selectedSeriesSummary || {};
|
||||||
const overview = (raw.overview || "").toString().trim();
|
const imageUrl = (summary.banner_url || summary.poster_url || raw.image_url || "").toString().trim();
|
||||||
|
const overview = (summary.overview || raw.overview || "").toString().trim();
|
||||||
const tvdbUrl = buildTvdbUrl(item);
|
const tvdbUrl = buildTvdbUrl(item);
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
@@ -145,9 +151,9 @@
|
|||||||
el.seriesPoster.classList.add("hidden");
|
el.seriesPoster.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
el.seriesFirstAired.textContent = fallbackText(raw.first_air_time);
|
el.seriesFirstAired.textContent = fallbackText(summary.first_aired || raw.first_air_time);
|
||||||
el.seriesNetwork.textContent = fallbackText(raw.network);
|
el.seriesNetwork.textContent = fallbackText(summary.network || raw.network);
|
||||||
el.seriesStatus.textContent = fallbackText(raw.status);
|
el.seriesStatus.textContent = fallbackText(summary.status || raw.status);
|
||||||
el.seriesOverview.textContent = overview || "No overview available.";
|
el.seriesOverview.textContent = overview || "No overview available.";
|
||||||
|
|
||||||
if (tvdbUrl) {
|
if (tvdbUrl) {
|
||||||
@@ -161,6 +167,11 @@
|
|||||||
el.seriesDetails.classList.remove("hidden");
|
el.seriesDetails.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSeriesSummary(seriesId) {
|
||||||
|
const data = await api(`/api/tvdb/series/${encodeURIComponent(seriesId)}/summary`);
|
||||||
|
state.selectedSeriesSummary = data || null;
|
||||||
|
}
|
||||||
|
|
||||||
function updateMeta() {
|
function updateMeta() {
|
||||||
const epCount = state.selectedEpisodes.length;
|
const epCount = state.selectedEpisodes.length;
|
||||||
const fileCount = state.selectedFiles.length;
|
const fileCount = state.selectedFiles.length;
|
||||||
@@ -262,8 +273,15 @@
|
|||||||
const right = document.createElement("div");
|
const right = document.createElement("div");
|
||||||
right.appendChild(makeBtn("Select", async () => {
|
right.appendChild(makeBtn("Select", async () => {
|
||||||
state.selectedSeries = item;
|
state.selectedSeries = item;
|
||||||
|
state.selectedSeriesSummary = null;
|
||||||
el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`;
|
el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`;
|
||||||
renderSelectedSeriesDetails();
|
renderSelectedSeriesDetails();
|
||||||
|
try {
|
||||||
|
await loadSeriesSummary(item.id);
|
||||||
|
renderSelectedSeriesDetails();
|
||||||
|
} catch (_err) {
|
||||||
|
// Keep UI responsive with search payload fallback if summary lookup fails.
|
||||||
|
}
|
||||||
await loadEpisodes();
|
await loadEpisodes();
|
||||||
}));
|
}));
|
||||||
li.appendChild(left);
|
li.appendChild(left);
|
||||||
|
|||||||
+24
-3
@@ -299,6 +299,21 @@ button.secondary {
|
|||||||
|
|
||||||
#panelSearch #searchResults {
|
#panelSearch #searchResults {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
height: 220px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panelSearch #searchResults li {
|
||||||
|
min-height: 38px;
|
||||||
|
height: 38px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panelSearch #searchResults li > span {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-details {
|
.series-details {
|
||||||
@@ -308,12 +323,18 @@ button.secondary {
|
|||||||
|
|
||||||
.series-media {
|
.series-media {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-media img {
|
.series-media img {
|
||||||
width: 96px;
|
width: 100%;
|
||||||
height: 144px;
|
max-width: none;
|
||||||
object-fit: cover;
|
height: 92px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: center center;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #d7dee9;
|
border: 1px solid #d7dee9;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
|
|||||||
Reference in New Issue
Block a user