mirror of https://github.com/iptv-org/iptv
Merge branch 'master' into Sphinxroot-patch-1
commit
736ba5b4f4
@ -0,0 +1,110 @@
|
||||
const { program } = require('commander')
|
||||
const ProgressBar = require('progress')
|
||||
const axios = require('axios')
|
||||
const https = require('https')
|
||||
const parser = require('./helpers/parser')
|
||||
const utils = require('./helpers/utils')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
program
|
||||
.usage('[OPTIONS]...')
|
||||
.option('-c, --country <country>', 'Comma-separated list of country codes', '')
|
||||
.option('-e, --exclude <exclude>', 'Comma-separated list of country codes to be excluded', '')
|
||||
.option('--delay <delay>', 'Delay between parser requests', 1000)
|
||||
.option('--timeout <timeout>', 'Set timeout for each request', 5000)
|
||||
.parse(process.argv)
|
||||
|
||||
const config = program.opts()
|
||||
const instance = axios.create({
|
||||
timeout: config.timeout,
|
||||
maxContentLength: 200000,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
})
|
||||
|
||||
async function main() {
|
||||
log.start()
|
||||
|
||||
log.print(`Parsing 'index.m3u'...\n`)
|
||||
let playlists = parser.parseIndex()
|
||||
playlists = utils
|
||||
.filterPlaylists(playlists, config.country, config.exclude)
|
||||
.filter(i => i.url !== 'channels/unsorted.m3u')
|
||||
|
||||
for (const playlist of playlists) {
|
||||
await parser
|
||||
.parsePlaylist(playlist.url)
|
||||
.then(detectResolution)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function detectResolution(playlist) {
|
||||
const channels = []
|
||||
const bar = new ProgressBar(`Processing '${playlist.url}': [:bar] :current/:total (:percent) `, {
|
||||
total: playlist.channels.length
|
||||
})
|
||||
let updated = false
|
||||
for (const channel of playlist.channels) {
|
||||
bar.tick()
|
||||
if (!channel.resolution.height) {
|
||||
const CancelToken = axios.CancelToken
|
||||
const source = CancelToken.source()
|
||||
const timeout = setTimeout(() => {
|
||||
source.cancel()
|
||||
}, config.timeout)
|
||||
|
||||
const response = await instance
|
||||
.get(channel.url, { cancelToken: source.token })
|
||||
.then(res => {
|
||||
clearTimeout(timeout)
|
||||
|
||||
return res
|
||||
})
|
||||
.then(utils.sleep(config.delay))
|
||||
.catch(err => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
|
||||
if (response && response.status === 200) {
|
||||
if (/^#EXTM3U/.test(response.data)) {
|
||||
const resolution = parseResolution(response.data)
|
||||
if (resolution) {
|
||||
channel.resolution = resolution
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channels.push(channel)
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
log.print(`File '${playlist.url}' has been updated\n`)
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
function parseResolution(string) {
|
||||
const regex = /RESOLUTION=(\d+)x(\d+)/gm
|
||||
const match = string.matchAll(regex)
|
||||
const arr = Array.from(match).map(m => ({
|
||||
width: parseInt(m[1]),
|
||||
height: parseInt(m[2])
|
||||
}))
|
||||
|
||||
return arr.length
|
||||
? arr.reduce(function (prev, current) {
|
||||
return prev.height > current.height ? prev : current
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
|
||||
main()
|
@ -1,67 +1,43 @@
|
||||
const parser = require('./parser')
|
||||
const utils = require('./utils')
|
||||
const blacklist = require('./blacklist.json')
|
||||
const blacklist = require('./helpers/blacklist.json')
|
||||
const parser = require('./helpers/parser')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
async function main() {
|
||||
const playlists = parseIndex()
|
||||
log.start()
|
||||
|
||||
log.print(`Parsing 'index.m3u'...`)
|
||||
const playlists = parser.parseIndex()
|
||||
for (const playlist of playlists) {
|
||||
await loadPlaylist(playlist.url).then(removeBlacklisted).then(savePlaylist).then(done)
|
||||
log.print(`\nProcessing '${playlist.url}'...`)
|
||||
await parser
|
||||
.parsePlaylist(playlist.url)
|
||||
.then(removeBlacklisted)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
function parseIndex() {
|
||||
console.info(`Parsing 'index.m3u'...`)
|
||||
let playlists = parser.parseIndex()
|
||||
console.info(`Found ${playlists.length} playlist(s)\n`)
|
||||
|
||||
return playlists
|
||||
}
|
||||
|
||||
async function loadPlaylist(url) {
|
||||
console.info(`Processing '${url}'...`)
|
||||
return parser.parsePlaylist(url)
|
||||
log.print('\n')
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function removeBlacklisted(playlist) {
|
||||
console.info(` Looking for blacklisted channels...`)
|
||||
playlist.channels = playlist.channels.filter(channel => {
|
||||
return !blacklist.find(i => {
|
||||
const channelName = channel.name.toLowerCase()
|
||||
return (
|
||||
(i.name.toLowerCase() === channelName ||
|
||||
i.aliases.map(i => i.toLowerCase()).includes(channelName)) &&
|
||||
i.country === channel.filename
|
||||
)
|
||||
const channels = playlist.channels.filter(channel => {
|
||||
return !blacklist.find(item => {
|
||||
const hasSameName =
|
||||
item.name.toLowerCase() === channel.name.toLowerCase() ||
|
||||
item.aliases.map(alias => alias.toLowerCase()).includes(channel.name.toLowerCase())
|
||||
const fromSameCountry = channel.countries.find(c => c.code === item.country)
|
||||
|
||||
return hasSameName && fromSameCountry
|
||||
})
|
||||
})
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
async function savePlaylist(playlist) {
|
||||
console.info(` Saving playlist...`)
|
||||
const original = utils.readFile(playlist.url)
|
||||
const output = playlist.toString({ raw: true })
|
||||
|
||||
if (original === output) {
|
||||
console.info(`No changes have been made.`)
|
||||
return false
|
||||
} else {
|
||||
utils.createFile(playlist.url, output)
|
||||
console.info(`Playlist has been updated.`)
|
||||
if (playlist.channels.length !== channels.length) {
|
||||
log.print(`updated`)
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function done() {
|
||||
console.info(` `)
|
||||
}
|
||||
|
||||
function finish() {
|
||||
console.info('Done.')
|
||||
return playlist
|
||||
}
|
||||
|
||||
main()
|
||||
|
@ -1,152 +1,55 @@
|
||||
const { program } = require('commander')
|
||||
const parser = require('./parser')
|
||||
const utils = require('./utils')
|
||||
const axios = require('axios')
|
||||
const ProgressBar = require('progress')
|
||||
const https = require('https')
|
||||
|
||||
program
|
||||
.usage('[OPTIONS]...')
|
||||
.option('-d, --debug', 'Debug mode')
|
||||
.option('-r --resolution', 'Parse stream resolution')
|
||||
.option('-c, --country <country>', 'Comma-separated list of country codes', '')
|
||||
.option('-e, --exclude <exclude>', 'Comma-separated list of country codes to be excluded', '')
|
||||
.option('--delay <delay>', 'Delay between parser requests', 1000)
|
||||
.option('--timeout <timeout>', 'Set timeout for each request', 5000)
|
||||
.parse(process.argv)
|
||||
|
||||
const config = program.opts()
|
||||
|
||||
const instance = axios.create({
|
||||
timeout: config.timeout,
|
||||
maxContentLength: 200000,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
})
|
||||
const parser = require('./helpers/parser')
|
||||
const utils = require('./helpers/utils')
|
||||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
async function main() {
|
||||
console.info('Starting...')
|
||||
console.time('Done in')
|
||||
|
||||
const playlists = parseIndex()
|
||||
log.start()
|
||||
|
||||
log.print(`Parsing 'index.m3u'...`)
|
||||
let playlists = parser.parseIndex().filter(i => i.url !== 'channels/unsorted.m3u')
|
||||
for (const playlist of playlists) {
|
||||
await loadPlaylist(playlist.url)
|
||||
.then(sortChannels)
|
||||
.then(detectResolution)
|
||||
.then(savePlaylist)
|
||||
.then(done)
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
function parseIndex() {
|
||||
console.info(`\nParsing 'index.m3u'...`)
|
||||
let playlists = parser.parseIndex()
|
||||
playlists = utils
|
||||
.filterPlaylists(playlists, config.country, config.exclude)
|
||||
.filter(i => i.url !== 'channels/unsorted.m3u')
|
||||
console.info(`Found ${playlists.length} playlist(s)\n`)
|
||||
|
||||
return playlists
|
||||
}
|
||||
|
||||
async function loadPlaylist(url) {
|
||||
console.info(`Processing '${url}'...`)
|
||||
return parser.parsePlaylist(url)
|
||||
}
|
||||
log.print(`\nProcessing '${playlist.url}'...`)
|
||||
await parser
|
||||
.parsePlaylist(playlist.url)
|
||||
.then(formatPlaylist)
|
||||
.then(playlist => {
|
||||
if (file.read(playlist.url) !== playlist.toString()) {
|
||||
log.print('updated')
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
async function sortChannels(playlist) {
|
||||
console.info(` Sorting channels...`)
|
||||
playlist.channels = utils.sortBy(playlist.channels, ['name', 'url'])
|
||||
playlist.save()
|
||||
})
|
||||
}
|
||||
|
||||
return playlist
|
||||
log.print('\n')
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function detectResolution(playlist) {
|
||||
if (!config.resolution) return playlist
|
||||
console.log(' Detecting resolution...')
|
||||
const bar = new ProgressBar(' Progress: [:bar] :current/:total (:percent) ', {
|
||||
total: playlist.channels.length
|
||||
})
|
||||
const results = []
|
||||
async function formatPlaylist(playlist) {
|
||||
for (const channel of playlist.channels) {
|
||||
bar.tick()
|
||||
if (!channel.resolution.height) {
|
||||
const CancelToken = axios.CancelToken
|
||||
const source = CancelToken.source()
|
||||
const timeout = setTimeout(() => {
|
||||
source.cancel()
|
||||
}, config.timeout)
|
||||
|
||||
const response = await instance
|
||||
.get(channel.url, { cancelToken: source.token })
|
||||
.then(res => {
|
||||
clearTimeout(timeout)
|
||||
|
||||
return res
|
||||
})
|
||||
.then(utils.sleep(config.delay))
|
||||
.catch(err => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
|
||||
if (response && response.status === 200) {
|
||||
if (/^#EXTM3U/.test(response.data)) {
|
||||
const resolution = parseResolution(response.data)
|
||||
if (resolution) {
|
||||
channel.resolution = resolution
|
||||
}
|
||||
}
|
||||
}
|
||||
const code = file.getBasename(playlist.url)
|
||||
// add missing tvg-name
|
||||
if (!channel.tvg.name && code !== 'unsorted' && channel.name) {
|
||||
channel.tvg.name = channel.name.replace(/\"/gi, '')
|
||||
}
|
||||
|
||||
results.push(channel)
|
||||
// add missing tvg-id
|
||||
if (!channel.tvg.id && code !== 'unsorted' && channel.tvg.name) {
|
||||
const id = utils.name2id(channel.tvg.name)
|
||||
channel.tvg.id = id ? `${id}.${code}` : ''
|
||||
}
|
||||
// add missing country
|
||||
if (!channel.countries.length) {
|
||||
const name = utils.code2name(code)
|
||||
channel.countries = name ? [{ code, name }] : []
|
||||
channel.tvg.country = channel.countries.map(c => c.code.toUpperCase()).join(';')
|
||||
}
|
||||
// update group-title
|
||||
channel.group.title = channel.category
|
||||
}
|
||||
|
||||
playlist.channels = results
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
function parseResolution(string) {
|
||||
const regex = /RESOLUTION=(\d+)x(\d+)/gm
|
||||
const match = string.matchAll(regex)
|
||||
const arr = Array.from(match).map(m => ({
|
||||
width: parseInt(m[1]),
|
||||
height: parseInt(m[2])
|
||||
}))
|
||||
|
||||
return arr.length
|
||||
? arr.reduce(function (prev, current) {
|
||||
return prev.height > current.height ? prev : current
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
|
||||
async function savePlaylist(playlist) {
|
||||
const original = utils.readFile(playlist.url)
|
||||
const output = playlist.toString()
|
||||
|
||||
if (original === output) {
|
||||
console.info(`No changes have been made.`)
|
||||
return false
|
||||
} else {
|
||||
utils.createFile(playlist.url, output)
|
||||
console.info(`Playlist has been updated.`)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function done() {
|
||||
console.info(` `)
|
||||
}
|
||||
|
||||
function finish() {
|
||||
console.timeEnd('Done in')
|
||||
}
|
||||
|
||||
main()
|
||||
|
@ -0,0 +1,145 @@
|
||||
const categories = require('./categories')
|
||||
const utils = require('./utils')
|
||||
const file = require('./file')
|
||||
|
||||
const sfwCategories = categories.filter(c => !c.nsfw).map(c => c.name)
|
||||
const nsfwCategories = categories.filter(c => c.nsfw).map(c => c.name)
|
||||
|
||||
module.exports = class Channel {
|
||||
constructor(data) {
|
||||
this.raw = data.raw
|
||||
this.tvg = data.tvg
|
||||
this.http = data.http
|
||||
this.url = data.url
|
||||
this.logo = data.tvg.logo
|
||||
this.group = data.group
|
||||
this.name = this.parseName(data.name)
|
||||
this.status = this.parseStatus(data.name)
|
||||
this.resolution = this.parseResolution(data.name)
|
||||
this.category = this.parseCategory(data.group.title)
|
||||
this.countries = this.parseCountries(data.tvg.country)
|
||||
this.languages = this.parseLanguages(data.tvg.language)
|
||||
}
|
||||
|
||||
parseName(title) {
|
||||
return title
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map(s => s.trim())
|
||||
.filter(s => {
|
||||
return !/\[|\]/i.test(s) && !/\((\d+)P\)/i.test(s)
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
parseStatus(title) {
|
||||
const match = title.match(/\[(.*)\]/i)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
parseResolution(title) {
|
||||
const match = title.match(/\((\d+)P\)/i)
|
||||
const height = match ? parseInt(match[1]) : null
|
||||
|
||||
return { width: null, height }
|
||||
}
|
||||
|
||||
parseCategory(string) {
|
||||
const category = categories.find(c => c.id === string.toLowerCase())
|
||||
if (!category) return ''
|
||||
|
||||
return category.name
|
||||
}
|
||||
|
||||
parseCountries(string) {
|
||||
const list = string.split(';')
|
||||
return list
|
||||
.reduce((acc, curr) => {
|
||||
const codes = utils.region2codes(curr)
|
||||
if (codes.length) {
|
||||
for (let code of codes) {
|
||||
if (!acc.includes(code)) {
|
||||
acc.push(code)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
acc.push(curr)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
.map(code => {
|
||||
const name = code ? utils.code2name(code) : null
|
||||
if (!name) return null
|
||||
|
||||
return { code: code.toLowerCase(), name }
|
||||
})
|
||||
.filter(c => c)
|
||||
}
|
||||
|
||||
parseLanguages(string) {
|
||||
const list = string.split(';')
|
||||
return list
|
||||
.map(name => {
|
||||
const code = name ? utils.language2code(name) : null
|
||||
if (!code) return null
|
||||
|
||||
return { code, name }
|
||||
})
|
||||
.filter(l => l)
|
||||
}
|
||||
|
||||
isSFW() {
|
||||
return sfwCategories.includes(this.category)
|
||||
}
|
||||
|
||||
isNSFW() {
|
||||
return nsfwCategories.includes(this.category)
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
let info = `-1 tvg-id="${this.tvg.id}" tvg-name="${this.tvg.name}" tvg-country="${this.tvg.country}" tvg-language="${this.tvg.language}" tvg-logo="${this.logo}"`
|
||||
|
||||
info += ` group-title="${this.group.title}",${this.name}`
|
||||
|
||||
if (this.resolution.height) {
|
||||
info += ` (${this.resolution.height}p)`
|
||||
}
|
||||
|
||||
if (this.status) {
|
||||
info += ` [${this.status}]`
|
||||
}
|
||||
|
||||
if (this.http['referrer']) {
|
||||
info += `\n#EXTVLCOPT:http-referrer=${this.http['referrer']}`
|
||||
}
|
||||
|
||||
if (this.http['user-agent']) {
|
||||
info += `\n#EXTVLCOPT:http-user-agent=${this.http['user-agent']}`
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
toString(raw = false) {
|
||||
if (raw) return this.raw + '\n'
|
||||
|
||||
return '#EXTINF:' + this.getInfo() + '\n' + this.url + '\n'
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return {
|
||||
name: this.name,
|
||||
logo: this.logo || null,
|
||||
url: this.url,
|
||||
category: this.category || null,
|
||||
languages: this.languages,
|
||||
countries: this.countries,
|
||||
tvg: {
|
||||
id: this.tvg.id || null,
|
||||
name: this.tvg.name || null,
|
||||
url: this.tvg.url || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
const Channel = require('./Channel')
|
||||
const file = require('./file')
|
||||
|
||||
module.exports = class Playlist {
|
||||
constructor({ header, items, url, name, country }) {
|
||||
this.url = url
|
||||
this.name = name
|
||||
this.country = country
|
||||
this.header = header
|
||||
this.channels = items.map(item => new Channel(item)).filter(channel => channel.url)
|
||||
this.updated = false
|
||||
}
|
||||
|
||||
toString(options = {}) {
|
||||
const config = { raw: false, ...options }
|
||||
let parts = ['#EXTM3U']
|
||||
for (let key in this.header.attrs) {
|
||||
let value = this.header.attrs[key]
|
||||
if (value) {
|
||||
parts.push(`${key}="${value}"`)
|
||||
}
|
||||
}
|
||||
|
||||
let output = `${parts.join(' ')}\n`
|
||||
for (let channel of this.channels) {
|
||||
output += channel.toString(config.raw)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.updated) {
|
||||
file.create(this.url, this.toString())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
const markdownInclude = require('markdown-include')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const rootPath = path.resolve(__dirname) + '/../../'
|
||||
const file = {}
|
||||
|
||||
file.getBasename = function (filename) {
|
||||
return path.basename(filename, path.extname(filename))
|
||||
}
|
||||
|
||||
file.getFilename = function (filename) {
|
||||
return path.parse(filename).name
|
||||
}
|
||||
|
||||
file.createDir = function (dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
file.read = function (filename) {
|
||||
return fs.readFileSync(rootPath + filename, { encoding: 'utf8' })
|
||||
}
|
||||
|
||||
file.append = function (filename, data) {
|
||||
fs.appendFileSync(rootPath + filename, data)
|
||||
}
|
||||
|
||||
file.create = function (filename, data = '') {
|
||||
fs.writeFileSync(rootPath + filename, data)
|
||||
}
|
||||
|
||||
file.compileMarkdown = function (filename) {
|
||||
markdownInclude.compileFiles(rootPath + filename)
|
||||
}
|
||||
|
||||
module.exports = file
|
@ -0,0 +1,16 @@
|
||||
const log = {}
|
||||
|
||||
log.print = function (string) {
|
||||
process.stdout.write(string)
|
||||
}
|
||||
|
||||
log.start = function () {
|
||||
this.print('Starting...\n')
|
||||
console.time('Done in')
|
||||
}
|
||||
|
||||
log.finish = function () {
|
||||
console.timeEnd('Done in')
|
||||
}
|
||||
|
||||
module.exports = log
|
@ -0,0 +1,24 @@
|
||||
const playlistParser = require('iptv-playlist-parser')
|
||||
const Playlist = require('./Playlist')
|
||||
const utils = require('./utils')
|
||||
const file = require('./file')
|
||||
|
||||
const parser = {}
|
||||
|
||||
parser.parseIndex = function () {
|
||||
const content = file.read('index.m3u')
|
||||
const result = playlistParser.parse(content)
|
||||
|
||||
return result.items
|
||||
}
|
||||
|
||||
parser.parsePlaylist = async function (url) {
|
||||
const content = file.read(url)
|
||||
const result = playlistParser.parse(content)
|
||||
const name = file.getFilename(url)
|
||||
const country = utils.code2name(name)
|
||||
|
||||
return new Playlist({ header: result.header, items: result.items, url, country, name })
|
||||
}
|
||||
|
||||
module.exports = parser
|
@ -1,244 +0,0 @@
|
||||
const playlistParser = require('iptv-playlist-parser')
|
||||
const utils = require('./utils')
|
||||
const categories = require('./categories')
|
||||
const path = require('path')
|
||||
|
||||
const sfwCategories = categories.filter(c => !c.nsfw).map(c => c.name)
|
||||
const nsfwCategories = categories.filter(c => c.nsfw).map(c => c.name)
|
||||
|
||||
const parser = {}
|
||||
|
||||
parser.parseIndex = function () {
|
||||
const content = utils.readFile('index.m3u')
|
||||
const result = playlistParser.parse(content)
|
||||
|
||||
return result.items
|
||||
}
|
||||
|
||||
parser.parsePlaylist = function (filename) {
|
||||
const content = utils.readFile(filename)
|
||||
const result = playlistParser.parse(content)
|
||||
const name = path.parse(filename).name
|
||||
const country = utils.code2name(name)
|
||||
|
||||
return new Playlist({ header: result.header, items: result.items, url: filename, country, name })
|
||||
}
|
||||
|
||||
class Playlist {
|
||||
constructor({ header, items, url, name, country }) {
|
||||
this.url = url
|
||||
this.name = name
|
||||
this.country = country
|
||||
this.header = header
|
||||
this.channels = items
|
||||
.map(item => new Channel({ data: item, header, sourceUrl: url }))
|
||||
.filter(channel => channel.url)
|
||||
}
|
||||
|
||||
toString(options = {}) {
|
||||
const config = { raw: false, ...options }
|
||||
let parts = ['#EXTM3U']
|
||||
for (let key in this.header.attrs) {
|
||||
let value = this.header.attrs[key]
|
||||
if (value) {
|
||||
parts.push(`${key}="${value}"`)
|
||||
}
|
||||
}
|
||||
|
||||
let output = `${parts.join(' ')}\n`
|
||||
for (let channel of this.channels) {
|
||||
output += channel.toString(config.raw)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
class Channel {
|
||||
constructor({ data, header, sourceUrl }) {
|
||||
this.parseData(data)
|
||||
|
||||
this.filename = utils.getBasename(sourceUrl)
|
||||
if (!this.countries.length) {
|
||||
const countryName = utils.code2name(this.filename)
|
||||
this.countries = countryName ? [{ code: this.filename, name: countryName }] : []
|
||||
this.tvg.country = this.countries.map(c => c.code.toUpperCase()).join(';')
|
||||
}
|
||||
}
|
||||
|
||||
parseData(data) {
|
||||
const title = this.parseTitle(data.name)
|
||||
|
||||
this.tvg = data.tvg
|
||||
this.http = data.http
|
||||
this.url = data.url
|
||||
this.logo = data.tvg.logo
|
||||
this.name = title.channelName
|
||||
this.status = title.streamStatus
|
||||
this.resolution = title.streamResolution
|
||||
this.countries = this.parseCountries(data.tvg.country)
|
||||
this.languages = this.parseLanguages(data.tvg.language)
|
||||
this.category = this.parseCategory(data.group.title)
|
||||
this.raw = data.raw
|
||||
}
|
||||
|
||||
parseCountries(string) {
|
||||
let arr = string
|
||||
.split(';')
|
||||
.reduce((acc, curr) => {
|
||||
const codes = utils.region2codes(curr)
|
||||
if (codes.length) {
|
||||
for (let code of codes) {
|
||||
if (!acc.includes(code)) {
|
||||
acc.push(code)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
acc.push(curr)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
.filter(code => code && utils.code2name(code))
|
||||
|
||||
return arr.map(code => {
|
||||
return { code: code.toLowerCase(), name: utils.code2name(code) }
|
||||
})
|
||||
}
|
||||
|
||||
parseLanguages(string) {
|
||||
return string
|
||||
.split(';')
|
||||
.map(name => {
|
||||
const code = name ? utils.language2code(name) : null
|
||||
if (!code) return null
|
||||
|
||||
return {
|
||||
code,
|
||||
name
|
||||
}
|
||||
})
|
||||
.filter(l => l)
|
||||
}
|
||||
|
||||
parseCategory(string) {
|
||||
const category = categories.find(c => c.id === string.toLowerCase())
|
||||
|
||||
return category ? category.name : ''
|
||||
}
|
||||
|
||||
parseTitle(title) {
|
||||
const channelName = title
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map(s => s.trim())
|
||||
.filter(s => {
|
||||
return !/\[|\]/i.test(s) && !/\((\d+)P\)/i.test(s)
|
||||
})
|
||||
.join(' ')
|
||||
|
||||
const streamStatusMatch = title.match(/\[(.*)\]/i)
|
||||
const streamStatus = streamStatusMatch ? streamStatusMatch[1] : null
|
||||
|
||||
const streamResolutionMatch = title.match(/\((\d+)P\)/i)
|
||||
const streamResolutionHeight = streamResolutionMatch ? parseInt(streamResolutionMatch[1]) : null
|
||||
const streamResolution = { width: null, height: streamResolutionHeight }
|
||||
|
||||
return { channelName, streamStatus, streamResolution }
|
||||
}
|
||||
|
||||
get tvgCountry() {
|
||||
return this.tvg.country
|
||||
.split(';')
|
||||
.map(code => utils.code2name(code))
|
||||
.join(';')
|
||||
}
|
||||
|
||||
get tvgLanguage() {
|
||||
return this.tvg.language
|
||||
}
|
||||
|
||||
get tvgUrl() {
|
||||
return this.tvg.id && this.tvg.url ? this.tvg.url : ''
|
||||
}
|
||||
|
||||
get tvgId() {
|
||||
if (this.tvg.id) {
|
||||
return this.tvg.id
|
||||
} else if (this.filename !== 'unsorted') {
|
||||
const id = utils.name2id(this.tvgName)
|
||||
|
||||
return id ? `${id}.${this.filename}` : ''
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
get tvgName() {
|
||||
if (this.tvg.name) {
|
||||
return this.tvg.name
|
||||
} else if (this.filename !== 'unsorted') {
|
||||
return this.name.replace(/\"/gi, '')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
this.tvg.country = this.tvg.country.toUpperCase()
|
||||
|
||||
let info = `-1 tvg-id="${this.tvgId}" tvg-name="${this.tvgName}" tvg-country="${this.tvg.country}" tvg-language="${this.tvg.language}" tvg-logo="${this.logo}"`
|
||||
|
||||
info += ` group-title="${this.category}",${this.name}`
|
||||
|
||||
if (this.resolution.height) {
|
||||
info += ` (${this.resolution.height}p)`
|
||||
}
|
||||
|
||||
if (this.status) {
|
||||
info += ` [${this.status}]`
|
||||
}
|
||||
|
||||
if (this.http['referrer']) {
|
||||
info += `\n#EXTVLCOPT:http-referrer=${this.http['referrer']}`
|
||||
}
|
||||
|
||||
if (this.http['user-agent']) {
|
||||
info += `\n#EXTVLCOPT:http-user-agent=${this.http['user-agent']}`
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
toString(raw = false) {
|
||||
if (raw) return this.raw + '\n'
|
||||
|
||||
return '#EXTINF:' + this.getInfo() + '\n' + this.url + '\n'
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return {
|
||||
name: this.name,
|
||||
logo: this.logo || null,
|
||||
url: this.url,
|
||||
category: this.category || null,
|
||||
languages: this.languages,
|
||||
countries: this.countries,
|
||||
tvg: {
|
||||
id: this.tvgId || null,
|
||||
name: this.tvgName || null,
|
||||
url: this.tvgUrl || null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isSFW() {
|
||||
return sfwCategories.includes(this.category)
|
||||
}
|
||||
|
||||
isNSFW() {
|
||||
return nsfwCategories.includes(this.category)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = parser
|
@ -0,0 +1,35 @@
|
||||
const parser = require('./helpers/parser')
|
||||
const utils = require('./helpers/utils')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
async function main() {
|
||||
log.start()
|
||||
|
||||
log.print(`Parsing 'index.m3u'...`)
|
||||
let playlists = parser.parseIndex().filter(i => i.url !== 'channels/unsorted.m3u')
|
||||
for (const playlist of playlists) {
|
||||
log.print(`\nProcessing '${playlist.url}'...`)
|
||||
await parser
|
||||
.parsePlaylist(playlist.url)
|
||||
.then(sortChannels)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
log.print('\n')
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function sortChannels(playlist) {
|
||||
const channels = [...playlist.channels]
|
||||
utils.sortBy(channels, ['name', 'url'])
|
||||
|
||||
if (JSON.stringify(channels) !== JSON.stringify(playlist.channels)) {
|
||||
log.print('updated')
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
main()
|
Loading…
Reference in New Issue