Merge pull request #29977 from iptv-org/patch-2025.12.2
Patch 2025.12.2
This commit is contained in:
@@ -90,13 +90,13 @@ streams/fr.m3u
|
|||||||
│ 7 │ AlpedHuezTV.fr │ https://edge.vedge.infomaniak.com/livecast/ik:adhtv/chunklist.m3u8 │ HTTP_NOT_FOUND │
|
│ 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+).
|
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.
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
### How to remove my channel from playlist?
|
### How to remove my channel from playlist?
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { ROOT_DIR, STREAMS_DIR } from '../../constants'
|
|||||||
import { Logger, Collection } from '@freearhey/core'
|
import { Logger, Collection } from '@freearhey/core'
|
||||||
import { program, OptionValues } from 'commander'
|
import { program, OptionValues } from 'commander'
|
||||||
import { Storage } from '@freearhey/storage-js'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { Stream } from '../../models'
|
import { Playlist, Stream } from '../../models'
|
||||||
|
import { truncate } from '../../utils'
|
||||||
import { loadData } from '../../api'
|
import { loadData } from '../../api'
|
||||||
import { eachLimit } from 'async'
|
import { eachLimit } from 'async'
|
||||||
import dns from 'node:dns'
|
import dns from 'node:dns'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
import { truncate } from '../../utils'
|
|
||||||
|
|
||||||
const LIVE_UPDATE_INTERVAL = 5000
|
const LIVE_UPDATE_INTERVAL = 5000
|
||||||
const LIVE_UPDATE_MAX_STREAMS = 100
|
const LIVE_UPDATE_MAX_STREAMS = 100
|
||||||
@@ -21,6 +21,7 @@ const results: { [key: string]: string } = {}
|
|||||||
let interval: string | number | NodeJS.Timeout | undefined
|
let interval: string | number | NodeJS.Timeout | undefined
|
||||||
let streams = new Collection<Stream>()
|
let streams = new Collection<Stream>()
|
||||||
let isLiveUpdateEnabled = true
|
let isLiveUpdateEnabled = true
|
||||||
|
const errorStatusCodes = ['ENOTFOUND', 'HTTP_404_NOT_FOUND', 'HTTP_404_UNKONWN_ERROR']
|
||||||
|
|
||||||
program
|
program
|
||||||
.argument('[filepath...]', 'Path to file to test')
|
.argument('[filepath...]', 'Path to file to test')
|
||||||
@@ -37,12 +38,14 @@ program
|
|||||||
(value: string) => parseInt(value),
|
(value: string) => parseInt(value),
|
||||||
30000
|
30000
|
||||||
)
|
)
|
||||||
|
.option('--fix', 'Remove all broken links found from files')
|
||||||
.parse(process.argv)
|
.parse(process.argv)
|
||||||
|
|
||||||
const options: OptionValues = program.opts()
|
const options: OptionValues = program.opts()
|
||||||
|
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const tester = new StreamTester({ options })
|
const tester = new StreamTester({ options })
|
||||||
|
const rootStorage = new Storage(ROOT_DIR)
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (await isOffline()) {
|
if (await isOffline()) {
|
||||||
@@ -54,7 +57,6 @@ async function main() {
|
|||||||
await loadData()
|
await loadData()
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
const rootStorage = new Storage(ROOT_DIR)
|
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage: rootStorage
|
storage: rootStorage
|
||||||
})
|
})
|
||||||
@@ -94,8 +96,9 @@ async function runTest(stream: Stream) {
|
|||||||
|
|
||||||
const result: StreamTesterResult = await tester.test(stream)
|
const result: StreamTesterResult = await tester.test(stream)
|
||||||
|
|
||||||
|
stream.statusCode = result.status.code
|
||||||
|
|
||||||
let status = ''
|
let status = ''
|
||||||
const errorStatusCodes = ['ENOTFOUND', 'HTTP_404_NOT_FOUND', 'HTTP_404_UNKONWN_ERROR']
|
|
||||||
if (result.status.ok) status = chalk.green('OK')
|
if (result.status.ok) status = chalk.green('OK')
|
||||||
else if (errorStatusCodes.includes(result.status.code)) {
|
else if (errorStatusCodes.includes(result.status.code)) {
|
||||||
status = chalk.red(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)
|
clearInterval(interval)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -152,6 +169,10 @@ function onFinish(error: Error | null | undefined) {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.fix) {
|
||||||
|
await removeBrokenLinks()
|
||||||
|
}
|
||||||
|
|
||||||
drawTable()
|
drawTable()
|
||||||
|
|
||||||
if (errors > 0 || warnings > 0) {
|
if (errors > 0 || warnings > 0) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export class Stream extends sdk.Models.Stream {
|
|||||||
removed: boolean = false
|
removed: boolean = false
|
||||||
tvgId?: string
|
tvgId?: string
|
||||||
label: string | null
|
label: string | null
|
||||||
|
statusCode?: string
|
||||||
|
|
||||||
updateWithIssue(issueData: IssueData): this {
|
updateWithIssue(issueData: IssueData): this {
|
||||||
const data = {
|
const data = {
|
||||||
|
|||||||
3
tests/__data__/expected/playlist_test/ag.m3u
Normal file
3
tests/__data__/expected/playlist_test/ag.m3u
Normal 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
|
||||||
@@ -3,7 +3,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
url: 'https://query-streamlink.herokuapp.com/iptv-query?streaming-ip=https://www.twitch.tv/absliveantigua3',
|
url: 'https://query-streamlink.herokuapp.com/iptv-query?streaming-ip=https://www.twitch.tv/absliveantigua3',
|
||||||
http: { referrer: '', 'user-agent': '' },
|
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':
|
'https://tego-cdn2a.sibercdn.com/Live_TV-ABSTV-10/tracks-v3a1/rewind-7200.m3u8?token=e5f61e7be8363eb781b4bdfe591bf917dd529c1a-SjY3NzRTbDZQNnFQVkZaNkZja2RxV3JKc1VBa05zQkdMNStJakRGV0VTTzNrOEVGVUlIQmxta1NLV0o3bzdVdQ-1736094545-1736008145':
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
#EXTINF:-1 tvg-id="ABSTV.ag",ABS TV
|
#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
|
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]
|
#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
|
||||||
@@ -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 = {
|
type ExecError = {
|
||||||
status: number
|
status: number
|
||||||
stdout: string
|
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', () => {
|
describe('playlist:test', () => {
|
||||||
it('shows an error if the playlist contains a broken link', () => {
|
it('shows an error if the playlist contains a broken link', async () => {
|
||||||
const cmd = `${ENV_VAR} npm run playlist:test playlist_test/ag.m3u`
|
const cmd = `${ENV_VAR} npm run playlist:test streams/ag.m3u`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(cmd, { encoding: 'utf8' })
|
await exec(cmd, { encoding: 'utf8' })
|
||||||
|
if (process.env.DEBUG === 'true') console.log(cmd)
|
||||||
|
process.exit(0)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, 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)')
|
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' })
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user