This commit is contained in:
Aleksandr Statciuk 2022-02-07 00:04:56 +03:00
parent d604f35ba1
commit 9a4a62fd10
45 changed files with 733 additions and 35060 deletions

View file

@ -1,109 +1,90 @@
const { db, logger, generator, file } = require('../core')
const { create: createPlaylist } = require('../core/playlist')
const { db, logger, generator, file, api } = 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()
const streams = await loadStreams()
await generateCategories()
await generateCountries()
await generateLanguages()
await generateRegions()
await generateIndex()
await generateIndexNSFW()
await generateIndexCategory()
await generateIndexCountry()
await generateIndexLanguage()
await generateIndexRegion()
logger.info(`generating categories/...`)
await generator.generate('categories', streams)
await generateChannelsJson()
// await generateCountries(streams)
// await generateLanguages()
// await generateRegions()
// await generateIndex()
// await generateIndexNSFW()
// await generateIndexCategory()
// await generateIndexCountry()
// await generateIndexLanguage()
// await generateIndexRegion()
// await generateChannelsJson()
// await saveLogs()
}
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,
onLoad: function (items) {
return items.map(item => {
item.group_title = 'Other'
return item
})
}
}
)
await log('categories', {
name: 'Other',
slug: 'other',
count: otherCount
})
}
async function generateCountries() {
logger.info(`Generating countries/...`)
async function generateCountries(streams) {
logger.info(`generating countries/...`)
const countries = await loadCountries()
const regions = await loadRegions()
for (const country of countries) {
const { count } = await generator.generate(
let areaCodes = _.filter(regions, { countries: [country.code] }).map(r => r.code)
areaCodes.push(country.code)
const { count, items } = await generator.generate(
`${PUBLIC_PATH}/countries/${country.code.toLowerCase()}.m3u`,
streams,
{
countries: { $elemMatch: country }
public: true,
filter: s => _.intersection(areaCodes, s.broadcast_area).length
}
)
await log('countries', {
log.countries.push({
name: country.name,
code: country.code,
count
})
}
const { count: undefinedCount } = await generator.generate(
`${PUBLIC_PATH}/countries/undefined.m3u`,
{
countries: { $size: 0 }
},
{
onLoad: function (items) {
return items.map(item => {
item.group_title = 'Undefined'
return item
})
}
const { count } = await generator.generate(`${PUBLIC_PATH}/countries/undefined.m3u`, streams, {
public: true,
filter: s => !s.broadcast_area.length,
onLoad: items => {
return items.map(item => {
item.group_title = 'Undefined'
return item
})
}
)
await log('countries', {
name: 'Undefined',
code: 'UNDEFINED',
count: undefinedCount
})
log.countries.push({
name: 'Undefined',
id: 'UNDEFINED',
count
})
// const { count: undefinedCount } = await generator.generate(
// `${PUBLIC_PATH}/countries/undefined.m3u`,
// {
// countries: { $size: 0 }
// },
// {
// onLoad: function (items) {
// return items.map(item => {
// item.group_title = 'Undefined'
// return item
// })
// }
// }
// )
// await log('countries', {
// name: 'Undefined',
// code: 'UNDEFINED',
// count: undefinedCount
// })
}
async function generateLanguages() {
@ -395,47 +376,44 @@ async function generateIndexRegion() {
)
}
async function generateChannelsJson() {
logger.info('Generating channels.json...')
async function loadStreams() {
await api.channels.load()
let channels = await api.channels.all()
channels = _.keyBy(channels, 'id')
await generator.generate(
`${PUBLIC_PATH}/channels.json`,
{},
{ format: 'json', includeNSFW: true, uniqBy: null }
)
}
async function setUp() {
logger.info(`Loading database...`)
const items = await db.find({})
categories = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.categories)), 'slug'), ['name']).filter(
i => i
)
countries = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.countries)), 'code'), ['name']).filter(
i => i
)
languages = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.languages)), 'code'), ['name']).filter(
i => i
)
regions = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.regions)), 'code'), ['name']).filter(
i => i
)
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')
await api.countries.load()
let countries = await api.countries.all()
countries = _.keyBy(countries, 'code')
await api.categories.load()
let categories = await api.categories.all()
categories = _.keyBy(categories, 'id')
await api.languages.load()
let languages = await api.languages.all()
languages = _.keyBy(languages, 'code')
await api.guides.load()
let guides = await api.guides.all()
guides = _.groupBy(guides, 'channel')
await db.streams.load()
let streams = await db.streams.find({})
return streams.map(stream => {
const channel = channels[stream.channel_id] || null
stream.channel = channel
stream.broadcast_area = channel
? channel.broadcast_area.map(item => {
const [_, code] = item.split('/')
return code
})
: []
stream.categories = channel ? channel.categories.map(id => categories[id]) : []
stream.languages = channel ? channel.languages.map(code => languages[code]) : []
stream.guides = guides[stream.channel_id] ? guides[stream.channel_id].map(g => g.url) : []
return stream
})
}

View file

@ -16,11 +16,25 @@ class API {
find(query) {
return _.find(this.collection, query)
}
filter(query) {
return _.filter(this.collection, query)
}
all() {
return this.collection
}
}
const api = {}
api.channels = new API(`${DATA_DIR}/channels.json`)
api.countries = new API(`${DATA_DIR}/countries.json`)
api.guides = new API(`${DATA_DIR}/guides.json`)
api.categories = new API(`${DATA_DIR}/categories.json`)
api.languages = new API(`${DATA_DIR}/languages.json`)
api.regions = new API(`${DATA_DIR}/regions.json`)
api.statuses = new API(`${DATA_DIR}/statuses.json`)
api.blocklist = new API(`${DATA_DIR}/blocklist.json`)
module.exports = api

View file

@ -1,61 +1,75 @@
const Database = require('nedb-promises')
const nedb = require('nedb-promises')
const file = require('./file')
const DB_FILEPATH = process.env.DB_FILEPATH || './scripts/channels.db'
const DB_DIR = process.env.DB_DIR || './scripts/database'
const nedb = Database.create({
filename: file.resolve(DB_FILEPATH),
autoload: true,
onload(err) {
if (err) console.error(err)
},
compareStrings: (a, b) => {
a = a.replace(/\s/g, '_')
b = b.replace(/\s/g, '_')
class Database {
constructor(filepath) {
this.filepath = filepath
}
return a.localeCompare(b, undefined, {
sensitivity: 'accent',
numeric: true
load() {
this.db = nedb.create({
filename: file.resolve(this.filepath),
autoload: true,
onload: err => {
if (err) console.error(err)
},
compareStrings: (a, b) => {
a = a.replace(/\s/g, '_')
b = b.replace(/\s/g, '_')
return a.localeCompare(b, undefined, {
sensitivity: 'accent',
numeric: true
})
}
})
}
})
removeIndex(field) {
return this.db.removeIndex(field)
}
addIndex(options) {
return this.db.ensureIndex(options)
}
compact() {
return this.db.persistence.compactDatafile()
}
stopAutocompact() {
return this.db.persistence.stopAutocompaction()
}
reset() {
return file.clear(this.filepath)
}
count(query) {
return this.db.count(query)
}
insert(doc) {
return this.db.insert(doc)
}
update(query, update) {
return this.db.update(query, update)
}
find(query) {
return this.db.find(query)
}
remove(query, options) {
return this.db.remove(query, options)
}
}
const db = {}
db.removeIndex = function (field) {
return nedb.removeIndex(field)
}
db.addIndex = function (options) {
return nedb.ensureIndex(options)
}
db.compact = function () {
return nedb.persistence.compactDatafile()
}
db.reset = function () {
return file.clear(DB_FILEPATH)
}
db.count = function (query) {
return nedb.count(query)
}
db.insert = function (doc) {
return nedb.insert(doc)
}
db.update = function (query, update) {
return nedb.update(query, update)
}
db.find = function (query) {
return nedb.find(query)
}
db.remove = function (query, options) {
return nedb.remove(query, options)
}
db.streams = new Database(`${DB_DIR}/streams.db`)
module.exports = db

View file

@ -1,6 +1,9 @@
const { create: createPlaylist } = require('./playlist')
const store = require('./store')
const path = require('path')
const glob = require('glob')
const fs = require('mz/fs')
const _ = require('lodash')
const file = {}
@ -64,4 +67,45 @@ file.basename = function (filepath) {
return path.basename(filepath)
}
// file.saveAsM3U = async function (filepath, items, options = {}) {
// const playlist = createPlaylist(filepath)
// const header = {}
// if (options.public) {
// let guides = items.map(item => item.guides)
// guides = _.uniq(_.flatten(guides)).sort().join(',')
// header['x-tvg-url'] = guides
// }
// playlist.setHeader(header)
// for (const item of items) {
// const stream = store.create(item)
// let attrs
// if (options.public) {
// attrs = {
// 'tvg-id': stream.get('tvg_id'),
// 'tvg-country': stream.get('tvg_country'),
// 'tvg-language': stream.get('tvg_language'),
// 'tvg-logo': stream.get('tvg_logo'),
// 'user-agent': stream.get('http.user-agent') || undefined,
// 'group-title': stream.get('group_title')
// }
// } else {
// attrs = {
// 'tvg-id': stream.get('tvg_id'),
// 'user-agent': stream.get('http.user-agent') || undefined
// }
// }
// playlist.add(stream.get('url'), stream.get('display_name'), attrs, {
// 'http-referrer': stream.get('http.referrer') || undefined,
// 'http-user-agent': stream.get('http.user-agent') || undefined
// })
// }
// return file.write(filepath, playlist.toString())
// }
module.exports = file

View file

@ -1,119 +1,23 @@
const { create: createPlaylist } = require('./playlist')
const store = require('./store')
const file = require('./file')
const logger = require('./logger')
const db = require('./db')
const _ = require('lodash')
const generators = require('../generators')
const LOGS_DIR = process.env.LOGS_DIR || 'scripts/logs/generators'
const generator = {}
generator.generate = async function (filepath, query = {}, options = {}) {
options = {
...{
format: 'm3u',
saveEmpty: false,
includeNSFW: false,
includeGuides: true,
includeBroken: false,
onLoad: r => r,
uniqBy: item => item.id || _.uniqueId(),
sortBy: null
},
...options
}
query['is_nsfw'] = options.includeNSFW ? { $in: [true, false] } : false
query['is_broken'] = options.includeBroken ? { $in: [true, false] } : false
let items = await db
.find(query)
.sort({ name: 1, 'status.level': 1, 'resolution.height': -1, url: 1 })
items = _.uniqBy(items, 'url')
if (!options.saveEmpty && !items.length) return { filepath, query, options, count: 0 }
if (options.uniqBy) items = _.uniqBy(items, options.uniqBy)
items = options.onLoad(items)
if (options.sortBy) items = _.sortBy(items, options.sortBy)
switch (options.format) {
case 'json':
await saveAsJSON(filepath, items, options)
break
case 'm3u':
default:
await saveAsM3U(filepath, items, options)
break
}
return { filepath, query, options, count: items.length }
}
async function saveAsM3U(filepath, items, options = {}) {
const playlist = await createPlaylist(filepath)
const header = {}
if (options.public) {
let guides = items.map(item => item.guides)
guides = _.uniq(_.flatten(guides)).sort().join(',')
header['x-tvg-url'] = guides
}
await playlist.header(header)
for (const item of items) {
const stream = store.create(item)
let attrs
if (options.public) {
attrs = {
'tvg-id': stream.get('tvg_id'),
'tvg-country': stream.get('tvg_country'),
'tvg-language': stream.get('tvg_language'),
'tvg-logo': stream.get('tvg_logo'),
'user-agent': stream.get('http.user-agent') || undefined,
'group-title': stream.get('group_title')
}
} else {
attrs = {
'tvg-id': stream.get('tvg_id'),
'user-agent': stream.get('http.user-agent') || undefined
}
generator.generate = async function (name, items = []) {
if (typeof generators[name] === 'function') {
try {
const logs = await generators[name].bind()(items)
await file.create(`${LOGS_DIR}/${name}.log`, logs.map(toJSON).join('\n'))
} catch (error) {
logger.error(`generators/${name}.js: ${error.message}`)
}
await playlist.link(stream.get('url'), stream.get('display_name'), attrs, {
'http-referrer': stream.get('http.referrer') || undefined,
'http-user-agent': stream.get('http.user-agent') || undefined
})
}
}
async function saveAsJSON(filepath, items, options) {
const output = items.map(item => {
const stream = store.create(item)
const categories = stream.get('categories').map(c => ({ name: c.name, slug: c.slug }))
const countries = stream.get('countries').map(c => ({ name: c.name, code: c.code }))
return {
name: stream.get('name'),
logo: stream.get('logo'),
url: stream.get('url'),
categories,
countries,
languages: stream.get('languages'),
tvg: {
id: stream.get('tvg_id'),
name: stream.get('name'),
url: stream.get('tvg_url')
}
}
})
await file.create(filepath, JSON.stringify(output))
}
generator.saveAsM3U = saveAsM3U
generator.saveAsJSON = saveAsJSON
module.exports = generator
function toJSON(item) {
return JSON.stringify(item)
}

View file

@ -1,49 +1,92 @@
const file = require('./file')
const store = require('./store')
const _ = require('lodash')
const playlist = {}
playlist.create = async function (filepath) {
playlist.filepath = filepath
const dir = file.dirname(filepath)
file.createDir(dir)
await file.create(filepath, '')
class Playlist {
constructor() {
this.links = []
}
return playlist
setHeader(attrs = {}) {
this.header = attrs
}
add(url, title, attrs, vlcOpts) {
this.links.push({ url, title, attrs, vlcOpts })
}
toString() {
let output = `#EXTM3U`
for (const attr in this.header) {
const value = this.header[attr]
output += ` ${attr}="${value}"`
}
output += `\n`
for (const link of this.links) {
output += `#EXTINF:-1`
for (const name in link.attrs) {
const value = link.attrs[name]
if (value !== undefined) {
output += ` ${name}="${value}"`
}
}
output += `,${link.title}\n`
for (const name in link.vlcOpts) {
const value = link.vlcOpts[name]
if (value !== undefined) {
output += `#EXTVLCOPT:${name}=${value}\n`
}
}
output += `${link.url}\n`
}
return output
}
}
playlist.header = async function (attrs) {
let header = `#EXTM3U`
for (const name in attrs) {
const value = attrs[name]
header += ` ${name}="${value}"`
playlist.create = function (items = [], options = {}) {
const p = new Playlist()
const header = {}
if (options.public) {
let guides = items.map(item => item.guides)
guides = _.uniq(_.flatten(guides)).sort().join(',')
header['x-tvg-url'] = guides
}
header += `\n`
p.setHeader(header)
await file.append(playlist.filepath, header)
for (const item of items) {
const stream = store.create(item)
return playlist
}
playlist.link = async function (url, title, attrs, vlcOpts) {
let link = `#EXTINF:-1`
for (const name in attrs) {
const value = attrs[name]
if (value !== undefined) {
link += ` ${name}="${value}"`
let attrs
if (options.public) {
attrs = {
'tvg-id': stream.get('tvg_id'),
'tvg-country': stream.get('tvg_country'),
'tvg-language': stream.get('tvg_language'),
'tvg-logo': stream.get('tvg_logo'),
'user-agent': stream.get('http.user-agent') || undefined,
'group-title': stream.get('group_title')
}
} else {
attrs = {
'tvg-id': stream.get('tvg_id'),
'user-agent': stream.get('http.user-agent') || undefined
}
}
}
link += `,${title}\n`
for (const name in vlcOpts) {
const value = vlcOpts[name]
if (value !== undefined) {
link += `#EXTVLCOPT:${name}=${value}\n`
}
}
link += `${url}\n`
await file.append(playlist.filepath, link)
p.add(stream.get('url'), stream.get('title'), attrs, {
'http-referrer': stream.get('http.referrer') || undefined,
'http-user-agent': stream.get('http.user-agent') || undefined
})
}
return playlist
return p
}
module.exports = playlist

View file

@ -1 +0,0 @@
codes.json

View file

@ -1,147 +0,0 @@
{
"auto": {
"name": "Auto",
"slug": "auto",
"nsfw": false
},
"animation": {
"name": "Animation",
"slug": "animation",
"nsfw": false
},
"business": {
"name": "Business",
"slug": "business",
"nsfw": false
},
"classic": {
"name": "Classic",
"slug": "classic",
"nsfw": false
},
"comedy": {
"name": "Comedy",
"slug": "comedy",
"nsfw": false
},
"cooking": {
"name": "Cooking",
"slug": "cooking",
"nsfw": false
},
"culture": {
"name": "Culture",
"slug": "culture",
"nsfw": false
},
"documentary": {
"name": "Documentary",
"slug": "documentary",
"nsfw": false
},
"education": {
"name": "Education",
"slug": "education",
"nsfw": false
},
"entertainment": {
"name": "Entertainment",
"slug": "entertainment",
"nsfw": false
},
"family": {
"name": "Family",
"slug": "family",
"nsfw": false
},
"general": {
"name": "General",
"slug": "general",
"nsfw": false
},
"kids": {
"name": "Kids",
"slug": "kids",
"nsfw": false
},
"legislative": {
"name": "Legislative",
"slug": "legislative",
"nsfw": false
},
"lifestyle": {
"name": "Lifestyle",
"slug": "lifestyle",
"nsfw": false
},
"local": {
"name": "Local",
"slug": "local",
"nsfw": false
},
"movies": {
"name": "Movies",
"slug": "movies",
"nsfw": false
},
"music": {
"name": "Music",
"slug": "music",
"nsfw": false
},
"news": {
"name": "News",
"slug": "news",
"nsfw": false
},
"outdoor": {
"name": "Outdoor",
"slug": "outdoor",
"nsfw": false
},
"relax": {
"name": "Relax",
"slug": "relax",
"nsfw": false
},
"religious": {
"name": "Religious",
"slug": "religious",
"nsfw": false
},
"series": {
"name": "Series",
"slug": "series",
"nsfw": false
},
"science": {
"name": "Science",
"slug": "science",
"nsfw": false
},
"shop": {
"name": "Shop",
"slug": "shop",
"nsfw": false
},
"sports": {
"name": "Sports",
"slug": "sports",
"nsfw": false
},
"travel": {
"name": "Travel",
"slug": "travel",
"nsfw": false
},
"weather": {
"name": "Weather",
"slug": "weather",
"nsfw": false
},
"xxx": {
"name": "XXX",
"slug": "xxx",
"nsfw": true
}
}

View file

@ -1,264 +0,0 @@
{
"AD": { "name": "Andorra", "code": "AD", "lang": "cat" },
"AE": { "name": "United Arab Emirates", "code": "AE", "lang": "ara" },
"AF": { "name": "Afghanistan", "code": "AF", "lang": "pus" },
"AG": { "name": "Antigua and Barbuda", "code": "AG", "lang": "eng" },
"AI": { "name": "Anguilla", "code": "AI", "lang": "eng" },
"AL": { "name": "Albania", "code": "AL", "lang": "sqi" },
"AM": { "name": "Armenia", "code": "AM", "lang": "hye" },
"AO": { "name": "Angola", "code": "AO", "lang": "por" },
"AQ": { "name": "Antarctica", "code": "AQ", "lang": null },
"AR": { "name": "Argentina", "code": "AR", "lang": "spa" },
"AS": { "name": "American Samoa", "code": "AS", "lang": "eng" },
"AT": { "name": "Austria", "code": "AT", "lang": "deu" },
"AU": { "name": "Australia", "code": "AU", "lang": "eng" },
"AW": { "name": "Aruba", "code": "AW", "lang": "nld" },
"AX": { "name": "Åland", "code": "AX", "lang": "swe" },
"AZ": { "name": "Azerbaijan", "code": "AZ", "lang": "aze" },
"BA": { "name": "Bosnia and Herzegovina", "code": "BA", "lang": "bos" },
"BB": { "name": "Barbados", "code": "BB", "lang": "eng" },
"BD": { "name": "Bangladesh", "code": "BD", "lang": "ben" },
"BE": { "name": "Belgium", "code": "BE", "lang": "nld" },
"BF": { "name": "Burkina Faso", "code": "BF", "lang": "fra" },
"BG": { "name": "Bulgaria", "code": "BG", "lang": "bul" },
"BH": { "name": "Bahrain", "code": "BH", "lang": "ara" },
"BI": { "name": "Burundi", "code": "BI", "lang": "fra" },
"BJ": { "name": "Benin", "code": "BJ", "lang": "fra" },
"BL": { "name": "Saint Barthélemy", "code": "BL", "lang": "fra" },
"BM": { "name": "Bermuda", "code": "BM", "lang": "eng" },
"BN": { "name": "Brunei", "code": "BN", "lang": "msa" },
"BO": { "name": "Bolivia", "code": "BO", "lang": "spa" },
"BQ": { "name": "Bonaire", "code": "BQ", "lang": "nld" },
"BR": { "name": "Brazil", "code": "BR", "lang": "por" },
"BS": { "name": "Bahamas", "code": "BS", "lang": "eng" },
"BT": { "name": "Bhutan", "code": "BT", "lang": "dzo" },
"BV": { "name": "Bouvet Island", "code": "BV", "lang": "nor" },
"BW": { "name": "Botswana", "code": "BW", "lang": "eng" },
"BY": { "name": "Belarus", "code": "BY", "lang": "bel" },
"BZ": { "name": "Belize", "code": "BZ", "lang": "eng" },
"CA": { "name": "Canada", "code": "CA", "lang": "eng" },
"CC": { "name": "Cocos [Keeling] Islands", "code": "CC", "lang": "eng" },
"CD": {
"name": "Democratic Republic of the Congo",
"code": "CD",
"lang": "fra"
},
"CF": { "name": "Central African Republic", "code": "CF", "lang": "fra" },
"CG": { "name": "Republic of the Congo", "code": "CG", "lang": "fra" },
"CH": { "name": "Switzerland", "code": "CH", "lang": "deu" },
"CI": { "name": "Ivory Coast", "code": "CI", "lang": "fra" },
"CK": { "name": "Cook Islands", "code": "CK", "lang": "eng" },
"CL": { "name": "Chile", "code": "CL", "lang": "spa" },
"CM": { "name": "Cameroon", "code": "CM", "lang": "eng" },
"CN": { "name": "China", "code": "CN", "lang": "zho" },
"CO": { "name": "Colombia", "code": "CO", "lang": "spa" },
"CR": { "name": "Costa Rica", "code": "CR", "lang": "spa" },
"CU": { "name": "Cuba", "code": "CU", "lang": "spa" },
"CV": { "name": "Cape Verde", "code": "CV", "lang": "por" },
"CW": { "name": "Curacao", "code": "CW", "lang": "nld" },
"CX": { "name": "Christmas Island", "code": "CX", "lang": "eng" },
"CY": { "name": "Cyprus", "code": "CY", "lang": "ell" },
"CZ": { "name": "Czech Republic", "code": "CZ", "lang": "ces" },
"DE": { "name": "Germany", "code": "DE", "lang": "deu" },
"DJ": { "name": "Djibouti", "code": "DJ", "lang": "fra" },
"DK": { "name": "Denmark", "code": "DK", "lang": "dan" },
"DM": { "name": "Dominica", "code": "DM", "lang": "eng" },
"DO": { "name": "Dominican Republic", "code": "DO", "lang": "spa" },
"DZ": { "name": "Algeria", "code": "DZ", "lang": "ara" },
"EC": { "name": "Ecuador", "code": "EC", "lang": "spa" },
"EE": { "name": "Estonia", "code": "EE", "lang": "est" },
"EG": { "name": "Egypt", "code": "EG", "lang": "ara" },
"EH": { "name": "Western Sahara", "code": "EH", "lang": "spa" },
"ER": { "name": "Eritrea", "code": "ER", "lang": "tir" },
"ES": { "name": "Spain", "code": "ES", "lang": "spa" },
"ET": { "name": "Ethiopia", "code": "ET", "lang": "amh" },
"FI": { "name": "Finland", "code": "FI", "lang": "fin" },
"FJ": { "name": "Fiji", "code": "FJ", "lang": "eng" },
"FK": { "name": "Falkland Islands", "code": "FK", "lang": "eng" },
"FM": { "name": "Micronesia", "code": "FM", "lang": "eng" },
"FO": { "name": "Faroe Islands", "code": "FO", "lang": "fao" },
"FR": { "name": "France", "code": "FR", "lang": "fra" },
"GA": { "name": "Gabon", "code": "GA", "lang": "fra" },
"UK": { "name": "United Kingdom", "code": "UK", "lang": "eng" },
"GD": { "name": "Grenada", "code": "GD", "lang": "eng" },
"GE": { "name": "Georgia", "code": "GE", "lang": "kat" },
"GF": { "name": "French Guiana", "code": "GF", "lang": "fra" },
"GG": { "name": "Guernsey", "code": "GG", "lang": "eng" },
"GH": { "name": "Ghana", "code": "GH", "lang": "eng" },
"GI": { "name": "Gibraltar", "code": "GI", "lang": "eng" },
"GL": { "name": "Greenland", "code": "GL", "lang": "kal" },
"GM": { "name": "Gambia", "code": "GM", "lang": "eng" },
"GN": { "name": "Guinea", "code": "GN", "lang": "fra" },
"GP": { "name": "Guadeloupe", "code": "GP", "lang": "fra" },
"GQ": { "name": "Equatorial Guinea", "code": "GQ", "lang": "spa" },
"GR": { "name": "Greece", "code": "GR", "lang": "ell" },
"GS": {
"name": "South Georgia and the South Sandwich Islands",
"code": "GS",
"lang": "eng"
},
"GT": { "name": "Guatemala", "code": "GT", "lang": "spa" },
"GU": { "name": "Guam", "code": "GU", "lang": "eng" },
"GW": { "name": "Guinea-Bissau", "code": "GW", "lang": "por" },
"GY": { "name": "Guyana", "code": "GY", "lang": "eng" },
"HK": { "name": "Hong Kong", "code": "HK", "lang": "zho" },
"HM": { "name": "Heard Island and McDonald Islands", "code": "HM", "lang": "eng" },
"HN": { "name": "Honduras", "code": "HN", "lang": "spa" },
"HR": { "name": "Croatia", "code": "HR", "lang": "hrv" },
"HT": { "name": "Haiti", "code": "HT", "lang": "fra" },
"HU": { "name": "Hungary", "code": "HU", "lang": "hun" },
"ID": { "name": "Indonesia", "code": "ID", "lang": "ind" },
"IE": { "name": "Ireland", "code": "IE", "lang": "gle" },
"IL": { "name": "Israel", "code": "IL", "lang": "heb" },
"IM": { "name": "Isle of Man", "code": "IM", "lang": "eng" },
"IN": { "name": "India", "code": "IN", "lang": "hin" },
"IO": { "name": "British Indian Ocean Territory", "code": "IO", "lang": "eng" },
"IQ": { "name": "Iraq", "code": "IQ", "lang": "ara" },
"IR": { "name": "Iran", "code": "IR", "lang": "fas" },
"IS": { "name": "Iceland", "code": "IS", "lang": "isl" },
"IT": { "name": "Italy", "code": "IT", "lang": "ita" },
"JE": { "name": "Jersey", "code": "JE", "lang": "eng" },
"JM": { "name": "Jamaica", "code": "JM", "lang": "eng" },
"JO": { "name": "Jordan", "code": "JO", "lang": "ara" },
"JP": { "name": "Japan", "code": "JP", "lang": "jpn" },
"KE": { "name": "Kenya", "code": "KE", "lang": "eng" },
"KG": { "name": "Kyrgyzstan", "code": "KG", "lang": "kir" },
"KH": { "name": "Cambodia", "code": "KH", "lang": "khm" },
"KI": { "name": "Kiribati", "code": "KI", "lang": "eng" },
"KM": { "name": "Comoros", "code": "KM", "lang": "ara" },
"KN": { "name": "Saint Kitts and Nevis", "code": "KN", "lang": "eng" },
"KP": { "name": "North Korea", "code": "KP", "lang": "kor" },
"KR": { "name": "South Korea", "code": "KR", "lang": "kor" },
"KW": { "name": "Kuwait", "code": "KW", "lang": "ara" },
"KY": { "name": "Cayman Islands", "code": "KY", "lang": "eng" },
"KZ": { "name": "Kazakhstan", "code": "KZ", "lang": "kaz" },
"LA": { "name": "Laos", "code": "LA", "lang": "lao" },
"LB": { "name": "Lebanon", "code": "LB", "lang": "ara" },
"LC": { "name": "Saint Lucia", "code": "LC", "lang": "eng" },
"LI": { "name": "Liechtenstein", "code": "LI", "lang": "deu" },
"LK": { "name": "Sri Lanka", "code": "LK", "lang": "sin" },
"LR": { "name": "Liberia", "code": "LR", "lang": "eng" },
"LS": { "name": "Lesotho", "code": "LS", "lang": "eng" },
"LT": { "name": "Lithuania", "code": "LT", "lang": "lit" },
"LU": { "name": "Luxembourg", "code": "LU", "lang": "fra" },
"LV": { "name": "Latvia", "code": "LV", "lang": "lav" },
"LY": { "name": "Libya", "code": "LY", "lang": "ara" },
"MA": { "name": "Morocco", "code": "MA", "lang": "ara" },
"MC": { "name": "Monaco", "code": "MC", "lang": "fra" },
"MD": { "name": "Moldova", "code": "MD", "lang": "ron" },
"ME": { "name": "Montenegro", "code": "ME", "lang": "srp" },
"MF": { "name": "Saint Martin", "code": "MF", "lang": "eng" },
"MG": { "name": "Madagascar", "code": "MG", "lang": "fra" },
"MH": { "name": "Marshall Islands", "code": "MH", "lang": "eng" },
"MK": { "name": "North Macedonia", "code": "MK", "lang": "mkd" },
"ML": { "name": "Mali", "code": "ML", "lang": "fra" },
"MM": { "name": "Myanmar [Burma]", "code": "MM", "lang": "mya" },
"MN": { "name": "Mongolia", "code": "MN", "lang": "mon" },
"MO": { "name": "Macao", "code": "MO", "lang": "zho" },
"MP": { "name": "Northern Mariana Islands", "code": "MP", "lang": "eng" },
"MQ": { "name": "Martinique", "code": "MQ", "lang": "fra" },
"MR": { "name": "Mauritania", "code": "MR", "lang": "ara" },
"MS": { "name": "Montserrat", "code": "MS", "lang": "eng" },
"MT": { "name": "Malta", "code": "MT", "lang": "mlt" },
"MU": { "name": "Mauritius", "code": "MU", "lang": "eng" },
"MV": { "name": "Maldives", "code": "MV", "lang": "div" },
"MW": { "name": "Malawi", "code": "MW", "lang": "eng" },
"MX": { "name": "Mexico", "code": "MX", "lang": "spa" },
"MY": { "name": "Malaysia", "code": "MY", "lang": "msa" },
"MZ": { "name": "Mozambique", "code": "MZ", "lang": "por" },
"NA": { "name": "Namibia", "code": "NA", "lang": "eng" },
"NC": { "name": "New Caledonia", "code": "NC", "lang": "fra" },
"NE": { "name": "Niger", "code": "NE", "lang": "fra" },
"NF": { "name": "Norfolk Island", "code": "NF", "lang": "eng" },
"NG": { "name": "Nigeria", "code": "NG", "lang": "eng" },
"NI": { "name": "Nicaragua", "code": "NI", "lang": "spa" },
"NL": { "name": "Netherlands", "code": "NL", "lang": "nld" },
"NO": { "name": "Norway", "code": "NO", "lang": "nor" },
"NP": { "name": "Nepal", "code": "NP", "lang": "nep" },
"NR": { "name": "Nauru", "code": "NR", "lang": "eng" },
"NU": { "name": "Niue", "code": "NU", "lang": "eng" },
"NZ": { "name": "New Zealand", "code": "NZ", "lang": "eng" },
"OM": { "name": "Oman", "code": "OM", "lang": "ara" },
"PA": { "name": "Panama", "code": "PA", "lang": "spa" },
"PE": { "name": "Peru", "code": "PE", "lang": "spa" },
"PF": { "name": "French Polynesia", "code": "PF", "lang": "fra" },
"PG": { "name": "Papua New Guinea", "code": "PG", "lang": "eng" },
"PH": { "name": "Philippines", "code": "PH", "lang": "eng" },
"PK": { "name": "Pakistan", "code": "PK", "lang": "eng" },
"PL": { "name": "Poland", "code": "PL", "lang": "pol" },
"PM": { "name": "Saint Pierre and Miquelon", "code": "PM", "lang": "fra" },
"PN": { "name": "Pitcairn Islands", "code": "PN", "lang": "eng" },
"PR": { "name": "Puerto Rico", "code": "PR", "lang": "spa" },
"PS": { "name": "Palestine", "code": "PS", "lang": "ara" },
"PT": { "name": "Portugal", "code": "PT", "lang": "por" },
"PW": { "name": "Palau", "code": "PW", "lang": "eng" },
"PY": { "name": "Paraguay", "code": "PY", "lang": "spa" },
"QA": { "name": "Qatar", "code": "QA", "lang": "ara" },
"RE": { "name": "Réunion", "code": "RE", "lang": "fra" },
"RO": { "name": "Romania", "code": "RO", "lang": "ron" },
"RS": { "name": "Serbia", "code": "RS", "lang": "srp" },
"RU": { "name": "Russia", "code": "RU", "lang": "rus" },
"RW": { "name": "Rwanda", "code": "RW", "lang": "kin" },
"SA": { "name": "Saudi Arabia", "code": "SA", "lang": "ara" },
"SB": { "name": "Solomon Islands", "code": "SB", "lang": "eng" },
"SC": { "name": "Seychelles", "code": "SC", "lang": "fra" },
"SD": { "name": "Sudan", "code": "SD", "lang": "ara" },
"SE": { "name": "Sweden", "code": "SE", "lang": "swe" },
"SG": { "name": "Singapore", "code": "SG", "lang": "eng" },
"SH": { "name": "Saint Helena", "code": "SH", "lang": "eng" },
"SI": { "name": "Slovenia", "code": "SI", "lang": "slv" },
"SJ": { "name": "Svalbard and Jan Mayen", "code": "SJ", "lang": "nor" },
"SK": { "name": "Slovakia", "code": "SK", "lang": "slk" },
"SL": { "name": "Sierra Leone", "code": "SL", "lang": "eng" },
"SM": { "name": "San Marino", "code": "SM", "lang": "ita" },
"SN": { "name": "Senegal", "code": "SN", "lang": "fra" },
"SO": { "name": "Somalia", "code": "SO", "lang": "som" },
"SR": { "name": "Suriname", "code": "SR", "lang": "nld" },
"SS": { "name": "South Sudan", "code": "SS", "lang": "eng" },
"ST": { "name": "São Tomé and Príncipe", "code": "ST", "lang": "por" },
"SV": { "name": "El Salvador", "code": "SV", "lang": "spa" },
"SX": { "name": "Sint Maarten", "code": "SX", "lang": "nld" },
"SY": { "name": "Syria", "code": "SY", "lang": "ara" },
"SZ": { "name": "Swaziland", "code": "SZ", "lang": "eng" },
"TC": { "name": "Turks and Caicos Islands", "code": "TC", "lang": "eng" },
"TD": { "name": "Chad", "code": "TD", "lang": "fra" },
"TF": { "name": "French Southern Territories", "code": "TF", "lang": "fra" },
"TG": { "name": "Togo", "code": "TG", "lang": "fra" },
"TH": { "name": "Thailand", "code": "TH", "lang": "tha" },
"TJ": { "name": "Tajikistan", "code": "TJ", "lang": "tgk" },
"TK": { "name": "Tokelau", "code": "TK", "lang": "eng" },
"TL": { "name": "East Timor", "code": "TL", "lang": "por" },
"TM": { "name": "Turkmenistan", "code": "TM", "lang": "tuk" },
"TN": { "name": "Tunisia", "code": "TN", "lang": "ara" },
"TO": { "name": "Tonga", "code": "TO", "lang": "eng" },
"TR": { "name": "Turkey", "code": "TR", "lang": "tur" },
"TT": { "name": "Trinidad and Tobago", "code": "TT", "lang": "eng" },
"TV": { "name": "Tuvalu", "code": "TV", "lang": "eng" },
"TW": { "name": "Taiwan", "code": "TW", "lang": "zho" },
"TZ": { "name": "Tanzania", "code": "TZ", "lang": "swa" },
"UA": { "name": "Ukraine", "code": "UA", "lang": "ukr" },
"UG": { "name": "Uganda", "code": "UG", "lang": "eng" },
"UM": { "name": "U.S. Minor Outlying Islands", "code": "UM", "lang": "eng" },
"US": { "name": "United States", "code": "US", "lang": "eng" },
"UY": { "name": "Uruguay", "code": "UY", "lang": "spa" },
"UZ": { "name": "Uzbekistan", "code": "UZ", "lang": "uzb" },
"VA": { "name": "Vatican City", "code": "VA", "lang": "ita" },
"VC": { "name": "Saint Vincent and the Grenadines", "code": "VC", "lang": "eng" },
"VE": { "name": "Venezuela", "code": "VE", "lang": "spa" },
"VG": { "name": "British Virgin Islands", "code": "VG", "lang": "eng" },
"VI": { "name": "U.S. Virgin Islands", "code": "VI", "lang": "eng" },
"VN": { "name": "Vietnam", "code": "VN", "lang": "vie" },
"VU": { "name": "Vanuatu", "code": "VU", "lang": "bis" },
"WF": { "name": "Wallis and Futuna", "code": "WF", "lang": "fra" },
"WS": { "name": "Samoa", "code": "WS", "lang": "smo" },
"XK": { "name": "Kosovo", "code": "XK", "lang": "sqi" },
"YE": { "name": "Yemen", "code": "YE", "lang": "ara" },
"YT": { "name": "Mayotte", "code": "YT", "lang": "fra" },
"ZA": {
"name": "South Africa",
"code": "ZA",
"lang": "afr"
},
"ZM": { "name": "Zambia", "code": "ZM", "lang": "eng" },
"ZW": { "name": "Zimbabwe", "code": "ZW", "lang": "eng" }
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
const { create: createPlaylist } = require('../core/playlist')
const api = require('../core/api')
const file = require('../core/file')
const _ = require('lodash')
const PUBLIC_DIR = process.env.PUBLIC_DIR || '.gh-pages'
module.exports = async function (streams = []) {
const logs = []
await api.categories.load()
const categories = await api.categories.all()
for (const category of categories) {
let output = _.filter(streams, { channel: { categories: [category.id] } })
output = _.orderBy(
output,
['channel.name', 'status.level', 'resolution.height'],
['asc', 'asc', 'desc']
)
output = _.uniqBy(output, s => s.channel_id || _.uniqueId())
const playlist = createPlaylist(output, { public: true })
await file.create(`${PUBLIC_DIR}/categories/${category.id}.m3u`, playlist.toString())
logs.push({ id: category.id, count: output.length })
}
let output = _.filter(streams, s => !s.categories.length)
output = _.orderBy(
output,
['channel.name', 'status.level', 'resolution.height'],
['asc', 'asc', 'desc']
)
output = _.uniqBy(output, s => s.channel_id || _.uniqueId())
output = output.map(item => {
item.group_title = 'Other'
return item
})
const playlist = createPlaylist(output, { public: true })
await file.create(`${PUBLIC_DIR}/categories/other.m3u`, playlist.toString())
logs.push({ id: 'other', count: output.length })
return logs
}

View file

@ -0,0 +1 @@
exports.categories = require('./categories')

View file

@ -1,5 +1,5 @@
module.exports = function () {
if (this.group_title) return this.group_title
if (this.group_title !== undefined) return this.group_title
if (Array.isArray(this.categories)) {
return this.categories

View file

@ -1,5 +1,5 @@
exports.group_title = require('./group_title')
exports.display_name = require('./display_name')
exports.title = require('./title')
exports.tvg_country = require('./tvg_country')
exports.tvg_id = require('./tvg_id')
exports.tvg_language = require('./tvg_language')

View file

@ -1,5 +1,5 @@
module.exports = function () {
let title = this.title
let title = this.channel_name
if (this.resolution.height) {
title += ` (${this.resolution.height}p)`

View file

@ -1,5 +1,3 @@
module.exports = function () {
if (this.tvg_country) return this.tvg_country
return Array.isArray(this.countries) ? this.countries.map(i => i.code).join(';') : ''
return Array.isArray(this.broadcast_area) ? this.broadcast_area.join(';') : ''
}

View file

@ -1,3 +1,3 @@
module.exports = function () {
return this.id || ''
return this.channel_id || ''
}

View file

@ -1,3 +1,3 @@
module.exports = function () {
return this.logo || ''
return this.channel && this.channel.logo ? this.channel.logo : ''
}

View file

@ -1,5 +1,10 @@
const { parser } = require('../../core')
module.exports = function ({ title }) {
return parser.parseChannelName(title)
return title
.trim()
.split(' ')
.map(s => s.trim())
.filter(s => {
return !/\[|\]/i.test(s) && !/\((\d+)P\)/i.test(s)
})
.join(' ')
}