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;