This commit is contained in:
1 parent
d7aea0be65
commit
91f22b54b4
30 files changed
+2475
No files matched your search
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { parseChannelName, normalizeText } = require('../src/channel-name');
|
||||
|
||||
const sample =
|
||||
'View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (2026-11-06 19:25:00)]';
|
||||
|
||||
test('parses the DAZN sample', () => {
|
||||
const parsed = parseChannelName(sample, { timezone: 'Europe/Brussels' });
|
||||
assert.equal(parsed.title, 'Standard de Liège - SK Beveren');
|
||||
assert.equal(parsed.league, 'Jupiler Pro League');
|
||||
assert.equal(parsed.language, 'nl');
|
||||
assert.equal(parsed.startAt.toISOString(), '2026-11-06T18:25:00.000Z');
|
||||
});
|
||||
|
||||
test('parses a French stream', () => {
|
||||
const parsed = parseChannelName(
|
||||
'View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (fr) (2026-11-06 19:25:00)]',
|
||||
{ timezone: 'Europe/Brussels' }
|
||||
);
|
||||
assert.equal(parsed.language, 'fr');
|
||||
});
|
||||
|
||||
test('allows a missing language code', () => {
|
||||
const parsed = parseChannelName(
|
||||
'DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (2026-11-06 19:25:00)]',
|
||||
{ timezone: 'Europe/Brussels' }
|
||||
);
|
||||
assert.equal(parsed.language, null);
|
||||
});
|
||||
|
||||
test('returns null for unparseable names', () => {
|
||||
assert.equal(parseChannelName('DAZN 03', { timezone: 'Europe/Brussels' }), null);
|
||||
assert.equal(parseChannelName('', { timezone: 'Europe/Brussels' }), null);
|
||||
});
|
||||
|
||||
test('normalizes diacritics and whitespace', () => {
|
||||
assert.equal(normalizeText(' Standard de Liège - SK Beveren '), 'standard de liege - sk beveren');
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { buildXmltv, xmlEscape, xmltvDate } = require('../src/epg');
|
||||
|
||||
function event(key, title, startIso, endIso, league = 'Jupiler Pro League') {
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
league,
|
||||
language: 'nl',
|
||||
startAt: new Date(startIso),
|
||||
endAt: new Date(endIso),
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
|
||||
test('builds XMLTV with channels and projected programmes', () => {
|
||||
const a = event('a', 'Standard de Liège - SK Beveren', '2026-11-06T18:25:00Z', '2026-11-06T20:55:00Z');
|
||||
const b = event('b', 'Anderlecht & Genk', '2026-11-06T20:55:00Z', '2026-11-06T23:25:00Z');
|
||||
const c = event('c', 'Club Brugge - Antwerp', '2026-11-06T20:55:00Z', '2026-11-06T23:25:00Z');
|
||||
const state = {
|
||||
slots: [{ eventKey: 'a' }, { eventKey: null }, { eventKey: null }],
|
||||
events: new Map([['a', a]]),
|
||||
upcoming: [a, b, c],
|
||||
};
|
||||
const scheduler = { state, getSlotCount: () => 3 };
|
||||
|
||||
const xml = buildXmltv(scheduler, Date.parse('2026-11-06T19:00:00Z'));
|
||||
|
||||
assert.ok(xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>'));
|
||||
assert.ok(xml.includes('<channel id="jupiler-1">'));
|
||||
assert.ok(xml.includes('<channel id="jupiler-3">'));
|
||||
assert.ok(xml.includes('start="20261106182500 +0000"'));
|
||||
assert.ok(xml.includes('stop="20261106205500 +0000"'));
|
||||
assert.ok(xml.includes('channel="jupiler-1"'));
|
||||
assert.ok(xml.includes('Standard de Liège') === false);
|
||||
assert.ok(xml.includes('Standard de Liège - SK Beveren'));
|
||||
assert.ok(xml.includes('Anderlecht & Genk'));
|
||||
assert.ok(xml.includes('Club Brugge - Antwerp'));
|
||||
});
|
||||
|
||||
test('escapes XML and formats UTC dates', () => {
|
||||
assert.equal(xmlEscape('a & <b> "c" \'d\''), 'a & <b> "c" 'd'');
|
||||
assert.equal(xmltvDate(new Date('2026-11-06T18:25:00Z')), '20261106182500 +0000');
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { buildEvents, compareEvents, isEligible } = require('../src/events');
|
||||
|
||||
const timezone = 'Europe/Brussels';
|
||||
|
||||
function stream(id, name) {
|
||||
return { stream_id: id, name };
|
||||
}
|
||||
|
||||
function build(streams, overrides = {}) {
|
||||
return buildEvents(streams, {
|
||||
searchTerm: 'Jupiler Pro League',
|
||||
language: 'nl',
|
||||
timezone,
|
||||
eventDurationMs: 150 * 60 * 1000,
|
||||
urlBuilder: (id) => [`http://upstream/live/u/p/${id}.ts`],
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('filters by search term and language, dedupes the same match', () => {
|
||||
const { events } = build([
|
||||
stream(
|
||||
1,
|
||||
'DAZN 01 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (2026-11-06 19:25:00)]'
|
||||
),
|
||||
stream(
|
||||
2,
|
||||
'DAZN 02 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (2026-11-06 19:25:00)]'
|
||||
),
|
||||
stream(
|
||||
3,
|
||||
'DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (fr) (2026-11-06 19:25:00)]'
|
||||
),
|
||||
stream(4, 'DAZN 04 [Anderlecht - Genk | Jupiler Pro League (2026-11-06 19:25:00)]'),
|
||||
stream(5, 'DAZN 05 [Other League (nl) (2026-11-06 19:25:00)]'),
|
||||
]);
|
||||
|
||||
assert.equal(events.size, 2);
|
||||
const [first, second] = [...events.values()];
|
||||
assert.equal(first.title, 'Standard de Liège - SK Beveren');
|
||||
assert.deepEqual(first.sources.map((source) => source.streamId), ['1', '2']);
|
||||
assert.equal(first.language, 'nl');
|
||||
assert.equal(second.sources[0].streamId, '4');
|
||||
});
|
||||
|
||||
test('reports unparseable matching names as warnings', () => {
|
||||
const { events, warnings } = build([stream(1, 'DAZN 01 | Jupiler Pro League zonder tijd')]);
|
||||
assert.equal(events.size, 0);
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.equal(warnings[0].reason, 'unparseable_name');
|
||||
});
|
||||
|
||||
test('an event is eligible inside the start grace and before its end', () => {
|
||||
const event = {
|
||||
startAt: new Date('2026-11-06T18:25:00.000Z'),
|
||||
endAt: new Date('2026-11-06T20:55:00.000Z'),
|
||||
};
|
||||
const options = { startGraceMs: 10 * 60 * 1000 };
|
||||
assert.equal(isEligible(event, Date.parse('2026-11-06T18:16:00.000Z'), options), true);
|
||||
assert.equal(isEligible(event, Date.parse('2026-11-06T18:14:00.000Z'), options), false);
|
||||
assert.equal(isEligible(event, Date.parse('2026-11-06T20:54:00.000Z'), options), true);
|
||||
assert.equal(isEligible(event, Date.parse('2026-11-06T20:56:00.000Z'), options), false);
|
||||
});
|
||||
|
||||
test('sorting is by start time and then arrival order', () => {
|
||||
const a = {
|
||||
key: 'a',
|
||||
startAt: new Date('2026-11-06T19:00:00Z'),
|
||||
firstSeenSeq: 2,
|
||||
};
|
||||
const b = {
|
||||
key: 'b',
|
||||
startAt: new Date('2026-11-06T19:00:00Z'),
|
||||
firstSeenSeq: 1,
|
||||
};
|
||||
const c = {
|
||||
key: 'c',
|
||||
startAt: new Date('2026-11-06T18:00:00Z'),
|
||||
firstSeenSeq: 3,
|
||||
};
|
||||
assert.deepEqual([a, b, c].sort(compareEvents).map((event) => event.key), ['c', 'b', 'a']);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { rewritePlaylist, signUrl, verifyToken } = require('../src/hls');
|
||||
|
||||
test('rewrites playlist URIs to signed proxy URLs', () => {
|
||||
const base = 'http://upstream.example/live/user/pass/index.m3u8';
|
||||
const body = [
|
||||
'#EXTM3U',
|
||||
'#EXT-X-KEY:METHOD=AES-128,URI="keys/key.bin"',
|
||||
'#EXTINF:6,',
|
||||
'segment1.ts',
|
||||
'#EXTINF:6,',
|
||||
'http://cdn.example/segment2.ts',
|
||||
'#EXT-X-ENDLIST',
|
||||
].join('\n');
|
||||
|
||||
const rewritten = rewritePlaylist(body, base, 2, 'http://proxy.example');
|
||||
const lines = rewritten.split('\n');
|
||||
|
||||
assert.equal(lines[0], '#EXTM3U');
|
||||
assert.match(lines[1], /^#EXT-X-KEY:METHOD=AES-128,URI="http:\/\/proxy\.example\/hls\/2\/.+"/);
|
||||
assert.match(lines[3], /^http:\/\/proxy\.example\/hls\/2\/.+/);
|
||||
assert.match(lines[5], /^http:\/\/proxy\.example\/hls\/2\/.+/);
|
||||
|
||||
const token = lines[3].split('/hls/2/')[1];
|
||||
assert.equal(
|
||||
verifyToken(2, token),
|
||||
'http://upstream.example/live/user/pass/segment1.ts'
|
||||
);
|
||||
|
||||
const keyToken = lines[1].match(/URI="http:\/\/proxy\.example\/hls\/2\/([^"]+)"/)[1];
|
||||
assert.equal(
|
||||
verifyToken(2, keyToken),
|
||||
'http://upstream.example/live/user/pass/keys/key.bin'
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects tampered tokens', () => {
|
||||
const token = signUrl('http://upstream.example/segment.ts', 1);
|
||||
assert.equal(verifyToken(1, token), 'http://upstream.example/segment.ts');
|
||||
assert.equal(verifyToken(2, token), null);
|
||||
const tampered = token.slice(0, -1) + (token.endsWith('a') ? 'b' : 'a');
|
||||
assert.equal(verifyToken(1, tampered), null);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { buildPlaylist } = require('../src/playlist');
|
||||
|
||||
function fakeRequest() {
|
||||
return { headers: { host: 'example.test:8080' }, socket: {} };
|
||||
}
|
||||
|
||||
const event = {
|
||||
key: 'a',
|
||||
title: 'Standard de Liège - SK Beveren',
|
||||
startAt: new Date('2026-11-06T18:25:00Z'),
|
||||
endAt: new Date('2026-11-06T20:55:00Z'),
|
||||
};
|
||||
|
||||
test('playlist always contains three fixed channels', () => {
|
||||
const scheduler = {
|
||||
getSlotCount: () => 3,
|
||||
getSlotEvent: (index) => (index === 0 ? event : null),
|
||||
};
|
||||
const body = buildPlaylist(fakeRequest(), scheduler);
|
||||
const lines = body.trim().split('\n');
|
||||
|
||||
assert.equal(lines[0], '#EXTM3U');
|
||||
assert.equal(
|
||||
lines[1],
|
||||
'#EXTINF:-1 tvg-id="jupiler-1" tvg-name="Jupiler 1",Jupiler 1 | Standard de Liège - SK Beveren (19:25)'
|
||||
);
|
||||
assert.equal(lines[2], 'http://example.test:8080/stream/1');
|
||||
assert.ok(body.includes('Jupiler 2 | Geen wedstrijd'));
|
||||
assert.ok(body.includes('Jupiler 3 | Geen wedstrijd'));
|
||||
assert.ok(body.includes('http://example.test:8080/stream/3'));
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { createState, reconcile, summarize } = require('../src/scheduler');
|
||||
|
||||
const options = { maxChannels: 3, startGraceMs: 24 * 60 * 60 * 1000 };
|
||||
|
||||
function makeEvent(id, startIso, endIso) {
|
||||
return {
|
||||
key: id,
|
||||
title: id,
|
||||
league: 'Jupiler Pro League',
|
||||
language: 'nl',
|
||||
startAt: new Date(startIso),
|
||||
endAt: new Date(endIso),
|
||||
sources: [{ streamId: id, name: id, candidates: [`http://upstream/${id}.ts`] }],
|
||||
missingCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function current(...events) {
|
||||
return new Map(events.map((event) => [event.key, event]));
|
||||
}
|
||||
|
||||
function slotKeys(state) {
|
||||
return state.slots.map((slot) => slot.eventKey);
|
||||
}
|
||||
|
||||
test('fills the first three slots by start time and queues the rest', () => {
|
||||
const state = createState(3);
|
||||
const events = current(
|
||||
makeEvent('A', '2026-11-06T16:00:00Z', '2026-11-06T20:00:00Z'),
|
||||
makeEvent('B', '2026-11-06T16:30:00Z', '2026-11-06T20:30:00Z'),
|
||||
makeEvent('C', '2026-11-06T17:00:00Z', '2026-11-06T21:00:00Z'),
|
||||
makeEvent('D', '2026-11-06T17:30:00Z', '2026-11-06T21:30:00Z'),
|
||||
makeEvent('E', '2026-11-06T18:00:00Z', '2026-11-06T22:00:00Z')
|
||||
);
|
||||
|
||||
reconcile(state, events, Date.parse('2026-11-06T18:00:00Z'), options);
|
||||
|
||||
assert.deepEqual(slotKeys(state), ['A', 'B', 'C']);
|
||||
const summary = summarize(state);
|
||||
assert.deepEqual(summary.queue.map((event) => event.key), ['D', 'E']);
|
||||
});
|
||||
|
||||
test('a freed slot is filled by the oldest waiting event', () => {
|
||||
const state = createState(3);
|
||||
const events = current(
|
||||
makeEvent('A', '2026-11-06T16:00:00Z', '2026-11-06T20:00:00Z'),
|
||||
makeEvent('B', '2026-11-06T16:30:00Z', '2026-11-06T20:30:00Z'),
|
||||
makeEvent('C', '2026-11-06T17:00:00Z', '2026-11-06T21:00:00Z'),
|
||||
makeEvent('D', '2026-11-06T17:30:00Z', '2026-11-06T21:30:00Z'),
|
||||
makeEvent('E', '2026-11-06T18:00:00Z', '2026-11-06T22:00:00Z')
|
||||
);
|
||||
|
||||
reconcile(state, events, Date.parse('2026-11-06T18:00:00Z'), options);
|
||||
assert.deepEqual(slotKeys(state), ['A', 'B', 'C']);
|
||||
|
||||
reconcile(state, events, Date.parse('2026-11-06T20:01:00Z'), options);
|
||||
assert.deepEqual(slotKeys(state), ['D', 'B', 'C']);
|
||||
});
|
||||
|
||||
test('an existing assignment is not preempted by an earlier event', () => {
|
||||
const state = createState(3);
|
||||
const events = current(
|
||||
makeEvent('A', '2026-11-06T16:00:00Z', '2026-11-06T20:00:00Z'),
|
||||
makeEvent('B', '2026-11-06T16:30:00Z', '2026-11-06T20:30:00Z'),
|
||||
makeEvent('C', '2026-11-06T17:00:00Z', '2026-11-06T21:00:00Z')
|
||||
);
|
||||
reconcile(state, events, Date.parse('2026-11-06T18:00:00Z'), options);
|
||||
|
||||
const withEarlier = current(
|
||||
...events.values(),
|
||||
makeEvent('X', '2026-11-06T17:45:00Z', '2026-11-06T21:45:00Z')
|
||||
);
|
||||
reconcile(state, withEarlier, Date.parse('2026-11-06T18:10:00Z'), options);
|
||||
|
||||
assert.deepEqual(slotKeys(state), ['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
test('an assigned event keeps its slot until its end even when it disappears', () => {
|
||||
const state = createState(3);
|
||||
const a = makeEvent('A', '2026-11-06T16:00:00Z', '2026-11-06T20:00:00Z');
|
||||
const b = makeEvent('B', '2026-11-06T16:30:00Z', '2026-11-06T20:30:00Z');
|
||||
reconcile(state, current(a, b), Date.parse('2026-11-06T18:00:00Z'), options);
|
||||
assert.deepEqual(slotKeys(state), ['A', 'B', null]);
|
||||
|
||||
reconcile(state, current(), Date.parse('2026-11-06T19:00:00Z'), options);
|
||||
reconcile(state, current(), Date.parse('2026-11-06T19:30:00Z'), options);
|
||||
assert.deepEqual(slotKeys(state), ['A', 'B', null]);
|
||||
|
||||
reconcile(state, current(), Date.parse('2026-11-06T20:31:00Z'), options);
|
||||
assert.deepEqual(slotKeys(state), [null, null, null]);
|
||||
});
|
||||
|
||||
test('an unassigned event that disappears twice is dropped from the queue', () => {
|
||||
const state = createState(3);
|
||||
const events = current(
|
||||
makeEvent('A', '2026-11-06T16:00:00Z', '2026-11-06T20:00:00Z'),
|
||||
makeEvent('B', '2026-11-06T16:30:00Z', '2026-11-06T20:30:00Z'),
|
||||
makeEvent('C', '2026-11-06T17:00:00Z', '2026-11-06T21:00:00Z'),
|
||||
makeEvent('D', '2026-11-06T17:30:00Z', '2026-11-06T21:30:00Z'),
|
||||
makeEvent('E', '2026-11-06T18:00:00Z', '2026-11-06T22:00:00Z')
|
||||
);
|
||||
|
||||
reconcile(state, events, Date.parse('2026-11-06T18:00:00Z'), options);
|
||||
const withoutD = current(
|
||||
events.get('A'),
|
||||
events.get('B'),
|
||||
events.get('C'),
|
||||
events.get('E')
|
||||
);
|
||||
|
||||
reconcile(state, withoutD, Date.parse('2026-11-06T18:20:00Z'), options);
|
||||
reconcile(state, withoutD, Date.parse('2026-11-06T18:40:00Z'), options);
|
||||
reconcile(state, withoutD, Date.parse('2026-11-06T20:01:00Z'), options);
|
||||
|
||||
assert.deepEqual(slotKeys(state), ['E', 'B', 'C']);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { parseLocalTimestamp, zonedTimeToUtc } = require('../src/time');
|
||||
|
||||
test('parses winter time in Europe/Brussels', () => {
|
||||
const date = parseLocalTimestamp('2026-11-06 19:25:00', 'Europe/Brussels');
|
||||
assert.equal(date.toISOString(), '2026-11-06T18:25:00.000Z');
|
||||
});
|
||||
|
||||
test('parses summer time in Europe/Brussels', () => {
|
||||
const date = parseLocalTimestamp('2026-07-01 19:25:00', 'Europe/Brussels');
|
||||
assert.equal(date.toISOString(), '2026-07-01T17:25:00.000Z');
|
||||
});
|
||||
|
||||
test('zonedTimeToUtc handles UTC', () => {
|
||||
const date = zonedTimeToUtc(2026, 11, 6, 19, 25, 0, 'UTC');
|
||||
assert.equal(date.toISOString(), '2026-11-06T19:25:00.000Z');
|
||||
});
|
||||
|
||||
test('rejects invalid timestamps', () => {
|
||||
assert.equal(parseLocalTimestamp('not-a-date', 'Europe/Brussels'), null);
|
||||
assert.equal(parseLocalTimestamp('2026-13-06 19:25:00', 'Europe/Brussels'), null);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const { settings } = require('../src/settings');
|
||||
const {
|
||||
fetchLiveStreams,
|
||||
liveUrlCandidates,
|
||||
parseM3u,
|
||||
resolveFallbackUrls,
|
||||
resetM3uCache,
|
||||
} = require('../src/xtream-client');
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
}
|
||||
|
||||
function close(server) {
|
||||
return new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
test('fetches live streams from player_api and resolves get.php fallback URLs', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url.startsWith('/player_api.php')) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(
|
||||
JSON.stringify([
|
||||
{
|
||||
stream_id: 101,
|
||||
name: 'DAZN 01 [A - B | Jupiler Pro League (nl) (2026-11-06 19:25:00)]',
|
||||
},
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (req.url.startsWith('/get.php')) {
|
||||
res.setHeader('Content-Type', 'application/x-mpegurl');
|
||||
res.end(
|
||||
[
|
||||
'#EXTM3U',
|
||||
'#EXTINF:-1 tvg-id="dazn1" tvg-name="DAZN 01",DAZN 01 [A - B | Jupiler Pro League (nl) (2026-11-06 19:25:00)]',
|
||||
'http://cdn.example/live/u/p/101.ts',
|
||||
].join('\n')
|
||||
);
|
||||
return;
|
||||
}
|
||||
res.statusCode = 404;
|
||||
res.end('nope');
|
||||
});
|
||||
|
||||
await listen(server);
|
||||
const port = server.address().port;
|
||||
const previous = { ...settings };
|
||||
settings.xtreamUrl = `http://127.0.0.1:${port}`;
|
||||
settings.xtreamUsername = 'user';
|
||||
settings.xtreamPassword = 'pass';
|
||||
resetM3uCache();
|
||||
|
||||
try {
|
||||
const streams = await fetchLiveStreams();
|
||||
assert.equal(streams.length, 1);
|
||||
assert.equal(streams[0].stream_id, 101);
|
||||
|
||||
const urls = liveUrlCandidates(101);
|
||||
assert.ok(urls.includes('http://127.0.0.1:' + port + '/live/user/pass/101.ts'));
|
||||
|
||||
const fallbacks = await resolveFallbackUrls(101, 'DAZN 01');
|
||||
assert.deepEqual(fallbacks, ['http://cdn.example/live/u/p/101.ts']);
|
||||
} finally {
|
||||
Object.assign(settings, previous);
|
||||
resetM3uCache();
|
||||
await close(server);
|
||||
}
|
||||
});
|
||||
|
||||
test('parses M3U entries by stream id and tvg-name', () => {
|
||||
const index = parseM3u(
|
||||
[
|
||||
'#EXTM3U',
|
||||
'#EXTINF:-1 tvg-id="x" tvg-name="DAZN 01 [A - B | Jupiler Pro League (nl) (2026-11-06 19:25:00)]",display',
|
||||
'http://cdn.example/live/u/p/55.m3u8?token=1',
|
||||
].join('\n')
|
||||
);
|
||||
assert.equal(index.byStreamId.get('55'), 'http://cdn.example/live/u/p/55.m3u8?token=1');
|
||||
assert.equal(
|
||||
index.byName.get('dazn 01 [a - b | jupiler pro league (nl) (2026-11-06 19:25:00)]'),
|
||||
'http://cdn.example/live/u/p/55.m3u8?token=1'
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user