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")
|
||||
def get_series_episodes(
|
||||
series_id: int,
|
||||
|
||||
+24
-6
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user