fix Prime/Amazon event art: BOXART cover, HERO backdrop, real logos.
Add imageBackdrop and retune scoring so card rails use BOXART-16X9, detail/hero prefer HERO-16X9, and channels-logo PNGs no longer win as cover or logo. TV 0.14.61.
This commit is contained in:
parent
d549609dbe
commit
7ace6ab214
4 changed files with 81 additions and 11 deletions
|
|
@ -20,8 +20,8 @@ android {
|
|||
applicationId = "nl.vonas.mediacluster.tv"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 123
|
||||
versionName = "0.14.60"
|
||||
versionCode = 124
|
||||
versionName = "0.14.61"
|
||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||
|
|
|
|||
|
|
@ -727,6 +727,7 @@ class ApiClient(
|
|||
startLocal = optClean("startLocal"),
|
||||
stopLocal = optClean("stopLocal"),
|
||||
imageLandscape = optClean("imageLandscape"),
|
||||
imageBackdrop = optClean("imageBackdrop"),
|
||||
imagePortrait = optClean("imagePortrait"),
|
||||
logoUrl = optClean("logoUrl"),
|
||||
images = images,
|
||||
|
|
@ -1114,6 +1115,7 @@ data class ScheduleEvent(
|
|||
val startLocal: String? = null,
|
||||
val stopLocal: String? = null,
|
||||
val imageLandscape: String? = null,
|
||||
val imageBackdrop: String? = null,
|
||||
val imagePortrait: String? = null,
|
||||
val logoUrl: String? = null,
|
||||
val images: List<String> = emptyList(),
|
||||
|
|
|
|||
|
|
@ -685,7 +685,7 @@ fun EventDetailScreen(
|
|||
|
||||
// Full-bleed landscape backdrop
|
||||
AsyncImage(
|
||||
model = eventArtUrl(ev),
|
||||
model = eventBackdropUrl(ev),
|
||||
contentDescription = ev.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopCenter,
|
||||
|
|
@ -917,7 +917,7 @@ private fun EventHero(
|
|||
val event = events.find { it.id == id } ?: current
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
AsyncImage(
|
||||
model = eventArtUrl(event),
|
||||
model = eventBackdropUrl(event),
|
||||
contentDescription = event.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopCenter,
|
||||
|
|
@ -1568,24 +1568,38 @@ private fun detailMetaLine(event: ScheduleEvent): String =
|
|||
displayChannel(event),
|
||||
).distinct().joinToString(" · ")
|
||||
|
||||
/** Card/rail cover art — prefer API imageLandscape (BOXART-16X9). */
|
||||
private fun eventArtUrl(event: ScheduleEvent): String? =
|
||||
event.imageLandscape?.takeIf { it.isNotBlank() }
|
||||
?: event.images.firstOrNull { looksLikeArtwork(it) && !it.contains("hero-16x9", ignoreCase = true) }
|
||||
?: event.images.firstOrNull { looksLikeArtwork(it) }
|
||||
?: event.imagePortrait?.takeIf { it.isNotBlank() }
|
||||
?: event.images.firstOrNull { it.isNotBlank() && !it.contains("assets/channels", ignoreCase = true) }
|
||||
|
||||
/** Detail/hero full-bleed — prefer HERO backdrop, then card landscape. */
|
||||
private fun eventBackdropUrl(event: ScheduleEvent): String? =
|
||||
event.imageBackdrop?.takeIf { it.isNotBlank() }
|
||||
?: event.images.firstOrNull { it.contains("hero-16x9", ignoreCase = true) }
|
||||
?: eventArtUrl(event)
|
||||
|
||||
/** True when artwork is suitable for a full-bleed hero (no bare logos / channel PNGs). */
|
||||
private fun looksLikeArtwork(url: String): Boolean {
|
||||
val u = url.trim()
|
||||
if (u.isEmpty()) return false
|
||||
val lower = u.lowercase()
|
||||
if (lower.contains("assets/channels")) return false
|
||||
if (lower.contains("/logo") || lower.contains("logo.") || lower.contains("logo_")) return false
|
||||
if (lower.contains("channels-logo")) return false
|
||||
if (lower.contains("/logos/") && lower.endsWith(".png")) return false
|
||||
if (lower.contains("/logo") || lower.contains("logo.") || lower.contains("logo_")) {
|
||||
// Composite BOXART URLs embed logo overlay paths — still real artwork.
|
||||
if (!lower.contains("boxart") && !lower.contains("hero-")) return false
|
||||
}
|
||||
if (lower.endsWith(".svg")) return false
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasUsableHeroArt(event: ScheduleEvent): Boolean {
|
||||
if (!event.imageBackdrop.isNullOrBlank()) return true
|
||||
if (!event.imageLandscape.isNullOrBlank()) return true
|
||||
if (event.images.any { looksLikeArtwork(it) }) return true
|
||||
// Portrait can still fill a hero better than a black void — only if not a channel logo path.
|
||||
|
|
@ -1612,6 +1626,7 @@ private fun normalizeChannelKey(value: String?): String =
|
|||
private fun donorImageScore(e: ScheduleEvent): Int {
|
||||
var s = 0
|
||||
if (!e.imageLandscape.isNullOrBlank()) s += 20
|
||||
if (!e.imageBackdrop.isNullOrBlank()) s += 10
|
||||
if (e.images.any { it.contains("thumb", true) || it.contains("akamaized.net/live", true) }) s += 30
|
||||
if (!e.logoUrl.isNullOrBlank()) s += 5
|
||||
s += e.images.size.coerceAtMost(5)
|
||||
|
|
@ -1674,6 +1689,7 @@ private fun enrichSingleEvent(
|
|||
if (donor == null) return cleaned
|
||||
return cleaned.copy(
|
||||
imageLandscape = cleaned.imageLandscape ?: donor.imageLandscape,
|
||||
imageBackdrop = cleaned.imageBackdrop ?: donor.imageBackdrop,
|
||||
imagePortrait = cleaned.imagePortrait ?: donor.imagePortrait,
|
||||
logoUrl = cleaned.logoUrl ?: donor.logoUrl,
|
||||
images = cleaned.images.ifEmpty { donor.images },
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ export type ScheduleEventPublic = {
|
|||
stop: string;
|
||||
startLocal: string | null;
|
||||
stopLocal: string | null;
|
||||
/** 16:9 card/cover art (Prime: BOXART-16X9 preferred). */
|
||||
imageLandscape: string | null;
|
||||
/** Full-bleed detail/hero backdrop (Prime: HERO-16X9); fall back to landscape on clients. */
|
||||
imageBackdrop: string | null;
|
||||
imagePortrait: string | null;
|
||||
logoUrl: string | null;
|
||||
images: string[];
|
||||
|
|
@ -125,9 +128,24 @@ function pickImages(raw: RawEvent): string[] {
|
|||
return out;
|
||||
}
|
||||
|
||||
/** Standalone channel/brand logo (not composite boxart that embeds a logo overlay). */
|
||||
function isPureLogoUrl(url: string): boolean {
|
||||
const u = url.toLowerCase();
|
||||
if (u.includes("boxart-") || u.includes("hero-") || u.includes("poster")) return false;
|
||||
if (u.includes("channels-logo") || u.includes("/logos/")) return true;
|
||||
if (u.includes("assets/channels")) return true;
|
||||
// Bare logo PNG without Amazon content-art path segments
|
||||
if (u.includes("logo") && u.includes(".png") && !u.includes("le-target-images")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function scoreLandscape(url: string): number {
|
||||
const u = url.toLowerCase();
|
||||
let score = 0;
|
||||
// Prime/Amazon: card cover = BOXART-16X9 (composite with corner logo overlay is OK)
|
||||
if (u.includes("boxart-16x9") || u.includes("boxart_16x9")) score += 90;
|
||||
if (u.includes("hero-16x9") || u.includes("hero_16x9")) score += 35;
|
||||
if (u.includes("boxart-4x3") || u.includes("boxart_4x3")) score -= 25;
|
||||
// Ziggo Sport FAAB live thumbs (akamaized …/live/…/thumb.jpg?scale=640x360)
|
||||
if (u.includes("thumb")) score += 55;
|
||||
if (u.includes("akamaized.net/live")) score += 50;
|
||||
|
|
@ -137,21 +155,48 @@ function scoreLandscape(url: string): number {
|
|||
if (u.includes("width=960") && u.includes("height=540")) score += 40;
|
||||
if (u.includes("width=1280") || u.includes("height=720")) score += 30;
|
||||
if (u.includes(".jpg") || u.includes(".jpeg") || u.includes(".webp")) score += 5;
|
||||
// Channel logo PNGs are not landscape artwork
|
||||
if (u.includes("assets/channels")) score -= 70;
|
||||
if (u.includes("/channels/") && u.includes(".png")) score -= 50;
|
||||
if (u.includes("logo")) score -= 40;
|
||||
// Pure logos are never card art
|
||||
if (isPureLogoUrl(url)) score -= 120;
|
||||
else if (u.includes("assets/channels")) score -= 70;
|
||||
else if (u.includes("/channels/") && u.includes(".png") && !u.includes("boxart")) score -= 50;
|
||||
// Only penalize "logo" when it is not part of a BOXART/HERO content composite
|
||||
if (u.includes("logo") && !u.includes("boxart") && !u.includes("hero-") && !u.includes("hero_")) {
|
||||
score -= 40;
|
||||
if (u.includes(".png")) score -= 50;
|
||||
}
|
||||
if (u.includes("_c.") || u.includes("_c?")) score -= 20;
|
||||
if (u.includes(".png") && u.includes("logo")) score -= 50;
|
||||
return score;
|
||||
}
|
||||
|
||||
/** Detail/hero full-bleed: prefer clean HERO-16X9 over card BOXART. */
|
||||
function scoreBackdrop(url: string): number {
|
||||
const u = url.toLowerCase();
|
||||
let score = 0;
|
||||
if (u.includes("hero-16x9") || u.includes("hero_16x9")) score += 95;
|
||||
if (u.includes("boxart-16x9") || u.includes("boxart_16x9")) score += 40;
|
||||
if (u.includes("boxart-4x3") || u.includes("boxart_4x3")) score -= 20;
|
||||
if (u.includes("thumb")) score += 45;
|
||||
if (u.includes("akamaized.net/live")) score += 40;
|
||||
if (u.includes("scale=")) score += 25;
|
||||
if (u.includes("_l.") || u.includes("_l?")) score += 40;
|
||||
if (u.includes("width=1280") || u.includes("height=720") || u.includes("1920")) score += 15;
|
||||
if (u.includes(".jpg") || u.includes(".jpeg") || u.includes(".webp")) score += 5;
|
||||
if (isPureLogoUrl(url)) score -= 120;
|
||||
else if (u.includes("logo") && !u.includes("boxart") && !u.includes("hero-") && !u.includes("hero_")) {
|
||||
score -= 50;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function scorePortrait(url: string): number {
|
||||
const u = url.toLowerCase();
|
||||
let score = 0;
|
||||
if (u.includes("boxart-4x3") || u.includes("boxart_4x3")) score += 80;
|
||||
if (u.includes("_c.") || u.includes("_c?")) score += 50;
|
||||
if (u.includes("width=199") || u.includes("height=298")) score += 30;
|
||||
if (u.includes("assets/channels") || u.includes("logo")) score -= 40;
|
||||
if (u.includes("boxart-16x9") || u.includes("hero-16x9")) score -= 30;
|
||||
if (isPureLogoUrl(url) || u.includes("assets/channels")) score -= 80;
|
||||
else if (u.includes("logo") && !u.includes("boxart")) score -= 40;
|
||||
if (u.includes("thumb") || u.includes("akamaized.net/live")) score -= 20;
|
||||
if (u.includes("_l.")) score -= 10;
|
||||
return score;
|
||||
|
|
@ -160,6 +205,10 @@ function scorePortrait(url: string): number {
|
|||
function scoreLogo(url: string): number {
|
||||
const u = url.toLowerCase();
|
||||
let score = 0;
|
||||
// Content art must never win as logo (BOXART composites embed logo overlay paths)
|
||||
if (u.includes("boxart-") || u.includes("hero-") || u.includes("le-target-images")) score -= 200;
|
||||
if (isPureLogoUrl(url)) score += 100;
|
||||
if (u.includes("channels-logo")) score += 40;
|
||||
if (u.includes("assets/channels")) score += 80;
|
||||
if (u.includes("ziggo_sport")) score += 45;
|
||||
if (u.includes("/channels/") && u.includes(".png")) score += 55;
|
||||
|
|
@ -192,6 +241,7 @@ function normalizeChannelKey(value: string | null | undefined): string {
|
|||
function donorImageScore(e: ScheduleEventPublic): number {
|
||||
let s = 0;
|
||||
if (e.imageLandscape) s += 20;
|
||||
if (e.imageBackdrop) s += 10;
|
||||
if (e.images.some((u) => /thumb|akamaized\.net\/live|scale=/i.test(u))) s += 30;
|
||||
if (e.logoUrl) s += 5;
|
||||
if (e.images.length) s += Math.min(e.images.length, 5);
|
||||
|
|
@ -236,6 +286,7 @@ function fillMissingImages(events: ScheduleEventPublic[]): ScheduleEventPublic[]
|
|||
return {
|
||||
...e,
|
||||
imageLandscape: e.imageLandscape || donor.imageLandscape,
|
||||
imageBackdrop: e.imageBackdrop || donor.imageBackdrop,
|
||||
imagePortrait: e.imagePortrait || donor.imagePortrait,
|
||||
logoUrl: e.logoUrl || donor.logoUrl,
|
||||
images: e.images.length ? e.images : [...donor.images],
|
||||
|
|
@ -353,6 +404,7 @@ function toPublic(raw: RawEvent, nowMs: number): ScheduleEventPublic | null {
|
|||
startLocal: str(raw.start_local) || null,
|
||||
stopLocal: str(raw.stop_local) || null,
|
||||
imageLandscape: bestByScore(images, scoreLandscape),
|
||||
imageBackdrop: bestByScore(images, scoreBackdrop),
|
||||
imagePortrait: bestByScore(images, scorePortrait),
|
||||
logoUrl: bestByScore(images, scoreLogo),
|
||||
images,
|
||||
|
|
|
|||
Loading…
Reference in a new issue