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
This commit is contained in:
Five Boroughs 2026-02-14 21:46:18 +01:00
parent 8f1dd2ffeb
commit 3c5032ee71
No known key found for this signature in database
2 changed files with 33 additions and 14 deletions

View file

@ -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({

View file

@ -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':