mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
* feat(epg): manual EPG-to-channel mapping with mapping fallback in all EPG paths
* fix: epg mapping in live tv list
* fix(epg): harden manual EPG mapping — upgrade safety, perf, playlist-scoped keys
Follow-up fixes on top of the manual EPG-to-channel mapping feature:
- epg-database: dedupe existing epg_programs rows before creating the
unique (channel_id, start, title) index — a plain CREATE UNIQUE INDEX
crashed the EPG worker on upgrade when historical duplicates exist;
replace INSERT OR REPLACE with ON CONFLICT DO UPDATE so the
epg_programs_fts delete trigger is not bypassed (REPLACE skips delete
triggers unless recursive_triggers is on), with a plain-INSERT
fallback when the index cannot be created
- db: add idx_content_epg_channel — the mapping fallback scanned the
whole content table on every single-channel EPG lookup
- keys: scope Xtream mapping keys per playlist via shared
buildXtreamEpgMappingKey (xtream:{playlistId}:{id}) — bare stream ids
collide across portals; the backend fallback now joins categories to
resolve the playlist id
- pwa: hide "Map EPG channel" entries behind the supportsEpgMapping
capability — the menu item was a dead end in the PWA
- parser: parse the XMLTV offset sign from the string — Math.sign(0)
dropped the minutes of ±00:xx offsets
- cleanup: typed window.electron access instead of ad-hoc casts, drop
unused resolveChannelId and dialog data field, shared
EpgMappingDialogComponent.open() for all seven call sites
- dialog UX: minimum-characters search hint, save/remove snackbars,
current mapping shows the EPG channel display name,
takeUntilDestroyed on the search stream
- i18n: fill the new keys in all 17 locales
- tests: cover mapping CRUD/search escaping, the dedup-index guard,
offset parsing and playlist-scoped keys; update stale stream-resolver
specs for the new 50-item limit and 10s timeout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(epg): escape backslashes in EPG channel search LIKE pattern
CodeQL js/incomplete-sanitization: a lone trailing backslash in the
search term paired with the closing wildcard under the ESCAPE clause
and corrupted the pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping lookups in the viewport preview queue
Expose the existing getEpgMappingsBatch operation over a new
EPG_MAPPING_GET_BATCH IPC channel and use it in resolveManualMappings —
the per-entry lookup issued one IPC round-trip per visible channel on
every scroll event.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping prefetch in the collection preview loader
Resolve all candidate mapping keys for an Xtream preview batch with a
single getEpgMappingsBatch IPC call instead of per-channel lookups.
Also fix a worker early-exit: a channel without tvgId/name returned out
of the shared-iterator loop and silently killed one of the three
concurrent preview workers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import {
|
|
parseXmltvDate,
|
|
StreamingEpgParser,
|
|
type ParsedChannel,
|
|
type ParsedProgram,
|
|
} from './epg-streaming-parser';
|
|
|
|
describe('parseXmltvDate', () => {
|
|
it('normalizes XMLTV timestamps with timezone offsets', () => {
|
|
expect(parseXmltvDate('20260415053700 +0000')).toBe(
|
|
'2026-04-15T05:37:00.000Z'
|
|
);
|
|
});
|
|
|
|
it('converts positive offsets to UTC', () => {
|
|
expect(parseXmltvDate('20260415053700 +0200')).toBe(
|
|
'2026-04-15T03:37:00.000Z'
|
|
);
|
|
});
|
|
|
|
it('converts negative offsets to UTC', () => {
|
|
expect(parseXmltvDate('20260415053700 -0530')).toBe(
|
|
'2026-04-15T11:07:00.000Z'
|
|
);
|
|
});
|
|
|
|
it('keeps the minutes component of zero-hour offsets', () => {
|
|
expect(parseXmltvDate('20260415053700 +0030')).toBe(
|
|
'2026-04-15T05:07:00.000Z'
|
|
);
|
|
expect(parseXmltvDate('20260415053700 -0030')).toBe(
|
|
'2026-04-15T06:07:00.000Z'
|
|
);
|
|
});
|
|
|
|
it('handles half-hour and 45-minute offsets', () => {
|
|
expect(parseXmltvDate('20260415053700 +0545')).toBe(
|
|
'2026-04-14T23:52:00.000Z'
|
|
);
|
|
});
|
|
|
|
it('treats offset-less timestamps as UTC', () => {
|
|
expect(parseXmltvDate('20260415053700')).toBe('2026-04-15T05:37:00Z');
|
|
});
|
|
});
|
|
|
|
describe('StreamingEpgParser', () => {
|
|
it('flushes pending channels before the first programme batch', () => {
|
|
const callbackOrder: string[] = [];
|
|
const channelIdsByBatch: string[][] = [];
|
|
const programmeChannelsByBatch: string[][] = [];
|
|
|
|
const parser = new StreamingEpgParser(
|
|
(channels: ParsedChannel[]) => {
|
|
callbackOrder.push('channels');
|
|
channelIdsByBatch.push(channels.map((channel) => channel.id));
|
|
},
|
|
(programs: ParsedProgram[]) => {
|
|
callbackOrder.push('programs');
|
|
programmeChannelsByBatch.push(
|
|
programs.map((program) => program.channel)
|
|
);
|
|
},
|
|
() => undefined
|
|
);
|
|
|
|
const xml = [
|
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
'<tv>',
|
|
...Array.from({ length: 101 }, (_, index) => {
|
|
const id = `channel-${index + 1}`;
|
|
return `<channel id="${id}"><display-name>${id}</display-name></channel>`;
|
|
}),
|
|
'<programme start="20260415053700 +0000" stop="20260415062100 +0000" channel="channel-101"><title>Late channel</title></programme>',
|
|
'</tv>',
|
|
].join('');
|
|
|
|
parser.write(xml);
|
|
parser.finish();
|
|
|
|
expect(callbackOrder).toEqual(['channels', 'channels', 'programs']);
|
|
expect(channelIdsByBatch).toEqual([
|
|
Array.from({ length: 100 }, (_, index) => `channel-${index + 1}`),
|
|
['channel-101'],
|
|
]);
|
|
expect(programmeChannelsByBatch).toEqual([['channel-101']]);
|
|
});
|
|
});
|