iptv-checker-module
Version:
Check the status of IPTV channels in an .m3u playlist file
230 lines (186 loc) • 4.97 kB
JavaScript
const Axios = require('axios')
const { customAlphabet } = require('nanoid')
const util = require('util')
const { parse } = require('iptv-playlist-parser')
const { isWebUri } = require('valid-url')
const { existsSync, readFile } = require('fs')
const execAsync = util.promisify(require('child_process').exec)
const readFileAsync = util.promisify(readFile)
const nanoid = customAlphabet(
'1234567890abcdefghijklmnopqrstuvwxyz'.toUpperCase(),
6
)
const axios = Axios.create({
method: 'GET',
timeout: 6e4, // 60 second timeout
responseType: 'text',
})
axios.interceptors.response.use(
response => {
const { 'content-type': contentType = '' } = response.headers
if (!/mpegurl/.test(contentType)) {
throw new Error('URL is not an .m3u playlist file')
}
return response.data
},
error => {
let msg
if (error.response) {
msg = error.response.data
} else {
msg = `Error fetching playlist`
}
return Promise.reject(new Error(msg))
}
)
let cache = new Set()
function hashUrl(u) {
return Buffer.from(u).toString(`hex`)
}
function addToCache({ url }) {
let id = hashUrl(url)
cache.add(id)
}
function checkCache({ url }) {
let id = hashUrl(url)
return cache.has(id)
}
async function parsePlaylist(input) {
if (input instanceof Object && Reflect.has(input, `items`)) return input
let data = input
if (Buffer.isBuffer(input)) {
data = input.toString(`utf8`)
} else if (typeof input === `string`) {
if (isWebUri(input)) {
data = await axios(input)
} else if (existsSync(input)) {
data = await readFileAsync(input, { encoding: `utf8` })
}
}
return parse(data)
}
function parseMessage(reason, { url }) {
if (!reason) return
const msgArr = reason.split('\n')
if (msgArr.length === 0) return
const line = msgArr.find(line => {
return line.indexOf(url) === 0
})
if (!line) {
if (/^Command failed/.test(reason)) return `Timed out`
return reason
}
return line.replace(`${url}: `, '')
}
function debugLogger({ debug }) {
let envDebug = /iptv-checker-module/.test(String(process.env.DEBUG))
if (!debug && !envDebug) return () => {}
return msg => {
if (typeof msg === `object`) {
return console.log(JSON.stringify(msg, null, 1))
} else console.log(msg)
}
}
function isJSON(str) {
try {
return !!JSON.parse(str)
} catch (e) {
return false
}
}
function chunk(arr, size) {
return Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
arr.slice(i * size, i * size + size)
)
}
function orderBy(arr, props, orders) {
return [...arr].sort((a, b) =>
props.reduce((acc, prop, i) => {
if (acc === 0) {
const [p1, p2] =
orders && orders[i] === 'desc'
? [b[prop], a[prop]]
: [a[prop], b[prop]]
acc = p1 > p2 ? 1 : p1 < p2 ? -1 : 0
}
return acc
}, 0)
)
}
function checkItem(item) {
const { url, http = {} } = item
let { referrer = ``, 'user-agent': itemUserAgent = `` } = http
let {
config: { userAgent, timeout, useItemHttpHeaders },
debugLogger,
} = this
let args = [
`ffprobe`,
`-of json`,
`-v error`,
`-hide_banner`,
`-show_format`,
`-show_streams`,
]
/* ! Single-quote wrap all user input to prevent shell injection attacks */
if (useItemHttpHeaders) {
userAgent = itemUserAgent.length ? itemUserAgent : userAgent
if (referrer.length) {
args.push(`-headers`, `'Referer: ${referrer}'`)
}
}
if (userAgent) {
args.push(`-user_agent`, `'${userAgent}'`)
}
args.push(`'${url}'`)
args = args.join(` `)
debugLogger(`[${item.uid}] EXECUTING: "${args}"`)
return execAsync(args, { timeout })
.then(({ stdout }) => {
if (!isJSON(stdout)) {
return { ok: false, reason: parseMessage(stdout, item) }
}
const metadata = JSON.parse(stdout)
return { ok: true, metadata }
})
.catch(err => ({ ok: false, reason: parseMessage(err.message, item) }))
}
async function validateStatus(item) {
if (this.config.debug) item.uid = nanoid()
item.status = await checkItem.call(this, item)
if (item.status.ok) {
this.stats.online++
this.debugLogger(`[${item.uid}] OK: ${item.url}`.green)
} else {
this.stats.offline++
this.debugLogger(
`[${item.uid}] FAILED: ${item.url}`.red +
` (${item.status.reason})`.yellow
)
}
delete item.uid
await this.config.itemCallback.call(null, item)
return item
}
function statsLogger({ config, stats, debugLogger }) {
if (!config.debug) return
let colors = {
total: `white`,
online: `green`,
offline: `red`,
duplicates: `yellow`,
}
for (let [key, val] of Object.entries(stats)) {
debugLogger(`${key.toUpperCase()}: ${val}`[colors[key]])
}
}
module.exports = {
addToCache,
checkCache,
chunk,
debugLogger,
orderBy,
parsePlaylist,
statsLogger,
validateStatus,
}