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.
This commit is contained in:
SergeantPanda 2026-06-04 22:00:15 -05:00
parent 3835954024
commit c4d73eae39
4 changed files with 39 additions and 10 deletions

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -168,7 +168,7 @@ const SDSettings = ({ sourceId, customProperties }) => {
<Switch
label="Fetch Program Posters"
description="WARNING: USES ADDITIONAL API REQUESTS. Poster images are fetched on-demand and cached locally for 30 days. Initial viewing of new programs will consume API requests against your Schedules Direct rate limit."
description="WARNING: USES ADDITIONAL API REQUESTS. Poster artwork is fetched during EPG refresh; image bytes are cached by nginx on first view (24h). Initial fetch and viewing new programs consume API requests against your Schedules Direct rate limit."
checked={fetchPosters}
onChange={(e) => handlePosterToggle(e.currentTarget.checked)}
disabled={saving}