From ef1518a8dd02c26f45ccc3c935393eada0e26a91 Mon Sep 17 00:00:00 2001 From: FiveBoroughs Date: Wed, 4 Feb 2026 11:20:32 +0100 Subject: [PATCH] Fix: Auto-parse EPG programs for unlinked channels on preview When selecting an EPG channel in the channel form, if that EPG entry's programs had never been parsed, the preview showed "No current program". This was because parse_programs_for_tvg_id requires a linked channel. - Add force parameter to parse_programs_for_tvg_id to bypass channel check - Auto-trigger parsing from current-programs API when no programs exist - Check Redis task lock to prevent duplicate parse tasks from concurrent requests - Frontend retries up to 3 times with 10s delay to pick up parsed results - Skip parsing for dummy EPG sources --- apps/epg/api_views.py | 14 +++++++++ apps/epg/tasks.py | 4 +-- frontend/src/api.js | 8 ++++- frontend/src/components/forms/Channel.jsx | 36 +++++++++++++++-------- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index b3d0bb56..a24de560 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -811,6 +811,20 @@ class CurrentProgramsAPIView(APIView): program_data = ProgramDataSerializer(program).data program_data["epg_data_id"] = epg_data.id current_programs.append(program_data) + else: + # Check if ANY programs exist (vs none parsed yet) + has_any_programs = ProgramData.objects.filter(epg=epg_data).exists() + if not has_any_programs and epg_data.epg_source.source_type != 'dummy': + from core.utils import RedisClient + redis_client = RedisClient.get_client() + lock_id = f"task_lock_parse_epg_programs_{epg_data.id}" + if not redis_client.exists(lock_id): + from apps.epg.tasks import parse_programs_for_tvg_id + parse_programs_for_tvg_id.delay(epg_data.id, force=True) + current_programs.append({ + "epg_data_id": epg_data.id, + "parsing": True, + }) return Response(current_programs, status=status.HTTP_200_OK) diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index 8efe3618..be6d5bfc 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -1254,7 +1254,7 @@ def parse_channels_only(source): @shared_task(time_limit=3600, soft_time_limit=3500) -def parse_programs_for_tvg_id(epg_id): +def parse_programs_for_tvg_id(epg_id, force=False): if not acquire_task_lock('parse_epg_programs', epg_id): logger.info(f"Program parse for {epg_id} already in progress, skipping duplicate task") return "Task already running" @@ -1295,7 +1295,7 @@ def parse_programs_for_tvg_id(epg_id): release_task_lock('parse_epg_programs', epg_id) return - if not Channel.objects.filter(epg_data=epg).exists(): + if not force and not Channel.objects.filter(epg_data=epg).exists(): logger.info(f"No channels matched to EPG {epg.tvg_id}") lock_renewer.stop() release_task_lock('parse_epg_programs', epg_id) diff --git a/frontend/src/api.js b/frontend/src/api.js index 2967ba18..19e7d70b 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1540,7 +1540,13 @@ export default class API { ); // The endpoint returns an array, get the first item - return response && response.length > 0 ? response[0] : null; + if (response && response.length > 0) { + if (response[0].parsing) { + return { parsing: true }; + } + return response[0]; + } + return null; } catch (e) { console.error('Failed to retrieve current program for EPG', e); return null; diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index bb367c87..f3c63b1d 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -474,24 +474,34 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => { setIsLoadingProgram(true); setHasFetchedProgram(false); - API.getCurrentProgramForEpg(epgDataId) - .then((program) => { - if (!cancelled) { - setCurrentProgram(program); + const fetchWithRetry = async (retriesLeft = 3) => { + try { + const program = await API.getCurrentProgramForEpg(epgDataId); + if (cancelled) return; + + if (program && program.parsing && retriesLeft > 0) { + // Programs are being parsed, retry after delay + await new Promise((resolve) => setTimeout(resolve, 10000)); + if (cancelled) return; + return fetchWithRetry(retriesLeft - 1); } - }) - .catch((error) => { - if (!cancelled) { - console.error('Failed to fetch current program:', error); - setCurrentProgram(null); - } - }) - .finally(() => { + if (!cancelled) { + setCurrentProgram(program && !program.parsing ? program : null); setIsLoadingProgram(false); setHasFetchedProgram(true); } - }); + } catch (error) { + if (!cancelled) { + console.error('Failed to fetch current program:', error); + setCurrentProgram(null); + setIsLoadingProgram(false); + setHasFetchedProgram(true); + } + } + }; + + fetchWithRetry(); return () => { cancelled = true;