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.
@@ -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
- console.log(results)
52
+ console.log(results)
53
53
  })
54
54
 
55
55
  // using local path
56
56
  checker.checkPlaylist('path/to/playlist.m3u').then(results => {
57
- console.log(results)
57
+ console.log(results)
58
58
  })
59
59
 
60
60
  // using playlist as string
61
61
  checker.checkPlaylist(string).then(results => {
62
- console.log(results)
62
+ console.log(results)
63
63
  })
64
64
  ```
65
65
 
66
66
  #### Results
67
67
 
68
- On success:
68
+ _On success:_
69
69
 
70
- ```jsonc
70
+ ```js
71
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
- ]
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
- On error:
187
+ _On error:_
188
188
 
189
- ```jsonc
189
+ ```js
190
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
- ]
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).
@@ -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.reason !== `Duplicate`
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.reason === `Duplicate`
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.reason === `Duplicate`) {
114
+ } else if (item.status.code === `DUPLICATE`) {
117
115
  writeToFile(duplicatesFile, item)
118
116
  } else {
119
- writeToFile(offlineFile, item, item.status.reason)
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.23.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#readme",
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 helper = require('./helper')
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 (helper.checkCache(item)) {
60
+ if (cache.check(item)) {
55
61
  duplicates.push(item)
56
62
 
57
63
  return null
58
64
  } else {
59
- helper.addToCache(item)
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, reason: `Duplicate` }
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
- await this.config.beforeEach(item)
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
- this.logger.debug(`OK: ${item.url}`.green)
108
+ logger.debug(`OK: ${item.url}`.green)
98
109
  } else {
99
- this.logger.debug(`FAILED: ${item.url} (${item.status.reason})`.red)
110
+ logger.debug(`FAILED: ${item.url} (${item.status.message})`.red)
100
111
  }
101
112
 
102
- await this.config.afterEach(item)
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
+ }
@@ -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, `reason`) ||
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`, async () => {
20
+ test(`Should process a playlist URL`, done => {
24
21
  const url = 'https://iptv-org.github.io/iptv/languages/amh.m3u'
25
- const results = await checker.checkPlaylist(url)
22
+ checker
23
+ .checkPlaylist(url)
24
+ .then(results => {
25
+ expect(resultTester(results)).toBeTruthy()
26
+ done()
27
+ })
28
+ .catch(done)
29
+ })
26
30
 
27
- expect(resultTester(results)).toBeTruthy()
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`, async () => {
43
+ test(`Should process a relative playlist file path`, done => {
31
44
  const path = 'test/input/dummy.m3u'
32
- const results = await checker.checkPlaylist(path)
33
-
34
- expect(resultTester(results)).toBeTruthy()
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`, async () => {
38
- const results = await checker.checkPlaylist(playlistPath)
39
-
40
- expect(resultTester(results)).toBeTruthy()
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`, async () => {
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
- const results = await checker.checkPlaylist(playlistBuffer)
46
-
47
- expect(resultTester(results)).toBeTruthy()
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`, async () => {
51
- const results = await checker.checkPlaylist(playlistFile)
52
-
53
- expect(resultTester(results)).toBeTruthy()
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
- 'Playlist is not valid'
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 .m3u playlist file'
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
- }