diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index fe1b1e7c..8d2bb96e 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -3137,8 +3137,8 @@ def fetch_schedules_direct(source, stations_only=False, force=False): # ------------------------------------------------------------------------- # Step 9: Apply SD station logos to matched channels if enabled # ------------------------------------------------------------------------- - use_sd_logos = (source.custom_properties or {}).get('use_sd_logos', False) - if use_sd_logos: + auto_apply_sd_logos = (source.custom_properties or {}).get('auto_apply_sd_logos', False) + if auto_apply_sd_logos: try: from apps.channels.models import Channel as ChannelModel, Logo diff --git a/frontend/src/components/forms/EPG.jsx b/frontend/src/components/forms/EPG.jsx index b94fadcd..409307e9 100644 --- a/frontend/src/components/forms/EPG.jsx +++ b/frontend/src/components/forms/EPG.jsx @@ -27,6 +27,7 @@ import ScheduleInput from './ScheduleInput'; import { addEPG, updateEPG } from '../../utils/forms/DummyEpgUtils.js'; import { showNotification } from '../../utils/notificationUtils.js'; import API from '../../api.js'; +import useEPGsStore from '../../store/epgs'; // Countries are fetched dynamically from the SD API on component mount. // Fallback list used if the API call fails. @@ -49,34 +50,52 @@ const SD_COUNTRIES_FALLBACK = [ // 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_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` }, + { + 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 storeCustomProps = useEPGsStore((s) => + sourceId ? s.epgs[sourceId]?.custom_properties : null + ); + const resolvedCp = storeCustomProps || customProperties || {}; + + const [autoApplySDLogos, setAutoApplySDLogos] = useState( + !!resolvedCp.auto_apply_sd_logos + ); + const [logoStyle, setLogoStyle] = useState(resolvedCp.logo_style || 'dark'); + const [fetchPosters, setFetchPosters] = useState(!!resolvedCp.fetch_posters); const [saving, setSaving] = useState(false); - // Sync from parent when customProperties changes + // Sync from store (preferred) or parent props when the form opens or settings save useEffect(() => { - const newCp = customProperties || {}; - setUseSDLogos(newCp.use_sd_logos || false); + const newCp = storeCustomProps || customProperties || {}; + setAutoApplySDLogos(!!newCp.auto_apply_sd_logos); 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]); + setFetchPosters(!!newCp.fetch_posters); + }, [storeCustomProps, customProperties]); const saveSetting = async (key, value) => { setSaving(true); @@ -87,9 +106,9 @@ const SDSettings = ({ sourceId, customProperties }) => { } }; - const handleLogoToggle = (checked) => { - setUseSDLogos(checked); - saveSetting('use_sd_logos', checked); + const handleAutoApplyToggle = (checked) => { + setAutoApplySDLogos(checked); + saveSetting('auto_apply_sd_logos', checked); }; const handleLogoChange = (style) => { @@ -102,25 +121,14 @@ const SDSettings = ({ sourceId, customProperties }) => { saveSetting('fetch_posters', checked); }; - const logosDisabled = false; - return ( - handleLogoToggle(e.currentTarget.checked)} - disabled={saving} - size="sm" - mb="sm" - /> - - + Station Logo Style - Choose which logo variant to use for SD stations. + Choose which logo variant to store in EPG data. Available for Set Logo + from EPG even when auto-apply is disabled. {SD_LOGO_STYLES.map((style) => ( @@ -128,16 +136,17 @@ const SDSettings = ({ sourceId, customProperties }) => { key={style.value} onClick={() => handleLogoChange(style.value)} style={{ - border: !logosDisabled && logoStyle === style.value - ? '2px solid var(--mantine-color-blue-5)' - : '2px solid var(--mantine-color-default-border)', + border: + 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'), + opacity: saving ? 0.6 : 1, + cursor: saving ? 'wait' : 'pointer', flex: 1, textAlign: 'center', - pointerEvents: logosDisabled ? 'none' : 'auto', + pointerEvents: saving ? 'none' : 'auto', }} > { height: 50, objectFit: 'contain', display: 'block', - backgroundColor: style.value === 'white' || style.value === 'light' - ? '#333' - : 'transparent', + backgroundColor: + style.value === 'white' || style.value === 'light' + ? '#333' + : 'transparent', borderRadius: 2, - filter: logosDisabled ? 'grayscale(100%)' : 'none', }} /> - + {style.label} ))} + handleAutoApplyToggle(e.currentTarget.checked)} + disabled={saving} + size="sm" + mb="sm" + /> + { const [addingLineup, setAddingLineup] = useState(null); const [removingLineup, setRemovingLineup] = useState(null); const maxLineups = 4; - const SD_DOCS_URL = 'https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#tasks-your-client-must-perform'; + const SD_DOCS_URL = + 'https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#tasks-your-client-must-perform'; const fetchActiveLineups = useCallback(async () => { setLoadingLineups(true); @@ -219,12 +243,12 @@ const SDLineupManager = ({ sourceId }) => { fetchActiveLineups(); // Fetch country list from SD API per their recommendation to not hardcode fetch('https://json.schedulesdirect.org/20141201/available/countries') - .then(r => r.json()) - .then(data => { + .then((r) => r.json()) + .then((data) => { const all = Object.values(data).flat(); const mapped = all - .filter(c => c.shortName && c.fullName) - .map(c => ({ value: c.shortName, label: c.fullName })) + .filter((c) => c.shortName && c.fullName) + .map((c) => ({ value: c.shortName, label: c.fullName })) .sort((a, b) => a.label.localeCompare(b.label)); if (mapped.length > 0) setCountries(mapped); }) @@ -237,7 +261,11 @@ const SDLineupManager = ({ sourceId }) => { setSearching(true); setSearchResults([]); try { - const data = await API.searchSDLineups(sourceId, country, postalCode.trim()); + const data = await API.searchSDLineups( + sourceId, + country, + postalCode.trim() + ); if (data) { setSearchResults(data.lineups || []); } @@ -253,7 +281,10 @@ const SDLineupManager = ({ sourceId }) => { if (!result) return; // Update changesRemaining from response - if (result.changes_remaining !== undefined && result.changes_remaining !== null) { + if ( + result.changes_remaining !== undefined && + result.changes_remaining !== null + ) { setChangesRemaining(result.changes_remaining); } @@ -264,22 +295,38 @@ const SDLineupManager = ({ sourceId }) => { } if (result.error === 'duplicate_lineup') { - showNotification({ title: 'Already added', message: result.message, color: 'yellow' }); + showNotification({ + title: 'Already added', + message: result.message, + color: 'yellow', + }); return; } if (result.error === 'max_lineups_reached') { - showNotification({ title: 'Maximum lineups reached', message: result.message, color: 'orange' }); + showNotification({ + title: 'Maximum lineups reached', + message: result.message, + color: 'orange', + }); return; } if (result.error) { - showNotification({ title: 'Unable to add lineup', message: result.message, color: 'red' }); + showNotification({ + title: 'Unable to add lineup', + message: result.message, + color: 'red', + }); return; } if (result.code === 0) { - showNotification({ title: 'Lineup added', message: lineup.name, color: 'green' }); + showNotification({ + title: 'Lineup added', + message: lineup.name, + color: 'green', + }); await fetchActiveLineups(); } } finally { @@ -292,8 +339,15 @@ const SDLineupManager = ({ sourceId }) => { try { const result = await API.deleteSDLineup(sourceId, lineup.lineup); if (result && result.code === 0) { - showNotification({ title: 'Lineup removed', message: lineup.name, color: 'blue' }); - if (result.changes_remaining !== undefined && result.changes_remaining !== null) { + showNotification({ + title: 'Lineup removed', + message: lineup.name, + color: 'blue', + }); + if ( + result.changes_remaining !== undefined && + result.changes_remaining !== null + ) { setChangesRemaining(result.changes_remaining); } await fetchActiveLineups(); @@ -344,7 +398,9 @@ const SDLineupManager = ({ sourceId }) => { }} > - {lineup.name} + + {lineup.name} + {lineup.transport} · {lineup.location} · {lineup.lineup} @@ -370,33 +426,67 @@ const SDLineupManager = ({ sourceId }) => { {atMax && ( - }> + } + > Maximum of {maxLineups} lineups reached. Remove one to add another. )} {changesRemaining === 0 && ( - }> - You have reached your daily Schedules Direct lineup addition limit. SD allows 6 adds per 24-hour period.{' '} + } + > + You have reached your daily Schedules Direct lineup addition limit. SD + allows 6 adds per 24-hour period.{' '} {changesResetAt && ( - Limit resets at {new Date(changesResetAt).toUTCString()}. + + Limit resets at{' '} + {new Date(changesResetAt).toUTCString()}.{' '} + )} - {!changesResetAt && Limit resets 24 hours after the first add of the day. } - Learn more + {!changesResetAt && ( + Limit resets 24 hours after the first add of the day. + )} + + Learn more + )} {changesRemaining === 1 && ( - }> - You have 1 lineup addition remaining today. Use it carefully — Schedules Direct limits adds to 6 per 24-hour period.{' '} - Learn more + } + > + You have 1 lineup addition remaining today. Use it + carefully — Schedules Direct limits adds to 6 per 24-hour period.{' '} + + Learn more + )} {changesRemaining === 2 && ( - }> - You have 2 lineup additions remaining today. Schedules Direct limits adds to 6 per 24-hour period.{' '} - Learn more + } + > + You have 2 lineup additions remaining today. + Schedules Direct limits adds to 6 per 24-hour period.{' '} + + Learn more + )} @@ -430,7 +520,13 @@ const SDLineupManager = ({ sourceId }) => { {searchResults.length > 0 && ( - + {searchResults.map((lineup) => { @@ -438,12 +534,20 @@ const SDLineupManager = ({ sourceId }) => { return ( - {lineup.name} - {lineup.transport} · {lineup.location} + + {lineup.name} + + + {lineup.transport} · {lineup.location} + - + {isActive ? ( - Active + + Active + ) : ( diff --git a/frontend/src/components/tables/EPGsTable.jsx b/frontend/src/components/tables/EPGsTable.jsx index 8b464ee2..c1dc9ef6 100644 --- a/frontend/src/components/tables/EPGsTable.jsx +++ b/frontend/src/components/tables/EPGsTable.jsx @@ -399,7 +399,8 @@ const EPGsTable = () => { const [sorting, setSorting] = useState([]); const editEPG = async (epg = null) => { - setEPG(epg); + const freshEpg = epg?.id ? (epgs[epg.id] || epg) : epg; + setEPG(freshEpg); // Open the appropriate modal based on source type if (epg?.source_type === 'dummy') { setDummyEpgModalOpen(true);