From aafd79f24264f7e14f6880281e0de849a21ccf80 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:12:23 +0300 Subject: [PATCH 1/9] Update tests/__data__ --- .../__data__/expected/playlist_update/fr.m3u | 2 +- .../__data__/expected/playlist_update/uk.m3u | 2 +- .../__data__/expected/playlist_update/us.m3u | 2 +- .../input/playlist_update/playlist.mjs | 111 ++++++++++++++++++ .../playlist_update/{ => streams}/br.m3u | 0 .../{ => streams}/br_example.m3u | 0 .../playlist_update/{ => streams}/bz.m3u | 0 .../playlist_update/{ => streams}/cy.m3u | 0 .../playlist_update/{ => streams}/uk.m3u | 0 9 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 tests/__data__/input/playlist_update/playlist.mjs rename tests/__data__/input/playlist_update/{ => streams}/br.m3u (100%) rename tests/__data__/input/playlist_update/{ => streams}/br_example.m3u (100%) rename tests/__data__/input/playlist_update/{ => streams}/bz.m3u (100%) rename tests/__data__/input/playlist_update/{ => streams}/cy.m3u (100%) rename tests/__data__/input/playlist_update/{ => streams}/uk.m3u (100%) diff --git a/tests/__data__/expected/playlist_update/fr.m3u b/tests/__data__/expected/playlist_update/fr.m3u index 08556c744c..d09c920ed6 100644 --- a/tests/__data__/expected/playlist_update/fr.m3u +++ b/tests/__data__/expected/playlist_update/fr.m3u @@ -1,5 +1,5 @@ #EXTM3U -#EXTINF:-1 tvg-id="TFX.fr@SD",TFX +#EXTINF:-1 tvg-id="TFX.fr@SD",TFX (1080p) #EXTVLCOPT:http-referrer=https://pkpakiplay.xyz/ #EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 https://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.m3u8?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY diff --git a/tests/__data__/expected/playlist_update/uk.m3u b/tests/__data__/expected/playlist_update/uk.m3u index 2333877803..9caa57c157 100644 --- a/tests/__data__/expected/playlist_update/uk.m3u +++ b/tests/__data__/expected/playlist_update/uk.m3u @@ -3,5 +3,5 @@ http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 #EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 -#EXTINF:-1 tvg-id="BeanoTV.uk@SD",Beano TV +#EXTINF:-1 tvg-id="BeanoTV.uk@SD",Beano TV (1080p) https://a5b4bacecd47433dad06d3189fc7422e.mediatailor.us-east-1.amazonaws.com/v1/manifest/04fd913bb278d8775298c26fdca9d9841f37601f/RakutenTV-eu_BeanoTV/b1f233d5-847c-437d-aa4f-f73e67a85323/2.m3u8|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"|Origin="https://origin.xyz" diff --git a/tests/__data__/expected/playlist_update/us.m3u b/tests/__data__/expected/playlist_update/us.m3u index d16ed56287..4c76334ffd 100644 --- a/tests/__data__/expected/playlist_update/us.m3u +++ b/tests/__data__/expected/playlist_update/us.m3u @@ -1,3 +1,3 @@ #EXTM3U -#EXTINF:-1 tvg-id="FastTV.us@SD",Fast TV +#EXTINF:-1 tvg-id="FastTV.us@SD",Fast TV (1080p) https://3fa797d5.wurl.com/manifest/f36d25e7e52f1ba8d7e56eb859c636563214f541/T05PX01vdG9yVHJlbmRGYXN0VFZfSExT/b5e5e0e2-12b3-4312-93c9-c0a7c50b41ca/4.m3u8 diff --git a/tests/__data__/input/playlist_update/playlist.mjs b/tests/__data__/input/playlist_update/playlist.mjs new file mode 100644 index 0000000000..2f1d74c465 --- /dev/null +++ b/tests/__data__/input/playlist_update/playlist.mjs @@ -0,0 +1,111 @@ +export default { + type: 'playlist', + isMasterPlaylist: true, + independentSegments: true, + source: + '#EXTM3U\n## Generated with https://github.com/shaka-project/shaka-packager version v3.4.2-c819dea-release\n\n#EXT-X-INDEPENDENT-SEGMENTS\n\n#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="pl",NAME="stream_4",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2"\n\n#EXT-X-STREAM-INF:BANDWIDTH=3688787,AVERAGE-BANDWIDTH=8948756,CODECS="avc1.424028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_0.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=1988787,AVERAGE-BANDWIDTH=3688345,CODECS="avc1.42401f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_1.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=1188787,AVERAGE-BANDWIDTH=1774276,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=853x480,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_2.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=688787,AVERAGE-BANDWIDTH=766090,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_3.m3u8\n', + variants: [ + { + uri: 'stream_0.m3u8', + isIFrameOnly: false, + bandwidth: 3688787, + averageBandwidth: 8948756, + codecs: 'avc1.424028,mp4a.40.2', + resolution: { width: 1920, height: 1080 }, + frameRate: 25, + audio: [ + { + type: 'AUDIO', + uri: 'stream_4.m3u8', + groupId: 'default-audio-group', + language: 'pl', + name: 'stream_4', + isDefault: false, + autoselect: true, + channels: '2' + } + ], + video: [], + subtitles: [], + closedCaptions: [], + currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } + }, + { + uri: 'stream_1.m3u8', + isIFrameOnly: false, + bandwidth: 1988787, + averageBandwidth: 3688345, + codecs: 'avc1.42401f,mp4a.40.2', + resolution: { width: 1280, height: 720 }, + frameRate: 25, + audio: [ + { + type: 'AUDIO', + uri: 'stream_4.m3u8', + groupId: 'default-audio-group', + language: 'pl', + name: 'stream_4', + isDefault: false, + autoselect: true, + channels: '2' + } + ], + video: [], + subtitles: [], + closedCaptions: [], + currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } + }, + { + uri: 'stream_2.m3u8', + isIFrameOnly: false, + bandwidth: 1188787, + averageBandwidth: 1774276, + codecs: 'avc1.42401e,mp4a.40.2', + resolution: { width: 853, height: 480 }, + frameRate: 25, + audio: [ + { + type: 'AUDIO', + uri: 'stream_4.m3u8', + groupId: 'default-audio-group', + language: 'pl', + name: 'stream_4', + isDefault: false, + autoselect: true, + channels: '2' + } + ], + video: [], + subtitles: [], + closedCaptions: [], + currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } + }, + { + uri: 'stream_3.m3u8', + isIFrameOnly: false, + bandwidth: 688787, + averageBandwidth: 766090, + codecs: 'avc1.42401e,mp4a.40.2', + resolution: { width: 640, height: 360 }, + frameRate: 25, + audio: [ + { + type: 'AUDIO', + uri: 'stream_4.m3u8', + groupId: 'default-audio-group', + language: 'pl', + name: 'stream_4', + isDefault: false, + autoselect: true, + channels: '2' + } + ], + video: [], + subtitles: [], + closedCaptions: [], + currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } + } + ], + sessionDataList: [], + sessionKeyList: [] +} diff --git a/tests/__data__/input/playlist_update/br.m3u b/tests/__data__/input/playlist_update/streams/br.m3u similarity index 100% rename from tests/__data__/input/playlist_update/br.m3u rename to tests/__data__/input/playlist_update/streams/br.m3u diff --git a/tests/__data__/input/playlist_update/br_example.m3u b/tests/__data__/input/playlist_update/streams/br_example.m3u similarity index 100% rename from tests/__data__/input/playlist_update/br_example.m3u rename to tests/__data__/input/playlist_update/streams/br_example.m3u diff --git a/tests/__data__/input/playlist_update/bz.m3u b/tests/__data__/input/playlist_update/streams/bz.m3u similarity index 100% rename from tests/__data__/input/playlist_update/bz.m3u rename to tests/__data__/input/playlist_update/streams/bz.m3u diff --git a/tests/__data__/input/playlist_update/cy.m3u b/tests/__data__/input/playlist_update/streams/cy.m3u similarity index 100% rename from tests/__data__/input/playlist_update/cy.m3u rename to tests/__data__/input/playlist_update/streams/cy.m3u diff --git a/tests/__data__/input/playlist_update/uk.m3u b/tests/__data__/input/playlist_update/streams/uk.m3u similarity index 100% rename from tests/__data__/input/playlist_update/uk.m3u rename to tests/__data__/input/playlist_update/streams/uk.m3u From 39ca336efd9a1622ff33bc8e000e16cc33e681a9 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:12:28 +0300 Subject: [PATCH 2/9] Update update.test.ts --- tests/commands/playlist/update.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/playlist/update.test.ts b/tests/commands/playlist/update.test.ts index d8552e3d25..8e4cf99e01 100644 --- a/tests/commands/playlist/update.test.ts +++ b/tests/commands/playlist/update.test.ts @@ -8,7 +8,7 @@ const ENV_VAR = beforeEach(() => { fs.emptyDirSync('tests/__data__/output') - fs.copySync('tests/__data__/input/playlist_update', 'tests/__data__/output/streams') + fs.copySync('tests/__data__/input/playlist_update/streams', 'tests/__data__/output/streams') }) describe('playlist:update', () => { From c4f2d20ca4991cd3ef277faa317e1c2c016332fc Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:13:01 +0300 Subject: [PATCH 3/9] Install new dependencies --- package-lock.json | 25 ++++++++++++++++++++++--- package.json | 2 ++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1606c334e6..2386f566bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,9 +36,11 @@ "commander": "^14.0.0", "console-table-printer": "^2.14.6", "cross-env": "^10.0.0", + "es-toolkit": "^1.45.1", "eslint": "^9.32.0", "glob": "^11.0.3", "globals": "^16.3.0", + "hls-parser": "^0.16.0", "iptv-playlist-parser": "^0.15.1", "jest": "^30.0.5", "jest-expect-message": "^1.1.3", @@ -4204,6 +4206,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", @@ -4635,9 +4647,10 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -4979,6 +4992,12 @@ "node": ">= 0.4" } }, + "node_modules/hls-parser": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/hls-parser/-/hls-parser-0.16.0.tgz", + "integrity": "sha512-jb8tX36Nn49MtvSBTWcQjD4ktwJtzrfOlnI8zpo4zQjTq0ZGmG0RsoCQXwvKQGvt4CBFrs8Q3znq0HoNCpt5BA==", + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", diff --git a/package.json b/package.json index 7b642c0309..245df23164 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,11 @@ "commander": "^14.0.0", "console-table-printer": "^2.14.6", "cross-env": "^10.0.0", + "es-toolkit": "^1.45.1", "eslint": "^9.32.0", "glob": "^11.0.3", "globals": "^16.3.0", + "hls-parser": "^0.16.0", "iptv-playlist-parser": "^0.15.1", "jest": "^30.0.5", "jest-expect-message": "^1.1.3", From 273816b9a34f3f1fbbb4f10b9f1373580eac6a61 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:13:15 +0300 Subject: [PATCH 4/9] Update utils.ts --- scripts/utils.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/utils.ts b/scripts/utils.ts index ecce8da58b..0d27fe79d2 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,4 +1,9 @@ +import { MasterPlaylist, MediaPlaylist, Variant } from 'hls-parser/types' +import { parse as parsePlaylist } from 'hls-parser' +import { TESTING } from './constants.js' import normalizeUrl from 'normalize-url' +import { orderBy } from 'es-toolkit' +import axios from 'axios' export function isURI(string: string): boolean { try { @@ -21,3 +26,33 @@ export function truncate(string: string, limit: number = 100) { return string.slice(0, limit - 3) + '...' } + +export async function getStreamInfo( + url: string, + options: { httpUserAgent: string | null; httpReferrer: string | null } +): Promise { + let playlist: MasterPlaylist | MediaPlaylist | undefined + + if (TESTING) { + playlist = (await import('../tests/__data__/input/playlist_update/playlist.mjs')) + .default as unknown as MasterPlaylist + } else { + try { + const response = await axios(url, { + signal: AbortSignal.timeout(30000), + headers: { + 'User-Agent': options.httpUserAgent || 'Mozilla/5.0', + Referer: options.httpReferrer + } + }) + + playlist = parsePlaylist(response.data) + } catch {} + } + + if (playlist && playlist.isMasterPlaylist && playlist.variants.length) { + return orderBy(playlist.variants, ['bandwidth'], ['desc'])[0] + } + + return undefined +} From 40a9d2903e365e396d5a8ebca45bf11d7c66d0f0 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:13:25 +0300 Subject: [PATCH 5/9] Update update.ts --- scripts/commands/playlist/update.ts | 30 +++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/scripts/commands/playlist/update.ts b/scripts/commands/playlist/update.ts index 1c5f17ee33..38dd69b7e0 100644 --- a/scripts/commands/playlist/update.ts +++ b/scripts/commands/playlist/update.ts @@ -2,10 +2,10 @@ import { IssueLoader, PlaylistParser } from '../../core' import { Playlist, Issue, Stream } from '../../models' import { loadData, data as apiData } from '../../api' import { Logger, Collection } from '@freearhey/core' +import { isURI, getStreamInfo } from '../../utils' import { Storage } from '@freearhey/storage-js' import { STREAMS_DIR } from '../../constants' import * as sdk from '@iptv-org/sdk' -import { isURI } from '../../utils' const processedIssues = new Collection() @@ -136,24 +136,38 @@ async function addStreams({ const requests = issues.filter( issue => issue.labels.includes('streams:add') && issue.labels.includes('approved') ) - requests.forEach((issue: Issue) => { + + for (const issue of requests.all()) { const data = issue.data - if (data.missing('stream_id') || data.missing('stream_url')) return - if (streams.includes((_stream: Stream) => _stream.url === data.getString('stream_url'))) return + if (data.missing('stream_id') || data.missing('stream_url')) continue + if (streams.includes((_stream: Stream) => _stream.url === data.getString('stream_url'))) + continue const streamUrl = data.getString('stream_url') || '' - if (!isURI(streamUrl)) return + if (!isURI(streamUrl)) continue const streamId = data.getString('stream_id') || '' const [channelId, feedId] = streamId.split('@') const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId) - if (!channel) return + if (!channel) continue const label = data.getString('label') || '' - const quality = data.getString('quality') || null const httpUserAgent = data.getString('http_user_agent') || null const httpReferrer = data.getString('http_referrer') || null + let quality = data.getString('quality') || null + if (!quality) { + const streamInfo = await getStreamInfo(streamUrl, { httpUserAgent, httpReferrer }) + + if (streamInfo) { + const height = streamInfo?.resolution?.height + + if (height) { + quality = `${height}p` + } + } + } + const stream = new Stream({ channel: channelId, feed: feedId, @@ -169,5 +183,5 @@ async function addStreams({ streams.add(stream) processedIssues.add(issue.number) - }) + } } From aa5125890a396adc21aa84be293149be37c848d6 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:15:22 +0300 Subject: [PATCH 6/9] Update tests/__data__ --- .../__data__/expected/playlist_format/at.m3u | 5 + .../__data__/expected/playlist_format/in.m3u | 2 +- .../__data__/expected/playlist_format/nl.m3u | 4 +- .../__data__/expected/playlist_update/fr.m3u | 4 +- tests/__data__/input/issues.js | 2 +- tests/__data__/input/playlist_format/at.m3u | 5 + tests/__data__/input/playlist_format/in.m3u | 2 +- .../input/playlist_update/manifest.mpd | 45 +++++++ .../input/playlist_update/playlist.m3u8 | 12 ++ .../input/playlist_update/playlist.mjs | 111 ------------------ 10 files changed, 74 insertions(+), 118 deletions(-) create mode 100644 tests/__data__/expected/playlist_format/at.m3u create mode 100644 tests/__data__/input/playlist_format/at.m3u create mode 100644 tests/__data__/input/playlist_update/manifest.mpd create mode 100644 tests/__data__/input/playlist_update/playlist.m3u8 delete mode 100644 tests/__data__/input/playlist_update/playlist.mjs diff --git a/tests/__data__/expected/playlist_format/at.m3u b/tests/__data__/expected/playlist_format/at.m3u new file mode 100644 index 0000000000..171c8c8b13 --- /dev/null +++ b/tests/__data__/expected/playlist_format/at.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="",Hitradio O3 (1080p) +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/master.m3u8 +#EXTINF:-1 tvg-id="",Hitradio O3 (720p) +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/manifest.mpd diff --git a/tests/__data__/expected/playlist_format/in.m3u b/tests/__data__/expected/playlist_format/in.m3u index f733f449a4..324b076d47 100644 --- a/tests/__data__/expected/playlist_format/in.m3u +++ b/tests/__data__/expected/playlist_format/in.m3u @@ -1,5 +1,5 @@ #EXTM3U -#EXTINF:-1 tvg-id="",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] +#EXTINF:-1 tvg-id="",Manorama News -2 (1080p) [U3] (Alt) [Geo-blocked] [Not 24/7] #EXTVLCOPT:http-referrer=http://test.com #EXTVLCOPT:http-user-agent=Mozilla/5.0 https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 diff --git a/tests/__data__/expected/playlist_format/nl.m3u b/tests/__data__/expected/playlist_format/nl.m3u index b9d1f9e4cf..d9aea26043 100644 --- a/tests/__data__/expected/playlist_format/nl.m3u +++ b/tests/__data__/expected/playlist_format/nl.m3u @@ -1,11 +1,11 @@ #EXTM3U #EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked] http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8 +#EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 (1080p) [Geo-blocked] +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 #EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 (342p) http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 #EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 (302p) [Geo-blocked] #EXTVLCOPT:http-referrer=http://imn.iq #EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8?|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0+(iPhone;+CPU+iPhone+OS+17_7+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/18.0+Mobile/15E148+Safari/604.1"|Origin="https://origin.xyz" -#EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 [Geo-blocked] -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 diff --git a/tests/__data__/expected/playlist_update/fr.m3u b/tests/__data__/expected/playlist_update/fr.m3u index d09c920ed6..12427bf56b 100644 --- a/tests/__data__/expected/playlist_update/fr.m3u +++ b/tests/__data__/expected/playlist_update/fr.m3u @@ -1,5 +1,5 @@ #EXTM3U -#EXTINF:-1 tvg-id="TFX.fr@SD",TFX (1080p) +#EXTINF:-1 tvg-id="TFX.fr@SD",TFX (720p) #EXTVLCOPT:http-referrer=https://pkpakiplay.xyz/ #EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 -https://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.m3u8?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY +https://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.mpd?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY diff --git a/tests/__data__/input/issues.js b/tests/__data__/input/issues.js index 82d14a73fa..206a5571a7 100644 --- a/tests/__data__/input/issues.js +++ b/tests/__data__/input/issues.js @@ -538,7 +538,7 @@ module.exports = [ closed_at: null, author_association: 'COLLABORATOR', active_lock_reason: null, - body: '### Stream ID\n\nTFX.fr\n\n### Stream URL\n\nhttps://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.m3u8?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY\n\n### Label\n\nNone\n\n### HTTP User Agent\n\nMozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1\n\n### HTTP Referrer\n\nhttps://pkpakiplay.xyz/\n\n### Notes (optional)\n\nSource: https://github.com/iptv-org/iptv-org.github.io/issues/1381\n\n### Contributing Guide\n\n- [X] I have read [Contributing Guide](https://github.com/iptv-org/iptv/blob/master/CONTRIBUTING.md)', + body: '### Stream ID\n\nTFX.fr\n\n### Stream URL\n\nhttps://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.mpd?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY\n\n### Label\n\nNone\n\n### HTTP User Agent\n\nMozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1\n\n### HTTP Referrer\n\nhttps://pkpakiplay.xyz/\n\n### Notes (optional)\n\nSource: https://github.com/iptv-org/iptv-org.github.io/issues/1381\n\n### Contributing Guide\n\n- [X] I have read [Contributing Guide](https://github.com/iptv-org/iptv/blob/master/CONTRIBUTING.md)', reactions: { url: 'https://api.github.com/repos/iptv-org/iptv/issues/14175/reactions', total_count: 0, diff --git a/tests/__data__/input/playlist_format/at.m3u b/tests/__data__/input/playlist_format/at.m3u new file mode 100644 index 0000000000..9436c2c875 --- /dev/null +++ b/tests/__data__/input/playlist_format/at.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="HitradioO3.at@SD",Hitradio O3 +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/manifest.mpd +#EXTINF:-1 tvg-id="HitradioO3.at@SD",Hitradio O3 +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/master.m3u8 \ No newline at end of file diff --git a/tests/__data__/input/playlist_format/in.m3u b/tests/__data__/input/playlist_format/in.m3u index 0a013ba7a1..fda7a41b14 100644 --- a/tests/__data__/input/playlist_format/in.m3u +++ b/tests/__data__/input/playlist_format/in.m3u @@ -1,3 +1,3 @@ #EXTM3U -#EXTINF:-1 tvg-id="mn.in" http-referrer="http://test.com" http-user-agent="Mozilla/5.0",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] +#EXTINF:-1 tvg-id="mn.in" http-referrer="http://test.com" http-user-agent="Mozilla/5.0",Manorama News -2 [U3] (Alt) [Geo-blocked] [Not 24/7] https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 diff --git a/tests/__data__/input/playlist_update/manifest.mpd b/tests/__data__/input/playlist_update/manifest.mpd new file mode 100644 index 0000000000..80aded62a5 --- /dev/null +++ b/tests/__data__/input/playlist_update/manifest.mpd @@ -0,0 +1,45 @@ + + + + + dash/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/__data__/input/playlist_update/playlist.m3u8 b/tests/__data__/input/playlist_update/playlist.m3u8 new file mode 100644 index 0000000000..28ff1f079d --- /dev/null +++ b/tests/__data__/input/playlist_update/playlist.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +## Generated with https://github.com/shaka-project/shaka-packager version v3.4.2-c819dea-release +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="pl",NAME="stream_4",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2" +#EXT-X-STREAM-INF:BANDWIDTH=3688787,AVERAGE-BANDWIDTH=8948756,CODECS="avc1.424028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_0.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1988787,AVERAGE-BANDWIDTH=3688345,CODECS="avc1.42401f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_1.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1188787,AVERAGE-BANDWIDTH=1774276,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=853x480,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_2.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=688787,AVERAGE-BANDWIDTH=766090,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_3.m3u8 diff --git a/tests/__data__/input/playlist_update/playlist.mjs b/tests/__data__/input/playlist_update/playlist.mjs deleted file mode 100644 index 2f1d74c465..0000000000 --- a/tests/__data__/input/playlist_update/playlist.mjs +++ /dev/null @@ -1,111 +0,0 @@ -export default { - type: 'playlist', - isMasterPlaylist: true, - independentSegments: true, - source: - '#EXTM3U\n## Generated with https://github.com/shaka-project/shaka-packager version v3.4.2-c819dea-release\n\n#EXT-X-INDEPENDENT-SEGMENTS\n\n#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="pl",NAME="stream_4",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2"\n\n#EXT-X-STREAM-INF:BANDWIDTH=3688787,AVERAGE-BANDWIDTH=8948756,CODECS="avc1.424028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_0.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=1988787,AVERAGE-BANDWIDTH=3688345,CODECS="avc1.42401f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_1.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=1188787,AVERAGE-BANDWIDTH=1774276,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=853x480,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_2.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=688787,AVERAGE-BANDWIDTH=766090,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE\nstream_3.m3u8\n', - variants: [ - { - uri: 'stream_0.m3u8', - isIFrameOnly: false, - bandwidth: 3688787, - averageBandwidth: 8948756, - codecs: 'avc1.424028,mp4a.40.2', - resolution: { width: 1920, height: 1080 }, - frameRate: 25, - audio: [ - { - type: 'AUDIO', - uri: 'stream_4.m3u8', - groupId: 'default-audio-group', - language: 'pl', - name: 'stream_4', - isDefault: false, - autoselect: true, - channels: '2' - } - ], - video: [], - subtitles: [], - closedCaptions: [], - currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } - }, - { - uri: 'stream_1.m3u8', - isIFrameOnly: false, - bandwidth: 1988787, - averageBandwidth: 3688345, - codecs: 'avc1.42401f,mp4a.40.2', - resolution: { width: 1280, height: 720 }, - frameRate: 25, - audio: [ - { - type: 'AUDIO', - uri: 'stream_4.m3u8', - groupId: 'default-audio-group', - language: 'pl', - name: 'stream_4', - isDefault: false, - autoselect: true, - channels: '2' - } - ], - video: [], - subtitles: [], - closedCaptions: [], - currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } - }, - { - uri: 'stream_2.m3u8', - isIFrameOnly: false, - bandwidth: 1188787, - averageBandwidth: 1774276, - codecs: 'avc1.42401e,mp4a.40.2', - resolution: { width: 853, height: 480 }, - frameRate: 25, - audio: [ - { - type: 'AUDIO', - uri: 'stream_4.m3u8', - groupId: 'default-audio-group', - language: 'pl', - name: 'stream_4', - isDefault: false, - autoselect: true, - channels: '2' - } - ], - video: [], - subtitles: [], - closedCaptions: [], - currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } - }, - { - uri: 'stream_3.m3u8', - isIFrameOnly: false, - bandwidth: 688787, - averageBandwidth: 766090, - codecs: 'avc1.42401e,mp4a.40.2', - resolution: { width: 640, height: 360 }, - frameRate: 25, - audio: [ - { - type: 'AUDIO', - uri: 'stream_4.m3u8', - groupId: 'default-audio-group', - language: 'pl', - name: 'stream_4', - isDefault: false, - autoselect: true, - channels: '2' - } - ], - video: [], - subtitles: [], - closedCaptions: [], - currentRenditions: { audio: 0, video: 0, subtitles: 0, closedCaptions: 0 } - } - ], - sessionDataList: [], - sessionKeyList: [] -} From 8c19ff41fe1dd6a3f320d4728b1a272e3b952d14 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:15:34 +0300 Subject: [PATCH 7/9] Install mpd-parser --- package-lock.json | 88 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 89 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2386f566bb..cc90c2a09b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "lodash.uniqueid": "^4.0.1", "m3u-linter": "^0.4.2", "mediainfo.js": "^0.3.6", + "mpd-parser": "^1.3.1", "node-cleanup": "^2.1.2", "normalize-url": "^8.1.0", "socks-proxy-agent": "^8.0.5", @@ -499,6 +500,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -3455,6 +3465,30 @@ "win32" ] }, + "node_modules/@videojs/vhs-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", + "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4109,6 +4143,11 @@ "node": ">=8" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4910,6 +4949,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", @@ -6636,6 +6685,15 @@ "node": ">=6" } }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "license": "MIT", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -6656,6 +6714,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mpd-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz", + "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7043,6 +7116,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7738,6 +7820,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==", + "license": "Apache-2.0" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index 245df23164..918989f2fa 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "lodash.uniqueid": "^4.0.1", "m3u-linter": "^0.4.2", "mediainfo.js": "^0.3.6", + "mpd-parser": "^1.3.1", "node-cleanup": "^2.1.2", "normalize-url": "^8.1.0", "socks-proxy-agent": "^8.0.5", From 1ebefa6992b6c0a82047e8251f7912831943f012 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:20:19 +0300 Subject: [PATCH 8/9] Update utils.ts --- scripts/utils.ts | 114 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 98 insertions(+), 16 deletions(-) diff --git a/scripts/utils.ts b/scripts/utils.ts index 0d27fe79d2..eb3b4e2f0b 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,9 +1,13 @@ -import { MasterPlaylist, MediaPlaylist, Variant } from 'hls-parser/types' -import { parse as parsePlaylist } from 'hls-parser' +import axios, { AxiosProxyConfig, AxiosRequestConfig } from 'axios' +import { parse as parsePlaylist, setOptions } from 'hls-parser' +import { parse as parseManifest } from 'mpd-parser' +import { SocksProxyAgent } from 'socks-proxy-agent' +import { ProxyParser } from './core/proxyParser.js' import { TESTING } from './constants.js' import normalizeUrl from 'normalize-url' import { orderBy } from 'es-toolkit' -import axios from 'axios' +import path from 'node:path' +import fs from 'node:fs' export function isURI(string: string): boolean { try { @@ -27,32 +31,110 @@ export function truncate(string: string, limit: number = 100) { return string.slice(0, limit - 3) + '...' } +type StreamInfo = { + resolution: { width: number; height: number } + bandwidth: number + frameRate: number + codecs: string +} + export async function getStreamInfo( url: string, - options: { httpUserAgent: string | null; httpReferrer: string | null } -): Promise { - let playlist: MasterPlaylist | MediaPlaylist | undefined - + options: { + httpUserAgent?: string | null + httpReferrer?: string | null + timeout?: number + proxy?: string + } +): Promise { + let data: string | undefined if (TESTING) { - playlist = (await import('../tests/__data__/input/playlist_update/playlist.mjs')) - .default as unknown as MasterPlaylist + if (url.includes('.m3u8')) { + data = fs.readFileSync( + path.resolve(__dirname, '../tests/__data__/input/playlist_update/playlist.m3u8'), + 'utf8' + ) + } else if (url.includes('.mpd')) { + data = fs.readFileSync( + path.resolve(__dirname, '../tests/__data__/input/playlist_update/manifest.mpd'), + 'utf8' + ) + } } else { try { - const response = await axios(url, { - signal: AbortSignal.timeout(30000), + const timeout = options.timeout || 1000 + let request: AxiosRequestConfig = { + signal: AbortSignal.timeout(timeout), + responseType: 'text', headers: { 'User-Agent': options.httpUserAgent || 'Mozilla/5.0', Referer: options.httpReferrer } - }) + } - playlist = parsePlaylist(response.data) + if (options.proxy !== undefined) { + const proxyParser = new ProxyParser() + const proxy = proxyParser.parse(options.proxy) as AxiosProxyConfig + if ( + proxy.protocol && + ['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol)) + ) { + const socksProxyAgent = new SocksProxyAgent(options.proxy) + + request = { ...request, ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } } + } else { + request = { ...request, ...{ proxy } } + } + } + + const response = await axios(url, request) + + data = response.data } catch {} } - if (playlist && playlist.isMasterPlaylist && playlist.variants.length) { - return orderBy(playlist.variants, ['bandwidth'], ['desc'])[0] + if (!data) return undefined + + let info: StreamInfo | undefined + + if (url.includes('.m3u8')) { + setOptions({ silent: true }) + + try { + const playlist = parsePlaylist(data) + + if (playlist && playlist.isMasterPlaylist && playlist.variants.length) { + const v = orderBy(playlist.variants, ['bandwidth'], ['desc'])[0] + + if (v && v.resolution && v.frameRate && v.codecs) { + info = { + resolution: { width: v.resolution.width, height: v.resolution.height }, + bandwidth: v.bandwidth, + frameRate: v.frameRate, + codecs: v.codecs + } + } + } + } catch {} + } else if (url.includes('.mpd')) { + const manifest = parseManifest(data, { + manifestUri: url, + eventHandler: ({ type, message }) => console.log(`${type}: ${message}`) + }) + + const playlist = orderBy(manifest.playlists, [p => p.attributes.BANDWIDTH], ['desc'])[0] + + if (playlist) { + const attr = playlist.attributes + + info = { + resolution: { width: attr.RESOLUTION.width, height: attr.RESOLUTION.height }, + bandwidth: attr.BANDWIDTH, + frameRate: attr['FRAME-RATE'], + codecs: attr.CODECS + } + } } - return undefined + return info } From 81a822ade2c894c1786dc0fcfbe36d147a550881 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:20:21 +0300 Subject: [PATCH 9/9] Update format.ts --- scripts/commands/playlist/format.ts | 52 +++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/scripts/commands/playlist/format.ts b/scripts/commands/playlist/format.ts index 15b4bc017e..442b1cdf2f 100644 --- a/scripts/commands/playlist/format.ts +++ b/scripts/commands/playlist/format.ts @@ -1,13 +1,34 @@ import { Collection, Logger } from '@freearhey/core' +import { OptionValues, program } from 'commander' import { Stream, Playlist } from '../../models' import { Storage } from '@freearhey/storage-js' import { STREAMS_DIR } from '../../constants' import { PlaylistParser } from '../../core' +import { getStreamInfo } from '../../utils' +import cliProgress from 'cli-progress' import { loadData } from '../../api' -import { program } from 'commander' +import { eachLimit } from 'async' import path from 'node:path' +import os from 'node:os' -program.argument('[filepath...]', 'Path to file to format').parse(process.argv) +program + .argument('[filepath...]', 'Path to file to format') + .option( + '-p, --parallel ', + 'Batch size of streams to test concurrently', + (value: string) => parseInt(value), + os.cpus().length + ) + .option('-x, --proxy ', 'Use the specified proxy') + .option( + '-t, --timeout ', + 'The number of milliseconds before the request will be aborted', + (value: string) => parseInt(value), + 1000 + ) + .parse(process.argv) + +const options: OptionValues = program.opts() async function main() { const logger = new Logger() @@ -58,6 +79,33 @@ async function main() { return stream }) + logger.info('adding the missing quality...') + const progressBar = new cliProgress.SingleBar({ + clearOnComplete: true, + format: `[{bar}] {percentage}% | {value}/{total}` + }) + progressBar.start(streams.count(), 0) + await eachLimit(streams.all(), options.parallel, async (stream: Stream) => { + progressBar.increment() + if (stream.quality) return + + const streamInfo = await getStreamInfo(stream.url, { + httpUserAgent: stream.user_agent, + httpReferrer: stream.referrer, + timeout: options.timeout, + proxy: options.proxy + }) + + if (streamInfo) { + const height = streamInfo?.resolution?.height + + if (height) { + stream.quality = `${height}p` + } + } + }) + progressBar.stop() + logger.info('sorting links...') streams = streams.sortBy( [