mirror of https://github.com/iptv-org/iptv
Create scripts/commands
parent
b775228d1e
commit
213da67762
@ -0,0 +1,50 @@
|
||||
const { program } = require('commander')
|
||||
const { db, logger, timer, checker, store, file, parser } = require('../core')
|
||||
|
||||
const options = program
|
||||
.requiredOption('-c, --cluster-id <cluster-id>', 'The ID of cluster to load', parser.parseNumber)
|
||||
.option('-t, --timeout <timeout>', 'Set timeout for each request', parser.parseNumber, 60000)
|
||||
.option('-d, --delay <delay>', 'Set delay for each request', parser.parseNumber, 0)
|
||||
.option('--debug', 'Enable debug mode')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
const config = {
|
||||
timeout: options.timeout,
|
||||
delay: options.delay,
|
||||
debug: options.debug
|
||||
}
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
|
||||
async function main() {
|
||||
logger.info('Starting...')
|
||||
logger.info(`Timeout: ${options.timeout}ms`)
|
||||
logger.info(`Delay: ${options.delay}ms`)
|
||||
timer.start()
|
||||
|
||||
const clusterLog = `${LOGS_PATH}/check-streams/cluster_${options.clusterId}.log`
|
||||
logger.info(`Loading cluster: ${options.clusterId}`)
|
||||
logger.info(`Creating '${clusterLog}'...`)
|
||||
await file.create(clusterLog)
|
||||
const items = await db.find({ cluster_id: options.clusterId })
|
||||
const total = items.length
|
||||
logger.info(`Found ${total} links`)
|
||||
|
||||
logger.info('Checking...')
|
||||
const results = {}
|
||||
for (const [i, item] of items.entries()) {
|
||||
const message = `[${i + 1}/${total}] ${item.filepath}: ${item.url}`
|
||||
const result = await checker.check(item, config)
|
||||
if (!result.error) {
|
||||
logger.info(message)
|
||||
} else {
|
||||
logger.info(`${message} (${result.error})`)
|
||||
}
|
||||
await file.append(clusterLog, JSON.stringify(result) + '\n')
|
||||
}
|
||||
|
||||
logger.info(`Done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
||||
}
|
||||
|
||||
main()
|
@ -0,0 +1,14 @@
|
||||
const { db, logger } = require('../core')
|
||||
|
||||
async function main() {
|
||||
const removed = await db.remove(
|
||||
{ 'status.code': { $in: ['timeout', 'offline'] } },
|
||||
{ multi: true }
|
||||
)
|
||||
|
||||
db.compact()
|
||||
|
||||
logger.info(`Removed ${removed} links`)
|
||||
}
|
||||
|
||||
main()
|
@ -0,0 +1,104 @@
|
||||
const { db, file, parser, store, logger } = require('../core')
|
||||
const transliteration = require('transliteration')
|
||||
const { program } = require('commander')
|
||||
const _ = require('lodash')
|
||||
|
||||
const options = program
|
||||
.option(
|
||||
'--max-clusters <max-clusters>',
|
||||
'Set maximum number of clusters',
|
||||
parser.parseNumber,
|
||||
200
|
||||
)
|
||||
.option('--input-dir <input-dir>', 'Set path to input directory', 'channels')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
const links = []
|
||||
|
||||
async function main() {
|
||||
logger.info('Starting...')
|
||||
logger.info(`Number of clusters: ${options.maxClusters}`)
|
||||
|
||||
await loadChannels()
|
||||
await saveToDatabase()
|
||||
|
||||
logger.info('Done')
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function loadChannels() {
|
||||
logger.info(`Loading links...`)
|
||||
|
||||
const files = await file.list(`${options.inputDir}/**/*.m3u`)
|
||||
for (const filepath of files) {
|
||||
const items = await parser.parsePlaylist(filepath)
|
||||
for (const item of items) {
|
||||
item.filepath = filepath
|
||||
links.push(item)
|
||||
}
|
||||
}
|
||||
logger.info(`Found ${links.length} links`)
|
||||
}
|
||||
|
||||
async function saveToDatabase() {
|
||||
logger.info('Saving to the database...')
|
||||
|
||||
await db.reset()
|
||||
const chunks = split(_.shuffle(links), options.maxClusters)
|
||||
for (const [i, chunk] of chunks.entries()) {
|
||||
for (const item of chunk) {
|
||||
const stream = store.create()
|
||||
stream.set('name', { title: item.name })
|
||||
stream.set('id', { id: item.tvg.id })
|
||||
stream.set('filepath', { filepath: item.filepath })
|
||||
stream.set('src_country', { filepath: item.filepath })
|
||||
stream.set('tvg_country', { tvg_country: item.tvg.country })
|
||||
stream.set('countries', { tvg_country: item.tvg.country })
|
||||
stream.set('regions', { countries: stream.get('countries') })
|
||||
stream.set('languages', { tvg_language: item.tvg.language })
|
||||
stream.set('categories', { group_title: item.group.title })
|
||||
stream.set('tvg_url', { tvg_url: item.tvg.url })
|
||||
stream.set('guides', { tvg_url: item.tvg.url })
|
||||
stream.set('logo', { logo: item.tvg.logo })
|
||||
stream.set('resolution', { title: item.name })
|
||||
stream.set('status', { title: item.name })
|
||||
stream.set('url', { url: item.url })
|
||||
stream.set('http', { http: item.http })
|
||||
stream.set('is_nsfw', { categories: stream.get('categories') })
|
||||
stream.set('is_broken', { status: stream.get('status') })
|
||||
stream.set('updated', { updated: false })
|
||||
stream.set('cluster_id', { cluster_id: i + 1 })
|
||||
|
||||
if (!stream.get('id')) {
|
||||
const id = generateChannelId(stream.get('name'), stream.get('src_country'))
|
||||
stream.set('id', { id })
|
||||
}
|
||||
|
||||
await db.insert(stream.data())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function split(arr, n) {
|
||||
let result = []
|
||||
for (let i = n; i > 0; i--) {
|
||||
result.push(arr.splice(0, Math.ceil(arr.length / i)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function generateChannelId(name, src_country) {
|
||||
if (name && src_country) {
|
||||
const slug = transliteration
|
||||
.transliterate(name)
|
||||
.replace(/\+/gi, 'Plus')
|
||||
.replace(/[^a-z\d]+/gi, '')
|
||||
const code = src_country.code.toLowerCase()
|
||||
|
||||
return `${slug}.${code}`
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
const { logger, db } = require('../core')
|
||||
|
||||
async function main() {
|
||||
const docs = await db.find({}).sort({ cluster_id: 1 })
|
||||
const cluster_id = docs.reduce((acc, curr) => {
|
||||
if (!acc.includes(curr.cluster_id)) acc.push(curr.cluster_id)
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const matrix = { cluster_id }
|
||||
const output = `::set-output name=matrix::${JSON.stringify(matrix)}`
|
||||
logger.info(output)
|
||||
}
|
||||
|
||||
main()
|
@ -0,0 +1,321 @@
|
||||
const { db, logger, generator, file } = require('../core')
|
||||
const _ = require('lodash')
|
||||
|
||||
let languages = []
|
||||
let countries = []
|
||||
let categories = []
|
||||
let regions = []
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
const PUBLIC_PATH = process.env.PUBLIC_PATH || '.gh-pages'
|
||||
|
||||
async function main() {
|
||||
await setUp()
|
||||
|
||||
await generateCategories()
|
||||
await generateCountries()
|
||||
await generateLanguages()
|
||||
await generateRegions()
|
||||
await generateIndex()
|
||||
await generateIndexNSFW()
|
||||
await generateIndexCategory()
|
||||
await generateIndexCountry()
|
||||
await generateIndexLanguage()
|
||||
await generateIndexRegion()
|
||||
|
||||
await generateChannelsJson()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function generateCategories() {
|
||||
logger.info(`Generating categories/...`)
|
||||
|
||||
for (const category of categories) {
|
||||
const { count } = await generator.generate(
|
||||
`${PUBLIC_PATH}/categories/${category.slug}.m3u`,
|
||||
{ categories: { $elemMatch: category } },
|
||||
{ saveEmpty: true, includeNSFW: true }
|
||||
)
|
||||
|
||||
await log('categories', {
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: otherCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/categories/other.m3u`,
|
||||
{ categories: { $size: 0 } },
|
||||
{ saveEmpty: true }
|
||||
)
|
||||
|
||||
await log('categories', {
|
||||
name: 'Other',
|
||||
slug: 'other',
|
||||
count: otherCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateCountries() {
|
||||
logger.info(`Generating countries/...`)
|
||||
|
||||
for (const country of countries) {
|
||||
const { count } = await generator.generate(
|
||||
`${PUBLIC_PATH}/countries/${country.code.toLowerCase()}.m3u`,
|
||||
{
|
||||
countries: { $elemMatch: country }
|
||||
}
|
||||
)
|
||||
|
||||
await log('countries', {
|
||||
name: country.name,
|
||||
code: country.code,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: intCount } = await generator.generate(`${PUBLIC_PATH}/countries/int.m3u`, {
|
||||
tvg_country: 'INT'
|
||||
})
|
||||
|
||||
await log('countries', {
|
||||
name: 'International',
|
||||
code: 'INT',
|
||||
count: intCount
|
||||
})
|
||||
|
||||
const { count: undefinedCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/countries/undefined.m3u`,
|
||||
{
|
||||
countries: { $size: 0 }
|
||||
}
|
||||
)
|
||||
|
||||
await log('countries', {
|
||||
name: 'Undefined',
|
||||
code: 'UNDEFINED',
|
||||
count: undefinedCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateLanguages() {
|
||||
logger.info(`Generating languages/...`)
|
||||
|
||||
for (const language of _.uniqBy(languages, 'code')) {
|
||||
const { count } = await generator.generate(`${PUBLIC_PATH}/languages/${language.code}.m3u`, {
|
||||
languages: { $elemMatch: language }
|
||||
})
|
||||
|
||||
await log('languages', {
|
||||
name: language.name,
|
||||
code: language.code,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: undefinedCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/languages/undefined.m3u`,
|
||||
{
|
||||
languages: { $size: 0 }
|
||||
}
|
||||
)
|
||||
|
||||
await log('languages', {
|
||||
name: 'Undefined',
|
||||
code: 'undefined',
|
||||
count: undefinedCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateRegions() {
|
||||
logger.info(`Generating regions/...`)
|
||||
|
||||
for (const region of regions) {
|
||||
const { count } = await generator.generate(
|
||||
`${PUBLIC_PATH}/regions/${region.code.toLowerCase()}.m3u`,
|
||||
{
|
||||
regions: { $elemMatch: region }
|
||||
}
|
||||
)
|
||||
|
||||
await log('regions', {
|
||||
name: region.name,
|
||||
code: region.code,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: undefinedCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/regions/undefined.m3u`,
|
||||
{ regions: { $size: 0 } },
|
||||
{ saveEmpty: true }
|
||||
)
|
||||
|
||||
await log('regions', {
|
||||
name: 'Undefined',
|
||||
code: 'UNDEFINED',
|
||||
count: undefinedCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateIndexNSFW() {
|
||||
logger.info(`Generating index.nsfw.m3u...`)
|
||||
|
||||
await generator.generate(`${PUBLIC_PATH}/index.nsfw.m3u`, {}, { includeNSFW: true })
|
||||
}
|
||||
|
||||
async function generateIndex() {
|
||||
logger.info(`Generating index.m3u...`)
|
||||
|
||||
await generator.generate(`${PUBLIC_PATH}/index.m3u`, {})
|
||||
}
|
||||
|
||||
async function generateIndexCategory() {
|
||||
logger.info(`Generating index.category.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.category.m3u`,
|
||||
{},
|
||||
{ sortBy: item => item.group_title }
|
||||
)
|
||||
}
|
||||
|
||||
async function generateIndexCountry() {
|
||||
logger.info(`Generating index.country.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.country.m3u`,
|
||||
{},
|
||||
{
|
||||
onLoad: function (items) {
|
||||
let results = items
|
||||
.filter(item => !item.countries.length)
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = ''
|
||||
return newItem
|
||||
})
|
||||
for (const country of _.sortBy(Object.values(countries), ['name'])) {
|
||||
let filtered = items
|
||||
.filter(item => {
|
||||
return item.countries.map(c => c.code).includes(country.code)
|
||||
})
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = country.name
|
||||
return newItem
|
||||
})
|
||||
results = results.concat(filtered)
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
sortBy: item => item.group_title
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function generateIndexLanguage() {
|
||||
logger.info(`Generating index.language.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.language.m3u`,
|
||||
{},
|
||||
{
|
||||
onLoad: function (items) {
|
||||
let results = items
|
||||
.filter(item => !item.languages.length)
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = ''
|
||||
return newItem
|
||||
})
|
||||
for (const language of languages) {
|
||||
let filtered = items
|
||||
.filter(item => {
|
||||
return item.languages.map(c => c.code).includes(language.code)
|
||||
})
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = language.name
|
||||
return newItem
|
||||
})
|
||||
results = results.concat(filtered)
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
sortBy: item => item.group_title
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function generateIndexRegion() {
|
||||
logger.info(`Generating index.region.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.region.m3u`,
|
||||
{},
|
||||
{
|
||||
onLoad: function (items) {
|
||||
let results = items
|
||||
.filter(item => !item.regions.length)
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = ''
|
||||
return newItem
|
||||
})
|
||||
for (const region of regions) {
|
||||
let filtered = items
|
||||
.filter(item => {
|
||||
return item.regions.map(c => c.code).includes(region.code)
|
||||
})
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = region.name
|
||||
return newItem
|
||||
})
|
||||
results = results.concat(filtered)
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
sortBy: item => item.group_title
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function generateChannelsJson() {
|
||||
logger.info('Generating channels.json...')
|
||||
|
||||
await generator.generate(`${PUBLIC_PATH}/channels.json`, {}, { format: 'json' })
|
||||
}
|
||||
|
||||
async function setUp() {
|
||||
logger.info(`Loading database...`)
|
||||
const items = await db.find({})
|
||||
categories = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.categories)), 'slug'), ['name'])
|
||||
countries = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.countries)), 'code'), ['name'])
|
||||
languages = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.languages)), 'code'), ['name'])
|
||||
regions = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.regions)), 'code'), ['name'])
|
||||
|
||||
const categoriesLog = `${LOGS_PATH}/generate-playlists/categories.log`
|
||||
const countriesLog = `${LOGS_PATH}/generate-playlists/countries.log`
|
||||
const languagesLog = `${LOGS_PATH}/generate-playlists/languages.log`
|
||||
const regionsLog = `${LOGS_PATH}/generate-playlists/regions.log`
|
||||
|
||||
logger.info(`Creating '${categoriesLog}'...`)
|
||||
await file.create(categoriesLog)
|
||||
logger.info(`Creating '${countriesLog}'...`)
|
||||
await file.create(countriesLog)
|
||||
logger.info(`Creating '${languagesLog}'...`)
|
||||
await file.create(languagesLog)
|
||||
logger.info(`Creating '${regionsLog}'...`)
|
||||
await file.create(regionsLog)
|
||||
}
|
||||
|
||||
async function log(type, data) {
|
||||
await file.append(`${LOGS_PATH}/generate-playlists/${type}.log`, JSON.stringify(data) + '\n')
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
const _ = require('lodash')
|
||||
const statuses = require('../data/statuses')
|
||||
const languages = require('../data/languages')
|
||||
const { db, store, parser, file, logger } = require('../core')
|
||||
|
||||
let epgCodes = []
|
||||
let streams = []
|
||||
let checkResults = {}
|
||||
const origins = {}
|
||||
const items = []
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
const EPG_CODES_FILEPATH = process.env.EPG_CODES_FILEPATH || 'scripts/data/codes.json'
|
||||
|
||||
async function main() {
|
||||
await setUp()
|
||||
await loadDatabase()
|
||||
await loadCheckResults()
|
||||
await findStreamOrigins()
|
||||
await updateStreams()
|
||||
await updateDatabase()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function loadDatabase() {
|
||||
logger.info('Loading database...')
|
||||
|
||||
streams = await db.find({})
|
||||
|
||||
logger.info(`Found ${streams.length} streams`)
|
||||
}
|
||||
|
||||
async function loadCheckResults() {
|
||||
logger.info('Loading check results from logs/...')
|
||||
|
||||
const files = await file.list(`${LOGS_PATH}/check-streams/cluster_*.log`)
|
||||
for (const filepath of files) {
|
||||
const results = await parser.parseLogs(filepath)
|
||||
for (const result of results) {
|
||||
checkResults[result._id] = result
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Found ${Object.values(checkResults).length} results`)
|
||||
}
|
||||
|
||||
async function findStreamOrigins() {
|
||||
logger.info('Searching for stream origins...')
|
||||
|
||||
for (const { error, requests } of Object.values(checkResults)) {
|
||||
if (error || !Array.isArray(requests) || !requests.length) continue
|
||||
|
||||
let origin = requests.shift()
|
||||
origin = new URL(origin.url)
|
||||
for (const request of requests) {
|
||||
const curr = new URL(request.url)
|
||||
const key = curr.href.replace(/(^\w+:|^)/, '')
|
||||
if (!origins[key] && curr.host === origin.host) {
|
||||
origins[key] = origin.href
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Found ${_.uniq(Object.values(origins)).length} origins`)
|
||||
}
|
||||
|
||||
async function updateStreams() {
|
||||
logger.info('Updating streams...')
|
||||
|
||||
let updated = 0
|
||||
for (const item of streams) {
|
||||
const stream = store.create(item)
|
||||
const result = checkResults[item._id]
|
||||
|
||||
if (result) {
|
||||
const { error, streams, requests } = result
|
||||
const status = parseStatus(error)
|
||||
const resolution = parseResolution(streams)
|
||||
const origin = findOrigin(requests)
|
||||
|
||||
if (status) {
|
||||
stream.set('status', { status })
|
||||
stream.set('is_broken', { status: stream.get('status') })
|
||||
}
|
||||
|
||||
if (resolution) {
|
||||
stream.set('resolution', { resolution })
|
||||
}
|
||||
|
||||
if (origin) {
|
||||
stream.set('url', { url: origin })
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream.has('logo')) {
|
||||
const logo = findLogo(stream.get('id'))
|
||||
stream.set('logo', { logo })
|
||||
}
|
||||
|
||||
if (!stream.has('guides')) {
|
||||
const guides = findGuides(stream.get('id'))
|
||||
stream.set('guides', { guides })
|
||||
}
|
||||
|
||||
if (!stream.has('countries') && stream.get('src_country')) {
|
||||
const countries = [stream.get('src_country')]
|
||||
stream.set('countries', { countries })
|
||||
}
|
||||
|
||||
if (!stream.has('languages')) {
|
||||
const languages = findLanguages(stream.get('countries'), stream.get('src_country'))
|
||||
stream.set('languages', { languages })
|
||||
}
|
||||
|
||||
if (stream.changed) {
|
||||
stream.set('updated', true)
|
||||
items.push(stream.data())
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Updated ${updated} items`)
|
||||
}
|
||||
|
||||
async function updateDatabase() {
|
||||
logger.info('Updating database...')
|
||||
|
||||
for (const item of items) {
|
||||
await db.update({ _id: item._id }, item)
|
||||
}
|
||||
db.compact()
|
||||
|
||||
logger.info('Done')
|
||||
}
|
||||
|
||||
async function setUp() {
|
||||
try {
|
||||
const codes = await file.read(EPG_CODES_FILEPATH)
|
||||
epgCodes = JSON.parse(codes)
|
||||
} catch (err) {
|
||||
logger.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function findLanguages(countries, src_country) {
|
||||
if (countries && Array.isArray(countries)) {
|
||||
let codes = countries.map(country => country.lang)
|
||||
codes = _.uniq(codes)
|
||||
|
||||
return codes.map(code => languages.find(l => l.code === code)).filter(l => l)
|
||||
}
|
||||
|
||||
if (src_country) {
|
||||
const code = src_country.lang
|
||||
const lang = languages.find(l => l.code === code)
|
||||
|
||||
return lang ? [lang] : []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function findOrigin(requests) {
|
||||
if (origins && Array.isArray(requests)) {
|
||||
requests = requests.map(r => r.url.replace(/(^\w+:|^)/, ''))
|
||||
for (const url of requests) {
|
||||
if (origins[url]) {
|
||||
return origins[url]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseResolution(streams) {
|
||||
const resolution = streams
|
||||
.filter(s => s.codec_type === 'video')
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.height > acc.height) return { width: curr.width, height: curr.height }
|
||||
return acc
|
||||
},
|
||||
{ width: 0, height: 0 }
|
||||
)
|
||||
|
||||
if (resolution.width > 0 && resolution.height > 0) return resolution
|
||||
return null
|
||||
}
|
||||
|
||||
function parseStatus(error) {
|
||||
if (error) {
|
||||
if (error.includes('timed out')) {
|
||||
return statuses['timeout']
|
||||
} else if (error.includes('403')) {
|
||||
return statuses['geo_blocked']
|
||||
}
|
||||
return statuses['offline']
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findLogo(id) {
|
||||
const item = epgCodes.find(i => i.tvg_id === id)
|
||||
if (item && item.logo) {
|
||||
return item.logo
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findGuides(id) {
|
||||
const item = epgCodes.find(i => i.tvg_id === id)
|
||||
if (item && Array.isArray(item.guides)) {
|
||||
return item.guides
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
const _ = require('lodash')
|
||||
const { generator, db, logger } = require('../core')
|
||||
|
||||
async function main() {
|
||||
let items = await db
|
||||
.find({})
|
||||
.sort({ name: 1, 'status.level': 1, 'resolution.height': -1, url: 1 })
|
||||
items = _.uniqBy(items, 'url')
|
||||
const files = _.groupBy(items, 'filepath')
|
||||
|
||||
for (const filepath in files) {
|
||||
const items = files[filepath]
|
||||
await generator.saveAsM3U(filepath, items, { includeGuides: false })
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
@ -0,0 +1,142 @@
|
||||
const { file, markdown, parser, logger } = require('../core')
|
||||
const { program } = require('commander')
|
||||
|
||||
let categories = []
|
||||
let countries = []
|
||||
let languages = []
|
||||
let regions = []
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
|
||||
const options = program
|
||||
.option('-c, --config <config>', 'Set path to config file', '.readme/config.json')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
async function main() {
|
||||
await setUp()
|
||||
|
||||
await generateCategoryTable()
|
||||
await generateLanguageTable()
|
||||
await generateRegionTable()
|
||||
await generateCountryTable()
|
||||
|
||||
await updateReadme()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function generateCategoryTable() {
|
||||
logger.info('Generating category table...')
|
||||
|
||||
const rows = []
|
||||
for (const category of categories) {
|
||||
rows.push({
|
||||
category: category.name,
|
||||
channels: category.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/categories/${category.slug}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Category', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_categories.md', table)
|
||||
}
|
||||
|
||||
async function generateCountryTable() {
|
||||
logger.info('Generating country table...')
|
||||
|
||||
const rows = []
|
||||
for (const country of countries) {
|
||||
const flag = getCountryFlag(country.code)
|
||||
const prefix = flag ? `${flag} ` : ''
|
||||
|
||||
rows.push({
|
||||
country: prefix + country.name,
|
||||
channels: country.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/countries/${country.code.toLowerCase()}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Country', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_countries.md', table)
|
||||
}
|
||||
|
||||
async function generateRegionTable() {
|
||||
logger.info('Generating region table...')
|
||||
|
||||
const rows = []
|
||||
for (const region of regions) {
|
||||
rows.push({
|
||||
region: region.name,
|
||||
channels: region.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/regions/${region.code.toLowerCase()}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Region', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_regions.md', table)
|
||||
}
|
||||
|
||||
async function generateLanguageTable() {
|
||||
logger.info('Generating language table...')
|
||||
|
||||
const rows = []
|
||||
for (const language of languages) {
|
||||
rows.push({
|
||||
language: language.name,
|
||||
channels: language.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/languages/${language.code}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Language', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_languages.md', table)
|
||||
}
|
||||
|
||||
async function updateReadme() {
|
||||
logger.info('Updating README.md...')
|
||||
|
||||
const config = require(file.resolve(options.config))
|
||||
await file.createDir(file.dirname(config.build))
|
||||
await markdown.compile(options.config)
|
||||
}
|
||||
|
||||
async function setUp() {
|
||||
categories = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/categories.log`)
|
||||
countries = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/countries.log`)
|
||||
languages = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/languages.log`)
|
||||
regions = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/regions.log`)
|
||||
}
|
||||
|
||||
function getCountryFlag(code) {
|
||||
switch (code) {
|
||||
case 'UK':
|
||||
return '🇬🇧'
|
||||
case 'INT':
|
||||
return '🌍'
|
||||
case 'UNDEFINED':
|
||||
return ''
|
||||
default:
|
||||
return code.replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397))
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
const blocklist = require('../data/blocklist')
|
||||
const parser = require('iptv-playlist-parser')
|
||||
const { file, logger } = require('../core')
|
||||
const { program } = require('commander')
|
||||
|
||||
const options = program
|
||||
.option('--input-dir <input-dir>', 'Set path to input directory', 'channels')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
async function main() {
|
||||
const files = await file.list(`${options.inputDir}/**/*.m3u`)
|
||||
const errors = []
|
||||
for (const filepath of files) {
|
||||
const content = await file.read(filepath)
|
||||
const playlist = parser.parse(content)
|
||||
const basename = file.basename(filepath)
|
||||
const [_, country] = basename.match(/([a-z]{2})(|_.*)\.m3u/i) || [null, null]
|
||||
|
||||
const items = playlist.items
|
||||
.map(item => {
|
||||
const details = check(item, country)
|
||||
|
||||
return details ? { ...item, details } : null
|
||||
})
|
||||
.filter(i => i)
|
||||
|
||||
items.forEach(item => {
|
||||
errors.push(
|
||||
`${filepath}:${item.line} '${item.details.name}' is on the blocklist due to claims of copyright holders (${item.details.reference})`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
errors.forEach(error => {
|
||||
logger.error(error)
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
logger.info('')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function check(channel, country) {
|
||||
return blocklist.find(item => {
|
||||
const regexp = new RegExp(item.regex, 'i')
|
||||
const hasSameName = regexp.test(channel.name)
|
||||
const fromSameCountry = country === item.country.toLowerCase()
|
||||
|
||||
return hasSameName && fromSameCountry
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
Loading…
Reference in New Issue