From c4d73eae3983e767a5d5f696aea67a62554a73dc Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 4 Jun 2026 22:00:15 -0500 Subject: [PATCH] fix(epg): repair SD poster display and backfill missing artwork Build absolute poster proxy URLs via build_absolute_uri_with_port so program detail images resolve correctly in dev and behind reverse proxies. Backfill sd_icon for programs when poster fetch is enabled but a delta refresh skips program metadata. Add a dev-mode frontend fallback for relative image paths and correct the poster caching description in the SD settings UI. --- apps/epg/serializers.py | 11 +++++++--- apps/epg/tasks.py | 22 ++++++++++++++++--- .../src/components/ProgramDetailModal.jsx | 14 +++++++++--- frontend/src/components/forms/EPG.jsx | 2 +- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/apps/epg/serializers.py b/apps/epg/serializers.py index c196c4db..ed99efb4 100644 --- a/apps/epg/serializers.py +++ b/apps/epg/serializers.py @@ -1,4 +1,4 @@ -from core.utils import validate_flexible_url +from core.utils import validate_flexible_url, build_absolute_uri_with_port from rest_framework import serializers from .models import EPGSource, EPGData, ProgramData from apps.channels.models import Channel, Stream @@ -169,9 +169,14 @@ class ProgramDetailSerializer(ProgramDataSerializer): data['icon'] = cp.get('icon') data['images'] = cp.get('images') or [] - # SD poster: expose as proxy URL so frontend never needs SD auth + # SD poster: expose as absolute proxy URL so frontend/img tags never need SD auth if cp.get('sd_icon'): - data['poster_url'] = f"/api/epg/programs/{obj.id}/poster/" + poster_path = f"/api/epg/programs/{obj.id}/poster/" + request = self.context.get('request') + if request: + data['poster_url'] = build_absolute_uri_with_port(request, poster_path) + else: + data['poster_url'] = poster_path else: data['poster_url'] = None diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index f9230e6b..fe1b1e7c 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -2995,7 +2995,23 @@ def fetch_schedules_direct(source, stations_only=False, force=False): # Step 8: Fetch program artwork (posters) if enabled # ------------------------------------------------------------------------- fetch_posters = (source.custom_properties or {}).get('fetch_posters', False) - if fetch_posters and program_metadata: + poster_program_ids = set(program_metadata.keys()) if program_metadata else set() + if fetch_posters and not poster_program_ids: + # Backfill when posters were enabled after initial fetch, or a delta + # refresh skipped program metadata but programs still lack sd_icon. + poster_program_ids = set( + ProgramData.objects.filter( + epg_id__in=mapped_epg_ids, + program_id__isnull=False, + ).exclude(custom_properties__has_key='sd_icon') + .values_list('program_id', flat=True) + ) + if poster_program_ids: + logger.info( + f"Poster backfill: {len(poster_program_ids)} programs missing sd_icon." + ) + + if fetch_posters and poster_program_ids: logger.info("Poster fetch enabled — retrieving program artwork from Schedules Direct.") _sd_send_ws_sync(source.id, "parsing_programs", 98, message="Fetching program artwork...") @@ -3007,7 +3023,7 @@ def fetch_schedules_direct(source, stations_only=False, force=False): artwork_lookup_ids = set() pid_to_artwork_key = {} # maps original programID -> the key we looked up - for pid in program_metadata: + for pid in poster_program_ids: if pid.startswith('EP'): sh_root = 'SH' + pid[2:10] + '0000' artwork_lookup_ids.add(sh_root) @@ -3116,7 +3132,7 @@ def fetch_schedules_direct(source, stations_only=False, force=False): logger.warning(f"Poster artwork fetch failed (non-fatal): {art_error}", exc_info=True) elif fetch_posters: - logger.info("Poster fetch enabled but no new program metadata downloaded — skipping artwork.") + logger.info("Poster fetch enabled but all mapped programs already have artwork.") # ------------------------------------------------------------------------- # Step 9: Apply SD station logos to matched channels if enabled diff --git a/frontend/src/components/ProgramDetailModal.jsx b/frontend/src/components/ProgramDetailModal.jsx index d3d9c5e5..fb715269 100644 --- a/frontend/src/components/ProgramDetailModal.jsx +++ b/frontend/src/components/ProgramDetailModal.jsx @@ -40,11 +40,19 @@ function formatDurationMinutes(startTime, endTime) { return `${minutes}m`; } +function resolveApiUrl(url) { + if (!url || url.startsWith('http')) return url; + const apiHost = import.meta.env.DEV + ? `http://${window.location.hostname}:5656` + : ''; + return `${apiHost}${url}`; +} + function resolveImageUrl(detail) { if (detail?.tmdb_poster_url) return detail.tmdb_poster_url; - if (detail?.poster_url) return detail.poster_url; - if (detail?.images?.length > 0) return detail.images[0].url; - if (detail?.icon) return detail.icon; + if (detail?.poster_url) return resolveApiUrl(detail.poster_url); + if (detail?.images?.length > 0) return resolveApiUrl(detail.images[0].url); + if (detail?.icon) return resolveApiUrl(detail.icon); return null; } diff --git a/frontend/src/components/forms/EPG.jsx b/frontend/src/components/forms/EPG.jsx index 6215e40d..0273c43c 100644 --- a/frontend/src/components/forms/EPG.jsx +++ b/frontend/src/components/forms/EPG.jsx @@ -168,7 +168,7 @@ const SDSettings = ({ sourceId, customProperties }) => { handlePosterToggle(e.currentTarget.checked)} disabled={saving}