Update scripts

This commit is contained in:
freearhey
2025-07-18 22:51:01 +03:00
parent 3418a58991
commit a4fd7d7ae7
28 changed files with 572 additions and 382 deletions

View File

@@ -1,17 +1,9 @@
import { Logger, Storage, Collection } from '@freearhey/core' import { Logger, Collection, Storage } from '@freearhey/core'
import { ChannelsParser } from '../../core'
import path from 'path'
import { SITES_DIR, API_DIR } from '../../constants' import { SITES_DIR, API_DIR } from '../../constants'
import { GuideChannel } from '../../models'
import { ChannelsParser } from '../../core'
import epgGrabber from 'epg-grabber' import epgGrabber from 'epg-grabber'
import path from 'path'
type OutputItem = {
channel: string | null
feed: string | null
site: string
site_id: string
site_name: string
lang: string
}
async function main() { async function main() {
const logger = new Logger() const logger = new Logger()
@@ -20,31 +12,24 @@ async function main() {
logger.info('loading channels...') logger.info('loading channels...')
const sitesStorage = new Storage(SITES_DIR) const sitesStorage = new Storage(SITES_DIR)
const parser = new ChannelsParser({ storage: sitesStorage }) const parser = new ChannelsParser({
storage: sitesStorage
})
let files: string[] = [] const files: string[] = await sitesStorage.list('**/*.channels.xml')
files = await sitesStorage.list('**/*.channels.xml')
let parsedChannels = new Collection() const channels = new Collection()
for (const filepath of files) { for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath)) const channelList = await parser.parse(filepath)
channelList.channels.forEach((data: epgGrabber.Channel) => {
channels.add(new GuideChannel(data))
})
} }
logger.info(` found ${parsedChannels.count()} channel(s)`) logger.info(`found ${channels.count()} channel(s)`)
const output = parsedChannels.map((channel: epgGrabber.Channel): OutputItem => { const output = channels.map((channel: GuideChannel) => channel.toJSON())
const xmltv_id = channel.xmltv_id || ''
const [channelId, feedId] = xmltv_id.split('@')
return {
channel: channelId || null,
feed: feedId || null,
site: channel.site || '',
site_id: channel.site_id || '',
site_name: channel.name,
lang: channel.lang || ''
}
})
const apiStorage = new Storage(API_DIR) const apiStorage = new Storage(API_DIR)
const outputFilename = 'guides.json' const outputFilename = 'guides.json'

View File

@@ -17,7 +17,8 @@ async function main() {
loader.download('feeds.json'), loader.download('feeds.json'),
loader.download('timezones.json'), loader.download('timezones.json'),
loader.download('guides.json'), loader.download('guides.json'),
loader.download('streams.json') loader.download('streams.json'),
loader.download('logos.json')
]) ])
} }

View File

@@ -1,17 +1,17 @@
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core' import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
import type { DataProcessorData } from '../../types/dataProcessor'
import type { DataLoaderData } from '../../types/dataLoader'
import { ChannelSearchableData } from '../../types/channel'
import { Channel, ChannelList, Feed } from '../../models'
import { DataProcessor, DataLoader } from '../../core'
import { select, input } from '@inquirer/prompts' import { select, input } from '@inquirer/prompts'
import { ChannelsParser, XML } from '../../core' import { ChannelsParser } from '../../core'
import { Channel, Feed } from '../../models'
import { DATA_DIR } from '../../constants' import { DATA_DIR } from '../../constants'
import nodeCleanup from 'node-cleanup' import nodeCleanup from 'node-cleanup'
import sjs from '@freearhey/search-js'
import epgGrabber from 'epg-grabber'
import { Command } from 'commander' import { Command } from 'commander'
import readline from 'readline' import readline from 'readline'
import sjs from '@freearhey/search-js'
import { DataProcessor, DataLoader } from '../../core'
import type { DataLoaderData } from '../../types/dataLoader'
import type { DataProcessorData } from '../../types/dataProcessor'
import epgGrabber from 'epg-grabber'
import { ChannelSearchableData } from '../../types/channel'
type ChoiceValue = { type: string; value?: Feed | Channel } type ChoiceValue = { type: string; value?: Feed | Channel }
type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean } type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean }
@@ -34,11 +34,11 @@ program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(proc
const filepath = program.args[0] const filepath = program.args[0]
const logger = new Logger() const logger = new Logger()
const storage = new Storage() const storage = new Storage()
let parsedChannels = new Collection() let channelList = new ChannelList({ channels: [] })
main(filepath) main(filepath)
nodeCleanup(() => { nodeCleanup(() => {
save(filepath) save(filepath, channelList)
}) })
export default async function main(filepath: string) { export default async function main(filepath: string) {
@@ -51,18 +51,18 @@ export default async function main(filepath: string) {
const dataStorage = new Storage(DATA_DIR) const dataStorage = new Storage(DATA_DIR)
const loader = new DataLoader({ storage: dataStorage }) const loader = new DataLoader({ storage: dataStorage })
const data: DataLoaderData = await loader.load() const data: DataLoaderData = await loader.load()
const { feedsGroupedByChannelId, channels, channelsKeyById }: DataProcessorData = const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
processor.process(data) processor.process(data)
logger.info('loading channels...') logger.info('loading channels...')
const parser = new ChannelsParser({ storage }) const parser = new ChannelsParser({ storage })
parsedChannels = await parser.parse(filepath) channelList = await parser.parse(filepath)
const parsedChannelsWithoutId = parsedChannels.filter( const parsedChannelsWithoutId = channelList.channels.filter(
(channel: epgGrabber.Channel) => !channel.xmltv_id (channel: epgGrabber.Channel) => !channel.xmltv_id
) )
logger.info( logger.info(
`found ${parsedChannels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)` `found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
) )
logger.info('creating search index...') logger.info('creating search index...')
@@ -73,10 +73,10 @@ export default async function main(filepath: string) {
logger.info('starting...\n') logger.info('starting...\n')
for (const parsedChannel of parsedChannelsWithoutId.all()) { for (const channel of parsedChannelsWithoutId.all()) {
try { try {
parsedChannel.xmltv_id = await selectChannel( channel.xmltv_id = await selectChannel(
parsedChannel, channel,
searchIndex, searchIndex,
feedsGroupedByChannelId, feedsGroupedByChannelId,
channelsKeyById channelsKeyById
@@ -124,8 +124,8 @@ async function selectChannel(
case 'channel': { case 'channel': {
const selectedChannel = selected.value const selectedChannel = selected.value
if (!selectedChannel) return '' if (!selectedChannel) return ''
const selectedFeedId = await selectFeed(selectedChannel.id, feedsGroupedByChannelId) const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId)
if (selectedFeedId === '-') return selectedChannel.id if (selectedFeedId === '-') return selectedChannel.id || ''
return [selectedChannel.id, selectedFeedId].join('@') return [selectedChannel.id, selectedFeedId].join('@')
} }
} }
@@ -153,7 +153,7 @@ async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary
case 'feed': case 'feed':
const selectedFeed = selected.value const selectedFeed = selected.value
if (!selectedFeed) return '' if (!selectedFeed) return ''
return selectedFeed.id return selectedFeed.id || ''
} }
return '' return ''
@@ -205,10 +205,9 @@ function getFeedChoises(feeds: Collection): Choice[] {
return choises return choises
} }
function save(filepath: string) { function save(filepath: string, channelList: ChannelList) {
if (!storage.existsSync(filepath)) return if (!storage.existsSync(filepath)) return
const xml = new XML(parsedChannels) storage.saveSync(filepath, channelList.toString())
storage.saveSync(filepath, xml.toString())
logger.info(`\nFile '${filepath}' successfully saved`) logger.info(`\nFile '${filepath}' successfully saved`)
} }

View File

@@ -1,8 +1,9 @@
import { Logger, File, Collection, Storage } from '@freearhey/core' import { Logger, File, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core' import { ChannelsParser } from '../../core'
import { Channel } from 'epg-grabber' import { ChannelList } from '../../models'
import { Command } from 'commander'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import epgGrabber from 'epg-grabber'
import { Command } from 'commander'
const program = new Command() const program = new Command()
program program
@@ -21,17 +22,25 @@ type ParseOptions = {
const options: ParseOptions = program.opts() const options: ParseOptions = program.opts()
async function main() { async function main() {
function isPromise(promise: object[] | Promise<object[]>) {
return (
!!promise &&
typeof promise === 'object' &&
typeof (promise as Promise<object[]>).then === 'function'
)
}
const storage = new Storage() const storage = new Storage()
const parser = new ChannelsParser({ storage })
const logger = new Logger() const logger = new Logger()
const parser = new ChannelsParser({ storage })
const file = new File(options.config) const file = new File(options.config)
const dir = file.dirname() const dir = file.dirname()
const config = (await import(pathToFileURL(options.config).toString())).default const config = (await import(pathToFileURL(options.config).toString())).default
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
let channels = new Collection() let channelList = new ChannelList({ channels: [] })
if (await storage.exists(outputFilepath)) { if (await storage.exists(outputFilepath)) {
channels = await parser.parse(outputFilepath) channelList = await parser.parse(outputFilepath)
} }
const args: { const args: {
@@ -49,45 +58,31 @@ async function main() {
if (isPromise(parsedChannels)) { if (isPromise(parsedChannels)) {
parsedChannels = await parsedChannels parsedChannels = await parsedChannels
} }
parsedChannels = parsedChannels.map((channel: Channel) => { parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
channel.site = config.site channel.site = config.site
return channel return channel
}) })
let output = new Collection() const newChannelList = new ChannelList({ channels: [] })
parsedChannels.forEach((channel: Channel) => { parsedChannels.forEach((channel: epgGrabber.Channel) => {
const found: Channel | undefined = channels.first( if (!channel.site_id) return
(_channel: Channel) => _channel.site_id == channel.site_id
) const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
if (found) { if (found) {
channel.xmltv_id = found.xmltv_id channel.xmltv_id = found.xmltv_id
channel.lang = found.lang channel.lang = found.lang
} }
output.add(channel) newChannelList.add(channel)
}) })
output = output.orderBy([ newChannelList.sort()
(channel: Channel) => channel.lang || '_',
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
(channel: Channel) => channel.site_id
])
const xml = new XML(output) await storage.save(outputFilepath, newChannelList.toString())
await storage.save(outputFilepath, xml.toString())
logger.info(`File '${outputFilepath}' successfully saved`) logger.info(`File '${outputFilepath}' successfully saved`)
} }
main() main()
function isPromise(promise: object[] | Promise<object[]>) {
return (
!!promise &&
typeof promise === 'object' &&
typeof (promise as Promise<object[]>).then === 'function'
)
}

View File

@@ -1,11 +1,13 @@
import { Storage, Collection, Dictionary, File } from '@freearhey/core' import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
import { ChannelsParser } from '../../core' import { DataProcessorData } from '../../types/dataProcessor'
import { Channel, Feed } from '../../models' import { Storage, Dictionary, File } from '@freearhey/core'
import { DataLoaderData } from '../../types/dataLoader'
import { ChannelList } from '../../models'
import { DATA_DIR } from '../../constants'
import epgGrabber from 'epg-grabber'
import { program } from 'commander' import { program } from 'commander'
import chalk from 'chalk' import chalk from 'chalk'
import langs from 'langs' import langs from 'langs'
import { DATA_DIR } from '../../constants'
import epgGrabber from 'epg-grabber'
program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv) program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv)
@@ -19,15 +21,14 @@ type ValidationError = {
} }
async function main() { async function main() {
const parser = new ChannelsParser({ storage: new Storage() }) const processor = new DataProcessor()
const dataStorage = new Storage(DATA_DIR) const dataStorage = new Storage(DATA_DIR)
const channelsData = await dataStorage.json('channels.json') const loader = new DataLoader({ storage: dataStorage })
const channels = new Collection(channelsData).map(data => new Channel(data)) const data: DataLoaderData = await loader.load()
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
const feedsData = await dataStorage.json('feeds.json') const parser = new ChannelsParser({
const feeds = new Collection(feedsData).map(data => new Feed(data)) storage: new Storage()
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId()) })
let totalFiles = 0 let totalFiles = 0
let totalErrors = 0 let totalErrors = 0
@@ -38,11 +39,11 @@ async function main() {
const file = new File(filepath) const file = new File(filepath)
if (file.extension() !== 'xml') continue if (file.extension() !== 'xml') continue
const parsedChannels = await parser.parse(filepath) const channelList: ChannelList = await parser.parse(filepath)
const bufferBySiteId = new Dictionary() const bufferBySiteId = new Dictionary()
const errors: ValidationError[] = [] const errors: ValidationError[] = []
parsedChannels.forEach((channel: epgGrabber.Channel) => { channelList.channels.forEach((channel: epgGrabber.Channel) => {
const bufferId: string = channel.site_id const bufferId: string = channel.site_id
if (bufferBySiteId.missing(bufferId)) { if (bufferBySiteId.missing(bufferId)) {
bufferBySiteId.set(bufferId, true) bufferBySiteId.set(bufferId, true)

View File

@@ -1,9 +1,10 @@
import { Logger, Timer, Storage, Collection } from '@freearhey/core' import { Logger, Timer, Storage, Collection } from '@freearhey/core'
import { Option, program } from 'commander'
import { QueueCreator, Job, ChannelsParser } from '../../core' import { QueueCreator, Job, ChannelsParser } from '../../core'
import { Option, program } from 'commander'
import { SITES_DIR } from '../../constants'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
import path from 'path' import path from 'path'
import { SITES_DIR } from '../../constants' import { ChannelList } from '../../models'
program program
.addOption(new Option('-s, --site <name>', 'Name of the site to parse')) .addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
@@ -31,7 +32,7 @@ program
'--days <days>', '--days <days>',
'Override the number of days for which the program will be loaded (defaults to the value from the site config)' 'Override the number of days for which the program will be loaded (defaults to the value from the site config)'
) )
.argParser(value => (value !== undefined ? parseInt(value) : undefined)) .argParser(value => parseInt(value))
.env('DAYS') .env('DAYS')
) )
.addOption( .addOption(
@@ -87,31 +88,35 @@ async function main() {
files = await storage.list(options.channels) files = await storage.list(options.channels)
} }
let parsedChannels = new Collection() let channels = new Collection()
for (const filepath of files) { for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath)) const channelList: ChannelList = await parser.parse(filepath)
channels = channels.concat(channelList.channels)
} }
if (options.lang) { if (options.lang) {
parsedChannels = parsedChannels.filter((channel: Channel) => { channels = channels.filter((channel: Channel) => {
if (!options.lang || !channel.lang) return true if (!options.lang || !channel.lang) return true
return options.lang.includes(channel.lang) return options.lang.includes(channel.lang)
}) })
} }
logger.info(` found ${parsedChannels.count()} channel(s)`)
logger.info(` found ${channels.count()} channel(s)`)
logger.info('run:') logger.info('run:')
runJob({ logger, parsedChannels }) runJob({ logger, channels })
} }
main() main()
async function runJob({ logger, parsedChannels }: { logger: Logger; parsedChannels: Collection }) { async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
const timer = new Timer() const timer = new Timer()
timer.start() timer.start()
const queueCreator = new QueueCreator({ const queueCreator = new QueueCreator({
parsedChannels, channels,
logger, logger,
options options
}) })

View File

@@ -1,21 +1,25 @@
import { IssueLoader, HTMLTable, ChannelsParser } from '../../core' import { IssueLoader, HTMLTable, ChannelsParser } from '../../core'
import { Logger, Storage, Collection } from '@freearhey/core' import { Logger, Storage, Collection } from '@freearhey/core'
import { ChannelList, Issue, Site } from '../../models'
import { SITES_DIR, ROOT_DIR } from '../../constants' import { SITES_DIR, ROOT_DIR } from '../../constants'
import { Issue, Site } from '../../models'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
async function main() { async function main() {
const logger = new Logger({ disabled: true }) const logger = new Logger({ level: -999 })
const loader = new IssueLoader() const issueLoader = new IssueLoader()
const sitesStorage = new Storage(SITES_DIR) const sitesStorage = new Storage(SITES_DIR)
const channelsParser = new ChannelsParser({ storage: sitesStorage })
const sites = new Collection() const sites = new Collection()
logger.info('loading channels...')
const channelsParser = new ChannelsParser({
storage: sitesStorage
})
logger.info('loading list of sites') logger.info('loading list of sites')
const folders = await sitesStorage.list('*/') const folders = await sitesStorage.list('*/')
logger.info('loading issues...') logger.info('loading issues...')
const issues = await loader.load() const issues = await issueLoader.load()
logger.info('putting the data together...') logger.info('putting the data together...')
const brokenGuideReports = issues.filter(issue => const brokenGuideReports = issues.filter(issue =>
@@ -33,19 +37,21 @@ async function main() {
const files = await sitesStorage.list(`${domain}/*.channels.xml`) const files = await sitesStorage.list(`${domain}/*.channels.xml`)
for (const filepath of files) { for (const filepath of files) {
const channels = await channelsParser.parse(filepath) const channelList: ChannelList = await channelsParser.parse(filepath)
site.totalChannels += channels.count() site.totalChannels += channelList.channels.count()
site.markedChannels += channels.filter((channel: Channel) => channel.xmltv_id).count() site.markedChannels += channelList.channels
.filter((channel: Channel) => channel.xmltv_id)
.count()
} }
sites.add(site) sites.add(site)
} }
logger.info('creating sites table...') logger.info('creating sites table...')
const data = new Collection() const tableData = new Collection()
sites.forEach((site: Site) => { sites.forEach((site: Site) => {
data.add([ tableData.add([
{ value: `<a href="sites/${site.domain}">${site.domain}</a>` }, { value: `<a href="sites/${site.domain}">${site.domain}</a>` },
{ value: site.totalChannels, align: 'right' }, { value: site.totalChannels, align: 'right' },
{ value: site.markedChannels, align: 'right' }, { value: site.markedChannels, align: 'right' },
@@ -55,7 +61,7 @@ async function main() {
}) })
logger.info('updating sites.md...') logger.info('updating sites.md...')
const table = new HTMLTable(data.all(), [ const table = new HTMLTable(tableData.all(), [
{ name: 'Site', align: 'left' }, { name: 'Site', align: 'left' },
{ name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' }, { name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' },
{ name: 'Status', align: 'left' }, { name: 'Status', align: 'left' },

View File

@@ -1,5 +1,6 @@
import { parseChannels } from 'epg-grabber' import { parseChannels } from 'epg-grabber'
import { Storage, Collection } from '@freearhey/core' import { Storage } from '@freearhey/core'
import { ChannelList } from '../models'
type ChannelsParserProps = { type ChannelsParserProps = {
storage: Storage storage: Storage
@@ -12,13 +13,10 @@ export class ChannelsParser {
this.storage = storage this.storage = storage
} }
async parse(filepath: string) { async parse(filepath: string): Promise<ChannelList> {
let parsedChannels = new Collection()
const content = await this.storage.load(filepath) const content = await this.storage.load(filepath)
const channels = parseChannels(content) const parsed = parseChannels(content)
parsedChannels = parsedChannels.concat(new Collection(channels))
return parsedChannels return new ChannelList({ channels: parsed })
} }
} }

View File

@@ -49,7 +49,8 @@ export class DataLoader {
feeds, feeds,
timezones, timezones,
guides, guides,
streams streams,
logos
] = await Promise.all([ ] = await Promise.all([
this.storage.json('countries.json'), this.storage.json('countries.json'),
this.storage.json('regions.json'), this.storage.json('regions.json'),
@@ -61,7 +62,8 @@ export class DataLoader {
this.storage.json('feeds.json'), this.storage.json('feeds.json'),
this.storage.json('timezones.json'), this.storage.json('timezones.json'),
this.storage.json('guides.json'), this.storage.json('guides.json'),
this.storage.json('streams.json') this.storage.json('streams.json'),
this.storage.json('logos.json')
]) ])
return { return {
@@ -75,7 +77,8 @@ export class DataLoader {
feeds, feeds,
timezones, timezones,
guides, guides,
streams streams,
logos
} }
} }

View File

@@ -1,6 +1,6 @@
import { Channel, Feed, GuideChannel, Logo, Stream } from '../models'
import { DataLoaderData } from '../types/dataLoader' import { DataLoaderData } from '../types/dataLoader'
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
import { Channel, Feed, Guide, Stream } from '../models'
export class DataProcessor { export class DataProcessor {
constructor() {} constructor() {}
@@ -9,31 +9,48 @@ export class DataProcessor {
let channels = new Collection(data.channels).map(data => new Channel(data)) let channels = new Collection(data.channels).map(data => new Channel(data))
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
const guides = new Collection(data.guides).map(data => new Guide(data)) const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId()) const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
channel.getStreamId()
)
const streams = new Collection(data.streams).map(data => new Stream(data)) const streams = new Collection(data.streams).map(data => new Stream(data))
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId()) const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
const feeds = new Collection(data.feeds).map(data => let feeds = new Collection(data.feeds).map(data =>
new Feed(data) new Feed(data)
.withGuides(guidesGroupedByStreamId) .withGuideChannels(guideChannelsGroupedByStreamId)
.withStreams(streamsGroupedById) .withStreams(streamsGroupedById)
.withChannel(channelsKeyById) .withChannel(channelsKeyById)
) )
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId())
const logos = new Collection(data.logos).map(data =>
new Logo(data).withFeed(feedsKeyByStreamId)
)
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId))
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId) const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
channels = channels.map((channel: Channel) => channel.withFeeds(feedsGroupedByChannelId)) channels = channels.map((channel: Channel) =>
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
)
return { return {
guideChannelsGroupedByStreamId,
feedsGroupedByChannelId, feedsGroupedByChannelId,
guidesGroupedByStreamId, logosGroupedByChannelId,
logosGroupedByStreamId,
streamsGroupedById, streamsGroupedById,
feedsKeyByStreamId,
channelsKeyById, channelsKeyById,
guideChannels,
channels, channels,
streams, streams,
guides, feeds,
feeds logos
} }
} }
} }

View File

@@ -1,60 +0,0 @@
import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core'
import { Channel } from 'epg-grabber'
import { XMLTV } from '../core'
import path from 'path'
type GuideProps = {
channels: Collection
programs: Collection
logger: Logger
filepath: string
gzip: boolean
}
export class Guide {
channels: Collection
programs: Collection
logger: Logger
storage: Storage
filepath: string
gzip: boolean
constructor({ channels, programs, logger, filepath, gzip }: GuideProps) {
this.channels = channels
this.programs = programs
this.logger = logger
this.storage = new Storage(path.dirname(filepath))
this.filepath = filepath
this.gzip = gzip || false
}
async save() {
const channels = this.channels.uniqBy(
(channel: Channel) => `${channel.xmltv_id}:${channel.site}`
)
const programs = this.programs
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
timezone: 'UTC'
})
const xmltv = new XMLTV({
channels,
programs,
date: currDate
})
const xmlFilepath = this.filepath
const xmlFilename = path.basename(xmlFilepath)
this.logger.info(` saving to "${xmlFilepath}"...`)
await this.storage.save(xmlFilename, xmltv.toString())
if (this.gzip) {
const zip = new Zip()
const compressed = zip.compress(xmltv.toString())
const gzFilepath = `${this.filepath}.gz`
const gzFilename = path.basename(gzFilepath)
this.logger.info(` saving to "${gzFilepath}"...`)
await this.storage.save(gzFilename, compressed)
}
}
}

View File

@@ -1,7 +1,12 @@
import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core' import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
import { OptionValues } from 'commander' import { OptionValues } from 'commander'
import { Channel, Program } from 'epg-grabber' import { Channel, Feed, Guide } from '../models'
import { Guide } from '.' import path from 'path'
import { DataLoader, DataProcessor } from '.'
import { DataLoaderData } from '../types/dataLoader'
import { DataProcessorData } from '../types/dataProcessor'
import { DATA_DIR } from '../constants'
type GuideManagerProps = { type GuideManagerProps = {
options: OptionValues options: OptionValues
@@ -12,7 +17,6 @@ type GuideManagerProps = {
export class GuideManager { export class GuideManager {
options: OptionValues options: OptionValues
storage: Storage
logger: Logger logger: Logger
channels: Collection channels: Collection
programs: Collection programs: Collection
@@ -22,22 +26,51 @@ export class GuideManager {
this.logger = logger this.logger = logger
this.channels = channels this.channels = channels
this.programs = programs this.programs = programs
this.storage = new Storage()
} }
async createGuides() { async createGuides() {
const pathTemplate = new StringTemplate(this.options.output) const pathTemplate = new StringTemplate(this.options.output)
const processor = new DataProcessor()
const dataStorage = new Storage(DATA_DIR)
const loader = new DataLoader({ storage: dataStorage })
const data: DataLoaderData = await loader.load()
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
const groupedChannels = this.channels const groupedChannels = this.channels
.orderBy([(channel: Channel) => channel.index, (channel: Channel) => channel.xmltv_id]) .map((channel: epgGrabber.Channel) => {
.uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`) if (channel.xmltv_id && !channel.icon) {
.groupBy((channel: Channel) => { const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
if (foundFeed && foundFeed.hasLogo()) {
channel.icon = foundFeed.getLogoUrl()
} else {
const [channelId] = channel.xmltv_id.split('@')
const foundChannel: Channel = channelsKeyById.get(channelId)
if (foundChannel && foundChannel.hasLogo()) {
channel.icon = foundChannel.getLogoUrl()
}
}
}
return channel
})
.orderBy([
(channel: epgGrabber.Channel) => channel.index,
(channel: epgGrabber.Channel) => channel.xmltv_id
])
.uniqBy(
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
)
.groupBy((channel: epgGrabber.Channel) => {
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' }) return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
}) })
const groupedPrograms = this.programs const groupedPrograms = this.programs
.orderBy([(program: Program) => program.channel, (program: Program) => program.start]) .orderBy([
.groupBy((program: Program) => { (program: epgGrabber.Program) => program.channel,
(program: epgGrabber.Program) => program.start
])
.groupBy((program: epgGrabber.Program) => {
const lang = const lang =
program.titles && program.titles.length && program.titles[0].lang program.titles && program.titles.length && program.titles[0].lang
? program.titles[0].lang ? program.titles[0].lang
@@ -51,11 +84,28 @@ export class GuideManager {
filepath: groupKey, filepath: groupKey,
gzip: this.options.gzip, gzip: this.options.gzip,
channels: new Collection(groupedChannels.get(groupKey)), channels: new Collection(groupedChannels.get(groupKey)),
programs: new Collection(groupedPrograms.get(groupKey)), programs: new Collection(groupedPrograms.get(groupKey))
logger: this.logger
}) })
await guide.save() await this.save(guide)
}
}
async save(guide: Guide) {
const storage = new Storage(path.dirname(guide.filepath))
const xmlFilepath = guide.filepath
const xmlFilename = path.basename(xmlFilepath)
this.logger.info(` saving to "${xmlFilepath}"...`)
const xmltv = guide.toString()
await storage.save(xmlFilename, xmltv)
if (guide.gzip) {
const zip = new Zip()
const compressed = zip.compress(xmltv)
const gzFilepath = `${guide.filepath}.gz`
const gzFilename = path.basename(gzFilepath)
this.logger.info(` saving to "${gzFilepath}"...`)
await storage.save(gzFilename, compressed)
} }
} }
} }

View File

@@ -4,7 +4,6 @@ export * from './configLoader'
export * from './dataLoader' export * from './dataLoader'
export * from './dataProcessor' export * from './dataProcessor'
export * from './grabber' export * from './grabber'
export * from './guide'
export * from './guideManager' export * from './guideManager'
export * from './htmlTable' export * from './htmlTable'
export * from './issueLoader' export * from './issueLoader'
@@ -13,5 +12,3 @@ export * from './job'
export * from './proxyParser' export * from './proxyParser'
export * from './queue' export * from './queue'
export * from './queueCreator' export * from './queueCreator'
export * from './xml'
export * from './xmltv'

View File

@@ -23,6 +23,7 @@ export class IssueLoader {
repo: REPO, repo: REPO,
per_page: 100, per_page: 100,
labels, labels,
state: 'open',
headers: { headers: {
'X-GitHub-Api-Version': '2022-11-28' 'X-GitHub-Api-Version': '2022-11-28'
} }

View File

@@ -1,15 +1,14 @@
import { Storage, Collection, DateTime, Logger } from '@freearhey/core' import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
import { ChannelsParser, ConfigLoader, Queue } from './'
import { SITES_DIR, DATA_DIR } from '../constants' import { SITES_DIR, DATA_DIR } from '../constants'
import { GrabOptions } from '../commands/epg/grab'
import { ConfigLoader, Queue } from './'
import { SiteConfig } from 'epg-grabber' import { SiteConfig } from 'epg-grabber'
import path from 'path' import path from 'path'
import { GrabOptions } from '../commands/epg/grab'
import { Channel } from '../models'
type QueueCreatorProps = { type QueueCreatorProps = {
logger: Logger logger: Logger
options: GrabOptions options: GrabOptions
parsedChannels: Collection channels: Collection
} }
export class QueueCreator { export class QueueCreator {
@@ -17,44 +16,29 @@ export class QueueCreator {
logger: Logger logger: Logger
sitesStorage: Storage sitesStorage: Storage
dataStorage: Storage dataStorage: Storage
parser: ChannelsParser channels: Collection
parsedChannels: Collection
options: GrabOptions options: GrabOptions
constructor({ parsedChannels, logger, options }: QueueCreatorProps) { constructor({ channels, logger, options }: QueueCreatorProps) {
this.parsedChannels = parsedChannels this.channels = channels
this.logger = logger this.logger = logger
this.sitesStorage = new Storage() this.sitesStorage = new Storage()
this.dataStorage = new Storage(DATA_DIR) this.dataStorage = new Storage(DATA_DIR)
this.parser = new ChannelsParser({ storage: new Storage() })
this.options = options this.options = options
this.configLoader = new ConfigLoader() this.configLoader = new ConfigLoader()
} }
async create(): Promise<Queue> { async create(): Promise<Queue> {
const channelsContent = await this.dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new Channel(data))
let index = 0 let index = 0
const queue = new Queue() const queue = new Queue()
for (const channel of this.parsedChannels.all()) { for (const channel of this.channels.all()) {
channel.index = index++ channel.index = index++
if (!channel.site || !channel.site_id || !channel.name) continue if (!channel.site || !channel.site_id || !channel.name) continue
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
const config: SiteConfig = await this.configLoader.load(configPath) const config: SiteConfig = await this.configLoader.load(configPath)
if (channel.xmltv_id) { if (!channel.xmltv_id) {
if (!channel.icon) {
const found: Channel = channels.first(
(_channel: Channel) => _channel.id === channel.xmltv_id
)
if (found) {
channel.icon = found.logo
}
}
} else {
channel.xmltv_id = channel.site_id channel.xmltv_id = channel.site_id
} }

View File

@@ -1,56 +0,0 @@
import { Collection } from '@freearhey/core'
import { Channel } from 'epg-grabber'
export class XML {
items: Collection
constructor(items: Collection) {
this.items = items
}
toString() {
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.items.forEach((channel: Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id || ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
output += ` <channel site="${channel.site}" lang="${lang}" xmltv_id="${escapeString(
xmltv_id
)}" site_id="${site_id}"${logo}>${escapeString(channel.name)}</channel>\r\n`
})
output += '</channels>\r\n'
return output
}
}
function escapeString(value: string, defaultValue: string = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}

View File

@@ -1,28 +0,0 @@
import { DateTime, Collection } from '@freearhey/core'
import { generateXMLTV } from 'epg-grabber'
type XMLTVProps = {
channels: Collection
programs: Collection
date: DateTime
}
export class XMLTV {
channels: Collection
programs: Collection
date: DateTime
constructor({ channels, programs, date }: XMLTVProps) {
this.channels = channels
this.programs = programs
this.date = date
}
toString() {
return generateXMLTV({
channels: this.channels.all(),
programs: this.programs.all(),
date: this.date.toJSON()
})
}
}

View File

@@ -1,26 +1,28 @@
import { ChannelData, ChannelSearchableData } from '../types/channel' import { ChannelData, ChannelSearchableData } from '../types/channel'
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
import { Stream, Guide, Feed } from './' import { Stream, Feed, Logo, GuideChannel } from './'
export class Channel { export class Channel {
id: string id?: string
name: string name?: string
altNames?: Collection altNames?: Collection
network?: string network?: string
owners?: Collection owners?: Collection
countryCode: string countryCode?: string
subdivisionCode?: string subdivisionCode?: string
cityName?: string cityName?: string
categoryIds?: Collection categoryIds?: Collection
isNSFW: boolean isNSFW: boolean = false
launched?: string launched?: string
closed?: string closed?: string
replacedBy?: string replacedBy?: string
website?: string website?: string
logo?: string
feeds?: Collection feeds?: Collection
logos: Collection = new Collection()
constructor(data?: ChannelData) {
if (!data) return
constructor(data: ChannelData) {
this.id = data.id this.id = data.id
this.name = data.name this.name = data.name
this.altNames = new Collection(data.alt_names) this.altNames = new Collection(data.alt_names)
@@ -35,11 +37,16 @@ export class Channel {
this.closed = data.closed || undefined this.closed = data.closed || undefined
this.replacedBy = data.replaced_by || undefined this.replacedBy = data.replaced_by || undefined
this.website = data.website || undefined this.website = data.website || undefined
this.logo = data.logo
} }
withFeeds(feedsGroupedByChannelId: Dictionary): this { withFeeds(feedsGroupedByChannelId: Dictionary): this {
this.feeds = new Collection(feedsGroupedByChannelId.get(this.id)) if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
return this
}
withLogos(logosGroupedByChannelId: Dictionary): this {
if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id))
return this return this
} }
@@ -50,19 +57,19 @@ export class Channel {
return this.feeds return this.feeds
} }
getGuides(): Collection { getGuideChannels(): Collection {
let guides = new Collection() let channels = new Collection()
this.getFeeds().forEach((feed: Feed) => { this.getFeeds().forEach((feed: Feed) => {
guides = guides.concat(feed.getGuides()) channels = channels.concat(feed.getGuideChannels())
}) })
return guides return channels
} }
getGuideNames(): Collection { getGuideChannelNames(): Collection {
return this.getGuides() return this.getGuideChannels()
.map((guide: Guide) => guide.siteName) .map((channel: GuideChannel) => channel.siteName)
.uniq() .uniq()
} }
@@ -100,12 +107,56 @@ export class Channel {
return this.altNames || new Collection() return this.altNames || new Collection()
} }
getLogos(): Collection {
function feed(logo: Logo): number {
if (!logo.feed) return 1
if (logo.feed.isMain) return 1
return 0
}
function format(logo: Logo): number {
const levelByFormat: { [key: string]: number } = {
SVG: 0,
PNG: 3,
APNG: 1,
WebP: 1,
AVIF: 1,
JPEG: 2,
GIF: 1
}
return logo.format ? levelByFormat[logo.format] : 0
}
function size(logo: Logo): number {
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
}
return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false)
}
getLogo(): Logo | undefined {
return this.getLogos().first()
}
hasLogo(): boolean {
return this.getLogos().notEmpty()
}
getLogoUrl(): string {
const logo = this.getLogo()
if (!logo) return ''
return logo.url || ''
}
getSearchable(): ChannelSearchableData { getSearchable(): ChannelSearchableData {
return { return {
id: this.getId(), id: this.getId(),
name: this.getName(), name: this.getName(),
altNames: this.getAltNames().all(), altNames: this.getAltNames().all(),
guideNames: this.getGuideNames().all(), guideNames: this.getGuideChannelNames().all(),
streamNames: this.getStreamNames().all(), streamNames: this.getStreamNames().all(),
feedFullNames: this.getFeedFullNames().all() feedFullNames: this.getFeedFullNames().all()
} }

View File

@@ -0,0 +1,77 @@
import { Collection } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
export class ChannelList {
channels: Collection = new Collection()
constructor(data: { channels: epgGrabber.Channel[] }) {
this.channels = new Collection(data.channels)
}
add(channel: epgGrabber.Channel): this {
this.channels.add(channel)
return this
}
get(siteId: string): epgGrabber.Channel | undefined {
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
}
sort(): this {
this.channels = this.channels.orderBy([
(channel: epgGrabber.Channel) => channel.lang || '_',
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
(channel: epgGrabber.Channel) => channel.site_id
])
return this
}
toString() {
function escapeString(value: string, defaultValue: string = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.channels.forEach((channel: epgGrabber.Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
const site = channel.site || ''
const displayName = channel.name ? escapeString(channel.name) : ''
output += ` <channel site="${site}" lang="${lang}" xmltv_id="${xmltv_id}" site_id="${site_id}"${logo}>${displayName}</channel>\r\n`
})
output += '</channels>\r\n'
return output
}
}

View File

@@ -1,6 +1,6 @@
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
import { FeedData } from '../types/feed' import { FeedData } from '../types/feed'
import { Channel } from './channel' import { Logo, Channel } from '.'
export class Feed { export class Feed {
channelId: string channelId: string
@@ -12,8 +12,9 @@ export class Feed {
languageCodes: Collection languageCodes: Collection
timezoneIds: Collection timezoneIds: Collection
videoFormat: string videoFormat: string
guides?: Collection guideChannels?: Collection
streams?: Collection streams?: Collection
logos: Collection = new Collection()
constructor(data: FeedData) { constructor(data: FeedData) {
this.channelId = data.channel this.channelId = data.channel
@@ -42,20 +43,30 @@ export class Feed {
return this return this
} }
withGuides(guidesGroupedByStreamId: Dictionary): this { withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`)) this.guideChannels = new Collection(
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
)
if (this.isMain) { if (this.isMain) {
this.guides = this.guides.concat(new Collection(guidesGroupedByStreamId.get(this.channelId))) this.guideChannels = this.guideChannels.concat(
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
)
} }
return this return this
} }
getGuides(): Collection { withLogos(logosGroupedByStreamId: Dictionary): this {
if (!this.guides) return new Collection() this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
return this.guides return this
}
getGuideChannels(): Collection {
if (!this.guideChannels) return new Collection()
return this.guideChannels
} }
getStreams(): Collection { getStreams(): Collection {
@@ -73,4 +84,41 @@ export class Feed {
getStreamId(): string { getStreamId(): string {
return `${this.channelId}@${this.id}` return `${this.channelId}@${this.id}`
} }
getLogos(): Collection {
function format(logo: Logo): number {
const levelByFormat: { [key: string]: number } = {
SVG: 0,
PNG: 3,
APNG: 1,
WebP: 1,
AVIF: 1,
JPEG: 2,
GIF: 1
}
return logo.format ? levelByFormat[logo.format] : 0
}
function size(logo: Logo): number {
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
}
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
}
getLogo(): Logo | undefined {
return this.getLogos().first()
}
hasLogo(): boolean {
return this.getLogos().notEmpty()
}
getLogoUrl(): string {
const logo = this.getLogo()
if (!logo) return ''
return logo.url || ''
}
} }

View File

@@ -1,35 +1,35 @@
import type { GuideData } from '../types/guide' import { Collection, DateTime } from '@freearhey/core'
import { uniqueId } from 'lodash' import { generateXMLTV } from 'epg-grabber'
type GuideData = {
channels: Collection
programs: Collection
filepath: string
gzip: boolean
}
export class Guide { export class Guide {
channelId?: string channels: Collection
feedId?: string programs: Collection
siteDomain?: string filepath: string
siteId?: string gzip: boolean
siteName?: string
languageCode?: string
constructor(data?: GuideData) { constructor({ channels, programs, filepath, gzip }: GuideData) {
if (!data) return this.channels = channels
this.programs = programs
this.channelId = data.channel this.filepath = filepath
this.feedId = data.feed this.gzip = gzip || false
this.siteDomain = data.site
this.siteId = data.site_id
this.siteName = data.site_name
this.languageCode = data.lang
} }
getUUID(): string { toString() {
if (!this.getStreamId() || !this.siteId) return uniqueId() const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
timezone: 'UTC'
})
return this.getStreamId() + this.siteId return generateXMLTV({
} channels: this.channels.all(),
programs: this.programs.all(),
getStreamId(): string | undefined { date: currDate.toJSON()
if (!this.channelId) return undefined })
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
} }
} }

View File

@@ -0,0 +1,59 @@
import { Dictionary } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
import { Feed, Channel } from '.'
export class GuideChannel {
channelId?: string
channel?: Channel
feedId?: string
feed?: Feed
xmltvId?: string
languageCode?: string
siteId?: string
logoUrl?: string
siteDomain?: string
siteName?: string
constructor(data: epgGrabber.Channel) {
const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined]
this.channelId = channelId
this.feedId = feedId
this.xmltvId = data.xmltv_id
this.languageCode = data.lang
this.siteId = data.site_id
this.logoUrl = data.logo
this.siteDomain = data.site
this.siteName = data.name
}
withChannel(channelsKeyById: Dictionary): this {
if (this.channelId) this.channel = channelsKeyById.get(this.channelId)
return this
}
withFeed(feedsKeyByStreamId: Dictionary): this {
if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId())
return this
}
getStreamId(): string {
if (!this.channelId) return ''
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
toJSON() {
return {
channel: this.channelId || null,
feed: this.feedId || null,
site: this.siteDomain || '',
site_id: this.siteId || '',
site_name: this.siteName || '',
lang: this.languageCode || ''
}
}
}

View File

@@ -1,6 +1,9 @@
export * from './issue'
export * from './site'
export * from './channel' export * from './channel'
export * from './feed' export * from './feed'
export * from './stream'
export * from './guide' export * from './guide'
export * from './guideChannel'
export * from './issue'
export * from './logo'
export * from './site'
export * from './stream'
export * from './channelList'

41
scripts/models/logo.ts Normal file
View File

@@ -0,0 +1,41 @@
import { Collection, type Dictionary } from '@freearhey/core'
import type { LogoData } from '../types/logo'
import { type Feed } from './feed'
export class Logo {
channelId?: string
feedId?: string
feed?: Feed
tags: Collection = new Collection()
width: number = 0
height: number = 0
format?: string
url?: string
constructor(data?: LogoData) {
if (!data) return
this.channelId = data.channel
this.feedId = data.feed || undefined
this.tags = new Collection(data.tags)
this.width = data.width
this.height = data.height
this.format = data.format || undefined
this.url = data.url
}
withFeed(feedsKeyByStreamId: Dictionary): this {
if (!this.feedId) return this
this.feed = feedsKeyByStreamId.get(this.getStreamId())
return this
}
getStreamId(): string {
if (!this.channelId) return ''
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
}

View File

@@ -15,7 +15,6 @@ export type ChannelData = {
closed: string closed: string
replaced_by: string replaced_by: string
website: string website: string
logo: string
} }
export type ChannelSearchableData = { export type ChannelSearchableData = {

View File

@@ -16,4 +16,5 @@ export type DataLoaderData = {
timezones: object | object[] timezones: object | object[]
guides: object | object[] guides: object | object[]
streams: object | object[] streams: object | object[]
logos: object | object[]
} }

View File

@@ -1,12 +1,16 @@
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
export type DataProcessorData = { export type DataProcessorData = {
guideChannelsGroupedByStreamId: Dictionary
feedsGroupedByChannelId: Dictionary feedsGroupedByChannelId: Dictionary
guidesGroupedByStreamId: Dictionary logosGroupedByChannelId: Dictionary
logosGroupedByStreamId: Dictionary
feedsKeyByStreamId: Dictionary
streamsGroupedById: Dictionary streamsGroupedById: Dictionary
channelsKeyById: Dictionary channelsKeyById: Dictionary
guideChannels: Collection
channels: Collection channels: Collection
streams: Collection streams: Collection
guides: Collection
feeds: Collection feeds: Collection
logos: Collection
} }

9
scripts/types/logo.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
export type LogoData = {
channel: string
feed: string | null
tags: string[]
width: number
height: number
format: string | null
url: string
}