Scan the XML file with raw binary I/O to build a {tvg_id: [byte_offset, ...]} map. Persists the result to the EPGSourceIndex table. Most XMLTV files group programmes by channel, but some split a channel across multiple non-contiguous blocks, so we record block starts up to _OFFSET_C
(source_id)
| 4484 | |
| 4485 | |
| 4486 | def build_programme_index(source_id): |
| 4487 | """ |
| 4488 | Scan the XML file with raw binary I/O to build a {tvg_id: [byte_offset, ...]} map. |
| 4489 | Persists the result to the EPGSourceIndex table. Most XMLTV files group programmes |
| 4490 | by channel, but some split a channel across multiple non-contiguous blocks, so we |
| 4491 | record block starts up to _OFFSET_CAP and mark only channels that exceed the cap |
| 4492 | as interleaved. |
| 4493 | """ |
| 4494 | try: |
| 4495 | source = EPGSource.objects.get(id=source_id) |
| 4496 | except EPGSource.DoesNotExist: |
| 4497 | logger.error(f'[build_programme_index] EPGSource {source_id} not found') |
| 4498 | return |
| 4499 | |
| 4500 | file_path = _resolve_source_file(source) |
| 4501 | if not file_path or not os.path.exists(file_path): |
| 4502 | logger.warning( |
| 4503 | f'[build_programme_index] File not found for source {source_id}: {file_path}' |
| 4504 | ) |
| 4505 | return |
| 4506 | |
| 4507 | logger.debug( |
| 4508 | f'[build_programme_index] Building byte-offset index for source {source_id} from {file_path}' |
| 4509 | ) |
| 4510 | start = time.monotonic() |
| 4511 | index = {} |
| 4512 | prev_channel = None |
| 4513 | interleaved_channels = set() |
| 4514 | |
| 4515 | CHUNK = 8 * 1024 * 1024 # 8MB |
| 4516 | |
| 4517 | with open(file_path, 'rb') as f: |
| 4518 | buf = bytearray() |
| 4519 | buf_offset = 0 # absolute file offset of buf[0] |
| 4520 | |
| 4521 | while True: |
| 4522 | chunk = f.read(CHUNK) |
| 4523 | if not chunk and not buf: |
| 4524 | break |
| 4525 | buf.extend(chunk) |
| 4526 | search_from = 0 |
| 4527 | |
| 4528 | while True: |
| 4529 | idx, tag_end = _find_programme_tag(buf, search_from) |
| 4530 | if idx == -1: |
| 4531 | break |
| 4532 | if tag_end == -1 and chunk: |
| 4533 | break # incomplete tag at buffer edge, need more data |
| 4534 | |
| 4535 | abs_pos = buf_offset + idx |
| 4536 | m = _CHANNEL_ATTR_RE.search( |
| 4537 | buf, idx, tag_end + 1 if tag_end != -1 else idx + _MAX_START_TAG |
| 4538 | ) |
| 4539 | if m: |
| 4540 | channel_id = _decode_channel_id(m.group(1) or m.group(2)) |
| 4541 | if channel_id not in index: |
| 4542 | index[channel_id] = [abs_pos] |
| 4543 | elif channel_id != prev_channel: |