feat (ui): toevoegen van banner en series info - 01

This commit is contained in:
kodi
2026-03-09 09:32:28 +01:00
parent 1e946f675b
commit d3cf97d56d
3 changed files with 155 additions and 9 deletions
+107
View File
@@ -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")
def get_series_episodes(
series_id: int,
+24 -6
View File
@@ -4,6 +4,7 @@
const state = {
sessionId: initSessionId(),
selectedSeries: null,
selectedSeriesSummary: null,
episodes: [],
selectedEpisodes: [],
selectedFiles: [],
@@ -117,8 +118,12 @@
}
function buildTvdbUrl(item) {
const summary = state.selectedSeriesSummary || {};
const summaryUrl = (summary.tvdb_url || "").toString().trim();
if (summaryUrl) return summaryUrl;
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)}`;
const tvdbId = (raw.tvdb_id || item.id || "").toString().trim();
@@ -133,8 +138,9 @@
return;
}
const raw = item.raw || {};
const imageUrl = (raw.image_url || "").toString().trim();
const overview = (raw.overview || "").toString().trim();
const summary = state.selectedSeriesSummary || {};
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);
if (imageUrl) {
@@ -145,9 +151,9 @@
el.seriesPoster.classList.add("hidden");
}
el.seriesFirstAired.textContent = fallbackText(raw.first_air_time);
el.seriesNetwork.textContent = fallbackText(raw.network);
el.seriesStatus.textContent = fallbackText(raw.status);
el.seriesFirstAired.textContent = fallbackText(summary.first_aired || raw.first_air_time);
el.seriesNetwork.textContent = fallbackText(summary.network || raw.network);
el.seriesStatus.textContent = fallbackText(summary.status || raw.status);
el.seriesOverview.textContent = overview || "No overview available.";
if (tvdbUrl) {
@@ -161,6 +167,11 @@
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() {
const epCount = state.selectedEpisodes.length;
const fileCount = state.selectedFiles.length;
@@ -262,8 +273,15 @@
const right = document.createElement("div");
right.appendChild(makeBtn("Select", async () => {
state.selectedSeries = item;
state.selectedSeriesSummary = null;
el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`;
renderSelectedSeriesDetails();
try {
await loadSeriesSummary(item.id);
renderSelectedSeriesDetails();
} catch (_err) {
// Keep UI responsive with search payload fallback if summary lookup fails.
}
await loadEpisodes();
}));
li.appendChild(left);
+24 -3
View File
@@ -299,6 +299,21 @@ button.secondary {
#panelSearch #searchResults {
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 {
@@ -308,12 +323,18 @@ button.secondary {
.series-media {
margin-bottom: 8px;
width: 100%;
padding: 0 4px;
}
.series-media img {
width: 96px;
height: 144px;
object-fit: cover;
width: 100%;
max-width: none;
height: 92px;
object-fit: contain;
object-position: center center;
display: block;
margin: 0;
border-radius: 6px;
border: 1px solid #d7dee9;
background: #f8fafc;