iptv-checker 0.23.0 → 0.24.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/.readme/errors.md +53 -0
- package/README.md +163 -157
- package/bin/iptv-checker.js +6 -8
- package/package.json +3 -2
- package/src/cache.js +20 -0
- package/src/errors.js +61 -0
- package/src/ffprobe.js +163 -0
- package/src/http.js +155 -0
- package/src/index.js +21 -10
- package/src/parser.js +27 -0
- package/test/index.test.js +98 -26
- package/src/helper.js +0 -187
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
## Error Codes
|
|
2
|
+
|
|
3
|
+
| Code | Message |
|
|
4
|
+
| ------------------------------------ | ---------------------------------------- |
|
|
5
|
+
| HTTP_BAD_REQUEST | HTTP 400 Bad Request |
|
|
6
|
+
| HTTP_UNAUTHORIZED | HTTP 401 Unauthorized |
|
|
7
|
+
| HTTP_PAYMENT_REQUIRED | HTTP 402 Payment Required |
|
|
8
|
+
| HTTP_FORBIDDEN | HTTP 403 Forbidden |
|
|
9
|
+
| HTTP_NOT_FOUND | HTTP 404 Not Found |
|
|
10
|
+
| HTTP_METHOD_NOT_ALLOWED | HTTP 405 Method Not Allowed |
|
|
11
|
+
| HTTP_NOT_ACCEPTABLE | HTTP 406 Not Acceptable |
|
|
12
|
+
| HTTP_PROXY_AUTHENTICATION_REQUIRED | HTTP 407 Proxy Authentication Required |
|
|
13
|
+
| HTTP_REQUEST_TIMEOUT | HTTP 408 Request Timeout |
|
|
14
|
+
| HTTP_CONFLICT | HTTP 409 Conflict |
|
|
15
|
+
| HTTP_GONE | HTTP 410 Gone |
|
|
16
|
+
| HTTP_LENGTH_REQUIRED | HTTP 411 Length Required |
|
|
17
|
+
| HTTP_PRECONDITION_FAILED | HTTP 412 Precondition Failed |
|
|
18
|
+
| HTTP_REQUEST_TOO_LONG | HTTP 413 Request Entity Too Large |
|
|
19
|
+
| HTTP_REQUEST_URI_TOO_LONG | HTTP 414 Request-URI Too Long |
|
|
20
|
+
| HTTP_UNSUPPORTED_MEDIA_TYPE | HTTP 415 Unsupported Media Type |
|
|
21
|
+
| HTTP_REQUESTED_RANGE_NOT_SATISFIABLE | HTTP 416 Requested Range Not Satisfiable |
|
|
22
|
+
| HTTP_EXPECTATION_FAILED | HTTP 417 Expectation Failed |
|
|
23
|
+
| HTTP_IM_A_TEAPOT | HTTP 418 I'm a teapot |
|
|
24
|
+
| HTTP_INSUFFICIENT_SPACE_ON_RESOURCE | HTTP 419 Insufficient Space on Resource |
|
|
25
|
+
| HTTP_METHOD_FAILURE | HTTP 420 Method Failure |
|
|
26
|
+
| HTTP_MISDIRECTED_REQUEST | HTTP 421 Misdirected Request |
|
|
27
|
+
| HTTP_UNPROCESSABLE_ENTITY | HTTP 422 Unprocessable Entity |
|
|
28
|
+
| HTTP_LOCKED | HTTP 423 Locked |
|
|
29
|
+
| HTTP_FAILED_DEPENDENCY | HTTP 424 Failed Dependency |
|
|
30
|
+
| HTTP_PRECONDITION_REQUIRED | HTTP 428 Precondition Required |
|
|
31
|
+
| HTTP_TOO_MANY_REQUESTS | HTTP 429 Too Many Requests |
|
|
32
|
+
| HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE | HTTP 431 Request Header Fields Too Large |
|
|
33
|
+
| HTTP_UNAVAILABLE_FOR_LEGAL_REASONS | HTTP 451 Unavailable For Legal Reasons |
|
|
34
|
+
| HTTP_INTERNAL_SERVER_ERROR | HTTP 500 Internal Server Error |
|
|
35
|
+
| HTTP_NOT_IMPLEMENTED | HTTP 501 Not Implemented |
|
|
36
|
+
| HTTP_BAD_GATEWAY | HTTP 502 Bad Gateway |
|
|
37
|
+
| HTTP_SERVICE_UNAVAILABLE | HTTP 503 Service Unavailable |
|
|
38
|
+
| HTTP_GATEWAY_TIMEOUT | HTTP 504 Gateway Timeout |
|
|
39
|
+
| HTTP_HTTP_VERSION_NOT_SUPPORTED | HTTP 505 HTTP Version Not Supported |
|
|
40
|
+
| HTTP_INSUFFICIENT_STORAGE | HTTP 507 Insufficient Storage |
|
|
41
|
+
| HTTP_NETWORK_AUTHENTICATION_REQUIRED | HTTP 511 Network Authentication Required |
|
|
42
|
+
| HTTP_PROTOCOL_ERROR | HTTP Protocol Error |
|
|
43
|
+
| HTTP_PARSE_ERROR | HTTP Parse Error |
|
|
44
|
+
| HTTP_NETWORK_UNREACHABLE | HTTP Network Unreachable |
|
|
45
|
+
| HTTP_ECONNRESET | HTTP Connection Reset |
|
|
46
|
+
| HTTP_CONNECTION_REFUSED | HTTP Connection Refused |
|
|
47
|
+
| HTTP_UNDEFINED | HTTP Undefined Error |
|
|
48
|
+
| FFMPEG_INPUT_OUTPUT_ERROR | FFMPEG Input/output Error |
|
|
49
|
+
| FFMPEG_PROTOCOL_NOT_FOUND | FFMPEG Protocol Not Found |
|
|
50
|
+
| FFMPEG_INVALID_DATA | FFMPEG Invalid Data |
|
|
51
|
+
| FFMPEG_PROCESS_TIMEOUT | FFMPEG Process Timeout |
|
|
52
|
+
| FFMPEG_UNDEFINED | FFMPEG Undefined Error |
|
|
53
|
+
| FFMPEG_STREAMS_NOT_FOUND | FFMPEG Streams Not Found |
|
package/README.md
CHANGED
|
@@ -49,186 +49,192 @@ var checker = require('iptv-checker')
|
|
|
49
49
|
|
|
50
50
|
// using playlist url
|
|
51
51
|
checker.checkPlaylist('https://some-playlist.lol/list.m3u').then(results => {
|
|
52
|
-
|
|
52
|
+
console.log(results)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
// using local path
|
|
56
56
|
checker.checkPlaylist('path/to/playlist.m3u').then(results => {
|
|
57
|
-
|
|
57
|
+
console.log(results)
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
// using playlist as string
|
|
61
61
|
checker.checkPlaylist(string).then(results => {
|
|
62
|
-
|
|
62
|
+
console.log(results)
|
|
63
63
|
})
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
#### Results
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
_On success:_
|
|
69
69
|
|
|
70
|
-
```
|
|
70
|
+
```js
|
|
71
71
|
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
184
|
}
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
_On error:_
|
|
188
188
|
|
|
189
|
-
```
|
|
189
|
+
```js
|
|
190
190
|
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
code: ,
|
|
226
|
+
message: 'Request Timeout',
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
//...
|
|
230
|
+
]
|
|
229
231
|
}
|
|
230
232
|
```
|
|
231
233
|
|
|
234
|
+
#### Error codes
|
|
235
|
+
|
|
236
|
+
A full list of the error codes used and their descriptions can be found [here](.readme/errors.md).
|
|
237
|
+
|
|
232
238
|
## Contribution
|
|
233
239
|
|
|
234
240
|
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
|
@@ -21,9 +21,7 @@ const stats = {
|
|
|
21
21
|
argv
|
|
22
22
|
.version(version, '-v, --version')
|
|
23
23
|
.name('iptv-checker')
|
|
24
|
-
.description(
|
|
25
|
-
'Utility to check .m3u playlists entries. If no file path or url is provided, this program will attempt to read stdin'
|
|
26
|
-
)
|
|
24
|
+
.description('Utility to check M3U playlists entries')
|
|
27
25
|
.usage('[options] [file-or-url]')
|
|
28
26
|
.option('-o, --output <output>', 'Path to output directory')
|
|
29
27
|
.option(
|
|
@@ -51,7 +49,7 @@ argv
|
|
|
51
49
|
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = !+argv.insecure
|
|
52
50
|
|
|
53
51
|
const config = {
|
|
54
|
-
debug: argv.debug,
|
|
52
|
+
debug: argv.debug || false,
|
|
55
53
|
insecure: argv.insecure,
|
|
56
54
|
userAgent: argv.userAgent,
|
|
57
55
|
timeout: parseInt(argv.timeout),
|
|
@@ -89,10 +87,10 @@ async function init() {
|
|
|
89
87
|
|
|
90
88
|
stats.online = checked.items.filter(item => item.status.ok).length
|
|
91
89
|
stats.offline = checked.items.filter(
|
|
92
|
-
item => !item.status.ok && item.status.
|
|
90
|
+
item => !item.status.ok && item.status.code !== `DUPLICATE`
|
|
93
91
|
).length
|
|
94
92
|
stats.duplicates = checked.items.filter(
|
|
95
|
-
item => !item.status.ok && item.status.
|
|
93
|
+
item => !item.status.ok && item.status.code === `DUPLICATE`
|
|
96
94
|
).length
|
|
97
95
|
|
|
98
96
|
const result = [
|
|
@@ -113,10 +111,10 @@ async function init() {
|
|
|
113
111
|
function afterEach(item) {
|
|
114
112
|
if (item.status.ok) {
|
|
115
113
|
writeToFile(onlineFile, item)
|
|
116
|
-
} else if (item.status.
|
|
114
|
+
} else if (item.status.code === `DUPLICATE`) {
|
|
117
115
|
writeToFile(duplicatesFile, item)
|
|
118
116
|
} else {
|
|
119
|
-
writeToFile(offlineFile, item, item.status.
|
|
117
|
+
writeToFile(offlineFile, item, item.status.message)
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
if (!config.debug) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iptv-checker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Node.js CLI tool for checking links in IPTV playlists",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"preferGlobal": true,
|
|
7
|
-
"homepage": "https://github.com/freearhey/iptv-checker
|
|
7
|
+
"homepage": "https://github.com/freearhey/iptv-checker",
|
|
8
8
|
"bin": {
|
|
9
9
|
"iptv-checker": "bin/iptv-checker.js"
|
|
10
10
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"axios": "^0.21.1",
|
|
34
|
+
"axios-curlirize": "^1.3.7",
|
|
34
35
|
"colors": "^1.4.0",
|
|
35
36
|
"command-exists": "^1.2.9",
|
|
36
37
|
"commander": "^2.20.0",
|
package/src/cache.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module.exports.add = add
|
|
2
|
+
module.exports.check = check
|
|
3
|
+
|
|
4
|
+
let cache = new Set()
|
|
5
|
+
|
|
6
|
+
function add({ url }) {
|
|
7
|
+
let id = hashUrl(url)
|
|
8
|
+
|
|
9
|
+
cache.add(id)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function check({ url }) {
|
|
13
|
+
let id = hashUrl(url)
|
|
14
|
+
|
|
15
|
+
return cache.has(id)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hashUrl(u) {
|
|
19
|
+
return Buffer.from(u).toString(`hex`)
|
|
20
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// HTTP: https://github.com/prettymuchbryce/http-status-codes/blob/master/README.md#codes
|
|
2
|
+
// FFmpeg: https://github.com/FFmpeg/FFmpeg/blob/636631d9db82f5e86330ab42dacc8a106684b349/libavutil/error.c
|
|
3
|
+
// Linux: https://www.man7.org/linux/man-pages/man3/errno.3.html
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
HTTP_BAD_REQUEST: 'HTTP 400 Bad Request',
|
|
7
|
+
HTTP_UNAUTHORIZED: 'HTTP 401 Unauthorized',
|
|
8
|
+
HTTP_PAYMENT_REQUIRED: 'HTTP 402 Payment Required',
|
|
9
|
+
HTTP_FORBIDDEN: 'HTTP 403 Forbidden',
|
|
10
|
+
HTTP_NOT_FOUND: 'HTTP 404 Not Found',
|
|
11
|
+
HTTP_METHOD_NOT_ALLOWED: 'HTTP 405 Method Not Allowed',
|
|
12
|
+
HTTP_NOT_ACCEPTABLE: 'HTTP 406 Not Acceptable',
|
|
13
|
+
HTTP_PROXY_AUTHENTICATION_REQUIRED: 'HTTP 407 Proxy Authentication Required',
|
|
14
|
+
HTTP_REQUEST_TIMEOUT: 'HTTP 408 Request Timeout',
|
|
15
|
+
HTTP_CONFLICT: 'HTTP 409 Conflict',
|
|
16
|
+
HTTP_GONE: 'HTTP 410 Gone',
|
|
17
|
+
HTTP_LENGTH_REQUIRED: 'HTTP 411 Length Required',
|
|
18
|
+
HTTP_PRECONDITION_FAILED: 'HTTP 412 Precondition Failed',
|
|
19
|
+
HTTP_REQUEST_TOO_LONG: 'HTTP 413 Request Entity Too Large',
|
|
20
|
+
HTTP_REQUEST_URI_TOO_LONG: 'HTTP 414 Request-URI Too Long',
|
|
21
|
+
HTTP_UNSUPPORTED_MEDIA_TYPE: 'HTTP 415 Unsupported Media Type',
|
|
22
|
+
HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
|
|
23
|
+
'HTTP 416 Requested Range Not Satisfiable',
|
|
24
|
+
HTTP_EXPECTATION_FAILED: 'HTTP 417 Expectation Failed',
|
|
25
|
+
HTTP_IM_A_TEAPOT: "HTTP 418 I'm a teapot",
|
|
26
|
+
HTTP_INSUFFICIENT_SPACE_ON_RESOURCE:
|
|
27
|
+
'HTTP 419 Insufficient Space on Resource',
|
|
28
|
+
HTTP_METHOD_FAILURE: 'HTTP 420 Method Failure',
|
|
29
|
+
HTTP_MISDIRECTED_REQUEST: 'HTTP 421 Misdirected Request',
|
|
30
|
+
HTTP_UNPROCESSABLE_ENTITY: 'HTTP 422 Unprocessable Entity',
|
|
31
|
+
HTTP_LOCKED: 'HTTP 423 Locked',
|
|
32
|
+
HTTP_FAILED_DEPENDENCY: 'HTTP 424 Failed Dependency',
|
|
33
|
+
HTTP_PRECONDITION_REQUIRED: 'HTTP 428 Precondition Required',
|
|
34
|
+
HTTP_TOO_MANY_REQUESTS: 'HTTP 429 Too Many Requests',
|
|
35
|
+
HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE:
|
|
36
|
+
'HTTP 431 Request Header Fields Too Large',
|
|
37
|
+
HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 'HTTP 451 Unavailable For Legal Reasons',
|
|
38
|
+
HTTP_INTERNAL_SERVER_ERROR: 'HTTP 500 Internal Server Error',
|
|
39
|
+
HTTP_NOT_IMPLEMENTED: 'HTTP 501 Not Implemented',
|
|
40
|
+
HTTP_BAD_GATEWAY: 'HTTP 502 Bad Gateway',
|
|
41
|
+
HTTP_SERVICE_UNAVAILABLE: 'HTTP 503 Service Unavailable',
|
|
42
|
+
HTTP_GATEWAY_TIMEOUT: 'HTTP 504 Gateway Timeout',
|
|
43
|
+
HTTP_HTTP_VERSION_NOT_SUPPORTED: 'HTTP 505 HTTP Version Not Supported',
|
|
44
|
+
HTTP_INSUFFICIENT_STORAGE: 'HTTP 507 Insufficient Storage',
|
|
45
|
+
HTTP_NETWORK_AUTHENTICATION_REQUIRED:
|
|
46
|
+
'HTTP 511 Network Authentication Required',
|
|
47
|
+
HTTP_PROTOCOL_ERROR: 'HTTP Protocol Error',
|
|
48
|
+
HTTP_PARSE_ERROR: 'HTTP Parse Error',
|
|
49
|
+
HTTP_NETWORK_UNREACHABLE: 'HTTP Network Unreachable',
|
|
50
|
+
HTTP_ECONNRESET: 'HTTP Connection Reset',
|
|
51
|
+
HTTP_CONNECTION_REFUSED: 'HTTP Connection Refused',
|
|
52
|
+
HTTP_UNDEFINED: 'HTTP Undefined Error',
|
|
53
|
+
HTTP_CANNOT_ASSIGN_REQUESTED_ADDRESS: 'HTTP Cannot Assign Requested Address',
|
|
54
|
+
|
|
55
|
+
FFMPEG_INPUT_OUTPUT_ERROR: 'FFMPEG Input/output Error',
|
|
56
|
+
FFMPEG_PROTOCOL_NOT_FOUND: 'FFMPEG Protocol Not Found',
|
|
57
|
+
FFMPEG_INVALID_DATA: 'FFMPEG Invalid Data',
|
|
58
|
+
FFMPEG_PROCESS_TIMEOUT: 'FFMPEG Process Timeout',
|
|
59
|
+
FFMPEG_UNDEFINED: 'FFMPEG Undefined Error',
|
|
60
|
+
FFMPEG_STREAMS_NOT_FOUND: 'FFMPEG Streams Not Found',
|
|
61
|
+
}
|
package/src/ffprobe.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const util = require('util')
|
|
2
|
+
const exec = require('child_process').exec
|
|
3
|
+
const execAsync = util.promisify(exec)
|
|
4
|
+
const errors = require('./errors')
|
|
5
|
+
|
|
6
|
+
module.exports = ffprobe
|
|
7
|
+
|
|
8
|
+
function ffprobe(item, config, logger) {
|
|
9
|
+
const command = buildCommand(item, config)
|
|
10
|
+
logger.debug(`FFMPEG: "${command}"`)
|
|
11
|
+
const timeout = item.timeout || config.timeout
|
|
12
|
+
return execAsync(command, { timeout })
|
|
13
|
+
.then(({ stdout, stderr }) => {
|
|
14
|
+
if (stdout && isJSON(stdout) && stderr) {
|
|
15
|
+
const metadata = JSON.parse(stdout)
|
|
16
|
+
if (!metadata.streams.length) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
code: 'FFMPEG_STREAMS_NOT_FOUND',
|
|
20
|
+
message: errors['FFMPEG_STREAMS_NOT_FOUND'],
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const results = parseStderr(stderr)
|
|
24
|
+
metadata.requests = results.requests
|
|
25
|
+
|
|
26
|
+
return { ok: true, code: 'OK', metadata }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.debug('FFMPEG_UNDEFINED')
|
|
30
|
+
logger.debug(stdout)
|
|
31
|
+
logger.debug(stderr)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
code: 'FFMPEG_UNDEFINED',
|
|
36
|
+
message: errors['FFMPEG_UNDEFINED'],
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.catch(err => {
|
|
40
|
+
const code = parseError(err.message, item, config, logger)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
code,
|
|
45
|
+
message: errors[code],
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseStderr(stderr) {
|
|
51
|
+
const requests = stderr
|
|
52
|
+
.split('\r\n\n')
|
|
53
|
+
.map(parseRequest)
|
|
54
|
+
.filter(l => l)
|
|
55
|
+
|
|
56
|
+
return { requests }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildCommand(item, config) {
|
|
60
|
+
const userAgent =
|
|
61
|
+
item.http && item.http['user-agent']
|
|
62
|
+
? item.http['user-agent']
|
|
63
|
+
: config.userAgent
|
|
64
|
+
const referer =
|
|
65
|
+
item.http && item.http.referrer ? item.http.referrer : config.httpReferer
|
|
66
|
+
const timeout = item.timeout || config.timeout
|
|
67
|
+
let args = [
|
|
68
|
+
`ffprobe`,
|
|
69
|
+
`-of json`,
|
|
70
|
+
`-v verbose`,
|
|
71
|
+
`-hide_banner`,
|
|
72
|
+
`-show_streams`,
|
|
73
|
+
`-show_format`,
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if (timeout) {
|
|
77
|
+
args.push(`-timeout`, `"${timeout * 1000}"`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (referer) {
|
|
81
|
+
args.push(`-headers`, `"Referer: ${referer}"`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (userAgent) {
|
|
85
|
+
args.push(`-user_agent`, `"${userAgent}"`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
args.push(`"${item.url}"`)
|
|
89
|
+
|
|
90
|
+
args = args.join(` `)
|
|
91
|
+
|
|
92
|
+
return args
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseRequest(string) {
|
|
96
|
+
const urlMatch = string.match(/Opening '(.*)' for reading/)
|
|
97
|
+
const url = urlMatch ? urlMatch[1] : null
|
|
98
|
+
if (!url) return null
|
|
99
|
+
const requestMatch = string.match(/request: (.|[\r\n])+/gm)
|
|
100
|
+
const request = requestMatch ? requestMatch[0] : null
|
|
101
|
+
if (!request) return null
|
|
102
|
+
const arr = request
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map(l => l.trim())
|
|
105
|
+
.filter(l => l)
|
|
106
|
+
const methodMatch = arr[0].match(/request: (GET|POST)/)
|
|
107
|
+
const method = methodMatch ? methodMatch[1] : null
|
|
108
|
+
arr.shift()
|
|
109
|
+
if (!arr) return null
|
|
110
|
+
const headers = {}
|
|
111
|
+
arr.forEach(line => {
|
|
112
|
+
const parts = line.split(': ')
|
|
113
|
+
if (parts && parts[1]) {
|
|
114
|
+
headers[parts[0]] = parts[1]
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return { method, url, headers }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseError(output, item, config, logger) {
|
|
122
|
+
const url = item.url
|
|
123
|
+
const line = output.split('\n').find(l => l.startsWith(url))
|
|
124
|
+
const err = line ? line.replace(`${url}: `, '') : null
|
|
125
|
+
|
|
126
|
+
if (!err) {
|
|
127
|
+
return 'FFMPEG_PROCESS_TIMEOUT'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switch (err) {
|
|
131
|
+
case 'Protocol not found':
|
|
132
|
+
return 'FFMPEG_PROTOCOL_NOT_FOUND'
|
|
133
|
+
case 'Input/output error':
|
|
134
|
+
return 'FFMPEG_INPUT_OUTPUT_ERROR'
|
|
135
|
+
case 'Invalid data found when processing input':
|
|
136
|
+
return 'FFMPEG_INVALID_DATA'
|
|
137
|
+
case 'Server returned 400 Bad Request':
|
|
138
|
+
return 'HTTP_BAD_REQUEST'
|
|
139
|
+
case 'Server returned 401 Unauthorized (authorization failed)':
|
|
140
|
+
return 'HTTP_UNAUTHORIZED'
|
|
141
|
+
case 'Server returned 403 Forbidden (access denied)':
|
|
142
|
+
return 'HTTP_FORBIDDEN'
|
|
143
|
+
case 'Server returned 404 Not Found':
|
|
144
|
+
return 'HTTP_NOT_FOUND'
|
|
145
|
+
case 'Connection refused':
|
|
146
|
+
return 'HTTP_CONNECTION_REFUSED'
|
|
147
|
+
case "Can't assign requested address":
|
|
148
|
+
return 'HTTP_CANNOT_ASSIGN_REQUESTED_ADDRESS'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
logger.debug('FFMPEG_UNDEFINED')
|
|
152
|
+
logger.debug(err)
|
|
153
|
+
|
|
154
|
+
return 'FFMPEG_UNDEFINED'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isJSON(str) {
|
|
158
|
+
try {
|
|
159
|
+
return !!JSON.parse(str)
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/http.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const curlirize = require('axios-curlirize')
|
|
2
|
+
const axios = require('axios')
|
|
3
|
+
const https = require('https')
|
|
4
|
+
const errors = require('./errors')
|
|
5
|
+
|
|
6
|
+
module.exports.loadPlaylist = loadPlaylist
|
|
7
|
+
module.exports.loadStream = loadStream
|
|
8
|
+
|
|
9
|
+
const playlistClient = axios.create({
|
|
10
|
+
method: 'GET',
|
|
11
|
+
timeout: 60000, // 60 second timeout
|
|
12
|
+
responseType: 'text',
|
|
13
|
+
httpsAgent: new https.Agent({
|
|
14
|
+
rejectUnauthorized: false,
|
|
15
|
+
}),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
playlistClient.interceptors.response.use(
|
|
19
|
+
response => {
|
|
20
|
+
const { 'content-type': contentType = '' } = response.headers
|
|
21
|
+
if (!/mpegurl/.test(contentType)) {
|
|
22
|
+
throw new Error('URL is not an M3U playlist file')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return response.data
|
|
26
|
+
},
|
|
27
|
+
() => {
|
|
28
|
+
return Promise.reject('Error fetching playlist')
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const streamClient = axios.create({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
timeout: 60000,
|
|
35
|
+
httpsAgent: new https.Agent({
|
|
36
|
+
rejectUnauthorized: false,
|
|
37
|
+
}),
|
|
38
|
+
validateStatus: function (status) {
|
|
39
|
+
return (status >= 200 && status < 400) || status === 405
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
curlirize(streamClient, result => {
|
|
44
|
+
const { command } = result
|
|
45
|
+
console.log(`CURL: "${command}"`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
function loadPlaylist(url) {
|
|
49
|
+
return playlistClient(url)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadStream(item, config, logger) {
|
|
53
|
+
if (!/^(http|https)/.test(item.url)) return Promise.resolve()
|
|
54
|
+
|
|
55
|
+
const userAgent =
|
|
56
|
+
item.http && item.http['user-agent']
|
|
57
|
+
? item.http['user-agent']
|
|
58
|
+
: config.userAgent
|
|
59
|
+
const referer =
|
|
60
|
+
item.http && item.http.referrer ? item.http.referrer : config.httpReferer
|
|
61
|
+
const timeout = item.timeout || config.timeout
|
|
62
|
+
|
|
63
|
+
const headers = {}
|
|
64
|
+
if (userAgent) {
|
|
65
|
+
headers['User-Agent'] = userAgent
|
|
66
|
+
}
|
|
67
|
+
if (referer) {
|
|
68
|
+
headers['Referer'] = referer
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return streamClient(item.url, {
|
|
72
|
+
timeout,
|
|
73
|
+
headers,
|
|
74
|
+
curlirize: config.debug,
|
|
75
|
+
})
|
|
76
|
+
.then(() => Promise.resolve())
|
|
77
|
+
.catch(err => {
|
|
78
|
+
const code = parseError(err, config, logger)
|
|
79
|
+
|
|
80
|
+
return Promise.reject({
|
|
81
|
+
ok: false,
|
|
82
|
+
code,
|
|
83
|
+
message: errors[code],
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseError(err, config, logger) {
|
|
89
|
+
if (err.response) {
|
|
90
|
+
return parseResponseStatus(err.response.status)
|
|
91
|
+
} else if (err.message.startsWith('timeout')) {
|
|
92
|
+
return 'HTTP_REQUEST_TIMEOUT'
|
|
93
|
+
} else if (err.message.includes('ECONNREFUSED')) {
|
|
94
|
+
return 'HTTP_INTERNAL_SERVER_ERROR'
|
|
95
|
+
} else if (err.code === 'EPROTO') {
|
|
96
|
+
return 'HTTP_PROTOCOL_ERROR'
|
|
97
|
+
} else if (err.code === 'ENETUNREACH') {
|
|
98
|
+
return 'HTTP_NETWORK_UNREACHABLE'
|
|
99
|
+
} else if (err.code === 'ENOTFOUND') {
|
|
100
|
+
return 'HTTP_NOT_FOUND'
|
|
101
|
+
} else if (err.code === 'ECONNRESET') {
|
|
102
|
+
return 'HTTP_ECONNRESET'
|
|
103
|
+
} else if (err.code.startsWith('HPE')) {
|
|
104
|
+
return 'HTTP_PARSE_ERROR'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.debug('HTTP_UNDEFINED')
|
|
108
|
+
logger.debug(err)
|
|
109
|
+
|
|
110
|
+
return 'HTTP_UNDEFINED'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseResponseStatus(status) {
|
|
114
|
+
const codes = {
|
|
115
|
+
400: 'HTTP_BAD_REQUEST',
|
|
116
|
+
401: 'HTTP_UNAUTHORIZED',
|
|
117
|
+
402: 'HTTP_PAYMENT_REQUIRED',
|
|
118
|
+
403: 'HTTP_FORBIDDEN',
|
|
119
|
+
404: 'HTTP_NOT_FOUND',
|
|
120
|
+
405: 'HTTP_METHOD_NOT_ALLOWED',
|
|
121
|
+
406: 'HTTP_NOT_ACCEPTABLE',
|
|
122
|
+
407: 'HTTP_PROXY_AUTHENTICATION_REQUIRED',
|
|
123
|
+
408: 'HTTP_REQUEST_TIMEOUT',
|
|
124
|
+
409: 'HTTP_CONFLICT',
|
|
125
|
+
410: 'HTTP_GONE',
|
|
126
|
+
411: 'HTTP_LENGTH_REQUIRED',
|
|
127
|
+
412: 'HTTP_PRECONDITION_FAILED',
|
|
128
|
+
413: 'HTTP_REQUEST_TOO_LONG',
|
|
129
|
+
414: 'HTTP_REQUEST_URI_TOO_LONG',
|
|
130
|
+
415: 'HTTP_UNSUPPORTED_MEDIA_TYPE',
|
|
131
|
+
416: 'HTTP_REQUESTED_RANGE_NOT_SATISFIABLE',
|
|
132
|
+
417: 'HTTP_EXPECTATION_FAILED',
|
|
133
|
+
418: 'HTTP_IM_A_TEAPOT',
|
|
134
|
+
419: 'HTTP_INSUFFICIENT_SPACE_ON_RESOURCE',
|
|
135
|
+
420: 'HTTP_METHOD_FAILURE',
|
|
136
|
+
421: 'HTTP_MISDIRECTED_REQUEST',
|
|
137
|
+
422: 'HTTP_UNPROCESSABLE_ENTITY',
|
|
138
|
+
423: 'HTTP_LOCKED',
|
|
139
|
+
424: 'HTTP_FAILED_DEPENDENCY',
|
|
140
|
+
428: 'HTTP_PRECONDITION_REQUIRED',
|
|
141
|
+
429: 'HTTP_TOO_MANY_REQUESTS',
|
|
142
|
+
431: 'HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE',
|
|
143
|
+
451: 'HTTP_UNAVAILABLE_FOR_LEGAL_REASONS',
|
|
144
|
+
500: 'HTTP_INTERNAL_SERVER_ERROR',
|
|
145
|
+
501: 'HTTP_NOT_IMPLEMENTED',
|
|
146
|
+
502: 'HTTP_BAD_GATEWAY',
|
|
147
|
+
503: 'HTTP_SERVICE_UNAVAILABLE',
|
|
148
|
+
504: 'HTTP_GATEWAY_TIMEOUT',
|
|
149
|
+
505: 'HTTP_HTTP_VERSION_NOT_SUPPORTED',
|
|
150
|
+
507: 'HTTP_INSUFFICIENT_STORAGE',
|
|
151
|
+
511: 'HTTP_NETWORK_AUTHENTICATION_REQUIRED',
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return codes[status]
|
|
155
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,9 +2,12 @@ require('colors')
|
|
|
2
2
|
const chunk = require('lodash.chunk')
|
|
3
3
|
const { isUri } = require('valid-url')
|
|
4
4
|
const commandExists = require('command-exists')
|
|
5
|
-
const
|
|
5
|
+
const { parsePlaylist } = require('./parser')
|
|
6
|
+
const cache = require('./cache')
|
|
6
7
|
const Logger = require('./Logger')
|
|
7
8
|
const cpus = require('os').cpus()
|
|
9
|
+
const { loadStream } = require('./http')
|
|
10
|
+
const ffprobe = require('./ffprobe')
|
|
8
11
|
|
|
9
12
|
const defaultConfig = {
|
|
10
13
|
debug: false,
|
|
@@ -41,22 +44,25 @@ class IPTVChecker {
|
|
|
41
44
|
const duplicates = []
|
|
42
45
|
const config = this.config
|
|
43
46
|
const logger = this.logger
|
|
44
|
-
const playlist = await helper.parsePlaylist(input)
|
|
45
47
|
|
|
46
48
|
logger.debug({ config })
|
|
47
49
|
|
|
50
|
+
const playlist = await parsePlaylist(input).catch(err => {
|
|
51
|
+
throw new Error(err)
|
|
52
|
+
})
|
|
53
|
+
|
|
48
54
|
await config.setUp(playlist)
|
|
49
55
|
|
|
50
56
|
const items = playlist.items
|
|
51
57
|
.map(item => {
|
|
52
58
|
if (!isUri(item.url)) return null
|
|
53
59
|
|
|
54
|
-
if (
|
|
60
|
+
if (cache.check(item)) {
|
|
55
61
|
duplicates.push(item)
|
|
56
62
|
|
|
57
63
|
return null
|
|
58
64
|
} else {
|
|
59
|
-
|
|
65
|
+
cache.add(item)
|
|
60
66
|
|
|
61
67
|
return item
|
|
62
68
|
}
|
|
@@ -64,7 +70,7 @@ class IPTVChecker {
|
|
|
64
70
|
.filter(Boolean)
|
|
65
71
|
|
|
66
72
|
for (let item of duplicates) {
|
|
67
|
-
item.status = { ok: false,
|
|
73
|
+
item.status = { ok: false, code: 'DUPLICATE', message: `Duplicate` }
|
|
68
74
|
await config.afterEach(item)
|
|
69
75
|
results.push(item)
|
|
70
76
|
}
|
|
@@ -90,16 +96,21 @@ class IPTVChecker {
|
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
async checkStream(item) {
|
|
93
|
-
|
|
99
|
+
const { config, logger } = this
|
|
100
|
+
|
|
101
|
+
await config.beforeEach(item)
|
|
102
|
+
|
|
103
|
+
item.status = await loadStream(item, config, logger)
|
|
104
|
+
.then(() => ffprobe(item, config, logger))
|
|
105
|
+
.catch(status => status)
|
|
94
106
|
|
|
95
|
-
item.status = await helper.checkItem.call(this, item)
|
|
96
107
|
if (item.status.ok) {
|
|
97
|
-
|
|
108
|
+
logger.debug(`OK: ${item.url}`.green)
|
|
98
109
|
} else {
|
|
99
|
-
|
|
110
|
+
logger.debug(`FAILED: ${item.url} (${item.status.message})`.red)
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
await
|
|
113
|
+
await config.afterEach(item)
|
|
103
114
|
|
|
104
115
|
return item
|
|
105
116
|
}
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { parse } = require('iptv-playlist-parser')
|
|
2
|
+
const { existsSync, readFileSync } = require('fs')
|
|
3
|
+
const { isWebUri } = require('valid-url')
|
|
4
|
+
const { loadPlaylist } = require('./http')
|
|
5
|
+
|
|
6
|
+
module.exports.parsePlaylist = parsePlaylist
|
|
7
|
+
|
|
8
|
+
async function parsePlaylist(input) {
|
|
9
|
+
if (input instanceof Object && Reflect.has(input, `items`)) return input
|
|
10
|
+
|
|
11
|
+
let data = input
|
|
12
|
+
if (Buffer.isBuffer(input)) {
|
|
13
|
+
data = input.toString(`utf8`)
|
|
14
|
+
} else if (typeof input === `string`) {
|
|
15
|
+
if (isWebUri(input)) {
|
|
16
|
+
data = await loadPlaylist(input)
|
|
17
|
+
} else if (existsSync(input)) {
|
|
18
|
+
data = readFileSync(input, { encoding: `utf8` })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!data.startsWith('#EXTM3U')) {
|
|
23
|
+
return Promise.reject('Unable to parse a playlist')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return parse(data)
|
|
27
|
+
}
|
package/test/index.test.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
const { readFileSync } = require('fs')
|
|
2
2
|
const IPTVChecker = require('./../src/index')
|
|
3
|
-
const playlistPath = `${__dirname}/input/dummy.m3u`
|
|
4
|
-
const playlistFile = readFileSync(playlistPath, {
|
|
5
|
-
encoding: 'utf8',
|
|
6
|
-
})
|
|
7
3
|
const checker = new IPTVChecker({ timeout: 2000, parallel: 1 })
|
|
8
4
|
|
|
9
5
|
function resultTester(result) {
|
|
@@ -11,7 +7,8 @@ function resultTester(result) {
|
|
|
11
7
|
return (
|
|
12
8
|
Reflect.has(item, `status`) &&
|
|
13
9
|
Reflect.has(item.status, `ok`) &&
|
|
14
|
-
(Reflect.has(item.status, `
|
|
10
|
+
((Reflect.has(item.status, `message`) &&
|
|
11
|
+
Reflect.has(item.status, `code`)) ||
|
|
15
12
|
(Reflect.has(item.status, `metadata`) &&
|
|
16
13
|
Reflect.has(item.status.metadata, `requests`)))
|
|
17
14
|
)
|
|
@@ -20,37 +17,76 @@ function resultTester(result) {
|
|
|
20
17
|
|
|
21
18
|
jest.setTimeout(60000)
|
|
22
19
|
|
|
23
|
-
test(`Should process a playlist URL`,
|
|
20
|
+
test(`Should process a playlist URL`, done => {
|
|
24
21
|
const url = 'https://iptv-org.github.io/iptv/languages/amh.m3u'
|
|
25
|
-
|
|
22
|
+
checker
|
|
23
|
+
.checkPlaylist(url)
|
|
24
|
+
.then(results => {
|
|
25
|
+
expect(resultTester(results)).toBeTruthy()
|
|
26
|
+
done()
|
|
27
|
+
})
|
|
28
|
+
.catch(done)
|
|
29
|
+
})
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
test(`Should process a stream URL`, done => {
|
|
32
|
+
const url =
|
|
33
|
+
'http://cdn.theoplayer.com/video/elephants-dream/playlist-single-audio.m3u8'
|
|
34
|
+
checker
|
|
35
|
+
.checkStream({ url, timeout: 5000 })
|
|
36
|
+
.then(results => {
|
|
37
|
+
expect(results.status.ok).toBeTruthy()
|
|
38
|
+
done()
|
|
39
|
+
})
|
|
40
|
+
.catch(done)
|
|
28
41
|
})
|
|
29
42
|
|
|
30
|
-
test(`Should process a relative playlist file path`,
|
|
43
|
+
test(`Should process a relative playlist file path`, done => {
|
|
31
44
|
const path = 'test/input/dummy.m3u'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
checker
|
|
46
|
+
.checkPlaylist(path)
|
|
47
|
+
.then(results => {
|
|
48
|
+
expect(resultTester(results)).toBeTruthy()
|
|
49
|
+
done()
|
|
50
|
+
})
|
|
51
|
+
.catch(done)
|
|
35
52
|
})
|
|
36
53
|
|
|
37
|
-
test(`Should process an absolute playlist file path`,
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
test(`Should process an absolute playlist file path`, done => {
|
|
55
|
+
const playlistPath = `${__dirname}/input/dummy.m3u`
|
|
56
|
+
checker
|
|
57
|
+
.checkPlaylist(playlistPath)
|
|
58
|
+
.then(results => {
|
|
59
|
+
expect(resultTester(results)).toBeTruthy()
|
|
60
|
+
done()
|
|
61
|
+
})
|
|
62
|
+
.catch(done)
|
|
41
63
|
})
|
|
42
64
|
|
|
43
|
-
test(`Should process a playlist data Buffer`,
|
|
65
|
+
test(`Should process a playlist data Buffer`, done => {
|
|
66
|
+
const playlistFile = readFileSync(`${__dirname}/input/dummy.m3u`, {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
})
|
|
44
69
|
const playlistBuffer = Buffer.from(playlistFile)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
checker
|
|
71
|
+
.checkPlaylist(playlistBuffer)
|
|
72
|
+
.then(results => {
|
|
73
|
+
expect(resultTester(results)).toBeTruthy()
|
|
74
|
+
done()
|
|
75
|
+
})
|
|
76
|
+
.catch(done)
|
|
48
77
|
})
|
|
49
78
|
|
|
50
|
-
test(`Should process a playlist data string`,
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
test(`Should process a playlist data string`, done => {
|
|
80
|
+
const playlistFile = readFileSync(`${__dirname}/input/dummy.m3u`, {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
})
|
|
83
|
+
checker
|
|
84
|
+
.checkPlaylist(playlistFile)
|
|
85
|
+
.then(results => {
|
|
86
|
+
expect(resultTester(results)).toBeTruthy()
|
|
87
|
+
done()
|
|
88
|
+
})
|
|
89
|
+
.catch(done)
|
|
54
90
|
})
|
|
55
91
|
|
|
56
92
|
test(`Should throw with invalid input`, async () => {
|
|
@@ -62,7 +98,7 @@ test(`Should throw with invalid input`, async () => {
|
|
|
62
98
|
test(`Should throw with invalid file path`, async () => {
|
|
63
99
|
const badPath = `${__dirname}/input/badPath.m3u`
|
|
64
100
|
await expect(checker.checkPlaylist(badPath)).rejects.toThrow(
|
|
65
|
-
'
|
|
101
|
+
'Unable to parse a playlist'
|
|
66
102
|
)
|
|
67
103
|
})
|
|
68
104
|
|
|
@@ -74,6 +110,42 @@ test(`Should throw on URL fetch failure`, async () => {
|
|
|
74
110
|
|
|
75
111
|
test(`Should throw on invalid fetched input data`, async () => {
|
|
76
112
|
await expect(checker.checkPlaylist(`https://github.com`)).rejects.toThrow(
|
|
77
|
-
'URL is not an
|
|
113
|
+
'URL is not an M3U playlist file'
|
|
78
114
|
)
|
|
79
115
|
})
|
|
116
|
+
|
|
117
|
+
test(`Should handle request with forbidden HEAD method`, done => {
|
|
118
|
+
const url = 'https://live.ecomservice.bg/hls/stream.m3u8'
|
|
119
|
+
checker
|
|
120
|
+
.checkStream({ url, timeout: 2000 })
|
|
121
|
+
.then(results => {
|
|
122
|
+
expect(results.status.ok).toBe(true)
|
|
123
|
+
done()
|
|
124
|
+
})
|
|
125
|
+
.catch(done)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test(`Should handle HTTP_REQUEST_TIMEOUT`, done => {
|
|
129
|
+
const url = 'http://62.210.141.179:8000/live/ibrahim/123456/456.m3u8'
|
|
130
|
+
checker
|
|
131
|
+
.checkStream({ url, timeout: 2000 })
|
|
132
|
+
.then(results => {
|
|
133
|
+
expect(results.status.code).toBe('HTTP_REQUEST_TIMEOUT')
|
|
134
|
+
expect(results.status.message).toBe('HTTP 408 Request Timeout')
|
|
135
|
+
done()
|
|
136
|
+
})
|
|
137
|
+
.catch(done)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test(`Should handle HTTP_FORBIDDEN`, done => {
|
|
141
|
+
const url =
|
|
142
|
+
'https://artesimulcast.akamaized.net/hls/live/2030993/artelive_de/index.m3u8'
|
|
143
|
+
checker
|
|
144
|
+
.checkStream({ url, timeout: 2000 })
|
|
145
|
+
.then(results => {
|
|
146
|
+
expect(results.status.code).toBe('HTTP_FORBIDDEN')
|
|
147
|
+
expect(results.status.message).toBe('HTTP 403 Forbidden')
|
|
148
|
+
done()
|
|
149
|
+
})
|
|
150
|
+
.catch(done)
|
|
151
|
+
})
|
package/src/helper.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
const Axios = require('axios')
|
|
2
|
-
const util = require('util')
|
|
3
|
-
const { parse } = require('iptv-playlist-parser')
|
|
4
|
-
const { isWebUri } = require('valid-url')
|
|
5
|
-
const { existsSync, readFile } = require('fs')
|
|
6
|
-
const exec = require('child_process').exec
|
|
7
|
-
const execAsync = util.promisify(exec)
|
|
8
|
-
const readFileAsync = util.promisify(readFile)
|
|
9
|
-
|
|
10
|
-
let cache = new Set()
|
|
11
|
-
|
|
12
|
-
const axios = Axios.create({
|
|
13
|
-
method: 'GET',
|
|
14
|
-
timeout: 60000, // 60 second timeout
|
|
15
|
-
responseType: 'text',
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
axios.interceptors.response.use(
|
|
19
|
-
response => {
|
|
20
|
-
const { 'content-type': contentType = '' } = response.headers
|
|
21
|
-
if (!/mpegurl/.test(contentType)) {
|
|
22
|
-
throw new Error('URL is not an .m3u playlist file')
|
|
23
|
-
}
|
|
24
|
-
return response.data
|
|
25
|
-
},
|
|
26
|
-
() => {
|
|
27
|
-
let msg = `Error fetching playlist`
|
|
28
|
-
|
|
29
|
-
return Promise.reject(new Error(msg))
|
|
30
|
-
}
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
async function parsePlaylist(input) {
|
|
34
|
-
if (input instanceof Object && Reflect.has(input, `items`)) return input
|
|
35
|
-
|
|
36
|
-
let data = input
|
|
37
|
-
|
|
38
|
-
if (Buffer.isBuffer(input)) {
|
|
39
|
-
data = input.toString(`utf8`)
|
|
40
|
-
} else if (typeof input === `string`) {
|
|
41
|
-
if (isWebUri(input)) {
|
|
42
|
-
data = await axios(input)
|
|
43
|
-
} else if (existsSync(input)) {
|
|
44
|
-
data = await readFileAsync(input, { encoding: `utf8` })
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return parse(data)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function parseError(output, item) {
|
|
52
|
-
const url = item.url
|
|
53
|
-
const line = output.split('\n').find(l => {
|
|
54
|
-
return l.indexOf(url) === 0
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
if (!line) {
|
|
58
|
-
return 'Operation timed out'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return line.replace(`${url}: `, '')
|
|
62
|
-
}
|
|
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
|
-
|
|
99
|
-
function checkItem(item) {
|
|
100
|
-
const { config, logger } = this
|
|
101
|
-
|
|
102
|
-
const command = buildCommand(item, config)
|
|
103
|
-
|
|
104
|
-
logger.debug(`EXECUTING: "${command}"`)
|
|
105
|
-
|
|
106
|
-
return execAsync(command, { timeout: config.timeout })
|
|
107
|
-
.then(({ stdout, stderr }) => {
|
|
108
|
-
if (stdout && isJSON(stdout) && stderr) {
|
|
109
|
-
const metadata = JSON.parse(stdout)
|
|
110
|
-
if (!metadata.streams.length) {
|
|
111
|
-
return { ok: false, reason: 'No streams found' }
|
|
112
|
-
}
|
|
113
|
-
const results = parseStderr(stderr)
|
|
114
|
-
metadata.requests = results.requests
|
|
115
|
-
|
|
116
|
-
return { ok: true, metadata }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return { ok: false, reason: 'Parsing error' }
|
|
120
|
-
})
|
|
121
|
-
.catch(err => {
|
|
122
|
-
const reason = parseError(err.message, item)
|
|
123
|
-
|
|
124
|
-
return { ok: false, reason }
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function buildCommand(item, config) {
|
|
129
|
-
const userAgent = item.http['user-agent'] || config.userAgent
|
|
130
|
-
let args = [
|
|
131
|
-
`ffprobe`,
|
|
132
|
-
`-of json`,
|
|
133
|
-
`-v debug`,
|
|
134
|
-
`-hide_banner`,
|
|
135
|
-
`-show_streams`,
|
|
136
|
-
`-show_format`,
|
|
137
|
-
]
|
|
138
|
-
|
|
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}"`)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (userAgent) {
|
|
148
|
-
args.push(`-user_agent`, `"${userAgent}"`)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
args.push(`"${item.url}"`)
|
|
152
|
-
|
|
153
|
-
args = args.join(` `)
|
|
154
|
-
|
|
155
|
-
return args
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function hashUrl(u) {
|
|
159
|
-
return Buffer.from(u).toString(`hex`)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function addToCache({ url }) {
|
|
163
|
-
let id = hashUrl(url)
|
|
164
|
-
|
|
165
|
-
cache.add(id)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function checkCache({ url }) {
|
|
169
|
-
let id = hashUrl(url)
|
|
170
|
-
|
|
171
|
-
return cache.has(id)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function isJSON(str) {
|
|
175
|
-
try {
|
|
176
|
-
return !!JSON.parse(str)
|
|
177
|
-
} catch (e) {
|
|
178
|
-
return false
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
module.exports = {
|
|
183
|
-
addToCache,
|
|
184
|
-
checkCache,
|
|
185
|
-
parsePlaylist,
|
|
186
|
-
checkItem,
|
|
187
|
-
}
|