From 3c5032ee71dcb89d9e2bcf99472890bb44437a24 Mon Sep 17 00:00:00 2001 From: Five Boroughs <40718516+FiveBoroughs@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:46:18 +0100 Subject: [PATCH] Batch ProgramData query and invalidate index on EPG refresh - Replace per-entry ProgramData DB query with single batch fetch - Pass pre-loaded EPGData objects to find_current_program_for_tvg_id to avoid redundant select_related re-fetch - Delete Redis programme index before XMLTV download so stale byte offsets are never used during the refresh window --- apps/epg/api_views.py | 22 +++++++++++++++------- apps/epg/tasks.py | 25 ++++++++++++++++++------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index 4fa6b478..68ef4b34 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -765,7 +765,7 @@ class CurrentProgramsAPIView(APIView): ) try: - epg_data_ids = [int(id) for id in epg_data_ids] + epg_data_ids = [int(eid) for eid in epg_data_ids] except (ValueError, TypeError): return Response( {"error": "epg_data_ids must contain valid integers"}, @@ -777,12 +777,20 @@ class CurrentProgramsAPIView(APIView): epg_data_entries = EPGData.objects.select_related('epg_source').filter(id__in=epg_data_ids) + # Batch-fetch current programs for all requested EPG entries in one query + db_programs = ProgramData.objects.filter( + epg__in=epg_data_entries, start_time__lte=now, end_time__gt=now + ).select_related('epg') + # Map epg_data id -> first matching program + programs_by_epg = {} + for prog in db_programs: + if prog.epg_id not in programs_by_epg: + programs_by_epg[prog.epg_id] = prog + current_programs = [] for epg_data in epg_data_entries: - # Check DB first (already-parsed channels) - program = ProgramData.objects.filter( - epg=epg_data, start_time__lte=now, end_time__gt=now - ).first() + # Check batch-fetched DB results first + program = programs_by_epg.get(epg_data.id) if program: program_data = ProgramDataSerializer(program).data @@ -794,8 +802,8 @@ class CurrentProgramsAPIView(APIView): if epg_data.epg_source and epg_data.epg_source.source_type == 'dummy': continue - # Fall back to byte-offset index lookup - result = find_current_program_for_tvg_id(epg_data.id) + # Fall back to byte-offset index lookup, pass the object to avoid re-fetch + result = find_current_program_for_tvg_id(epg_data) if result == "timeout": current_programs.append({ diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index 05291d4a..6991c14b 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -296,6 +296,14 @@ def refresh_epg_data(source_id): # Continue with the normal processing... logger.info(f"Processing EPGSource: {source.name} (type: {source.source_type})") if source.source_type == 'xmltv': + # Invalidate the byte-offset index before downloading the new file + # so stale offsets are never used during the refresh window. + from core.utils import RedisClient + try: + RedisClient.get_client().delete(f'epg_programme_index_{source.id}') + except Exception: + pass + fetch_success = fetch_xmltv(source) if not fetch_success: logger.error(f"Failed to fetch XMLTV for source {source.name}") @@ -2499,19 +2507,22 @@ def build_programme_index_task(source_id): build_programme_index(source_id) -def find_current_program_for_tvg_id(epg_id): +def find_current_program_for_tvg_id(epg_or_id): """ - Look up the currently-airing program for an EPGData id using the - byte-offset index. Falls back to sequential scan if no index exists. + Look up the currently-airing program for an EPGData instance (or id) using + the byte-offset index. Falls back to sequential scan if no index exists. Returns dict, None, or "timeout". """ from core.utils import RedisClient - try: - epg = EPGData.objects.select_related('epg_source').get(id=epg_id) - except EPGData.DoesNotExist: - return None + if isinstance(epg_or_id, EPGData): + epg = epg_or_id + else: + try: + epg = EPGData.objects.select_related('epg_source').get(id=epg_or_id) + except EPGData.DoesNotExist: + return None source = epg.epg_source if not source or source.source_type == 'dummy':