From 91eaa64ebb19ed8176366ca2cc86cae6b4802c0b Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 18 Oct 2025 13:43:49 -0500 Subject: [PATCH] Enhancement: Force a specific EPG for auto channel sync channels. --- apps/m3u/tasks.py | 83 +++++++++++- .../src/components/forms/LiveGroupFilter.jsx | 126 ++++++++++++++++-- 2 files changed, 194 insertions(+), 15 deletions(-) diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index 344bc1a3..d29c294b 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -1548,7 +1548,7 @@ def sync_auto_channels(account_id, scan_start_time=None): # Get force_dummy_epg, group_override, and regex patterns from group custom_properties group_custom_props = {} - force_dummy_epg = False + force_dummy_epg = False # Backward compatibility: legacy option to disable EPG override_group_id = None name_regex_pattern = None name_replace_pattern = None @@ -1558,6 +1558,7 @@ def sync_auto_channels(account_id, scan_start_time=None): channel_sort_reverse = False stream_profile_id = None custom_logo_id = None + custom_epg_id = None # New option: select specific EPG source (takes priority over force_dummy_epg) if group_relation.custom_properties: group_custom_props = group_relation.custom_properties force_dummy_epg = group_custom_props.get("force_dummy_epg", False) @@ -1568,6 +1569,7 @@ def sync_auto_channels(account_id, scan_start_time=None): ) name_match_regex = group_custom_props.get("name_match_regex") channel_profile_ids = group_custom_props.get("channel_profile_ids") + custom_epg_id = group_custom_props.get("custom_epg_id") channel_sort_order = group_custom_props.get("channel_sort_order") channel_sort_reverse = group_custom_props.get( "channel_sort_reverse", False @@ -1862,10 +1864,42 @@ def sync_auto_channels(account_id, scan_start_time=None): # Handle EPG data updates current_epg_data = None - if stream.tvg_id and not force_dummy_epg: + if custom_epg_id: + # Use the custom EPG specified in group settings (e.g., a dummy EPG) + from apps.epg.models import EPGSource + try: + epg_source = EPGSource.objects.get(id=custom_epg_id) + # For dummy EPGs, select the first (and typically only) EPGData entry from this source + if epg_source.source_type == 'dummy': + current_epg_data = EPGData.objects.filter( + epg_source=epg_source + ).first() + if not current_epg_data: + logger.warning( + f"No EPGData found for dummy EPG source {epg_source.name} (ID: {custom_epg_id})" + ) + else: + # For non-dummy sources, try to find existing EPGData by tvg_id + if stream.tvg_id: + current_epg_data = EPGData.objects.filter( + tvg_id=stream.tvg_id, + epg_source=epg_source + ).first() + except EPGSource.DoesNotExist: + logger.warning( + f"Custom EPG source with ID {custom_epg_id} not found for existing channel, falling back to auto-match" + ) + # Fall back to auto-match by tvg_id + if stream.tvg_id and not force_dummy_epg: + current_epg_data = EPGData.objects.filter( + tvg_id=stream.tvg_id + ).first() + elif stream.tvg_id and not force_dummy_epg: + # Auto-match EPG by tvg_id (original behavior) current_epg_data = EPGData.objects.filter( tvg_id=stream.tvg_id ).first() + # If force_dummy_epg is True and no custom_epg_id, current_epg_data stays None if existing_channel.epg_data != current_epg_data: existing_channel.epg_data = current_epg_data @@ -1955,14 +1989,55 @@ def sync_auto_channels(account_id, scan_start_time=None): ChannelProfileMembership.objects.bulk_create(memberships) # Try to match EPG data - if stream.tvg_id and not force_dummy_epg: + if custom_epg_id: + # Use the custom EPG specified in group settings (e.g., a dummy EPG) + from apps.epg.models import EPGSource + try: + epg_source = EPGSource.objects.get(id=custom_epg_id) + # For dummy EPGs, select the first (and typically only) EPGData entry from this source + if epg_source.source_type == 'dummy': + epg_data = EPGData.objects.filter( + epg_source=epg_source + ).first() + if epg_data: + channel.epg_data = epg_data + channel.save(update_fields=["epg_data"]) + else: + logger.warning( + f"No EPGData found for dummy EPG source {epg_source.name} (ID: {custom_epg_id})" + ) + else: + # For non-dummy sources, try to find existing EPGData by tvg_id + if stream.tvg_id: + epg_data = EPGData.objects.filter( + tvg_id=stream.tvg_id, + epg_source=epg_source + ).first() + if epg_data: + channel.epg_data = epg_data + channel.save(update_fields=["epg_data"]) + except EPGSource.DoesNotExist: + logger.warning( + f"Custom EPG source with ID {custom_epg_id} not found, falling back to auto-match" + ) + # Fall back to auto-match by tvg_id + if stream.tvg_id and not force_dummy_epg: + epg_data = EPGData.objects.filter( + tvg_id=stream.tvg_id + ).first() + if epg_data: + channel.epg_data = epg_data + channel.save(update_fields=["epg_data"]) + elif stream.tvg_id and not force_dummy_epg: + # Auto-match EPG by tvg_id (original behavior) epg_data = EPGData.objects.filter( tvg_id=stream.tvg_id ).first() if epg_data: channel.epg_data = epg_data channel.save(update_fields=["epg_data"]) - elif stream.tvg_id and force_dummy_epg: + elif force_dummy_epg: + # Force dummy EPG with no custom EPG selected (set to None) channel.epg_data = None channel.save(update_fields=["epg_data"]) diff --git a/frontend/src/components/forms/LiveGroupFilter.jsx b/frontend/src/components/forms/LiveGroupFilter.jsx index 18928a0b..71b412b4 100644 --- a/frontend/src/components/forms/LiveGroupFilter.jsx +++ b/frontend/src/components/forms/LiveGroupFilter.jsx @@ -29,6 +29,7 @@ import { FixedSizeList as List } from 'react-window'; import LazyLogo from '../LazyLogo'; import LogoForm from './Logo'; import logo from '../../images/logo.png'; +import API from '../../api'; // Custom item component for MultiSelect with tooltip const OptionWithTooltip = forwardRef( @@ -53,6 +54,7 @@ const LiveGroupFilter = ({ const streamProfiles = useStreamProfilesStore((s) => s.profiles); const fetchStreamProfiles = useStreamProfilesStore((s) => s.fetchProfiles); const [groupFilter, setGroupFilter] = useState(''); + const [epgSources, setEpgSources] = useState([]); // Logo selection functionality const { @@ -75,6 +77,19 @@ const LiveGroupFilter = ({ } }, [streamProfiles.length, fetchStreamProfiles]); + // Fetch EPG sources when component mounts + useEffect(() => { + const fetchEPGSources = async () => { + try { + const sources = await API.getEPGs(); + setEpgSources(sources || []); + } catch (error) { + console.error('Failed to fetch EPG sources:', error); + } + }; + fetchEPGSources(); + }, []); + useEffect(() => { if (Object.keys(channelGroups).length === 0) { return; @@ -298,10 +313,10 @@ const LiveGroupFilter = ({ placeholder="Select options..." data={[ { - value: 'force_dummy_epg', - label: 'Force Dummy EPG', + value: 'force_epg', + label: 'Force EPG Source', description: - 'Assign a dummy EPG to all channels in this group if no EPG is matched', + 'Force a specific EPG source for all auto-synced channels, or disable EPG assignment entirely', }, { value: 'group_override', @@ -349,8 +364,12 @@ const LiveGroupFilter = ({ itemComponent={OptionWithTooltip} value={(() => { const selectedValues = []; - if (group.custom_properties?.force_dummy_epg) { - selectedValues.push('force_dummy_epg'); + if ( + group.custom_properties?.custom_epg_id !== + undefined || + group.custom_properties?.force_dummy_epg + ) { + selectedValues.push('force_epg'); } if ( group.custom_properties?.group_override !== @@ -409,13 +428,25 @@ const LiveGroupFilter = ({ ...(state.custom_properties || {}), }; - // Handle force_dummy_epg - if ( - selectedOptions.includes('force_dummy_epg') - ) { - newCustomProps.force_dummy_epg = true; + // Handle force_epg + if (selectedOptions.includes('force_epg')) { + // Migrate from old force_dummy_epg if present + if ( + newCustomProps.force_dummy_epg && + newCustomProps.custom_epg_id === undefined + ) { + // Migrate: force_dummy_epg=true becomes custom_epg_id=null + newCustomProps.custom_epg_id = null; + delete newCustomProps.force_dummy_epg; + } else if ( + newCustomProps.custom_epg_id === undefined + ) { + // New configuration: initialize with null (no EPG/default dummy) + newCustomProps.custom_epg_id = null; + } } else { - delete newCustomProps.force_dummy_epg; + // Only remove custom_epg_id when deselected + delete newCustomProps.custom_epg_id; } // Handle group_override @@ -1088,6 +1119,79 @@ const LiveGroupFilter = ({ )} + + {/* Show EPG selector when force_epg is selected */} + {(group.custom_properties?.custom_epg_id !== undefined || + group.custom_properties?.force_dummy_epg) && ( + +