mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(m3u): parse playlists with URLs longer than 2084 characters
Pluto TV style playlists (issue #1189) embed a session JWT in every stream URL (~2200 chars). validator.isURL inside iptv-playlist-parser rejected anything over its IE-era 2084-char default, and the parser's stalled item index then collapsed the whole playlist into a single channel. Sync the 4gray/iptv-playlist-parser fork with upstream v0.15.2, which removes URL validation entirely and adds an explicit branch so '#' comments and unknown directives are never treated as URLs. Two fork deltas are preserved on top: the radio attribute (radio player detection) and pipe stripping (item.url is cut at the first '|' while |User-Agent=/|Referer= params still land in item.http). The now-dead validator/is-valid-path dependencies are dropped from the fork. - pin iptv-playlist-parser to the fork commit SHA - add a parser contract spec guarding long URLs, comment handling, radio, pipe stripping, and header EPG attrs - document the parser fork contract in the M3U architecture doc Fixes #1189 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8fdac824fd
commit
e22f3ed5fa
5 files changed
+122
-49
No files matched your search
@@ -0,0 +1,100 @@
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
|
||||
/**
|
||||
* Contract tests for the 4gray/iptv-playlist-parser fork (jest maps the
|
||||
* module to the real parser source via test-stubs/iptv-playlist-parser.mjs).
|
||||
* Guards the fork-specific behaviors iptvnator depends on:
|
||||
* - no URL length/format validation (issue #1189: Pluto JWT URLs > 2084 chars)
|
||||
* - '#' comments never become URLs and never shift the item index
|
||||
* - the fork-only `radio` attribute survives upstream syncs
|
||||
* - `url` is stripped at the first '|' while pipe params land in `http.*`
|
||||
*/
|
||||
describe('iptv-playlist-parser contract (4gray fork)', () => {
|
||||
const plutoUrl = (id: string) =>
|
||||
`https://cfd-v4-service-channel-stitcher-use1-1.prd.pluto.tv/v2/stitch/hls/channel/${id}/master.m3u8?jwt=${'e'.repeat(2100)}&masterJWTPassthrough=true`;
|
||||
|
||||
it('parses playlists whose URLs are longer than 2084 characters (#1189)', () => {
|
||||
const playlist = [
|
||||
'#EXTM3U',
|
||||
'#EXTINF:-1 group-title="Anime & Geek" tvg-id="one" tvg-name="Beyblade",Beyblade',
|
||||
plutoUrl('one'),
|
||||
'#EXTINF:-1 group-title="TV Brasileira" tvg-id="two" tvg-name="TV Cultura",TV Cultura',
|
||||
plutoUrl('two'),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const result = parse(playlist);
|
||||
|
||||
expect(result.items.length).toBe(2);
|
||||
expect(result.items[0].name).toBe('Beyblade');
|
||||
expect(result.items[0].url).toBe(plutoUrl('one'));
|
||||
expect(result.items[1].name).toBe('TV Cultura');
|
||||
expect(result.items[1].url).toBe(plutoUrl('two'));
|
||||
});
|
||||
|
||||
it('ignores comments and unknown directives between #EXTINF and the URL', () => {
|
||||
const playlist = [
|
||||
'#EXTM3U',
|
||||
'#EXTINF:-1 tvg-id="one",Channel One',
|
||||
'# stray comment',
|
||||
'#EXT-X-SESSION-DATA:DATA-ID="com.example",VALUE="x"',
|
||||
'http://example.com/one.m3u8',
|
||||
'#EXTINF:-1 tvg-id="two",Channel Two',
|
||||
'http://example.com/two.m3u8',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const result = parse(playlist);
|
||||
|
||||
expect(result.items.length).toBe(2);
|
||||
expect(result.items[0].url).toBe('http://example.com/one.m3u8');
|
||||
expect(result.items[0].raw).toContain('# stray comment');
|
||||
expect(result.items[1].url).toBe('http://example.com/two.m3u8');
|
||||
});
|
||||
|
||||
it('parses the radio attribute used by the radio player', () => {
|
||||
const playlist = [
|
||||
'#EXTM3U',
|
||||
'#EXTINF:-1 radio="true" tvg-id="r1",Radio One',
|
||||
'http://example.com/radio1',
|
||||
'#EXTINF:-1 tvg-id="tv1",TV One',
|
||||
'http://example.com/tv1',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const result = parse(playlist);
|
||||
|
||||
expect(result.items[0].radio).toBe('true');
|
||||
expect(result.items[1].radio).toBe('');
|
||||
});
|
||||
|
||||
it('strips pipe options from url while keeping them in http.*', () => {
|
||||
const playlist = [
|
||||
'#EXTM3U',
|
||||
'#EXTINF:-1,Piped',
|
||||
'http://example.com/stream.ts|User-Agent=CustomUA&Referer=http://ref.example',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const result = parse(playlist);
|
||||
|
||||
expect(result.items[0].url).toBe('http://example.com/stream.ts');
|
||||
expect(result.items[0].http['user-agent']).toBe('CustomUA');
|
||||
expect(result.items[0].http.referrer).toBe('http://ref.example');
|
||||
});
|
||||
|
||||
it('exposes EPG urls from the playlist header', () => {
|
||||
const result = parse(
|
||||
[
|
||||
'#EXTM3U x-tvg-url="https://epg.example/guide.xml"',
|
||||
'#EXTINF:-1,One',
|
||||
'http://example.com/one.m3u8',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
expect(result.header.attrs['x-tvg-url']).toBe(
|
||||
'https://epg.example/guide.xml'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,19 @@ The M3U playlist module provides:
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## M3U Parsing (`iptv-playlist-parser` fork)
|
||||
|
||||
All four parse call sites (Electron `playlist-source.ts` import, `playlist-refresh.worker.ts`, `web-backend` `/parse`, PWA `playlists.service.ts`) use the
|
||||
[4gray/iptv-playlist-parser](https://github.com/4gray/iptv-playlist-parser) fork, pinned by commit SHA in `package.json`. The fork tracks upstream
|
||||
`freearhey/iptv-playlist-parser` (currently synced to v0.15.2) plus two deliberate deltas iptvnator depends on:
|
||||
|
||||
- **`radio` attribute** — `item.radio` (string, `'true'` triggers the radio player, EPG suppression, and external-player gating app-wide). Upstream does not have this field; it must survive every upstream sync.
|
||||
- **Pipe stripping** — `item.url` is cut at the first `|`; `|User-Agent=` / `|Referer=` params still land in `item.http`. Upstream 0.15.0 stopped stripping, but iptvnator consumes `item.url` verbatim in hls.js/mpv/vlc, catch-up URL building, and url-keyed favorites.
|
||||
|
||||
There is intentionally **no URL validation** (upstream removed it in 0.15.0): any non-empty non-`#` line after `#EXTINF` becomes the item URL. This is what fixes issue #1189 (Pluto TV JWT URLs longer than validator's 2084-char IE-era limit used to be rejected, and the stalled item index collapsed the whole playlist into one channel). `#` comment lines and unknown directives are appended to `item.raw` and never treated as URLs.
|
||||
|
||||
The behavioral contract is guarded by `apps/web/src/app/iptv-playlist-parser.contract.spec.ts` (jest maps the module to the real parser source) and by the fork's own test suite.
|
||||
|
||||
## State Management (libs/m3u-state/)
|
||||
|
||||
### State Structure
|
||||
|
||||
@@ -898,9 +898,8 @@ export class PlaylistsService {
|
||||
path?: string
|
||||
) {
|
||||
try {
|
||||
// Dynamic import keeps the ~130KB validator dep (transitively pulled
|
||||
// by iptv-playlist-parser) out of the eager bundle. parse() only runs
|
||||
// on user-triggered imports.
|
||||
// Dynamic import keeps the parser out of the eager bundle;
|
||||
// parse() only runs on user-triggered imports.
|
||||
const parserModule = await import('iptv-playlist-parser');
|
||||
const parse = resolvePlaylistParser(parserModule);
|
||||
const parsedPlaylist = parse(playlist);
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@
|
||||
"epg-parser": "^0.1.6",
|
||||
"fix-path": "5.0.0",
|
||||
"hls.js": "1.6.13",
|
||||
"iptv-playlist-parser": "github:4gray/iptv-playlist-parser",
|
||||
"iptv-playlist-parser": "github:4gray/iptv-playlist-parser#1b5cf3bd6131a07f57c339c4b74bfc4985b8adb3",
|
||||
"marked": "18.0.5",
|
||||
"mpegts.js": "1.8.0",
|
||||
"ms": "2.1.3",
|
||||
|
||||
Generated
+6
-45
@@ -126,8 +126,8 @@ importers:
|
||||
specifier: 1.6.13
|
||||
version: 1.6.13
|
||||
iptv-playlist-parser:
|
||||
specifier: github:4gray/iptv-playlist-parser
|
||||
version: https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/38a62d0b7c98f442bfa102ee8e1fe14169ae9386
|
||||
specifier: github:4gray/iptv-playlist-parser#1b5cf3bd6131a07f57c339c4b74bfc4985b8adb3
|
||||
version: https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/1b5cf3bd6131a07f57c339c4b74bfc4985b8adb3
|
||||
marked:
|
||||
specifier: 18.0.5
|
||||
version: 18.0.5
|
||||
@@ -7594,9 +7594,9 @@ packages:
|
||||
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
iptv-playlist-parser@https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/38a62d0b7c98f442bfa102ee8e1fe14169ae9386:
|
||||
resolution: {tarball: https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/38a62d0b7c98f442bfa102ee8e1fe14169ae9386}
|
||||
version: 0.12.2
|
||||
iptv-playlist-parser@https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/1b5cf3bd6131a07f57c339c4b74bfc4985b8adb3:
|
||||
resolution: {tarball: https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/1b5cf3bd6131a07f57c339c4b74bfc4985b8adb3}
|
||||
version: 0.15.2
|
||||
|
||||
iron-webcrypto@1.2.1:
|
||||
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
|
||||
@@ -7663,10 +7663,6 @@ packages:
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
hasBin: true
|
||||
|
||||
is-extglob@1.0.0:
|
||||
resolution: {integrity: sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7694,10 +7690,6 @@ packages:
|
||||
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-glob@2.0.1:
|
||||
resolution: {integrity: sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7718,10 +7710,6 @@ packages:
|
||||
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-invalid-path@0.1.0:
|
||||
resolution: {integrity: sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-lambda@1.0.1:
|
||||
resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
|
||||
|
||||
@@ -7819,10 +7807,6 @@ packages:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-valid-path@0.1.1:
|
||||
resolution: {integrity: sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-weakmap@2.0.2:
|
||||
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -11544,10 +11528,6 @@ packages:
|
||||
resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
validator@13.15.26:
|
||||
resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
varint@6.0.0:
|
||||
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
|
||||
|
||||
@@ -20610,10 +20590,7 @@ snapshots:
|
||||
|
||||
ipaddr.js@2.3.0: {}
|
||||
|
||||
iptv-playlist-parser@https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/38a62d0b7c98f442bfa102ee8e1fe14169ae9386:
|
||||
dependencies:
|
||||
is-valid-path: 0.1.1
|
||||
validator: 13.15.26
|
||||
iptv-playlist-parser@https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/1b5cf3bd6131a07f57c339c4b74bfc4985b8adb3: {}
|
||||
|
||||
iron-webcrypto@1.2.1: {}
|
||||
|
||||
@@ -20680,8 +20657,6 @@ snapshots:
|
||||
|
||||
is-docker@3.0.0: {}
|
||||
|
||||
is-extglob@1.0.0: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-finalizationregistry@1.1.1:
|
||||
@@ -20706,10 +20681,6 @@ snapshots:
|
||||
has-tostringtag: 1.0.2
|
||||
safe-regex-test: 1.1.0
|
||||
|
||||
is-glob@2.0.1:
|
||||
dependencies:
|
||||
is-extglob: 1.0.0
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
@@ -20724,10 +20695,6 @@ snapshots:
|
||||
|
||||
is-interactive@2.0.0: {}
|
||||
|
||||
is-invalid-path@0.1.0:
|
||||
dependencies:
|
||||
is-glob: 2.0.1
|
||||
|
||||
is-lambda@1.0.1: {}
|
||||
|
||||
is-map@2.0.3: {}
|
||||
@@ -20799,10 +20766,6 @@ snapshots:
|
||||
|
||||
is-unicode-supported@2.1.0: {}
|
||||
|
||||
is-valid-path@0.1.1:
|
||||
dependencies:
|
||||
is-invalid-path: 0.1.0
|
||||
|
||||
is-weakmap@2.0.2: {}
|
||||
|
||||
is-weakref@1.1.1:
|
||||
@@ -25378,8 +25341,6 @@ snapshots:
|
||||
|
||||
validate-npm-package-name@7.0.2: {}
|
||||
|
||||
validator@13.15.26: {}
|
||||
|
||||
varint@6.0.0: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
Reference in new issue
Block a user