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
This commit is contained in:
FiveBoroughs 2026-02-04 11:20:32 +01:00 committed by Five Boroughs
parent e733b9872f
commit ef1518a8dd
No known key found for this signature in database
4 changed files with 46 additions and 16 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -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;