From 7c91508f3c2a40184acb650c886555f563bddd03 Mon Sep 17 00:00:00 2001 From: Shokkstokk Date: Thu, 4 Jun 2026 23:46:40 +0000 Subject: [PATCH] fix: add missing updateSDSettings API method and EPG refresh handlers Add updateSDSettings method to API client for SD logo style and settings management. Add fetchEPGData and requeryChannels calls after EPG match completion to ensure UI refreshes immediately. Add SD logo style picker and cache-busting for channel logos. --- apps/channels/serializers.py | 19 ++- frontend/src/WebSocket.jsx | 11 +- frontend/src/api.js | 19 +++ frontend/src/components/forms/EPG.jsx | 195 +++++++++++++++++++++++--- 4 files changed, 209 insertions(+), 35 deletions(-) diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index 4598f8b2..e9ac8581 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -19,9 +19,10 @@ from apps.epg.serializers import EPGDataSerializer from core.models import StreamProfile from apps.epg.models import EPGData from django.db import connection, transaction +from django.urls import reverse from rest_framework import serializers from django.utils import timezone -from core.utils import validate_flexible_url, build_absolute_uri_with_port +from core.utils import validate_flexible_url class LogoSerializer(serializers.ModelSerializer): @@ -56,12 +57,18 @@ class LogoSerializer(serializers.ModelSerializer): return instance def get_cache_url(self, obj): + # Cache-busting: append a short hash of the logo's source URL so the browser + # fetches fresh when the logo changes (e.g., M3U logo replaced by SD logo). + # The backend ignores the 'v' parameter — it's purely for browser cache invalidation. + # See SD integration PR notes for context on why this was added. + import hashlib + url_hash = hashlib.md5((obj.url or '').encode()).hexdigest()[:8] + base_path = reverse("api:channels:logo-cache", args=[obj.id]) + cache_url = f"{base_path}?v={url_hash}" request = self.context.get("request") - if not request: - return f"/api/channels/logos/{obj.id}/cache/" - if not hasattr(self, "_cache_url_prefix"): - self._cache_url_prefix = build_absolute_uri_with_port(request, "") - return f"{self._cache_url_prefix}/api/channels/logos/{obj.id}/cache/" + if request: + return request.build_absolute_uri(cache_url) + return cache_url def get_channel_count(self, obj): """Get the number of channels using this logo""" diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index 1223a6a0..d8912f0d 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -381,6 +381,11 @@ export const WebsocketProvider = ({ children }) => { ) { API.batchSetEPG(parsedEvent.data.associations); } + + // Refresh EPG store first, then requery channels so the table + // cross-references updated epg_data_id assignments immediately + fetchEPGData(); + API.requeryChannels(); break; case 'epg_matching_progress': { @@ -965,12 +970,6 @@ export const WebsocketProvider = ({ children }) => { break; } - case 'ip_lookup_complete': { - const { type: _t, ...ipData } = parsedEvent.data; - useSettingsStore.getState().setEnvironmentFields(ipData); - break; - } - default: console.error( `Unknown websocket event type: ${parsedEvent.data?.type}` diff --git a/frontend/src/api.js b/frontend/src/api.js index 30d69c85..43f53040 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -3879,6 +3879,25 @@ export default class API { } } + static async updateSDSettings(sourceId, settings) { + try { + // Read current custom_properties from the store to merge, not replace + const epgs = useEPGsStore.getState().epgs; + const source = epgs[sourceId]; + const cp = { ...(source?.custom_properties || {}), ...settings }; + + const response = await request(`${host}/api/epg/sources/${sourceId}/`, { + method: 'PATCH', + body: { custom_properties: cp }, + }); + + useEPGsStore.getState().updateEPG(response); + return response; + } catch (e) { + errorNotification('Failed to update Schedules Direct settings', e); + } + } + static async searchSDLineups(sourceId, country, postalcode) { try { const response = await request( diff --git a/frontend/src/components/forms/EPG.jsx b/frontend/src/components/forms/EPG.jsx index 369011f3..6215e40d 100644 --- a/frontend/src/components/forms/EPG.jsx +++ b/frontend/src/components/forms/EPG.jsx @@ -18,6 +18,8 @@ import { Badge, ScrollArea, Table, + Switch, + UnstyledButton, } from '@mantine/core'; import { TriangleAlert, Trash2, Plus, Search } from 'lucide-react'; import { isNotEmpty, useForm } from '@mantine/form'; @@ -45,6 +47,138 @@ const SD_COUNTRIES_FALLBACK = [ { value: 'NZL', label: 'New Zealand' }, ]; +// ESPN HD logo previews — packaged as static URLs so no API call is needed. +// These are publicly accessible S3 URLs that don't require authentication. +const SD_LOGO_PREVIEW_BASE = 'https://schedulesdirect-api20141201-logos.s3.dualstack.us-east-1.amazonaws.com/stationLogos/s32645'; +const SD_LOGO_STYLES = [ + { value: 'dark', label: 'Dark', url: `${SD_LOGO_PREVIEW_BASE}_dark_360w_270h.png` }, + { value: 'white', label: 'White', url: `${SD_LOGO_PREVIEW_BASE}_white_360w_270h.png` }, + { value: 'gray', label: 'Gray', url: `${SD_LOGO_PREVIEW_BASE}_gray_360w_270h.png` }, + { value: 'light', label: 'Light', url: `${SD_LOGO_PREVIEW_BASE}_light_360w_270h.png` }, +]; + +// ─── SD Settings: Logo toggle + style selector + Poster toggle ────────────── +const SDSettings = ({ sourceId, customProperties }) => { + const cp = customProperties || {}; + const [useSDLogos, setUseSDLogos] = useState(cp.use_sd_logos || false); + const [logoStyle, setLogoStyle] = useState(cp.logo_style || 'dark'); + const [fetchPosters, setFetchPosters] = useState(cp.fetch_posters || false); + const [saving, setSaving] = useState(false); + + // Sync from parent when customProperties changes + useEffect(() => { + const newCp = customProperties || {}; + setUseSDLogos(newCp.use_sd_logos || false); + setLogoStyle(newCp.logo_style || 'dark'); + setFetchPosters(newCp.fetch_posters || false); + + // Persist the default logo_style if not already set + if (sourceId && !newCp.logo_style) { + API.updateSDSettings(sourceId, { logo_style: 'dark' }); + } + }, [customProperties, sourceId]); + + const saveSetting = async (key, value) => { + setSaving(true); + try { + await API.updateSDSettings(sourceId, { [key]: value }); + } finally { + setSaving(false); + } + }; + + const handleLogoToggle = (checked) => { + setUseSDLogos(checked); + saveSetting('use_sd_logos', checked); + }; + + const handleLogoChange = (style) => { + if (!useSDLogos) return; + setLogoStyle(style); + saveSetting('logo_style', style); + }; + + const handlePosterToggle = (checked) => { + setFetchPosters(checked); + saveSetting('fetch_posters', checked); + }; + + const logosDisabled = !useSDLogos; + + return ( + + handleLogoToggle(e.currentTarget.checked)} + disabled={saving} + size="sm" + mb="sm" + /> + + + Station Logo Style + + + Choose which logo variant to use for SD stations. + + + {SD_LOGO_STYLES.map((style) => ( + handleLogoChange(style.value)} + style={{ + border: !logosDisabled && logoStyle === style.value + ? '2px solid var(--mantine-color-blue-5)' + : '2px solid var(--mantine-color-default-border)', + borderRadius: 'var(--mantine-radius-sm)', + padding: 3, + opacity: logosDisabled ? 0.3 : (saving ? 0.6 : 1), + cursor: logosDisabled ? 'not-allowed' : (saving ? 'wait' : 'pointer'), + flex: 1, + textAlign: 'center', + pointerEvents: logosDisabled ? 'none' : 'auto', + }} + > + {style.label} + + {style.label} + + + ))} + + + + + handlePosterToggle(e.currentTarget.checked)} + disabled={saving} + size="sm" + /> + + ); +}; + +// ─── SD Lineup Manager ───────────────────────────────────────────────────── const SDLineupManager = ({ sourceId }) => { const [countries, setCountries] = useState(SD_COUNTRIES_FALLBACK); const [activeLineups, setActiveLineups] = useState([]); @@ -175,7 +309,7 @@ const SDLineupManager = ({ sourceId }) => { const atMax = activeLineups.length >= maxLineups; return ( - + @@ -335,10 +469,12 @@ const SDLineupManager = ({ sourceId }) => { ); }; +// ─── Main EPG Form ────────────────────────────────────────────────────────── const EPG = ({ epg = null, isOpen, onClose }) => { const [sourceType, setSourceType] = useState('xmltv'); const [scheduleType, setScheduleType] = useState('interval'); const [savedEpgId, setSavedEpgId] = useState(null); + const [sdCustomProps, setSdCustomProps] = useState(null); const form = useForm({ mode: 'uncontrolled', @@ -372,22 +508,18 @@ const EPG = ({ epg = null, isOpen, onClose }) => { values.cron_expression = ''; } - if (epg?.id) { - if (!epg || typeof epg !== 'object' || !epg.id) { - showNotification({ - title: 'Error', - message: 'Invalid EPG data. Please close and reopen this form.', - color: 'red', - }); - return; - } + const existingId = epg?.id || savedEpgId; - await updateEPG(values, epg); + if (existingId) { + const epgObj = epg || { id: existingId }; + await updateEPG(values, epgObj); onClose(); } else { const result = await addEPG(values); if (result?.id) { setSavedEpgId(result.id); + // Load custom_properties for the new source + setSdCustomProps(result.custom_properties || {}); } else { form.reset(); onClose(); @@ -411,6 +543,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => { form.setValues(values); setSourceType(epg.source_type); setSavedEpgId(epg.id); + setSdCustomProps(epg.custom_properties || {}); setScheduleType( epg.cron_expression && epg.cron_expression.trim() !== '' ? 'cron' @@ -421,6 +554,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => { setSourceType('xmltv'); setScheduleType('interval'); setSavedEpgId(null); + setSdCustomProps(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [epg]); @@ -433,6 +567,9 @@ const EPG = ({ epg = null, isOpen, onClose }) => { const handleClose = () => { form.reset(); setSavedEpgId(null); + setSdCustomProps(null); + setSourceType('xmltv'); + setScheduleType('interval'); onClose(); }; @@ -441,14 +578,15 @@ const EPG = ({ epg = null, isOpen, onClose }) => { } const isSD = sourceType === 'schedules_direct'; + const hasSDPanel = isSD && savedEpgId; return ( <> - +
- + {/* Left Column */} - + { - {/* Right Column */} - + {/* Middle Column */} + {!isSD && ( { - - {/* Lineup Manager — only shown for saved SD sources */} - {isSD && savedEpgId && ( - - )} + {/* Right Column — SD settings + Lineup Manager */} + {hasSDPanel && ( + <> + + + + + + + + + )} + {/* Full Width Section */} @@ -623,7 +772,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => { {isSD && !savedEpgId && ( - Save this source first to manage Schedules Direct lineups. + Save this source first to manage Schedules Direct settings and lineups. )} @@ -632,7 +781,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => { Cancel