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"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 123
|
versionCode = 124
|
||||||
versionName = "0.14.60"
|
versionName = "0.14.61"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -727,6 +727,7 @@ class ApiClient(
|
||||||
startLocal = optClean("startLocal"),
|
startLocal = optClean("startLocal"),
|
||||||
stopLocal = optClean("stopLocal"),
|
stopLocal = optClean("stopLocal"),
|
||||||
imageLandscape = optClean("imageLandscape"),
|
imageLandscape = optClean("imageLandscape"),
|
||||||
|
imageBackdrop = optClean("imageBackdrop"),
|
||||||
imagePortrait = optClean("imagePortrait"),
|
imagePortrait = optClean("imagePortrait"),
|
||||||
logoUrl = optClean("logoUrl"),
|
logoUrl = optClean("logoUrl"),
|
||||||
images = images,
|
images = images,
|
||||||
|
|
@ -1114,6 +1115,7 @@ data class ScheduleEvent(
|
||||||
val startLocal: String? = null,
|
val startLocal: String? = null,
|
||||||
val stopLocal: String? = null,
|
val stopLocal: String? = null,
|
||||||
val imageLandscape: String? = null,
|
val imageLandscape: String? = null,
|
||||||
|
val imageBackdrop: String? = null,
|
||||||
val imagePortrait: String? = null,
|
val imagePortrait: String? = null,
|
||||||
val logoUrl: String? = null,
|
val logoUrl: String? = null,
|
||||||
val images: List<String> = emptyList(),
|
val images: List<String> = emptyList(),
|
||||||
|
|
|
||||||
|
|
@ -685,7 +685,7 @@ fun EventDetailScreen(
|
||||||
|
|
||||||
// Full-bleed landscape backdrop
|
// Full-bleed landscape backdrop
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = eventArtUrl(ev),
|
model = eventBackdropUrl(ev),
|
||||||
contentDescription = ev.name,
|
contentDescription = ev.name,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
alignment = Alignment.TopCenter,
|
alignment = Alignment.TopCenter,
|
||||||
|
|
@ -917,7 +917,7 @@ private fun EventHero(
|
||||||
val event = events.find { it.id == id } ?: current
|
val event = events.find { it.id == id } ?: current
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = eventArtUrl(event),
|
model = eventBackdropUrl(event),
|
||||||
contentDescription = event.name,
|
contentDescription = event.name,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
alignment = Alignment.TopCenter,
|
alignment = Alignment.TopCenter,
|
||||||
|
|
@ -1568,24 +1568,38 @@ private fun detailMetaLine(event: ScheduleEvent): String =
|
||||||
displayChannel(event),
|
displayChannel(event),
|
||||||
).distinct().joinToString(" · ")
|
).distinct().joinToString(" · ")
|
||||||
|
|
||||||
|
/** Card/rail cover art — prefer API imageLandscape (BOXART-16X9). */
|
||||||
private fun eventArtUrl(event: ScheduleEvent): String? =
|
private fun eventArtUrl(event: ScheduleEvent): String? =
|
||||||
event.imageLandscape?.takeIf { it.isNotBlank() }
|
event.imageLandscape?.takeIf { it.isNotBlank() }
|
||||||
|
?: event.images.firstOrNull { looksLikeArtwork(it) && !it.contains("hero-16x9", ignoreCase = true) }
|
||||||
?: event.images.firstOrNull { looksLikeArtwork(it) }
|
?: event.images.firstOrNull { looksLikeArtwork(it) }
|
||||||
?: event.imagePortrait?.takeIf { it.isNotBlank() }
|
?: event.imagePortrait?.takeIf { it.isNotBlank() }
|
||||||
?: event.images.firstOrNull { it.isNotBlank() && !it.contains("assets/channels", ignoreCase = true) }
|
?: 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). */
|
/** True when artwork is suitable for a full-bleed hero (no bare logos / channel PNGs). */
|
||||||
private fun looksLikeArtwork(url: String): Boolean {
|
private fun looksLikeArtwork(url: String): Boolean {
|
||||||
val u = url.trim()
|
val u = url.trim()
|
||||||
if (u.isEmpty()) return false
|
if (u.isEmpty()) return false
|
||||||
val lower = u.lowercase()
|
val lower = u.lowercase()
|
||||||
if (lower.contains("assets/channels")) return false
|
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
|
if (lower.endsWith(".svg")) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasUsableHeroArt(event: ScheduleEvent): Boolean {
|
private fun hasUsableHeroArt(event: ScheduleEvent): Boolean {
|
||||||
|
if (!event.imageBackdrop.isNullOrBlank()) return true
|
||||||
if (!event.imageLandscape.isNullOrBlank()) return true
|
if (!event.imageLandscape.isNullOrBlank()) return true
|
||||||
if (event.images.any { looksLikeArtwork(it) }) 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.
|
// 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 {
|
private fun donorImageScore(e: ScheduleEvent): Int {
|
||||||
var s = 0
|
var s = 0
|
||||||
if (!e.imageLandscape.isNullOrBlank()) s += 20
|
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.images.any { it.contains("thumb", true) || it.contains("akamaized.net/live", true) }) s += 30
|
||||||
if (!e.logoUrl.isNullOrBlank()) s += 5
|
if (!e.logoUrl.isNullOrBlank()) s += 5
|
||||||
s += e.images.size.coerceAtMost(5)
|
s += e.images.size.coerceAtMost(5)
|
||||||
|
|
@ -1674,6 +1689,7 @@ private fun enrichSingleEvent(
|
||||||
if (donor == null) return cleaned
|
if (donor == null) return cleaned
|
||||||
return cleaned.copy(
|
return cleaned.copy(
|
||||||
imageLandscape = cleaned.imageLandscape ?: donor.imageLandscape,
|
imageLandscape = cleaned.imageLandscape ?: donor.imageLandscape,
|
||||||
|
imageBackdrop = cleaned.imageBackdrop ?: donor.imageBackdrop,
|
||||||
imagePortrait = cleaned.imagePortrait ?: donor.imagePortrait,
|
imagePortrait = cleaned.imagePortrait ?: donor.imagePortrait,
|
||||||
logoUrl = cleaned.logoUrl ?: donor.logoUrl,
|
logoUrl = cleaned.logoUrl ?: donor.logoUrl,
|
||||||
images = cleaned.images.ifEmpty { donor.images },
|
images = cleaned.images.ifEmpty { donor.images },
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ export type ScheduleEventPublic = {
|
||||||
stop: string;
|
stop: string;
|
||||||
startLocal: string | null;
|
startLocal: string | null;
|
||||||
stopLocal: string | null;
|
stopLocal: string | null;
|
||||||
|
/** 16:9 card/cover art (Prime: BOXART-16X9 preferred). */
|
||||||
imageLandscape: string | null;
|
imageLandscape: string | null;
|
||||||
|
/** Full-bleed detail/hero backdrop (Prime: HERO-16X9); fall back to landscape on clients. */
|
||||||
|
imageBackdrop: string | null;
|
||||||
imagePortrait: string | null;
|
imagePortrait: string | null;
|
||||||
logoUrl: string | null;
|
logoUrl: string | null;
|
||||||
images: string[];
|
images: string[];
|
||||||
|
|
@ -125,9 +128,24 @@ function pickImages(raw: RawEvent): string[] {
|
||||||
return out;
|
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 {
|
function scoreLandscape(url: string): number {
|
||||||
const u = url.toLowerCase();
|
const u = url.toLowerCase();
|
||||||
let score = 0;
|
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)
|
// Ziggo Sport FAAB live thumbs (akamaized …/live/…/thumb.jpg?scale=640x360)
|
||||||
if (u.includes("thumb")) score += 55;
|
if (u.includes("thumb")) score += 55;
|
||||||
if (u.includes("akamaized.net/live")) score += 50;
|
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=960") && u.includes("height=540")) score += 40;
|
||||||
if (u.includes("width=1280") || u.includes("height=720")) score += 30;
|
if (u.includes("width=1280") || u.includes("height=720")) score += 30;
|
||||||
if (u.includes(".jpg") || u.includes(".jpeg") || u.includes(".webp")) score += 5;
|
if (u.includes(".jpg") || u.includes(".jpeg") || u.includes(".webp")) score += 5;
|
||||||
// Channel logo PNGs are not landscape artwork
|
// Pure logos are never card art
|
||||||
if (u.includes("assets/channels")) score -= 70;
|
if (isPureLogoUrl(url)) score -= 120;
|
||||||
if (u.includes("/channels/") && u.includes(".png")) score -= 50;
|
else if (u.includes("assets/channels")) score -= 70;
|
||||||
if (u.includes("logo")) score -= 40;
|
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("_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;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scorePortrait(url: string): number {
|
function scorePortrait(url: string): number {
|
||||||
const u = url.toLowerCase();
|
const u = url.toLowerCase();
|
||||||
let score = 0;
|
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("_c.") || u.includes("_c?")) score += 50;
|
||||||
if (u.includes("width=199") || u.includes("height=298")) score += 30;
|
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("thumb") || u.includes("akamaized.net/live")) score -= 20;
|
||||||
if (u.includes("_l.")) score -= 10;
|
if (u.includes("_l.")) score -= 10;
|
||||||
return score;
|
return score;
|
||||||
|
|
@ -160,6 +205,10 @@ function scorePortrait(url: string): number {
|
||||||
function scoreLogo(url: string): number {
|
function scoreLogo(url: string): number {
|
||||||
const u = url.toLowerCase();
|
const u = url.toLowerCase();
|
||||||
let score = 0;
|
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("assets/channels")) score += 80;
|
||||||
if (u.includes("ziggo_sport")) score += 45;
|
if (u.includes("ziggo_sport")) score += 45;
|
||||||
if (u.includes("/channels/") && u.includes(".png")) score += 55;
|
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 {
|
function donorImageScore(e: ScheduleEventPublic): number {
|
||||||
let s = 0;
|
let s = 0;
|
||||||
if (e.imageLandscape) s += 20;
|
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.images.some((u) => /thumb|akamaized\.net\/live|scale=/i.test(u))) s += 30;
|
||||||
if (e.logoUrl) s += 5;
|
if (e.logoUrl) s += 5;
|
||||||
if (e.images.length) s += Math.min(e.images.length, 5);
|
if (e.images.length) s += Math.min(e.images.length, 5);
|
||||||
|
|
@ -236,6 +286,7 @@ function fillMissingImages(events: ScheduleEventPublic[]): ScheduleEventPublic[]
|
||||||
return {
|
return {
|
||||||
...e,
|
...e,
|
||||||
imageLandscape: e.imageLandscape || donor.imageLandscape,
|
imageLandscape: e.imageLandscape || donor.imageLandscape,
|
||||||
|
imageBackdrop: e.imageBackdrop || donor.imageBackdrop,
|
||||||
imagePortrait: e.imagePortrait || donor.imagePortrait,
|
imagePortrait: e.imagePortrait || donor.imagePortrait,
|
||||||
logoUrl: e.logoUrl || donor.logoUrl,
|
logoUrl: e.logoUrl || donor.logoUrl,
|
||||||
images: e.images.length ? e.images : [...donor.images],
|
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,
|
startLocal: str(raw.start_local) || null,
|
||||||
stopLocal: str(raw.stop_local) || null,
|
stopLocal: str(raw.stop_local) || null,
|
||||||
imageLandscape: bestByScore(images, scoreLandscape),
|
imageLandscape: bestByScore(images, scoreLandscape),
|
||||||
|
imageBackdrop: bestByScore(images, scoreBackdrop),
|
||||||
imagePortrait: bestByScore(images, scorePortrait),
|
imagePortrait: bestByScore(images, scorePortrait),
|
||||||
logoUrl: bestByScore(images, scoreLogo),
|
logoUrl: bestByScore(images, scoreLogo),
|
||||||
images,
|
images,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue