From 028ecc7046b45d5a14bc2960765c0a388079f229 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Tue, 9 Dec 2025 04:18:54 +0300 Subject: [PATCH 1/5] Update tests/__data__ --- tests/__data__/expected/playlist_test/ag.m3u | 3 +++ tests/__data__/input/playlist_test/results.js | 2 +- tests/__data__/input/playlist_test/{ => streams}/ag.m3u | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 tests/__data__/expected/playlist_test/ag.m3u rename tests/__data__/input/playlist_test/{ => streams}/ag.m3u (90%) diff --git a/tests/__data__/expected/playlist_test/ag.m3u b/tests/__data__/expected/playlist_test/ag.m3u new file mode 100644 index 0000000000..16dc963ff6 --- /dev/null +++ b/tests/__data__/expected/playlist_test/ag.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="ABSTV.ag",ABS TV +https://tego-cdn2a.sibercdn.com/Live_TV-ABSTV-10/tracks-v3a1/rewind-7200.m3u8?token=e5f61e7be8363eb781b4bdfe591bf917dd529c1a-SjY3NzRTbDZQNnFQVkZaNkZja2RxV3JKc1VBa05zQkdMNStJakRGV0VTTzNrOEVGVUlIQmxta1NLV0o3bzdVdQ-1736094545-1736008145 diff --git a/tests/__data__/input/playlist_test/results.js b/tests/__data__/input/playlist_test/results.js index 1899543830..58ecd5e05e 100644 --- a/tests/__data__/input/playlist_test/results.js +++ b/tests/__data__/input/playlist_test/results.js @@ -3,7 +3,7 @@ module.exports = { { url: 'https://query-streamlink.herokuapp.com/iptv-query?streaming-ip=https://www.twitch.tv/absliveantigua3', http: { referrer: '', 'user-agent': '' }, - status: { ok: false, code: 'HTTP_NOT_FOUND', message: 'HTTP 404 Not Found' } + status: { ok: false, code: 'HTTP_404_NOT_FOUND', message: 'HTTP 404 Not Found' } }, 'https://tego-cdn2a.sibercdn.com/Live_TV-ABSTV-10/tracks-v3a1/rewind-7200.m3u8?token=e5f61e7be8363eb781b4bdfe591bf917dd529c1a-SjY3NzRTbDZQNnFQVkZaNkZja2RxV3JKc1VBa05zQkdMNStJakRGV0VTTzNrOEVGVUlIQmxta1NLV0o3bzdVdQ-1736094545-1736008145': { diff --git a/tests/__data__/input/playlist_test/ag.m3u b/tests/__data__/input/playlist_test/streams/ag.m3u similarity index 90% rename from tests/__data__/input/playlist_test/ag.m3u rename to tests/__data__/input/playlist_test/streams/ag.m3u index f4716e013b..71345f8cc4 100644 --- a/tests/__data__/input/playlist_test/ag.m3u +++ b/tests/__data__/input/playlist_test/streams/ag.m3u @@ -2,4 +2,4 @@ #EXTINF:-1 tvg-id="ABSTV.ag",ABS TV https://tego-cdn2a.sibercdn.com/Live_TV-ABSTV-10/tracks-v3a1/rewind-7200.m3u8?token=e5f61e7be8363eb781b4bdfe591bf917dd529c1a-SjY3NzRTbDZQNnFQVkZaNkZja2RxV3JKc1VBa05zQkdMNStJakRGV0VTTzNrOEVGVUlIQmxta1NLV0o3bzdVdQ-1736094545-1736008145 #EXTINF:-1 tvg-id="ABSTV.ag@HD",ABS TV (1080p) [Not 24/7] -https://query-streamlink.herokuapp.com/iptv-query?streaming-ip=https://www.twitch.tv/absliveantigua3 \ No newline at end of file +https://query-streamlink.herokuapp.com/iptv-query?streaming-ip=https://www.twitch.tv/absliveantigua3 From a24ae8f8b701af261dfd252588c906e97f771900 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Tue, 9 Dec 2025 04:19:09 +0300 Subject: [PATCH 2/5] Update test.test.ts --- tests/commands/playlist/test.test.ts | 53 ++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/tests/commands/playlist/test.test.ts b/tests/commands/playlist/test.test.ts index 1bf978b016..1deeacda75 100644 --- a/tests/commands/playlist/test.test.ts +++ b/tests/commands/playlist/test.test.ts @@ -1,21 +1,62 @@ -import { execSync } from 'child_process' +import child_process from 'node:child_process' +import { pathToFileURL } from 'node:url' +import { promisify } from 'node:util' +import * as fs from 'fs-extra' +import { glob } from 'glob' + +const exec = promisify(child_process.exec) type ExecError = { status: number stdout: string } -const ENV_VAR = 'cross-env ROOT_DIR=tests/__data__/input DATA_DIR=tests/__data__/input/data' +const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/data ROOT_DIR=tests/__data__/output' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync('tests/__data__/input/playlist_test/streams', 'tests/__data__/output/streams') +}) describe('playlist:test', () => { - it('shows an error if the playlist contains a broken link', () => { - const cmd = `${ENV_VAR} npm run playlist:test playlist_test/ag.m3u` + it('shows an error if the playlist contains a broken link', async () => { + const cmd = `${ENV_VAR} npm run playlist:test streams/ag.m3u` + try { - execSync(cmd, { encoding: 'utf8' }) + await exec(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd) + process.exit(0) } catch (error) { if (process.env.DEBUG === 'true') console.log(cmd, error) - expect((error as ExecError).stdout).toContain('playlist_test/ag.m3u') + expect((error as ExecError).stdout).toContain('streams/ag.m3u') expect((error as ExecError).stdout).toContain('2 problems (1 errors, 1 warnings)') } }) + + it('it can remove all broken links from the playlist', async () => { + const cmd = `${ENV_VAR} npm run playlist:test streams/ag.m3u --- --fix` + try { + await exec(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd) + process.exit(0) + } catch (error) { + if (process.env.DEBUG === 'true') console.log(cmd, error) + const files = glob.sync('tests/__data__/expected/playlist_test/*.m3u').map(filepath => { + const fileUrl = pathToFileURL(filepath).toString() + const pathToRemove = pathToFileURL('tests/__data__/expected/playlist_test/').toString() + + return fileUrl.replace(pathToRemove, '') + }) + + files.forEach(filepath => { + expect(content(`tests/__data__/output/streams/${filepath}`)).toBe( + content(`tests/__data__/expected/playlist_test/${filepath}`) + ) + }) + } + }) }) + +function content(filepath: string) { + return fs.readFileSync(pathToFileURL(filepath), { encoding: 'utf8' }) +} From f5f07415f530024551275f84864e4f8249478937 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Tue, 9 Dec 2025 04:19:12 +0300 Subject: [PATCH 3/5] Update stream.ts --- scripts/models/stream.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/models/stream.ts b/scripts/models/stream.ts index 002a6cc2e1..31b3926f80 100644 --- a/scripts/models/stream.ts +++ b/scripts/models/stream.ts @@ -13,6 +13,7 @@ export class Stream extends sdk.Models.Stream { removed: boolean = false tvgId?: string label: string | null + statusCode?: string updateWithIssue(issueData: IssueData): this { const data = { From 6c00393c6bfcdceaca4d4e724f68ba056db9f0ad Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Tue, 9 Dec 2025 04:19:30 +0300 Subject: [PATCH 4/5] Update test.ts --- scripts/commands/playlist/test.ts | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/scripts/commands/playlist/test.ts b/scripts/commands/playlist/test.ts index 9446e8c4ad..d428aca167 100644 --- a/scripts/commands/playlist/test.ts +++ b/scripts/commands/playlist/test.ts @@ -4,13 +4,13 @@ import { ROOT_DIR, STREAMS_DIR } from '../../constants' import { Logger, Collection } from '@freearhey/core' import { program, OptionValues } from 'commander' import { Storage } from '@freearhey/storage-js' -import { Stream } from '../../models' +import { Playlist, Stream } from '../../models' +import { truncate } from '../../utils' import { loadData } from '../../api' import { eachLimit } from 'async' import dns from 'node:dns' import chalk from 'chalk' import os from 'node:os' -import { truncate } from '../../utils' const LIVE_UPDATE_INTERVAL = 5000 const LIVE_UPDATE_MAX_STREAMS = 100 @@ -21,6 +21,7 @@ const results: { [key: string]: string } = {} let interval: string | number | NodeJS.Timeout | undefined let streams = new Collection() let isLiveUpdateEnabled = true +const errorStatusCodes = ['ENOTFOUND', 'HTTP_404_NOT_FOUND', 'HTTP_404_UNKONWN_ERROR'] program .argument('[filepath...]', 'Path to file to test') @@ -37,12 +38,14 @@ program (value: string) => parseInt(value), 30000 ) + .option('--fix', 'Remove all broken links found from files') .parse(process.argv) const options: OptionValues = program.opts() const logger = new Logger() const tester = new StreamTester({ options }) +const rootStorage = new Storage(ROOT_DIR) async function main() { if (await isOffline()) { @@ -54,7 +57,6 @@ async function main() { await loadData() logger.info('loading streams...') - const rootStorage = new Storage(ROOT_DIR) const parser = new PlaylistParser({ storage: rootStorage }) @@ -94,8 +96,9 @@ async function runTest(stream: Stream) { const result: StreamTesterResult = await tester.test(stream) + stream.statusCode = result.status.code + let status = '' - const errorStatusCodes = ['ENOTFOUND', 'HTTP_404_NOT_FOUND', 'HTTP_404_UNKONWN_ERROR'] if (result.status.ok) status = chalk.green('OK') else if (errorStatusCodes.includes(result.status.code)) { status = chalk.red(result.status.code) @@ -144,7 +147,21 @@ function drawTable() { } } -function onFinish(error: Error | null | undefined) { +async function removeBrokenLinks() { + const streamsGrouped = streams.groupBy((stream: Stream) => stream.filepath) + for (const filepath of streamsGrouped.keys()) { + let streams: Collection = new Collection(streamsGrouped.get(filepath)) + + streams = streams.filter((stream: Stream) => + !stream.statusCode ? true : !errorStatusCodes.includes(stream.statusCode) + ) + + const playlist = new Playlist(streams, { public: false }) + await rootStorage.save(filepath, playlist.toString()) + } +} + +async function onFinish(error: Error | null | undefined) { clearInterval(interval) if (error) { @@ -152,6 +169,10 @@ function onFinish(error: Error | null | undefined) { process.exit(1) } + if (options.fix) { + await removeBrokenLinks() + } + drawTable() if (errors > 0 || warnings > 0) { From d1f7d89ebde363e5174de732104d188576841f2b Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Tue, 9 Dec 2025 04:30:48 +0300 Subject: [PATCH 5/5] Update CONTRIBUTING.md --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f5ec2cee4..92f4f851a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,13 +90,13 @@ streams/fr.m3u │ 7 │ AlpedHuezTV.fr │ https://edge.vedge.infomaniak.com/livecast/ik:adhtv/chunklist.m3u8 │ HTTP_NOT_FOUND │ ``` -After that, all you have to do is report any broken streams you find. +Also, if you add the `--fix` option to the command, the script will automatically remove all broken streams it finds from your local copy of playlists: -### How to replace a broken stream? +```sh +npm run playlist:test streams/fr.m3u --- --fix +``` -This can be done either by filling out this [form](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=streams%3Aedit&projects=&template=2_streams_edit.yml&title=Edit%3A+). - -Either by directly updating the files in the [/streams](/streams) folder and then creating a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests). +After that, all you need to do is report the broken streams you found via the [form](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=streams:remove&projects=&template=3_streams_report.yml&title=Broken%3A+) or create a [pull request](https://github.com/iptv-org/iptv/pulls) with updated playlists. ### How to remove my channel from playlist?