mirror of
https://github.com/iptv-org/iptv
synced 2025-12-16 10:26:48 -05:00
Update scripts
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
|
||||
|
||||
export class ApiClient {
|
||||
instance: AxiosInstance
|
||||
|
||||
constructor() {
|
||||
this.instance = axios.create({
|
||||
baseURL: 'https://iptv-org.github.io/api',
|
||||
responseType: 'stream'
|
||||
})
|
||||
}
|
||||
|
||||
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
|
||||
return this.instance.get(url, options)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Table } from 'console-table-printer'
|
||||
import { ComplexOptions } from 'console-table-printer/dist/src/models/external-table'
|
||||
|
||||
export class CliTable {
|
||||
table: Table
|
||||
|
||||
constructor(options?: ComplexOptions | string[]) {
|
||||
this.table = new Table(options)
|
||||
}
|
||||
|
||||
append(row) {
|
||||
this.table.addRow(row)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.table.printTable()
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.table.render()
|
||||
}
|
||||
}
|
||||
import { ComplexOptions } from 'console-table-printer/dist/src/models/external-table'
|
||||
import { Table } from 'console-table-printer'
|
||||
|
||||
export class CliTable {
|
||||
table: Table
|
||||
|
||||
constructor(options?: ComplexOptions | string[]) {
|
||||
this.table = new Table(options)
|
||||
}
|
||||
|
||||
append(row) {
|
||||
this.table.addRow(row)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.table.printTable()
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.table.render()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { ApiClient } from './apiClient'
|
||||
import { Storage } from '@freearhey/core'
|
||||
import cliProgress, { MultiBar } from 'cli-progress'
|
||||
import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader'
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export class DataLoader {
|
||||
client: ApiClient
|
||||
storage: Storage
|
||||
progressBar: MultiBar
|
||||
|
||||
constructor(props: DataLoaderProps) {
|
||||
this.client = new ApiClient()
|
||||
this.storage = props.storage
|
||||
this.progressBar = new cliProgress.MultiBar({
|
||||
stopOnComplete: true,
|
||||
hideCursor: true,
|
||||
forceRedraw: true,
|
||||
barsize: 36,
|
||||
format(options, params, payload) {
|
||||
const filename = payload.filename.padEnd(18, ' ')
|
||||
const barsize = options.barsize || 40
|
||||
const percent = (params.progress * 100).toFixed(2)
|
||||
const speed = payload.speed ? formatBytes(payload.speed) + '/s' : 'N/A'
|
||||
const total = formatBytes(params.total)
|
||||
const completeSize = Math.round(params.progress * barsize)
|
||||
const incompleteSize = barsize - completeSize
|
||||
const bar =
|
||||
options.barCompleteString && options.barIncompleteString
|
||||
? options.barCompleteString.substr(0, completeSize) +
|
||||
options.barGlue +
|
||||
options.barIncompleteString.substr(0, incompleteSize)
|
||||
: '-'.repeat(barsize)
|
||||
|
||||
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async load(): Promise<DataLoaderData> {
|
||||
const [
|
||||
countries,
|
||||
regions,
|
||||
subdivisions,
|
||||
languages,
|
||||
categories,
|
||||
blocklist,
|
||||
channels,
|
||||
feeds,
|
||||
logos,
|
||||
timezones,
|
||||
guides,
|
||||
streams,
|
||||
cities
|
||||
] = await Promise.all([
|
||||
this.storage.json('countries.json'),
|
||||
this.storage.json('regions.json'),
|
||||
this.storage.json('subdivisions.json'),
|
||||
this.storage.json('languages.json'),
|
||||
this.storage.json('categories.json'),
|
||||
this.storage.json('blocklist.json'),
|
||||
this.storage.json('channels.json'),
|
||||
this.storage.json('feeds.json'),
|
||||
this.storage.json('logos.json'),
|
||||
this.storage.json('timezones.json'),
|
||||
this.storage.json('guides.json'),
|
||||
this.storage.json('streams.json'),
|
||||
this.storage.json('cities.json')
|
||||
])
|
||||
|
||||
return {
|
||||
countries,
|
||||
regions,
|
||||
subdivisions,
|
||||
languages,
|
||||
categories,
|
||||
blocklist,
|
||||
channels,
|
||||
feeds,
|
||||
logos,
|
||||
timezones,
|
||||
guides,
|
||||
streams,
|
||||
cities
|
||||
}
|
||||
}
|
||||
|
||||
async download(filename: string) {
|
||||
if (!this.storage || !this.progressBar) return
|
||||
|
||||
const stream = await this.storage.createStream(filename)
|
||||
const progressBar = this.progressBar.create(0, 0, { filename })
|
||||
|
||||
this.client
|
||||
.get(filename, {
|
||||
responseType: 'stream',
|
||||
onDownloadProgress({ total, loaded, rate }) {
|
||||
if (total) progressBar.setTotal(total)
|
||||
progressBar.update(loaded, { speed: rate })
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
response.data.pipe(stream)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { DataProcessorData } from '../types/dataProcessor'
|
||||
import { DataLoaderData } from '../types/dataLoader'
|
||||
import { Collection } from '@freearhey/core'
|
||||
import {
|
||||
BlocklistRecord,
|
||||
Subdivision,
|
||||
Category,
|
||||
Language,
|
||||
Timezone,
|
||||
Channel,
|
||||
Country,
|
||||
Region,
|
||||
Stream,
|
||||
Guide,
|
||||
City,
|
||||
Feed,
|
||||
Logo
|
||||
} from '../models'
|
||||
|
||||
export class DataProcessor {
|
||||
process(data: DataLoaderData): DataProcessorData {
|
||||
let regions = new Collection(data.regions).map(data => new Region(data))
|
||||
let regionsKeyByCode = regions.keyBy((region: Region) => region.code)
|
||||
|
||||
const categories = new Collection(data.categories).map(data => new Category(data))
|
||||
const categoriesKeyById = categories.keyBy((category: Category) => category.id)
|
||||
|
||||
const languages = new Collection(data.languages).map(data => new Language(data))
|
||||
const languagesKeyByCode = languages.keyBy((language: Language) => language.code)
|
||||
|
||||
let subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data))
|
||||
let subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
|
||||
let subdivisionsGroupedByCountryCode = subdivisions.groupBy(
|
||||
(subdivision: Subdivision) => subdivision.countryCode
|
||||
)
|
||||
|
||||
let countries = new Collection(data.countries).map(data => new Country(data))
|
||||
let countriesKeyByCode = countries.keyBy((country: Country) => country.code)
|
||||
|
||||
const cities = new Collection(data.cities).map(data =>
|
||||
new City(data)
|
||||
.withRegions(regions)
|
||||
.withCountry(countriesKeyByCode)
|
||||
.withSubdivision(subdivisionsKeyByCode)
|
||||
)
|
||||
const citiesKeyByCode = cities.keyBy((city: City) => city.code)
|
||||
const citiesGroupedByCountryCode = cities.groupBy((city: City) => city.countryCode)
|
||||
const citiesGroupedBySubdivisionCode = cities.groupBy((city: City) => city.subdivisionCode)
|
||||
|
||||
const timezones = new Collection(data.timezones).map(data =>
|
||||
new Timezone(data).withCountries(countriesKeyByCode)
|
||||
)
|
||||
const timezonesKeyById = timezones.keyBy((timezone: Timezone) => timezone.id)
|
||||
|
||||
const blocklistRecords = new Collection(data.blocklist).map(data => new BlocklistRecord(data))
|
||||
const blocklistRecordsGroupedByChannelId = blocklistRecords.groupBy(
|
||||
(blocklistRecord: BlocklistRecord) => blocklistRecord.channelId
|
||||
)
|
||||
|
||||
let channels = new Collection(data.channels).map(data => new Channel(data))
|
||||
let channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||
|
||||
let feeds = new Collection(data.feeds).map(data => new Feed(data))
|
||||
let feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
||||
let feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
|
||||
|
||||
const logos = new Collection(data.logos).map(data => new Logo(data).withFeed(feedsGroupedById))
|
||||
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
|
||||
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
|
||||
|
||||
const streams = new Collection(data.streams).map(data =>
|
||||
new Stream(data).withLogos(logosGroupedByStreamId)
|
||||
)
|
||||
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||
|
||||
const guides = new Collection(data.guides).map(data => new Guide(data))
|
||||
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId())
|
||||
|
||||
regions = regions.map((region: Region) =>
|
||||
region
|
||||
.withCountries(countriesKeyByCode)
|
||||
.withRegions(regions)
|
||||
.withSubdivisions(subdivisions)
|
||||
.withCities(cities)
|
||||
)
|
||||
regionsKeyByCode = regions.keyBy((region: Region) => region.code)
|
||||
|
||||
countries = countries.map((country: Country) =>
|
||||
country
|
||||
.withCities(citiesGroupedByCountryCode)
|
||||
.withSubdivisions(subdivisionsGroupedByCountryCode)
|
||||
.withRegions(regions)
|
||||
.withLanguage(languagesKeyByCode)
|
||||
)
|
||||
countriesKeyByCode = countries.keyBy((country: Country) => country.code)
|
||||
|
||||
subdivisions = subdivisions.map((subdivision: Subdivision) =>
|
||||
subdivision
|
||||
.withCities(citiesGroupedBySubdivisionCode)
|
||||
.withCountry(countriesKeyByCode)
|
||||
.withRegions(regions)
|
||||
.withParent(subdivisionsKeyByCode)
|
||||
)
|
||||
subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
|
||||
subdivisionsGroupedByCountryCode = subdivisions.groupBy(
|
||||
(subdivision: Subdivision) => subdivision.countryCode
|
||||
)
|
||||
|
||||
channels = channels.map((channel: Channel) =>
|
||||
channel
|
||||
.withFeeds(feedsGroupedByChannelId)
|
||||
.withLogos(logosGroupedByChannelId)
|
||||
.withCategories(categoriesKeyById)
|
||||
.withCountry(countriesKeyByCode)
|
||||
.withSubdivision(subdivisionsKeyByCode)
|
||||
.withCategories(categoriesKeyById)
|
||||
)
|
||||
channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||
|
||||
feeds = feeds.map((feed: Feed) =>
|
||||
feed
|
||||
.withChannel(channelsKeyById)
|
||||
.withLanguages(languagesKeyByCode)
|
||||
.withTimezones(timezonesKeyById)
|
||||
.withBroadcastArea(
|
||||
citiesKeyByCode,
|
||||
subdivisionsKeyByCode,
|
||||
countriesKeyByCode,
|
||||
regionsKeyByCode
|
||||
)
|
||||
)
|
||||
feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
||||
feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
|
||||
|
||||
return {
|
||||
blocklistRecordsGroupedByChannelId,
|
||||
subdivisionsGroupedByCountryCode,
|
||||
feedsGroupedByChannelId,
|
||||
guidesGroupedByStreamId,
|
||||
logosGroupedByStreamId,
|
||||
subdivisionsKeyByCode,
|
||||
countriesKeyByCode,
|
||||
languagesKeyByCode,
|
||||
streamsGroupedById,
|
||||
categoriesKeyById,
|
||||
timezonesKeyById,
|
||||
regionsKeyByCode,
|
||||
blocklistRecords,
|
||||
channelsKeyById,
|
||||
citiesKeyByCode,
|
||||
subdivisions,
|
||||
categories,
|
||||
countries,
|
||||
languages,
|
||||
timezones,
|
||||
channels,
|
||||
regions,
|
||||
streams,
|
||||
cities,
|
||||
guides,
|
||||
feeds,
|
||||
logos
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,50 @@
|
||||
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>\r\n'
|
||||
|
||||
output += ' <thead>\r\n <tr>'
|
||||
for (const column of this.columns) {
|
||||
output += `<th align="left">${column.name}</th>`
|
||||
}
|
||||
output += '</tr>\r\n </thead>\r\n'
|
||||
|
||||
output += ' <tbody>\r\n'
|
||||
for (const item of this.data) {
|
||||
output += ' <tr>'
|
||||
let i = 0
|
||||
for (const prop in item) {
|
||||
const column = this.columns[i]
|
||||
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||
const align = column.align ? ` align="${column.align}"` : ''
|
||||
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
||||
i++
|
||||
}
|
||||
output += '</tr>\r\n'
|
||||
}
|
||||
output += ' </tbody>\r\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
import { Collection } from '@freearhey/core'
|
||||
|
||||
export type HTMLTableColumn = {
|
||||
name: string
|
||||
nowrap?: boolean
|
||||
align?: string
|
||||
}
|
||||
|
||||
export type HTMLTableItem = string[]
|
||||
|
||||
export class HTMLTable {
|
||||
data: Collection<HTMLTableItem>
|
||||
columns: Collection<HTMLTableColumn>
|
||||
|
||||
constructor(data: Collection<HTMLTableItem>, columns: Collection<HTMLTableColumn>) {
|
||||
this.data = data
|
||||
this.columns = columns
|
||||
}
|
||||
|
||||
toString() {
|
||||
let output = '<table>\r\n'
|
||||
|
||||
output += ' <thead>\r\n <tr>'
|
||||
this.columns.forEach((column: HTMLTableColumn) => {
|
||||
output += `<th align="left">${column.name}</th>`
|
||||
})
|
||||
|
||||
output += '</tr>\r\n </thead>\r\n'
|
||||
|
||||
output += ' <tbody>\r\n'
|
||||
this.data.forEach((item: HTMLTableItem) => {
|
||||
output += ' <tr>'
|
||||
let i = 0
|
||||
for (const prop in item) {
|
||||
const column = this.columns.all()[i]
|
||||
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||
const align = column.align ? ` align="${column.align}"` : ''
|
||||
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
||||
i++
|
||||
}
|
||||
output += '</tr>\r\n'
|
||||
})
|
||||
|
||||
output += ' </tbody>\r\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
export * from './apiClient'
|
||||
export * from './cliTable'
|
||||
export * from './dataProcessor'
|
||||
export * from './dataLoader'
|
||||
export * from './htmlTable'
|
||||
export * from './issueData'
|
||||
export * from './issueLoader'
|
||||
export * from './issueParser'
|
||||
export * from './logParser'
|
||||
export * from './markdown'
|
||||
export * from './numberParser'
|
||||
export * from './playlistParser'
|
||||
export * from './proxyParser'
|
||||
export * from './streamTester'
|
||||
export * from './cliTable'
|
||||
export * from './htmlTable'
|
||||
export * from './issueData'
|
||||
export * from './issueLoader'
|
||||
export * from './issueParser'
|
||||
export * from './logParser'
|
||||
export * from './markdown'
|
||||
export * from './numberParser'
|
||||
export * from './playlistParser'
|
||||
export * from './proxyParser'
|
||||
export * from './streamTester'
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
import { Dictionary } from '@freearhey/core'
|
||||
|
||||
export class IssueData {
|
||||
_data: Dictionary
|
||||
constructor(data: Dictionary) {
|
||||
this._data = data
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this._data.has(key)
|
||||
}
|
||||
|
||||
missing(key: string): boolean {
|
||||
return this._data.missing(key) || this._data.get(key) === undefined
|
||||
}
|
||||
|
||||
getBoolean(key: string): boolean {
|
||||
return Boolean(this._data.get(key))
|
||||
}
|
||||
|
||||
getString(key: string): string | undefined {
|
||||
const deleteSymbol = '~'
|
||||
|
||||
return this._data.get(key) === deleteSymbol ? '' : this._data.get(key)
|
||||
}
|
||||
|
||||
getArray(key: string): string[] | undefined {
|
||||
const deleteSymbol = '~'
|
||||
|
||||
if (this._data.missing(key)) return undefined
|
||||
|
||||
return this._data.get(key) === deleteSymbol ? [] : this._data.get(key).split('\r\n')
|
||||
}
|
||||
}
|
||||
import { Dictionary } from '@freearhey/core'
|
||||
|
||||
export class IssueData {
|
||||
_data: Dictionary<string>
|
||||
constructor(data: Dictionary<string>) {
|
||||
this._data = data
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this._data.has(key)
|
||||
}
|
||||
|
||||
missing(key: string): boolean {
|
||||
return this._data.missing(key) || this._data.get(key) === undefined
|
||||
}
|
||||
|
||||
getBoolean(key: string): boolean {
|
||||
return Boolean(this._data.get(key))
|
||||
}
|
||||
|
||||
getString(key: string): string | undefined {
|
||||
const deleteSymbol = '~'
|
||||
|
||||
return this._data.get(key) === deleteSymbol ? '' : this._data.get(key)
|
||||
}
|
||||
|
||||
getArray(key: string): string[] | undefined {
|
||||
const deleteSymbol = '~'
|
||||
|
||||
if (this._data.missing(key)) return undefined
|
||||
|
||||
const value = this._data.get(key)
|
||||
|
||||
return !value || value === deleteSymbol ? [] : value.split('\r\n')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { Collection } from '@freearhey/core'
|
||||
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
||||
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { IssueParser } from './'
|
||||
import { TESTING, OWNER, REPO } from '../constants'
|
||||
|
||||
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
||||
const octokit = new CustomOctokit()
|
||||
|
||||
export class IssueLoader {
|
||||
async load(props?: { labels: string | string[] }) {
|
||||
let labels = ''
|
||||
if (props && props.labels) {
|
||||
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
|
||||
}
|
||||
let issues: object[] = []
|
||||
if (TESTING) {
|
||||
issues = (await import('../../tests/__data__/input/issues.js')).default
|
||||
} else {
|
||||
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
per_page: 100,
|
||||
labels,
|
||||
status: 'open',
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const parser = new IssueParser()
|
||||
|
||||
return new Collection(issues).map(parser.parse)
|
||||
}
|
||||
}
|
||||
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
||||
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
||||
import { TESTING, OWNER, REPO } from '../constants'
|
||||
import { Collection } from '@freearhey/core'
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { IssueParser } from './'
|
||||
|
||||
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
||||
const octokit = new CustomOctokit()
|
||||
|
||||
export class IssueLoader {
|
||||
async load(props?: { labels: string | string[] }) {
|
||||
let labels = ''
|
||||
if (props && props.labels) {
|
||||
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
|
||||
}
|
||||
let issues: object[] = []
|
||||
if (TESTING) {
|
||||
issues = (await import('../../tests/__data__/input/issues.js')).default
|
||||
} else {
|
||||
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
per_page: 100,
|
||||
labels,
|
||||
status: 'open',
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const parser = new IssueParser()
|
||||
|
||||
return new Collection(issues).map(parser.parse)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import { Dictionary } from '@freearhey/core'
|
||||
import { Issue } from '../models'
|
||||
import { IssueData } from './issueData'
|
||||
|
||||
const FIELDS = new Dictionary({
|
||||
'Stream ID': 'streamId',
|
||||
'Channel ID': 'channelId',
|
||||
'Feed ID': 'feedId',
|
||||
'Stream URL': 'streamUrl',
|
||||
'New Stream URL': 'newStreamUrl',
|
||||
Label: 'label',
|
||||
Quality: 'quality',
|
||||
'HTTP User-Agent': 'httpUserAgent',
|
||||
'HTTP User Agent': 'httpUserAgent',
|
||||
'HTTP Referrer': 'httpReferrer',
|
||||
'What happened to the stream?': 'reason',
|
||||
Reason: 'reason',
|
||||
Notes: 'notes',
|
||||
Directives: 'directives'
|
||||
})
|
||||
|
||||
export class IssueParser {
|
||||
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
||||
const fields = typeof issue.body === 'string' ? issue.body.split('###') : []
|
||||
|
||||
const data = new Dictionary()
|
||||
fields.forEach((field: string) => {
|
||||
const parsed = typeof field === 'string' ? field.split(/\r?\n/).filter(Boolean) : []
|
||||
let _label = parsed.shift()
|
||||
_label = _label ? _label.replace(/ \(optional\)| \(required\)/, '').trim() : ''
|
||||
let _value = parsed.join('\r\n')
|
||||
_value = _value ? _value.trim() : ''
|
||||
|
||||
if (!_label || !_value) return data
|
||||
|
||||
const id: string = FIELDS.get(_label)
|
||||
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
||||
|
||||
if (!id) return
|
||||
|
||||
data.set(id, value)
|
||||
})
|
||||
|
||||
const labels = issue.labels.map(label => label.name)
|
||||
|
||||
return new Issue({ number: issue.number, labels, data: new IssueData(data) })
|
||||
}
|
||||
}
|
||||
import { Dictionary } from '@freearhey/core'
|
||||
import { IssueData } from './issueData'
|
||||
import { Issue } from '../models'
|
||||
|
||||
const FIELDS = new Dictionary({
|
||||
'Stream ID': 'streamId',
|
||||
'Channel ID': 'channelId',
|
||||
'Feed ID': 'feedId',
|
||||
'Stream URL': 'streamUrl',
|
||||
'New Stream URL': 'newStreamUrl',
|
||||
Label: 'label',
|
||||
Quality: 'quality',
|
||||
'HTTP User-Agent': 'httpUserAgent',
|
||||
'HTTP User Agent': 'httpUserAgent',
|
||||
'HTTP Referrer': 'httpReferrer',
|
||||
'What happened to the stream?': 'reason',
|
||||
Reason: 'reason',
|
||||
Notes: 'notes',
|
||||
Directives: 'directives'
|
||||
})
|
||||
|
||||
export class IssueParser {
|
||||
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
||||
const fields = typeof issue.body === 'string' ? issue.body.split('###') : []
|
||||
|
||||
const data = new Dictionary<string>()
|
||||
fields.forEach((field: string) => {
|
||||
const parsed = typeof field === 'string' ? field.split(/\r?\n/).filter(Boolean) : []
|
||||
let _label = parsed.shift()
|
||||
_label = _label ? _label.replace(/ \(optional\)| \(required\)/, '').trim() : ''
|
||||
let _value = parsed.join('\r\n')
|
||||
_value = _value ? _value.trim() : ''
|
||||
|
||||
if (!_label || !_value) return data
|
||||
|
||||
const id = FIELDS.get(_label)
|
||||
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
||||
|
||||
if (!id) return
|
||||
|
||||
data.set(id, value)
|
||||
})
|
||||
|
||||
const labels = issue.labels.map(label => label.name)
|
||||
|
||||
return new Issue({ number: issue.number, labels, data: new IssueData(data) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
type MarkdownConfig = {
|
||||
build: string
|
||||
template: string
|
||||
}
|
||||
|
||||
export class Markdown {
|
||||
build: string
|
||||
template: string
|
||||
|
||||
constructor(config: MarkdownConfig) {
|
||||
this.build = config.build
|
||||
this.template = config.template
|
||||
}
|
||||
|
||||
compile() {
|
||||
const workingDir = process.cwd()
|
||||
|
||||
const templatePath = path.resolve(workingDir, this.template)
|
||||
const template = fs.readFileSync(templatePath, 'utf8')
|
||||
const processedContent = this.processIncludes(template, workingDir)
|
||||
|
||||
if (this.build) {
|
||||
const outputPath = path.resolve(workingDir, this.build)
|
||||
fs.writeFileSync(outputPath, processedContent, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
private processIncludes(template: string, baseDir: string): string {
|
||||
const includeRegex = /#include\s+"([^"]+)"/g
|
||||
|
||||
return template.replace(includeRegex, (match, includePath) => {
|
||||
try {
|
||||
const fullPath = path.resolve(baseDir, includePath)
|
||||
const includeContent = fs.readFileSync(fullPath, 'utf8')
|
||||
return this.processIncludes(includeContent, baseDir)
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not include file ${includePath}: ${error}`)
|
||||
return match
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
type MarkdownConfig = {
|
||||
build: string
|
||||
template: string
|
||||
}
|
||||
|
||||
export class Markdown {
|
||||
build: string
|
||||
template: string
|
||||
|
||||
constructor(config: MarkdownConfig) {
|
||||
this.build = config.build
|
||||
this.template = config.template
|
||||
}
|
||||
|
||||
compile() {
|
||||
const workingDir = process.cwd()
|
||||
|
||||
const templatePath = path.resolve(workingDir, this.template)
|
||||
const template = fs.readFileSync(templatePath, 'utf8')
|
||||
const processedContent = this.processIncludes(template, workingDir)
|
||||
|
||||
if (this.build) {
|
||||
const outputPath = path.resolve(workingDir, this.build)
|
||||
fs.writeFileSync(outputPath, processedContent, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
private processIncludes(template: string, baseDir: string): string {
|
||||
const includeRegex = /#include\s+"([^"]+)"/g
|
||||
|
||||
return template.replace(includeRegex, (match, includePath) => {
|
||||
try {
|
||||
const fullPath = path.resolve(baseDir, includePath)
|
||||
const includeContent = fs.readFileSync(fullPath, 'utf8')
|
||||
return this.processIncludes(includeContent, baseDir)
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not include file ${includePath}: ${error}`)
|
||||
return match
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,43 @@
|
||||
import { Collection, Storage, Dictionary } from '@freearhey/core'
|
||||
import parser from 'iptv-playlist-parser'
|
||||
import { Stream } from '../models'
|
||||
|
||||
type PlaylistPareserProps = {
|
||||
storage: Storage
|
||||
feedsGroupedByChannelId: Dictionary
|
||||
logosGroupedByStreamId: Dictionary
|
||||
channelsKeyById: Dictionary
|
||||
}
|
||||
|
||||
export class PlaylistParser {
|
||||
storage: Storage
|
||||
feedsGroupedByChannelId: Dictionary
|
||||
logosGroupedByStreamId: Dictionary
|
||||
channelsKeyById: Dictionary
|
||||
|
||||
constructor({
|
||||
storage,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
channelsKeyById
|
||||
}: PlaylistPareserProps) {
|
||||
this.storage = storage
|
||||
this.feedsGroupedByChannelId = feedsGroupedByChannelId
|
||||
this.logosGroupedByStreamId = logosGroupedByStreamId
|
||||
this.channelsKeyById = channelsKeyById
|
||||
}
|
||||
|
||||
async parse(files: string[]): Promise<Collection> {
|
||||
let streams = new Collection()
|
||||
|
||||
for (const filepath of files) {
|
||||
if (!this.storage.existsSync(filepath)) continue
|
||||
|
||||
const _streams: Collection = await this.parseFile(filepath)
|
||||
streams = streams.concat(_streams)
|
||||
}
|
||||
|
||||
return streams
|
||||
}
|
||||
|
||||
async parseFile(filepath: string): Promise<Collection> {
|
||||
const content = await this.storage.load(filepath)
|
||||
const parsed: parser.Playlist = parser.parse(content)
|
||||
|
||||
const streams = new Collection(parsed.items).map((data: parser.PlaylistItem) => {
|
||||
const stream = new Stream()
|
||||
.fromPlaylistItem(data)
|
||||
.withFeed(this.feedsGroupedByChannelId)
|
||||
.withChannel(this.channelsKeyById)
|
||||
.withLogos(this.logosGroupedByStreamId)
|
||||
.setFilepath(filepath)
|
||||
|
||||
return stream
|
||||
})
|
||||
|
||||
return streams
|
||||
}
|
||||
}
|
||||
import { Storage } from '@freearhey/storage-js'
|
||||
import { Collection } from '@freearhey/core'
|
||||
import parser from 'iptv-playlist-parser'
|
||||
import { Stream } from '../models'
|
||||
|
||||
type PlaylistPareserProps = {
|
||||
storage: Storage
|
||||
}
|
||||
|
||||
export class PlaylistParser {
|
||||
storage: Storage
|
||||
|
||||
constructor({ storage }: PlaylistPareserProps) {
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
async parse(files: string[]): Promise<Collection<Stream>> {
|
||||
const parsed = new Collection<Stream>()
|
||||
|
||||
for (const filepath of files) {
|
||||
if (!this.storage.existsSync(filepath)) continue
|
||||
const _parsed: Collection<Stream> = await this.parseFile(filepath)
|
||||
parsed.concat(_parsed)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
async parseFile(filepath: string): Promise<Collection<Stream>> {
|
||||
const content = await this.storage.load(filepath)
|
||||
const parsed: parser.Playlist = parser.parse(content)
|
||||
|
||||
const streams = new Collection<Stream>()
|
||||
parsed.items.forEach((data: parser.PlaylistItem) => {
|
||||
const stream = Stream.fromPlaylistItem(data)
|
||||
stream.filepath = filepath
|
||||
|
||||
streams.add(stream)
|
||||
})
|
||||
|
||||
return streams
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +1,125 @@
|
||||
import { Stream } from '../models'
|
||||
import { TESTING } from '../constants'
|
||||
import mediaInfoFactory from 'mediainfo.js'
|
||||
import axios, { AxiosInstance, AxiosProxyConfig, AxiosRequestConfig } from 'axios'
|
||||
import { ProxyParser } from './proxyParser.js'
|
||||
import { OptionValues } from 'commander'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
|
||||
export type TestResult = {
|
||||
status: {
|
||||
ok: boolean
|
||||
code: string
|
||||
}
|
||||
}
|
||||
|
||||
export type StreamTesterProps = {
|
||||
options: OptionValues
|
||||
}
|
||||
|
||||
export class StreamTester {
|
||||
client: AxiosInstance
|
||||
options: OptionValues
|
||||
|
||||
constructor({ options }: StreamTesterProps) {
|
||||
const proxyParser = new ProxyParser()
|
||||
let request: AxiosRequestConfig = {
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
|
||||
if (options.proxy !== undefined) {
|
||||
const proxy = proxyParser.parse(options.proxy) as AxiosProxyConfig
|
||||
|
||||
if (
|
||||
proxy.protocol &&
|
||||
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
||||
) {
|
||||
const socksProxyAgent = new SocksProxyAgent(options.proxy)
|
||||
|
||||
request = { ...request, ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } }
|
||||
} else {
|
||||
request = { ...request, ...{ proxy } }
|
||||
}
|
||||
}
|
||||
|
||||
this.client = axios.create(request)
|
||||
this.options = options
|
||||
}
|
||||
|
||||
async test(stream: Stream): Promise<TestResult> {
|
||||
if (TESTING) {
|
||||
const results = (await import('../../tests/__data__/input/playlist_test/results.js')).default
|
||||
|
||||
return results[stream.url as keyof typeof results]
|
||||
} else {
|
||||
try {
|
||||
const res = await this.client(stream.url, {
|
||||
signal: AbortSignal.timeout(this.options.timeout),
|
||||
headers: {
|
||||
'User-Agent': stream.getUserAgent() || 'Mozilla/5.0',
|
||||
Referer: stream.getReferrer()
|
||||
}
|
||||
})
|
||||
|
||||
const mediainfo = await mediaInfoFactory({ format: 'object' })
|
||||
const buffer = await res.data
|
||||
const result = await mediainfo.analyzeData(
|
||||
() => buffer.byteLength,
|
||||
(size: any, offset: number | undefined) =>
|
||||
Buffer.from(buffer).subarray(offset, offset + size)
|
||||
)
|
||||
|
||||
if (result && result.media && result.media.track.length > 0) {
|
||||
return {
|
||||
status: {
|
||||
ok: true,
|
||||
code: 'OK'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: {
|
||||
ok: false,
|
||||
code: 'NO_VIDEO'
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
let code = 'UNKNOWN_ERROR'
|
||||
if (error.name === 'CanceledError') {
|
||||
code = 'TIMEOUT'
|
||||
} else if (error.name === 'AxiosError') {
|
||||
if (error.response) {
|
||||
const status = error.response?.status
|
||||
const statusText = error.response?.statusText.toUpperCase().replace(/\s+/, '_')
|
||||
code = `HTTP_${status}_${statusText}`
|
||||
} else {
|
||||
code = `AXIOS_${error.code}`
|
||||
}
|
||||
} else if (error.cause) {
|
||||
const cause = error.cause as Error & { code?: string }
|
||||
if (cause.code) {
|
||||
code = cause.code
|
||||
} else {
|
||||
code = cause.name
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: {
|
||||
ok: false,
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import axios, { AxiosInstance, AxiosProxyConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
import { ProxyParser } from './proxyParser.js'
|
||||
import mediaInfoFactory from 'mediainfo.js'
|
||||
import { OptionValues } from 'commander'
|
||||
import { TESTING } from '../constants'
|
||||
import { Stream } from '../models'
|
||||
|
||||
export type StreamTesterResult = {
|
||||
status: {
|
||||
ok: boolean
|
||||
code: string
|
||||
}
|
||||
}
|
||||
|
||||
export type StreamTesterError = {
|
||||
name: string
|
||||
code?: string
|
||||
cause?: Error & { code?: string }
|
||||
response?: AxiosResponse
|
||||
}
|
||||
|
||||
export type StreamTesterProps = {
|
||||
options: OptionValues
|
||||
}
|
||||
|
||||
export class StreamTester {
|
||||
client: AxiosInstance
|
||||
options: OptionValues
|
||||
|
||||
constructor({ options }: StreamTesterProps) {
|
||||
const proxyParser = new ProxyParser()
|
||||
let request: AxiosRequestConfig = {
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
|
||||
if (options.proxy !== undefined) {
|
||||
const proxy = proxyParser.parse(options.proxy) as AxiosProxyConfig
|
||||
|
||||
if (
|
||||
proxy.protocol &&
|
||||
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
||||
) {
|
||||
const socksProxyAgent = new SocksProxyAgent(options.proxy)
|
||||
|
||||
request = { ...request, ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } }
|
||||
} else {
|
||||
request = { ...request, ...{ proxy } }
|
||||
}
|
||||
}
|
||||
|
||||
this.client = axios.create(request)
|
||||
this.options = options
|
||||
}
|
||||
|
||||
async test(stream: Stream): Promise<StreamTesterResult> {
|
||||
if (TESTING) {
|
||||
const results = (await import('../../tests/__data__/input/playlist_test/results.js')).default
|
||||
|
||||
return results[stream.url as keyof typeof results]
|
||||
} else {
|
||||
try {
|
||||
const res = await this.client(stream.url, {
|
||||
signal: AbortSignal.timeout(this.options.timeout),
|
||||
headers: {
|
||||
'User-Agent': stream.user_agent || 'Mozilla/5.0',
|
||||
Referer: stream.referrer
|
||||
}
|
||||
})
|
||||
|
||||
const mediainfo = await mediaInfoFactory({ format: 'object' })
|
||||
const buffer = await res.data
|
||||
const result = await mediainfo.analyzeData(
|
||||
() => buffer.byteLength,
|
||||
(size: number, offset: number) => Buffer.from(buffer).subarray(offset, offset + size)
|
||||
)
|
||||
|
||||
if (result && result.media && result.media.track.length > 0) {
|
||||
return {
|
||||
status: {
|
||||
ok: true,
|
||||
code: 'OK'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: {
|
||||
ok: false,
|
||||
code: 'NO_VIDEO'
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const error = err as StreamTesterError
|
||||
|
||||
let code = 'UNKNOWN_ERROR'
|
||||
if (error.name === 'CanceledError') {
|
||||
code = 'TIMEOUT'
|
||||
} else if (error.name === 'AxiosError') {
|
||||
if (error.response) {
|
||||
const status = error.response?.status
|
||||
const statusText = error.response?.statusText.toUpperCase().replace(/\s+/, '_')
|
||||
code = `HTTP_${status}_${statusText}`
|
||||
} else {
|
||||
code = `AXIOS_${error.code}`
|
||||
}
|
||||
} else if (error.cause) {
|
||||
const cause = error.cause
|
||||
if (cause.code) {
|
||||
code = cause.code
|
||||
} else {
|
||||
code = cause.name
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: {
|
||||
ok: false,
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user