mirror of
https://github.com/iptv-org/iptv.git
synced 2025-05-12 01:50:04 -04:00
Update scripts
This commit is contained in:
parent
8a83f23243
commit
f1d2add19a
98 changed files with 2423 additions and 1499 deletions
|
@ -1,41 +0,0 @@
|
|||
const _ = require('lodash')
|
||||
const file = require('./file')
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || './scripts/tmp/data'
|
||||
|
||||
class API {
|
||||
constructor(filepath) {
|
||||
this.filepath = file.resolve(filepath)
|
||||
}
|
||||
|
||||
async load() {
|
||||
const data = await file.read(this.filepath)
|
||||
this.collection = JSON.parse(data)
|
||||
}
|
||||
|
||||
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.streams = new API(`${DATA_DIR}/streams.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.blocklist = new API(`${DATA_DIR}/blocklist.json`)
|
||||
api.subdivisions = new API(`${DATA_DIR}/subdivisions.json`)
|
||||
|
||||
module.exports = api
|
175
scripts/core/collection.ts
Normal file
175
scripts/core/collection.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
import _ from 'lodash'
|
||||
import { orderBy, Order } from 'natural-orderby'
|
||||
import { Dictionary } from './'
|
||||
|
||||
type Iteratee = (value: any, value2?: any) => void
|
||||
|
||||
export class Collection {
|
||||
_items: any[]
|
||||
|
||||
constructor(items?: any[]) {
|
||||
this._items = Array.isArray(items) ? items : []
|
||||
}
|
||||
|
||||
first(predicate?: Iteratee) {
|
||||
if (predicate) {
|
||||
return this._items.find(predicate)
|
||||
}
|
||||
|
||||
return this._items[0]
|
||||
}
|
||||
|
||||
last(predicate?: Iteratee) {
|
||||
if (predicate) {
|
||||
return _.findLast(this._items, predicate)
|
||||
}
|
||||
|
||||
return this._items[this._items.length - 1]
|
||||
}
|
||||
|
||||
find(iteratee: Iteratee): Collection {
|
||||
const found = this._items.filter(iteratee)
|
||||
|
||||
return new Collection(found)
|
||||
}
|
||||
|
||||
add(data: any) {
|
||||
this._items.push(data)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
intersects(collection: Collection): boolean {
|
||||
return _.intersection(this._items, collection.all()).length > 0
|
||||
}
|
||||
|
||||
count() {
|
||||
return this._items.length
|
||||
}
|
||||
|
||||
join(separator: string) {
|
||||
return this._items.join(separator)
|
||||
}
|
||||
|
||||
indexOf(value: string) {
|
||||
return this._items.indexOf(value)
|
||||
}
|
||||
|
||||
push(data: any) {
|
||||
this.add(data)
|
||||
}
|
||||
|
||||
uniq() {
|
||||
const items = _.uniq(this._items)
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
reduce(iteratee: Iteratee, accumulator: any) {
|
||||
const items = _.reduce(this._items, iteratee, accumulator)
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
filter(iteratee: Iteratee) {
|
||||
const items = _.filter(this._items, iteratee)
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
forEach(iteratee: Iteratee) {
|
||||
for (let item of this._items) {
|
||||
iteratee(item)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
remove(iteratee: Iteratee): Collection {
|
||||
const removed = _.remove(this._items, iteratee)
|
||||
|
||||
return new Collection(removed)
|
||||
}
|
||||
|
||||
concat(collection: Collection) {
|
||||
const items = this._items.concat(collection._items)
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._items.length === 0
|
||||
}
|
||||
|
||||
notEmpty(): boolean {
|
||||
return this._items.length > 0
|
||||
}
|
||||
|
||||
sort() {
|
||||
const items = this._items.sort()
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
orderBy(iteratees: Iteratee | Iteratee[], orders?: Order | Order[]) {
|
||||
const items = orderBy(this._items, iteratees, orders)
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
keyBy(iteratee: Iteratee) {
|
||||
const items = _.keyBy(this._items, iteratee)
|
||||
|
||||
return new Dictionary(items)
|
||||
}
|
||||
|
||||
empty() {
|
||||
return this._items.length === 0
|
||||
}
|
||||
|
||||
includes(value: any) {
|
||||
if (typeof value === 'function') {
|
||||
const found = this._items.find(value)
|
||||
|
||||
return !!found
|
||||
}
|
||||
|
||||
return this._items.includes(value)
|
||||
}
|
||||
|
||||
missing(value: any) {
|
||||
if (typeof value === 'function') {
|
||||
const found = this._items.find(value)
|
||||
|
||||
return !found
|
||||
}
|
||||
|
||||
return !this._items.includes(value)
|
||||
}
|
||||
|
||||
uniqBy(iteratee: Iteratee) {
|
||||
const items = _.uniqBy(this._items, iteratee)
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
groupBy(iteratee: Iteratee) {
|
||||
const object = _.groupBy(this._items, iteratee)
|
||||
|
||||
return new Dictionary(object)
|
||||
}
|
||||
|
||||
map(iteratee: Iteratee) {
|
||||
const items = this._items.map(iteratee)
|
||||
|
||||
return new Collection(items)
|
||||
}
|
||||
|
||||
all() {
|
||||
return this._items
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return JSON.stringify(this._items)
|
||||
}
|
||||
}
|
22
scripts/core/database.ts
Normal file
22
scripts/core/database.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Datastore from '@seald-io/nedb'
|
||||
import * as path from 'path'
|
||||
|
||||
export class Database {
|
||||
rootDir: string
|
||||
|
||||
constructor(rootDir: string) {
|
||||
this.rootDir = rootDir
|
||||
}
|
||||
|
||||
async load(filepath: string) {
|
||||
const absFilepath = path.join(this.rootDir, filepath)
|
||||
|
||||
return new Datastore({
|
||||
filename: path.resolve(absFilepath),
|
||||
autoload: true,
|
||||
onload: (error: Error): any => {
|
||||
if (error) console.error(error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
const date = {}
|
||||
|
||||
date.utc = d => {
|
||||
return dayjs.utc(d)
|
||||
}
|
||||
|
||||
module.exports = date
|
|
@ -1,82 +0,0 @@
|
|||
const nedb = require('nedb-promises')
|
||||
const fs = require('fs-extra')
|
||||
const file = require('./file')
|
||||
|
||||
const DB_DIR = process.env.DB_DIR || './scripts/tmp/database'
|
||||
|
||||
fs.ensureDirSync(DB_DIR)
|
||||
|
||||
class Database {
|
||||
constructor(filepath) {
|
||||
this.filepath = filepath
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
all() {
|
||||
return this.find({})
|
||||
}
|
||||
|
||||
remove(query, options) {
|
||||
return this.db.remove(query, options)
|
||||
}
|
||||
}
|
||||
|
||||
const db = {}
|
||||
|
||||
db.streams = new Database(`${DB_DIR}/streams.db`)
|
||||
|
||||
module.exports = db
|
31
scripts/core/dictionary.ts
Normal file
31
scripts/core/dictionary.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
export class Dictionary {
|
||||
dict: any
|
||||
|
||||
constructor(dict?: any) {
|
||||
this.dict = dict || {}
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.dict[key] = value
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return !!this.dict[key]
|
||||
}
|
||||
|
||||
missing(key: string): boolean {
|
||||
return !this.dict[key]
|
||||
}
|
||||
|
||||
get(key: string): any {
|
||||
return this.dict[key] ? this.dict[key] : undefined
|
||||
}
|
||||
|
||||
keys(): string[] {
|
||||
return Object.keys(this.dict)
|
||||
}
|
||||
|
||||
data() {
|
||||
return this.dict
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
const { create: createPlaylist } = require('./playlist')
|
||||
const store = require('./store')
|
||||
const path = require('path')
|
||||
const glob = require('glob')
|
||||
const fs = require('fs-extra')
|
||||
const _ = require('lodash')
|
||||
|
||||
const file = {}
|
||||
|
||||
file.list = function (pattern) {
|
||||
return new Promise(resolve => {
|
||||
glob(pattern, function (err, files) {
|
||||
resolve(files)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
file.getFilename = function (filepath) {
|
||||
return path.parse(filepath).name
|
||||
}
|
||||
|
||||
file.createDir = async function (dir) {
|
||||
if (await file.exists(dir)) return
|
||||
|
||||
return fs.mkdir(dir, { recursive: true }).catch(console.error)
|
||||
}
|
||||
|
||||
file.exists = function (filepath) {
|
||||
return fs.exists(path.resolve(filepath))
|
||||
}
|
||||
|
||||
file.read = function (filepath) {
|
||||
return fs.readFile(path.resolve(filepath), { encoding: 'utf8' }).catch(console.error)
|
||||
}
|
||||
|
||||
file.append = function (filepath, data) {
|
||||
return fs.appendFile(path.resolve(filepath), data).catch(console.error)
|
||||
}
|
||||
|
||||
file.create = function (filepath, data = '') {
|
||||
filepath = path.resolve(filepath)
|
||||
const dir = path.dirname(filepath)
|
||||
|
||||
return file
|
||||
.createDir(dir)
|
||||
.then(() => fs.writeFile(filepath, data, { encoding: 'utf8', flag: 'w' }))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
file.write = function (filepath, data = '') {
|
||||
return fs.writeFile(path.resolve(filepath), data).catch(console.error)
|
||||
}
|
||||
|
||||
file.clear = function (filepath) {
|
||||
return file.write(filepath, '')
|
||||
}
|
||||
|
||||
file.resolve = function (filepath) {
|
||||
return path.resolve(filepath)
|
||||
}
|
||||
|
||||
file.dirname = function (filepath) {
|
||||
return path.dirname(filepath)
|
||||
}
|
||||
|
||||
file.basename = function (filepath) {
|
||||
return path.basename(filepath)
|
||||
}
|
||||
|
||||
module.exports = file
|
31
scripts/core/file.ts
Normal file
31
scripts/core/file.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import * as path from 'path'
|
||||
|
||||
export class File {
|
||||
filepath: string
|
||||
content: string
|
||||
|
||||
constructor(filepath: string, content?: string) {
|
||||
this.filepath = filepath
|
||||
this.content = content || ''
|
||||
}
|
||||
|
||||
getFilename() {
|
||||
return path.parse(this.filepath).name
|
||||
}
|
||||
|
||||
dirname() {
|
||||
return path.dirname(this.filepath)
|
||||
}
|
||||
|
||||
basename() {
|
||||
return path.basename(this.filepath)
|
||||
}
|
||||
|
||||
append(data: string) {
|
||||
this.content = this.content + data
|
||||
}
|
||||
|
||||
extension() {
|
||||
return this.filepath.split('.').pop()
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
const { create: createPlaylist } = require('./playlist')
|
||||
const generators = require('../generators')
|
||||
const logger = require('./logger')
|
||||
const file = require('./file')
|
||||
|
||||
const PUBLIC_DIR = process.env.PUBLIC_DIR || '.gh-pages'
|
||||
const LOGS_DIR = process.env.LOGS_DIR || 'scripts/tmp/logs/generators'
|
||||
|
||||
const generator = {}
|
||||
|
||||
generator.generate = async function (name, streams = []) {
|
||||
if (typeof generators[name] === 'function') {
|
||||
try {
|
||||
let output = await generators[name].bind()(streams)
|
||||
output = Array.isArray(output) ? output : [output]
|
||||
for (const type of output) {
|
||||
const playlist = createPlaylist(type.items, { public: true })
|
||||
await file.create(`${PUBLIC_DIR}/${type.filepath}`, playlist.toString())
|
||||
}
|
||||
await file.create(`${LOGS_DIR}/${name}.log`, output.map(toJSON).join('\n'))
|
||||
} catch (error) {
|
||||
logger.error(`generators/${name}.js: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = generator
|
||||
|
||||
function toJSON(type) {
|
||||
type.count = type.items.length
|
||||
delete type.items
|
||||
return JSON.stringify(type)
|
||||
}
|
46
scripts/core/htmlTable.ts
Normal file
46
scripts/core/htmlTable.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
type Column = {
|
||||
name: string
|
||||
nowrap?: boolean
|
||||
align?: string
|
||||
}
|
||||
|
||||
type DataItem = string[]
|
||||
|
||||
export class HTMLTable {
|
||||
data: DataItem[]
|
||||
columns: Column[]
|
||||
|
||||
constructor(data: DataItem[], columns: Column[]) {
|
||||
this.data = data
|
||||
this.columns = columns
|
||||
}
|
||||
|
||||
toString() {
|
||||
let output = '<table>\n'
|
||||
|
||||
output += ' <thead>\n <tr>'
|
||||
for (let column of this.columns) {
|
||||
output += `<th align="left">${column.name}</th>`
|
||||
}
|
||||
output += '</tr>\n </thead>\n'
|
||||
|
||||
output += ' <tbody>\n'
|
||||
for (let item of this.data) {
|
||||
output += ' <tr>'
|
||||
let i = 0
|
||||
for (let prop in item) {
|
||||
const column = this.columns[i]
|
||||
let nowrap = column.nowrap ? ` nowrap` : ''
|
||||
let align = column.align ? ` align="${column.align}"` : ''
|
||||
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
||||
i++
|
||||
}
|
||||
output += '</tr>\n'
|
||||
}
|
||||
output += ' </tbody>\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
const { transliterate } = require('transliteration')
|
||||
|
||||
const id = {}
|
||||
|
||||
id.generate = function (name, code) {
|
||||
if (!name || !code) return null
|
||||
|
||||
name = name.replace(/ *\([^)]*\) */g, '')
|
||||
name = name.replace(/ *\[[^)]*\] */g, '')
|
||||
name = name.replace(/\+/gi, 'Plus')
|
||||
name = name.replace(/[^a-z\d]+/gi, '')
|
||||
name = name.trim()
|
||||
name = transliterate(name)
|
||||
code = code.toLowerCase()
|
||||
|
||||
return `${name}.${code}`
|
||||
}
|
||||
|
||||
module.exports = id
|
|
@ -1,14 +0,0 @@
|
|||
exports.db = require('./db')
|
||||
exports.logger = require('./logger')
|
||||
exports.file = require('./file')
|
||||
exports.timer = require('./timer')
|
||||
exports.parser = require('./parser')
|
||||
exports.checker = require('./checker')
|
||||
exports.generator = require('./generator')
|
||||
exports.playlist = require('./playlist')
|
||||
exports.store = require('./store')
|
||||
exports.markdown = require('./markdown')
|
||||
exports.api = require('./api')
|
||||
exports.id = require('./id')
|
||||
exports.m3u = require('./m3u')
|
||||
exports.date = require('./date')
|
14
scripts/core/index.ts
Normal file
14
scripts/core/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export * from './database'
|
||||
export * from './logger'
|
||||
export * from './playlistParser'
|
||||
export * from './numberParser'
|
||||
export * from './logParser'
|
||||
export * from './markdown'
|
||||
export * from './file'
|
||||
export * from './collection'
|
||||
export * from './dictionary'
|
||||
export * from './storage'
|
||||
export * from './url'
|
||||
export * from './issueLoader'
|
||||
export * from './issueParser'
|
||||
export * from './htmlTable'
|
46
scripts/core/issueLoader.ts
Normal file
46
scripts/core/issueLoader.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
||||
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { Collection, IssueParser } from './'
|
||||
import { TESTING, OWNER, REPO } from '../constants'
|
||||
|
||||
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
||||
const octokit = new CustomOctokit()
|
||||
|
||||
export class IssueLoader {
|
||||
async load({ labels }: { labels: string[] | string }) {
|
||||
labels = Array.isArray(labels) ? labels.join(',') : labels
|
||||
let issues: any[] = []
|
||||
if (TESTING) {
|
||||
switch (labels) {
|
||||
case 'streams:add':
|
||||
issues = (await import('../../tests/__data__/input/issues/streams_add')).default
|
||||
break
|
||||
case 'streams:add,approved':
|
||||
issues = (await import('../../tests/__data__/input/issues/streams_add_approved')).default
|
||||
break
|
||||
case 'streams:edit,approved':
|
||||
issues = (await import('../../tests/__data__/input/issues/streams_edit_approved')).default
|
||||
break
|
||||
case 'streams:remove,approved':
|
||||
issues = (await import('../../tests/__data__/input/issues/streams_remove_approved'))
|
||||
.default
|
||||
break
|
||||
}
|
||||
} else {
|
||||
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
per_page: 100,
|
||||
labels,
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const parser = new IssueParser()
|
||||
|
||||
return new Collection(issues).map(parser.parse)
|
||||
}
|
||||
}
|
48
scripts/core/issueParser.ts
Normal file
48
scripts/core/issueParser.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Dictionary } from './'
|
||||
|
||||
export class IssueParser {
|
||||
parse(issue: any): Dictionary {
|
||||
const data = new Dictionary()
|
||||
data.set('issue_number', issue.number)
|
||||
|
||||
const idDict = new Dictionary({
|
||||
'Channel ID': 'channel_id',
|
||||
'Channel ID (required)': 'channel_id',
|
||||
'Broken Link': 'stream_url',
|
||||
'Stream URL': 'stream_url',
|
||||
'Stream URL (optional)': 'stream_url',
|
||||
'Stream URL (required)': 'stream_url',
|
||||
Label: 'label',
|
||||
Quality: 'quality',
|
||||
'Channel Name': 'channel_name',
|
||||
'HTTP User-Agent': 'user_agent',
|
||||
'HTTP Referrer': 'http_referrer',
|
||||
Reason: 'reason',
|
||||
'What happened to the stream?': 'reason',
|
||||
'Possible Replacement (optional)': 'possible_replacement',
|
||||
Notes: 'notes',
|
||||
'Notes (optional)': 'notes'
|
||||
})
|
||||
|
||||
const fields = issue.body.split('###')
|
||||
|
||||
if (!fields.length) return data
|
||||
|
||||
fields.forEach((field: string) => {
|
||||
let [_label, , _value] = field.split(/\r?\n/)
|
||||
_label = _label ? _label.trim() : ''
|
||||
_value = _value ? _value.trim() : ''
|
||||
|
||||
if (!_label || !_value) return data
|
||||
|
||||
const id: string = idDict.get(_label)
|
||||
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
||||
|
||||
if (!id) return
|
||||
|
||||
data.set(id, value)
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
13
scripts/core/logParser.ts
Normal file
13
scripts/core/logParser.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export type LogItem = {
|
||||
filepath: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export class LogParser {
|
||||
parse(content: string): any[] {
|
||||
if (!content) return []
|
||||
const lines = content.split('\n')
|
||||
|
||||
return lines.map(line => (line ? JSON.parse(line) : null)).filter(l => l)
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
const { Signale } = require('signale')
|
||||
|
||||
const options = {}
|
||||
|
||||
const logger = new Signale(options)
|
||||
|
||||
logger.config({
|
||||
displayLabel: false,
|
||||
displayScope: false,
|
||||
displayBadge: false
|
||||
})
|
||||
|
||||
module.exports = logger
|
9
scripts/core/logger.ts
Normal file
9
scripts/core/logger.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import signale from 'signale'
|
||||
|
||||
const { Signale } = signale
|
||||
|
||||
export class Logger extends Signale {
|
||||
constructor(options?: any) {
|
||||
super(options)
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
const m3u = {}
|
||||
|
||||
m3u.create = function (links = [], header = {}) {
|
||||
let output = `#EXTM3U`
|
||||
for (const attr in header) {
|
||||
const value = header[attr]
|
||||
output += ` ${attr}="${value}"`
|
||||
}
|
||||
output += `\n`
|
||||
|
||||
for (const link of 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
|
||||
}
|
||||
|
||||
module.exports = m3u
|
|
@ -1,10 +0,0 @@
|
|||
const markdownInclude = require('markdown-include')
|
||||
const file = require('./file')
|
||||
|
||||
const markdown = {}
|
||||
|
||||
markdown.compile = function (filepath) {
|
||||
markdownInclude.compileFiles(file.resolve(filepath))
|
||||
}
|
||||
|
||||
module.exports = markdown
|
13
scripts/core/markdown.ts
Normal file
13
scripts/core/markdown.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import markdownInclude from 'markdown-include'
|
||||
|
||||
export class Markdown {
|
||||
filepath: string
|
||||
|
||||
constructor(filepath: string) {
|
||||
this.filepath = filepath
|
||||
}
|
||||
|
||||
compile() {
|
||||
markdownInclude.compileFiles(this.filepath)
|
||||
}
|
||||
}
|
10
scripts/core/numberParser.ts
Normal file
10
scripts/core/numberParser.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default class NumberParser {
|
||||
async parse(number: string) {
|
||||
const parsed = parseInt(number)
|
||||
if (isNaN(parsed)) {
|
||||
throw new Error('numberParser:parse() Input value is not a number')
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
const ipp = require('iptv-playlist-parser')
|
||||
const logger = require('./logger')
|
||||
const file = require('./file')
|
||||
|
||||
const parser = {}
|
||||
|
||||
parser.parsePlaylist = async function (filepath) {
|
||||
const content = await file.read(filepath)
|
||||
|
||||
return ipp.parse(content)
|
||||
}
|
||||
|
||||
parser.parseLogs = async function (filepath) {
|
||||
const content = await file.read(filepath)
|
||||
if (!content) return []
|
||||
const lines = content.split('\n')
|
||||
|
||||
return lines.map(line => (line ? JSON.parse(line) : null)).filter(l => l)
|
||||
}
|
||||
|
||||
parser.parseNumber = function (string) {
|
||||
const parsed = parseInt(string)
|
||||
if (isNaN(parsed)) {
|
||||
throw new Error('scripts/core/parser.js:parseNumber() Input value is not a number')
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
module.exports = parser
|
|
@ -1,53 +0,0 @@
|
|||
const store = require('./store')
|
||||
const m3u = require('./m3u')
|
||||
const _ = require('lodash')
|
||||
|
||||
const playlist = {}
|
||||
|
||||
class Playlist {
|
||||
constructor(items = [], options = {}) {
|
||||
this.header = {}
|
||||
|
||||
this.links = []
|
||||
for (const item of items) {
|
||||
const stream = store.create(item)
|
||||
|
||||
let attrs
|
||||
if (options.public) {
|
||||
attrs = {
|
||||
'tvg-id': stream.get('tvg_id'),
|
||||
'tvg-logo': stream.get('tvg_logo'),
|
||||
'group-title': stream.get('group_title'),
|
||||
'user-agent': stream.get('user_agent') || undefined
|
||||
}
|
||||
} else {
|
||||
attrs = {
|
||||
'tvg-id': stream.get('tvg_id'),
|
||||
'user-agent': stream.get('user_agent') || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const vlcOpts = {
|
||||
'http-referrer': stream.get('http_referrer') || undefined,
|
||||
'http-user-agent': stream.get('user_agent') || undefined
|
||||
}
|
||||
|
||||
this.links.push({
|
||||
url: stream.get('url'),
|
||||
title: stream.get('title'),
|
||||
attrs,
|
||||
vlcOpts
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return m3u.create(this.links, this.header)
|
||||
}
|
||||
}
|
||||
|
||||
playlist.create = function (items, options) {
|
||||
return new Playlist(items, options)
|
||||
}
|
||||
|
||||
module.exports = playlist
|
45
scripts/core/playlistParser.ts
Normal file
45
scripts/core/playlistParser.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import parser from 'iptv-playlist-parser'
|
||||
import { Playlist, Stream } from '../models'
|
||||
import { Collection, Storage } from './'
|
||||
|
||||
export class PlaylistParser {
|
||||
storage: Storage
|
||||
|
||||
constructor({ storage }: { storage: Storage }) {
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
async parse(filepath: string): Promise<Playlist> {
|
||||
const streams = new Collection()
|
||||
|
||||
const content = await this.storage.read(filepath)
|
||||
const parsed: parser.Playlist = parser.parse(content)
|
||||
|
||||
parsed.items.forEach((item: parser.PlaylistItem) => {
|
||||
const { name, label, quality } = parseTitle(item.name)
|
||||
const stream = new Stream({
|
||||
channel: item.tvg.id,
|
||||
name,
|
||||
label,
|
||||
quality,
|
||||
filepath,
|
||||
line: item.line,
|
||||
url: item.url,
|
||||
httpReferrer: item.http.referrer,
|
||||
userAgent: item.http['user-agent']
|
||||
})
|
||||
|
||||
streams.add(stream)
|
||||
})
|
||||
|
||||
return new Playlist(streams)
|
||||
}
|
||||
}
|
||||
|
||||
function parseTitle(title: string): { name: string; label: string; quality: string } {
|
||||
const [, label] = title.match(/ \[(.*)\]$/) || [null, '']
|
||||
const [, quality] = title.match(/ \(([0-9]+p)\)/) || [null, '']
|
||||
const name = title.replace(` (${quality})`, '').replace(` [${label}]`, '')
|
||||
|
||||
return { name, label, quality }
|
||||
}
|
82
scripts/core/storage.ts
Normal file
82
scripts/core/storage.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { File, Collection } from './'
|
||||
import * as path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import { glob } from 'glob'
|
||||
|
||||
export class Storage {
|
||||
rootDir: string
|
||||
|
||||
constructor(rootDir?: string) {
|
||||
this.rootDir = rootDir || './'
|
||||
}
|
||||
|
||||
list(pattern: string): Promise<string[]> {
|
||||
return glob(pattern, {
|
||||
cwd: this.rootDir
|
||||
})
|
||||
}
|
||||
|
||||
async createDir(dir: string): Promise<void> {
|
||||
if (await fs.exists(dir)) return
|
||||
|
||||
await fs.mkdir(dir, { recursive: true }).catch(console.error)
|
||||
}
|
||||
|
||||
async load(filepath: string): Promise<any> {
|
||||
return this.read(filepath)
|
||||
}
|
||||
|
||||
async read(filepath: string): Promise<any> {
|
||||
const absFilepath = path.join(this.rootDir, filepath)
|
||||
|
||||
return await fs.readFile(absFilepath, { encoding: 'utf8' })
|
||||
}
|
||||
|
||||
async json(filepath: string): Promise<any> {
|
||||
const absFilepath = path.join(this.rootDir, filepath)
|
||||
const content = await fs.readFile(absFilepath, { encoding: 'utf8' })
|
||||
|
||||
return JSON.parse(content)
|
||||
}
|
||||
|
||||
async exists(filepath: string): Promise<boolean> {
|
||||
const absFilepath = path.join(this.rootDir, filepath)
|
||||
|
||||
return await fs.exists(absFilepath)
|
||||
}
|
||||
|
||||
async write(filepath: string, data: string = ''): Promise<void> {
|
||||
const absFilepath = path.join(this.rootDir, filepath)
|
||||
const dir = path.dirname(absFilepath)
|
||||
|
||||
await this.createDir(dir)
|
||||
await fs.writeFile(absFilepath, data, { encoding: 'utf8', flag: 'w' })
|
||||
}
|
||||
|
||||
async append(filepath: string, data: string = ''): Promise<void> {
|
||||
const absFilepath = path.join(this.rootDir, filepath)
|
||||
|
||||
await fs.appendFile(absFilepath, data, { encoding: 'utf8', flag: 'w' })
|
||||
}
|
||||
|
||||
async clear(filepath: string): Promise<void> {
|
||||
await this.write(filepath)
|
||||
}
|
||||
|
||||
async createStream(filepath: string): Promise<NodeJS.WriteStream> {
|
||||
const absFilepath = path.join(this.rootDir, filepath)
|
||||
const dir = path.dirname(absFilepath)
|
||||
|
||||
await this.createDir(dir)
|
||||
|
||||
return fs.createWriteStream(absFilepath) as unknown as NodeJS.WriteStream
|
||||
}
|
||||
|
||||
async save(filepath: string, content: string): Promise<void> {
|
||||
await this.write(filepath, content)
|
||||
}
|
||||
|
||||
async saveFile(file: File): Promise<void> {
|
||||
await this.write(file.filepath, file.content)
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
const _ = require('lodash')
|
||||
const logger = require('./logger')
|
||||
const setters = require('../store/setters')
|
||||
const getters = require('../store/getters')
|
||||
|
||||
module.exports = {
|
||||
create(state = {}) {
|
||||
return {
|
||||
state,
|
||||
changed: false,
|
||||
set: function (prop, value) {
|
||||
const prevState = JSON.stringify(this.state)
|
||||
|
||||
const setter = setters[prop]
|
||||
if (typeof setter === 'function') {
|
||||
try {
|
||||
this.state[prop] = setter.bind()(value)
|
||||
} catch (error) {
|
||||
logger.error(`store/setters/${prop}.js: ${error.message}`)
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
this.state[prop] = value[prop]
|
||||
} else {
|
||||
this.state[prop] = value
|
||||
}
|
||||
|
||||
const newState = JSON.stringify(this.state)
|
||||
if (prevState !== newState) {
|
||||
this.changed = true
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
get: function (prop) {
|
||||
const getter = getters[prop]
|
||||
if (typeof getter === 'function') {
|
||||
try {
|
||||
return getter.bind(this.state)()
|
||||
} catch (error) {
|
||||
logger.error(`store/getters/${prop}.js: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
return prop.split('.').reduce((o, i) => (o ? o[i] : undefined), this.state)
|
||||
}
|
||||
},
|
||||
has: function (prop) {
|
||||
const value = this.get(prop)
|
||||
|
||||
return !_.isEmpty(value)
|
||||
},
|
||||
data: function () {
|
||||
return this.state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
const table = {}
|
||||
|
||||
table.create = function (data, cols) {
|
||||
let output = '<table>\n'
|
||||
|
||||
output += ' <thead>\n <tr>'
|
||||
for (let column of cols) {
|
||||
output += `<th align="left">${column.name}</th>`
|
||||
}
|
||||
output += '</tr>\n </thead>\n'
|
||||
|
||||
output += ' <tbody>\n'
|
||||
for (let item of data) {
|
||||
output += ' <tr>'
|
||||
let i = 0
|
||||
for (let prop in item) {
|
||||
const column = cols[i]
|
||||
let nowrap = column.nowrap ? ` nowrap` : ''
|
||||
let align = column.align ? ` align="${column.align}"` : ''
|
||||
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
||||
i++
|
||||
}
|
||||
output += '</tr>\n'
|
||||
}
|
||||
output += ' </tbody>\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
module.exports = table
|
|
@ -1,29 +0,0 @@
|
|||
const { performance } = require('perf_hooks')
|
||||
const dayjs = require('dayjs')
|
||||
const duration = require('dayjs/plugin/duration')
|
||||
const relativeTime = require('dayjs/plugin/relativeTime')
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(duration)
|
||||
|
||||
const timer = {}
|
||||
|
||||
let t0 = 0
|
||||
|
||||
timer.start = function () {
|
||||
t0 = performance.now()
|
||||
}
|
||||
|
||||
timer.format = function (f) {
|
||||
let t1 = performance.now()
|
||||
|
||||
return dayjs.duration(t1 - t0).format(f)
|
||||
}
|
||||
|
||||
timer.humanize = function (suffix = true) {
|
||||
let t1 = performance.now()
|
||||
|
||||
return dayjs.duration(t1 - t0).humanize(suffix)
|
||||
}
|
||||
|
||||
module.exports = timer
|
|
@ -1,11 +0,0 @@
|
|||
const normalize = require('normalize-url')
|
||||
|
||||
const url = {}
|
||||
|
||||
url.normalize = function (string) {
|
||||
const normalized = normalize(string, { stripWWW: false })
|
||||
|
||||
return decodeURIComponent(normalized).replace(/\s/g, '+')
|
||||
}
|
||||
|
||||
module.exports = url
|
20
scripts/core/url.ts
Normal file
20
scripts/core/url.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import normalizeUrl from 'normalize-url'
|
||||
|
||||
export class URL {
|
||||
url: string
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
normalize(): URL {
|
||||
const normalized = normalizeUrl(this.url, { stripWWW: false })
|
||||
this.url = decodeURIComponent(normalized).replace(/\s/g, '+')
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.url
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue