Merge pull request #29977 from iptv-org/patch-2025.12.2

Patch 2025.12.2
This commit is contained in:
archrootsda
2025-12-09 19:52:39 +01:00
committed by GitHub
7 changed files with 84 additions and 18 deletions

View File

@@ -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?

View File

@@ -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<Stream>()
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<Stream> = 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) {

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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':
{

View File

@@ -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
https://query-streamlink.herokuapp.com/iptv-query?streaming-ip=https://www.twitch.tv/absliveantigua3

View File

@@ -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' })
}