iptv-checker 0.20.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.travis.yml +1 -1
- package/README.md +195 -6
- package/bin/iptv-checker.js +0 -2
- package/package.json +4 -4
- package/src/helper.js +52 -13
- package/src/index.js +5 -8
- package/test/bin.test.js +12 -0
- package/test/index.test.js +2 -1
- package/test/input/dummy.m3u +0 -1
- package/test/input/timeout.m3u +3 -0
package/.travis.yml
CHANGED
package/README.md
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
# IPTV Checker [](https://travis-ci.com/freearhey/iptv-checker)
|
|
1
|
+
# IPTV Checker [](https://app.travis-ci.com/freearhey/iptv-checker)
|
|
2
2
|
|
|
3
3
|
Node.js CLI tool for checking links in IPTV playlists.
|
|
4
4
|
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
5
|
This tool is based on the `ffmpeg` library, so you need to install it on your computer first. You can find the right installer for your system here: https://www.ffmpeg.org/download.html
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### CLI
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm install -g iptv-checker
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
## Usage
|
|
16
|
-
|
|
17
15
|
#### Check local playlist file:
|
|
18
16
|
|
|
19
17
|
```sh
|
|
@@ -40,6 +38,197 @@ Arguments:
|
|
|
40
38
|
- `-k, --insecure`: allow insecure connections when using SSL
|
|
41
39
|
- `-p, --parallel`: Batch size of channels to check concurrently (default to 1)
|
|
42
40
|
|
|
41
|
+
### Node.js
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm install iptv-checker
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
var checker = require('iptv-checker')
|
|
49
|
+
|
|
50
|
+
// using playlist url
|
|
51
|
+
checker.checkPlaylist('https://some-playlist.lol/list.m3u').then(results => {
|
|
52
|
+
console.log(results)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// using local path
|
|
56
|
+
checker.checkPlaylist('path/to/playlist.m3u').then(results => {
|
|
57
|
+
console.log(results)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// using playlist as string
|
|
61
|
+
checker.checkPlaylist(string).then(results => {
|
|
62
|
+
console.log(results)
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Results
|
|
67
|
+
|
|
68
|
+
On success:
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
{
|
|
72
|
+
"header": {
|
|
73
|
+
"attrs": {},
|
|
74
|
+
"raw": "#EXTM3U x-tvg-url=\"\""
|
|
75
|
+
},
|
|
76
|
+
"items": [
|
|
77
|
+
{
|
|
78
|
+
"name": "KBSV/AssyriaSat (720p) [Not 24/7]",
|
|
79
|
+
"tvg": {
|
|
80
|
+
"id": "KBSVAssyriaSat.us",
|
|
81
|
+
"name": "",
|
|
82
|
+
"language": "Assyrian Neo-Aramaic;English",
|
|
83
|
+
"country": "US",
|
|
84
|
+
"logo": "https://i.imgur.com/zEWSSdf.jpg",
|
|
85
|
+
"url": "",
|
|
86
|
+
"rec": ""
|
|
87
|
+
},
|
|
88
|
+
"group": {
|
|
89
|
+
"title": "General"
|
|
90
|
+
},
|
|
91
|
+
"http": {
|
|
92
|
+
"referrer": "",
|
|
93
|
+
"user-agent": ""
|
|
94
|
+
},
|
|
95
|
+
"url": "http://66.242.170.53/hls/live/temp/index.m3u8",
|
|
96
|
+
"raw": "#EXTINF:-1 tvg-id=\"KBSVAssyriaSat.us\" tvg-country=\"US\" tvg-language=\"Assyrian Neo-Aramaic;English\" tvg-logo=\"https://i.imgur.com/zEWSSdf.jpg\" group-title=\"General\",KBSV/AssyriaSat (720p) [Not 24/7]\r\nhttp://66.242.170.53/hls/live/temp/index.m3u8",
|
|
97
|
+
"line": 2,
|
|
98
|
+
"catchup": {
|
|
99
|
+
"type": "",
|
|
100
|
+
"days": "",
|
|
101
|
+
"source": ""
|
|
102
|
+
},
|
|
103
|
+
"timeshift": "",
|
|
104
|
+
"status": {
|
|
105
|
+
"ok": true,
|
|
106
|
+
"metadata": {
|
|
107
|
+
"streams": [
|
|
108
|
+
{
|
|
109
|
+
"index": 0,
|
|
110
|
+
"codec_name": "h264",
|
|
111
|
+
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
|
|
112
|
+
"profile": "High",
|
|
113
|
+
"codec_type": "video",
|
|
114
|
+
"codec_tag_string": "[27][0][0][0]",
|
|
115
|
+
"codec_tag": "0x001b",
|
|
116
|
+
"width": 1280,
|
|
117
|
+
"height": 720,
|
|
118
|
+
"coded_width": 1280,
|
|
119
|
+
"coded_height": 720,
|
|
120
|
+
"closed_captions": 0,
|
|
121
|
+
"has_b_frames": 2,
|
|
122
|
+
"pix_fmt": "yuv420p",
|
|
123
|
+
"level": 31,
|
|
124
|
+
"chroma_location": "left",
|
|
125
|
+
"refs": 1,
|
|
126
|
+
"is_avc": "false",
|
|
127
|
+
"nal_length_size": "0",
|
|
128
|
+
"r_frame_rate": "30/1",
|
|
129
|
+
"avg_frame_rate": "0/0",
|
|
130
|
+
"time_base": "1/90000",
|
|
131
|
+
"start_pts": 943358850,
|
|
132
|
+
"start_time": "10481.765000",
|
|
133
|
+
"bits_per_raw_sample": "8",
|
|
134
|
+
"disposition": {
|
|
135
|
+
"default": 0,
|
|
136
|
+
"dub": 0,
|
|
137
|
+
"original": 0,
|
|
138
|
+
"comment": 0,
|
|
139
|
+
"lyrics": 0,
|
|
140
|
+
"karaoke": 0,
|
|
141
|
+
"forced": 0,
|
|
142
|
+
"hearing_impaired": 0,
|
|
143
|
+
"visual_impaired": 0,
|
|
144
|
+
"clean_effects": 0,
|
|
145
|
+
"attached_pic": 0,
|
|
146
|
+
"timed_thumbnails": 0
|
|
147
|
+
},
|
|
148
|
+
"tags": {
|
|
149
|
+
"variant_bitrate": "400000"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//...
|
|
153
|
+
],
|
|
154
|
+
"format": {
|
|
155
|
+
"filename": "http://66.242.170.53/hls/live/temp/index.m3u8",
|
|
156
|
+
"nb_streams": 2,
|
|
157
|
+
"nb_programs": 1,
|
|
158
|
+
"format_name": "hls",
|
|
159
|
+
"format_long_name": "Apple HTTP Live Streaming",
|
|
160
|
+
"start_time": "10481.560589",
|
|
161
|
+
"size": "214",
|
|
162
|
+
"probe_score": 100
|
|
163
|
+
},
|
|
164
|
+
"requests": [
|
|
165
|
+
{
|
|
166
|
+
"method": "GET",
|
|
167
|
+
"url": "http://66.242.170.53/hls/live/temp/index.m3u8",
|
|
168
|
+
"headers": {
|
|
169
|
+
"User-Agent": "Lavf/58.76.100",
|
|
170
|
+
"Accept": "*/*",
|
|
171
|
+
"Range": "bytes=0-",
|
|
172
|
+
"Connection": "close",
|
|
173
|
+
"Host": "66.242.170.53",
|
|
174
|
+
"Icy-MetaData": "1"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
//...
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//...
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
On error:
|
|
188
|
+
|
|
189
|
+
```jsonc
|
|
190
|
+
{
|
|
191
|
+
"header": {
|
|
192
|
+
"attrs": {},
|
|
193
|
+
"raw": "#EXTM3U x-tvg-url=\"\""
|
|
194
|
+
},
|
|
195
|
+
"items": [
|
|
196
|
+
{
|
|
197
|
+
"name": "Addis TV (720p)",
|
|
198
|
+
"tvg": {
|
|
199
|
+
"id": "AddisTV.et",
|
|
200
|
+
"name": "",
|
|
201
|
+
"language": "Amharic",
|
|
202
|
+
"country": "ET",
|
|
203
|
+
"logo": "https://i.imgur.com/KAg6MOI.png",
|
|
204
|
+
"url": "",
|
|
205
|
+
"rec": ""
|
|
206
|
+
},
|
|
207
|
+
"group": {
|
|
208
|
+
"title": ""
|
|
209
|
+
},
|
|
210
|
+
"http": {
|
|
211
|
+
"referrer": "",
|
|
212
|
+
"user-agent": ""
|
|
213
|
+
},
|
|
214
|
+
"url": "https://rrsatrtmp.tulix.tv/addis1/addis1multi.smil/playlist.m3u8",
|
|
215
|
+
"raw": "#EXTINF:-1 tvg-id=\"AddisTV.et\" tvg-country=\"ET\" tvg-language=\"Amharic\" tvg-logo=\"https://i.imgur.com/KAg6MOI.png\" group-title=\"Undefined\",Addis TV (720p)\r\nhttps://rrsatrtmp.tulix.tv/addis1/addis1multi.smil/playlist.m3u8",
|
|
216
|
+
"line": 2,
|
|
217
|
+
"catchup": {
|
|
218
|
+
"type": "",
|
|
219
|
+
"days": "",
|
|
220
|
+
"source": ""
|
|
221
|
+
},
|
|
222
|
+
"timeshift": "",
|
|
223
|
+
"status": {
|
|
224
|
+
"ok": false,
|
|
225
|
+
"reason": "Operation timed out"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
43
232
|
## Contribution
|
|
44
233
|
|
|
45
234
|
If you find a bug or want to contribute to the code or documentation, you can help by submitting an [issue](https://github.com/freearhey/iptv-checker/issues) or a [pull request](https://github.com/freearhey/iptv-checker/pulls).
|
package/bin/iptv-checker.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iptv-checker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Node.js CLI tool for checking links in IPTV playlists",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"commander": "^2.20.0",
|
|
37
37
|
"dateformat": "^3.0.3",
|
|
38
38
|
"get-stdin": "^7.0.0",
|
|
39
|
-
"iptv-playlist-parser": "^0.
|
|
39
|
+
"iptv-playlist-parser": "^0.11.0",
|
|
40
40
|
"jest": "^27.0.6",
|
|
41
41
|
"lodash.chunk": "^4.2.0",
|
|
42
42
|
"progress": "^2.0.3",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"babel-eslint": "^10.1.0",
|
|
47
47
|
"del": "^5.1.0",
|
|
48
|
-
"eslint": "^
|
|
48
|
+
"eslint": "^8.10.0",
|
|
49
49
|
"eslint-config-prettier": "^6.10.1",
|
|
50
50
|
"eslint-plugin-babel": "^5.3.0",
|
|
51
|
-
"eslint-plugin-jest": "^
|
|
51
|
+
"eslint-plugin-jest": "^26.1.1",
|
|
52
52
|
"eslint-plugin-prettier": "^3.1.2",
|
|
53
53
|
"mkdirp": "^1.0.4",
|
|
54
54
|
"prettier": "^2.0.4"
|
package/src/helper.js
CHANGED
|
@@ -48,7 +48,7 @@ async function parsePlaylist(input) {
|
|
|
48
48
|
return parse(data)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
51
|
+
function parseError(output, item) {
|
|
52
52
|
const url = item.url
|
|
53
53
|
const line = output.split('\n').find(l => {
|
|
54
54
|
return l.indexOf(url) === 0
|
|
@@ -61,6 +61,41 @@ function parseStdout(output, item) {
|
|
|
61
61
|
return line.replace(`${url}: `, '')
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function parseStderr(stderr) {
|
|
65
|
+
const requests = stderr
|
|
66
|
+
.split('\r\n\n')
|
|
67
|
+
.map(parseRequest)
|
|
68
|
+
.filter(l => l)
|
|
69
|
+
|
|
70
|
+
return { requests }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseRequest(string) {
|
|
74
|
+
const urlMatch = string.match(/Opening '(.*)' for reading/)
|
|
75
|
+
const url = urlMatch ? urlMatch[1] : null
|
|
76
|
+
if (!url) return null
|
|
77
|
+
const requestMatch = string.match(/request: (.|[\r\n])+/gm)
|
|
78
|
+
const request = requestMatch ? requestMatch[0] : null
|
|
79
|
+
if (!request) return null
|
|
80
|
+
const arr = request
|
|
81
|
+
.split('\n')
|
|
82
|
+
.map(l => l.trim())
|
|
83
|
+
.filter(l => l)
|
|
84
|
+
const methodMatch = arr[0].match(/request: (GET|POST)/)
|
|
85
|
+
const method = methodMatch ? methodMatch[1] : null
|
|
86
|
+
arr.shift()
|
|
87
|
+
if (!arr) return null
|
|
88
|
+
const headers = {}
|
|
89
|
+
arr.forEach(line => {
|
|
90
|
+
const parts = line.split(': ')
|
|
91
|
+
if (parts && parts[1]) {
|
|
92
|
+
headers[parts[0]] = parts[1]
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return { method, url, headers }
|
|
97
|
+
}
|
|
98
|
+
|
|
64
99
|
function checkItem(item) {
|
|
65
100
|
const { config, logger } = this
|
|
66
101
|
|
|
@@ -69,12 +104,14 @@ function checkItem(item) {
|
|
|
69
104
|
logger.debug(`EXECUTING: "${command}"`)
|
|
70
105
|
|
|
71
106
|
return execAsync(command, { timeout: config.timeout })
|
|
72
|
-
.then(({ stdout }) => {
|
|
73
|
-
if (stdout && isJSON(stdout)) {
|
|
107
|
+
.then(({ stdout, stderr }) => {
|
|
108
|
+
if (stdout && isJSON(stdout) && stderr) {
|
|
74
109
|
const metadata = JSON.parse(stdout)
|
|
75
110
|
if (!metadata.streams.length) {
|
|
76
111
|
return { ok: false, reason: 'No streams found' }
|
|
77
112
|
}
|
|
113
|
+
const results = parseStderr(stderr)
|
|
114
|
+
metadata.requests = results.requests
|
|
78
115
|
|
|
79
116
|
return { ok: true, metadata }
|
|
80
117
|
}
|
|
@@ -82,34 +119,36 @@ function checkItem(item) {
|
|
|
82
119
|
return { ok: false, reason: 'Parsing error' }
|
|
83
120
|
})
|
|
84
121
|
.catch(err => {
|
|
85
|
-
const reason =
|
|
122
|
+
const reason = parseError(err.message, item)
|
|
86
123
|
|
|
87
124
|
return { ok: false, reason }
|
|
88
125
|
})
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
function buildCommand(item, config) {
|
|
92
|
-
const
|
|
93
|
-
const { referrer = ``, 'user-agent': itemUserAgent = `` } = http
|
|
94
|
-
const userAgent = itemUserAgent.length ? itemUserAgent : config.userAgent
|
|
95
|
-
|
|
129
|
+
const userAgent = item.http['user-agent'] || config.userAgent
|
|
96
130
|
let args = [
|
|
97
131
|
`ffprobe`,
|
|
98
132
|
`-of json`,
|
|
99
|
-
`-v
|
|
133
|
+
`-v debug`,
|
|
100
134
|
`-hide_banner`,
|
|
101
135
|
`-show_streams`,
|
|
136
|
+
`-show_format`,
|
|
102
137
|
]
|
|
103
138
|
|
|
104
|
-
if (
|
|
105
|
-
args.push(`-
|
|
139
|
+
if (config.timeout) {
|
|
140
|
+
args.push(`-timeout`, `"${config.timeout * 1000}"`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (item.http.referrer) {
|
|
144
|
+
args.push(`-headers`, `"Referer: ${item.http.referrer}"`)
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
if (userAgent) {
|
|
109
|
-
args.push(`-user_agent`, `
|
|
148
|
+
args.push(`-user_agent`, `"${userAgent}"`)
|
|
110
149
|
}
|
|
111
150
|
|
|
112
|
-
args.push(`
|
|
151
|
+
args.push(`"${item.url}"`)
|
|
113
152
|
|
|
114
153
|
args = args.join(` `)
|
|
115
154
|
|
package/src/index.js
CHANGED
|
@@ -90,19 +90,16 @@ class IPTVChecker {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async checkStream(item) {
|
|
93
|
-
|
|
94
|
-
const logger = this.logger
|
|
95
|
-
|
|
96
|
-
await config.beforeEach(item)
|
|
93
|
+
await this.config.beforeEach(item)
|
|
97
94
|
|
|
98
|
-
item.status = await helper.checkItem.call(
|
|
95
|
+
item.status = await helper.checkItem.call(this, item)
|
|
99
96
|
if (item.status.ok) {
|
|
100
|
-
logger.debug(`OK: ${item.url}`.green)
|
|
97
|
+
this.logger.debug(`OK: ${item.url}`.green)
|
|
101
98
|
} else {
|
|
102
|
-
logger.debug(`FAILED: ${item.url} (${item.status.reason})`.red)
|
|
99
|
+
this.logger.debug(`FAILED: ${item.url} (${item.status.reason})`.red)
|
|
103
100
|
}
|
|
104
101
|
|
|
105
|
-
await config.afterEach(item)
|
|
102
|
+
await this.config.afterEach(item)
|
|
106
103
|
|
|
107
104
|
return item
|
|
108
105
|
}
|
package/test/bin.test.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { performance } = require('perf_hooks')
|
|
1
2
|
const { execSync } = require('child_process')
|
|
2
3
|
const mkdirp = require('mkdirp')
|
|
3
4
|
const del = require('del')
|
|
@@ -41,3 +42,14 @@ test(`Should process a playlist URL`, () => {
|
|
|
41
42
|
|
|
42
43
|
expect(stdoutResultTester(result)).toBeTruthy()
|
|
43
44
|
})
|
|
45
|
+
|
|
46
|
+
test(`Should respect timeout argument`, () => {
|
|
47
|
+
let t0 = performance.now()
|
|
48
|
+
execSync(
|
|
49
|
+
`node ${pwd}/bin/iptv-checker.js -t 7000 -o ${pwd}/test/output ${pwd}/test/input/timeout.m3u`,
|
|
50
|
+
{ encoding: 'utf8' }
|
|
51
|
+
)
|
|
52
|
+
let t1 = performance.now()
|
|
53
|
+
|
|
54
|
+
expect(t1 - t0).toBeGreaterThan(7000)
|
|
55
|
+
})
|
package/test/index.test.js
CHANGED
|
@@ -12,7 +12,8 @@ function resultTester(result) {
|
|
|
12
12
|
Reflect.has(item, `status`) &&
|
|
13
13
|
Reflect.has(item.status, `ok`) &&
|
|
14
14
|
(Reflect.has(item.status, `reason`) ||
|
|
15
|
-
Reflect.has(item.status, `metadata`)
|
|
15
|
+
(Reflect.has(item.status, `metadata`) &&
|
|
16
|
+
Reflect.has(item.status.metadata, `requests`)))
|
|
16
17
|
)
|
|
17
18
|
})
|
|
18
19
|
}
|
package/test/input/dummy.m3u
CHANGED