Merge branch 'master' of https://github.com/iptv-org/epg into be-2957-s1

This commit is contained in:
theofficialomega
2026-04-09 00:15:16 +02:00
170 changed files with 40849 additions and 6648 deletions

View File

@@ -26,7 +26,8 @@ async function loadData() {
data.feedsKeyByStreamId = feeds.keyBy((feed: sdk.Models.Feed) => feed.getStreamId())
data.feedsGroupedByChannelId = feeds.groupBy((feed: sdk.Models.Feed) => feed.channel)
searchIndex = sdk.SearchEngine.createIndex<sdk.Models.Channel>(channels)
const searchableData = channels.map((channel: sdk.Models.Channel) => channel.getSearchable())
searchIndex = sdk.SearchEngine.createIndex<sdk.Types.ChannelSearchableData>(searchableData.all())
}
async function downloadData() {

View File

@@ -7,7 +7,6 @@ import { Storage } from '@freearhey/storage-js'
import { Channel } from '../../models'
import * as sdk from '@iptv-org/sdk'
import { Command } from 'commander'
import readline from 'readline'
interface ChoiceValue {
type: string
@@ -20,17 +19,6 @@ interface Choice {
default?: boolean
}
if (process.platform === 'win32') {
readline
.createInterface({
input: process.stdin,
output: process.stdout
})
.on('SIGINT', function () {
process.emit('SIGINT')
})
}
const program = new Command()
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
@@ -43,12 +31,7 @@ let channelsFromXML = new Collection<Channel>()
main(filepath)
process.on('SIGINT', () => {
save(filepath, channelsFromXML)
process.exit(0)
})
process.on('SIGTERM', () => {
save(filepath, channelsFromXML)
process.exit(0)
if (process.platform === 'win32') process.kill(0)
})
export default async function main(filepath: string) {
@@ -90,8 +73,7 @@ export default async function main(filepath: string) {
}
async function selectChannel(channel: epgGrabber.Channel): Promise<string> {
const query = escapeRegex(channel.name)
const similarChannels = searchChannels(query)
const similarChannels = searchChannels(channel.name)
const choices = getChoicesForChannel(similarChannels).all()
const selected: ChoiceValue = await select({
@@ -199,7 +181,3 @@ function save(filepath: string, channelsFromXML: Collection<Channel>) {
console.log()
logger.info(`File '${filepath}' successfully saved`)
}
function escapeRegex(string: string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
}

View File

@@ -0,0 +1,144 @@
import { HTMLTableRow, HTMLTableDataItem, HTMLTableColumn } from '../../types/htmlTable'
import epgGrabber, { EPGGrabber } from 'epg-grabber'
import AxiosMockAdapter from 'axios-mock-adapter'
import { Storage } from '@freearhey/storage-js'
import { Channel, Worker } from '../../models'
import { Collection } from '@freearhey/core'
import { ROOT_DIR } from '../../constants'
import { Logger } from '@freearhey/core'
import { HTMLTable } from '../../core'
import epgParser from 'epg-parser'
import axios from 'axios'
async function main() {
const logger = new Logger({ level: process.env.NODE_ENV === 'test' ? -999 : 3 })
const rootStorage = new Storage(ROOT_DIR)
const workers = new Map<string, Worker>()
logger.info('loading workers.txt...')
const workersTxt = await rootStorage.load('workers.txt')
workersTxt.split('\r\n').forEach((host: string) => {
if (!host) return
const worker = new Worker({ host })
workers.set(host, worker)
})
for (const worker of workers.values()) {
logger.info(`processing "${worker.host}"...`)
const client = axios.create({
baseURL: worker.getBaseUrl(),
timeout: 60000
})
if (process.env.NODE_ENV === 'test') {
const mock = new AxiosMockAdapter(client)
if (worker.host === 'example.com') {
mock.onGet('worker.json').reply(404)
} else {
const testStorage = new Storage('tests/__data__/input/guides_update')
mock.onGet('worker.json').reply(200, await testStorage.load('worker.json'))
mock.onGet('channels.xml').reply(200, await testStorage.load('channels.xml'))
mock.onGet('guide.xml').reply(200, await testStorage.load('guide.xml'))
}
}
const workerJson = await client
.get('worker.json')
.then(res => res.data)
.catch(err => {
worker.status = err.status
logger.error(err.message)
})
if (!workerJson) {
worker.status = 'MISSING_WORKER_CONFIG'
logger.error('Unable to load "workers.json"')
continue
}
worker.channelsPath = workerJson.channels
worker.guidePath = workerJson.guide
if (!worker.channelsPath) {
worker.status = 'MISSING_CHANNELS_PATH'
logger.error('The "channels" property is missing from the workers config')
continue
}
if (!worker.guidePath) {
worker.status = 'MISSING_GUIDE_PATH'
logger.error('The "guide" property is missing from the workers config')
continue
}
const channelsXml = await client
.get(worker.channelsPath)
.then(res => res.data)
.catch(err => {
worker.status = err.status
logger.error(err.message)
})
if (!channelsXml) continue
const parsedChannels = EPGGrabber.parseChannelsXML(channelsXml)
worker.channels = new Collection(parsedChannels).map(
(channel: epgGrabber.Channel) => new Channel(channel.toObject())
)
const guideXml = await client
.get(worker.guidePath)
.then(res => res.data)
.catch(err => {
worker.status = err.status
logger.error(err.message)
})
if (!guideXml) continue
const parsedGuide = epgParser.parse(guideXml)
worker.lastUpdated = parsedGuide.date
worker.status = 'OK'
}
logger.info('creating guides table...')
const rows = new Collection<HTMLTableRow>()
workers.forEach((worker: Worker) => {
rows.add(
new Collection<HTMLTableDataItem>([
{ value: worker.host },
{ value: worker.getStatusEmoji(), align: 'center' },
{ value: worker.getChannelsCount().toString(), align: 'right' },
{ value: worker.getLastUpdated(), align: 'left' },
{
value:
worker.status === 'OK'
? `<a href="${worker.getChannelsUrl()}">${worker.channelsPath}</a><br><a href="${worker.getGuideUrl()}">${worker.guidePath}</a>`
: ''
}
])
)
})
logger.info('updating guides.md...')
const table = new HTMLTable(
rows,
new Collection<HTMLTableColumn>([
{ name: 'Host', align: 'left' },
{ name: 'Status', align: 'left' },
{ name: 'Channels', align: 'left' },
{ name: 'Last Updated', align: 'left' },
{ name: 'Links', align: 'left' }
])
)
const guidesTemplate = await new Storage().load('scripts/templates/_guides.md')
const guidesContent = guidesTemplate.replace('_TABLE_', table.toString())
await rootStorage.save('GUIDES.md', guidesContent)
}
main()

View File

@@ -80,7 +80,9 @@ export async function loadJs(filepath: string) {
export async function loadIssues(props?: { labels: string[] | string }) {
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
const octokit = new CustomOctokit()
const octokit = new CustomOctokit({
auth: process.env.GH_TOKEN
})
let labels = ''
if (props && props.labels) {

View File

@@ -3,3 +3,4 @@ export * from './issue'
export * from './site'
export * from './channel'
export * from './program'
export * from './worker'

73
scripts/models/worker.ts Normal file
View File

@@ -0,0 +1,73 @@
import relativeTime from 'dayjs/plugin/relativeTime'
import { Collection } from '@freearhey/core'
import { Channel } from './channel'
import utc from 'dayjs/plugin/utc'
import dayjs from 'dayjs'
dayjs.extend(relativeTime)
dayjs.extend(utc)
export interface WorkerData {
host: string
}
export class Worker {
host: string
channelsPath?: string
guidePath?: string
channels?: Collection<Channel>
status?: string
lastUpdated?: string
constructor(data: WorkerData) {
this.host = data.host
}
getBaseUrl(): string {
return `https://${this.host}`
}
getConfigUrl(): string {
const url = new URL('worker.json', this.getBaseUrl())
return url.href
}
getChannelsUrl(): string {
if (!this.channelsPath) return ''
const url = new URL(this.channelsPath, this.getBaseUrl())
return url.href
}
getGuideUrl(): string {
if (!this.guidePath) return ''
const url = new URL(this.guidePath, this.getBaseUrl())
return url.href
}
getStatusEmoji(): string {
if (!this.status) return '⚪'
if (this.status === 'OK') return '🟢'
return '🔴'
}
getChannelsCount(): number {
if (!this.channels) return 0
return this.channels.count()
}
getLastUpdated(): string {
if (!this.lastUpdated) return '-'
let now = dayjs()
if (process.env.NODE_ENV === 'test') now = dayjs.utc('2026-02-13')
return dayjs.utc(this.lastUpdated).from(now)
}
}

View File

@@ -0,0 +1,5 @@
# Guides
_TABLE_
[How can I add my server to the list?](CONTRIBUTING.md#how-to-add-my-server-to-the-guides-md)