From a4fd7d7ae74a68eec7ca15346d50513c73aee650 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Fri, 18 Jul 2025 22:51:01 +0300 Subject: [PATCH] Update scripts --- scripts/commands/api/generate.ts | 47 +++++---------- scripts/commands/api/load.ts | 3 +- scripts/commands/channels/edit.ts | 45 +++++++------- scripts/commands/channels/parse.ts | 55 ++++++++--------- scripts/commands/channels/validate.ts | 31 +++++----- scripts/commands/epg/grab.ts | 25 ++++---- scripts/commands/sites/update.ts | 28 +++++---- scripts/core/channelsParser.ts | 12 ++-- scripts/core/dataLoader.ts | 9 ++- scripts/core/dataProcessor.ts | 35 ++++++++--- scripts/core/guide.ts | 60 ------------------- scripts/core/guideManager.ts | 76 ++++++++++++++++++++---- scripts/core/index.ts | 3 - scripts/core/issueLoader.ts | 1 + scripts/core/queueCreator.ts | 32 +++------- scripts/core/xml.ts | 56 ------------------ scripts/core/xmltv.ts | 28 --------- scripts/models/channel.ts | 85 +++++++++++++++++++++------ scripts/models/channelList.ts | 77 ++++++++++++++++++++++++ scripts/models/feed.ts | 64 +++++++++++++++++--- scripts/models/guide.ts | 54 ++++++++--------- scripts/models/guideChannel.ts | 59 +++++++++++++++++++ scripts/models/index.ts | 9 ++- scripts/models/logo.ts | 41 +++++++++++++ scripts/types/channel.d.ts | 1 - scripts/types/dataLoader.d.ts | 1 + scripts/types/dataProcessor.d.ts | 8 ++- scripts/types/logo.d.ts | 9 +++ 28 files changed, 572 insertions(+), 382 deletions(-) delete mode 100644 scripts/core/guide.ts delete mode 100644 scripts/core/xml.ts delete mode 100644 scripts/core/xmltv.ts create mode 100644 scripts/models/channelList.ts create mode 100644 scripts/models/guideChannel.ts create mode 100644 scripts/models/logo.ts create mode 100644 scripts/types/logo.d.ts diff --git a/scripts/commands/api/generate.ts b/scripts/commands/api/generate.ts index b43bc84b..b0e078c4 100644 --- a/scripts/commands/api/generate.ts +++ b/scripts/commands/api/generate.ts @@ -1,17 +1,9 @@ -import { Logger, Storage, Collection } from '@freearhey/core' -import { ChannelsParser } from '../../core' -import path from 'path' +import { Logger, Collection, Storage } from '@freearhey/core' import { SITES_DIR, API_DIR } from '../../constants' +import { GuideChannel } from '../../models' +import { ChannelsParser } from '../../core' import epgGrabber from 'epg-grabber' - -type OutputItem = { - channel: string | null - feed: string | null - site: string - site_id: string - site_name: string - lang: string -} +import path from 'path' async function main() { const logger = new Logger() @@ -20,31 +12,24 @@ async function main() { logger.info('loading channels...') const sitesStorage = new Storage(SITES_DIR) - const parser = new ChannelsParser({ storage: sitesStorage }) + const parser = new ChannelsParser({ + storage: sitesStorage + }) - let files: string[] = [] - files = await sitesStorage.list('**/*.channels.xml') + const files: string[] = await sitesStorage.list('**/*.channels.xml') - let parsedChannels = new Collection() + const channels = new Collection() 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 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 output = channels.map((channel: GuideChannel) => channel.toJSON()) const apiStorage = new Storage(API_DIR) const outputFilename = 'guides.json' diff --git a/scripts/commands/api/load.ts b/scripts/commands/api/load.ts index 9e731c7f..7a8f753d 100644 --- a/scripts/commands/api/load.ts +++ b/scripts/commands/api/load.ts @@ -17,7 +17,8 @@ async function main() { loader.download('feeds.json'), loader.download('timezones.json'), loader.download('guides.json'), - loader.download('streams.json') + loader.download('streams.json'), + loader.download('logos.json') ]) } diff --git a/scripts/commands/channels/edit.ts b/scripts/commands/channels/edit.ts index a67b6fe2..4a5e714a 100644 --- a/scripts/commands/channels/edit.ts +++ b/scripts/commands/channels/edit.ts @@ -1,17 +1,17 @@ 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 { ChannelsParser, XML } from '../../core' -import { Channel, Feed } from '../../models' +import { ChannelsParser } from '../../core' import { DATA_DIR } from '../../constants' import nodeCleanup from 'node-cleanup' +import sjs from '@freearhey/search-js' +import epgGrabber from 'epg-grabber' import { Command } from 'commander' 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 Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean } @@ -34,11 +34,11 @@ program.argument('', 'Path to *.channels.xml file to edit').parse(proc const filepath = program.args[0] const logger = new Logger() const storage = new Storage() -let parsedChannels = new Collection() +let channelList = new ChannelList({ channels: [] }) main(filepath) nodeCleanup(() => { - save(filepath) + save(filepath, channelList) }) 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 loader = new DataLoader({ storage: dataStorage }) const data: DataLoaderData = await loader.load() - const { feedsGroupedByChannelId, channels, channelsKeyById }: DataProcessorData = + const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data) logger.info('loading channels...') const parser = new ChannelsParser({ storage }) - parsedChannels = await parser.parse(filepath) - const parsedChannelsWithoutId = parsedChannels.filter( + channelList = await parser.parse(filepath) + const parsedChannelsWithoutId = channelList.channels.filter( (channel: epgGrabber.Channel) => !channel.xmltv_id ) 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...') @@ -73,10 +73,10 @@ export default async function main(filepath: string) { logger.info('starting...\n') - for (const parsedChannel of parsedChannelsWithoutId.all()) { + for (const channel of parsedChannelsWithoutId.all()) { try { - parsedChannel.xmltv_id = await selectChannel( - parsedChannel, + channel.xmltv_id = await selectChannel( + channel, searchIndex, feedsGroupedByChannelId, channelsKeyById @@ -124,8 +124,8 @@ async function selectChannel( case 'channel': { const selectedChannel = selected.value if (!selectedChannel) return '' - const selectedFeedId = await selectFeed(selectedChannel.id, feedsGroupedByChannelId) - if (selectedFeedId === '-') return selectedChannel.id + const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId) + if (selectedFeedId === '-') return selectedChannel.id || '' return [selectedChannel.id, selectedFeedId].join('@') } } @@ -153,7 +153,7 @@ async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary case 'feed': const selectedFeed = selected.value if (!selectedFeed) return '' - return selectedFeed.id + return selectedFeed.id || '' } return '' @@ -205,10 +205,9 @@ function getFeedChoises(feeds: Collection): Choice[] { return choises } -function save(filepath: string) { +function save(filepath: string, channelList: ChannelList) { if (!storage.existsSync(filepath)) return - const xml = new XML(parsedChannels) - storage.saveSync(filepath, xml.toString()) + storage.saveSync(filepath, channelList.toString()) logger.info(`\nFile '${filepath}' successfully saved`) } diff --git a/scripts/commands/channels/parse.ts b/scripts/commands/channels/parse.ts index 572b5ed6..fb0e0447 100644 --- a/scripts/commands/channels/parse.ts +++ b/scripts/commands/channels/parse.ts @@ -1,8 +1,9 @@ -import { Logger, File, Collection, Storage } from '@freearhey/core' -import { ChannelsParser, XML } from '../../core' -import { Channel } from 'epg-grabber' -import { Command } from 'commander' +import { Logger, File, Storage } from '@freearhey/core' +import { ChannelsParser } from '../../core' +import { ChannelList } from '../../models' import { pathToFileURL } from 'node:url' +import epgGrabber from 'epg-grabber' +import { Command } from 'commander' const program = new Command() program @@ -21,17 +22,25 @@ type ParseOptions = { const options: ParseOptions = program.opts() async function main() { + function isPromise(promise: object[] | Promise) { + return ( + !!promise && + typeof promise === 'object' && + typeof (promise as Promise).then === 'function' + ) + } + const storage = new Storage() - const parser = new ChannelsParser({ storage }) const logger = new Logger() + const parser = new ChannelsParser({ storage }) const file = new File(options.config) const dir = file.dirname() const config = (await import(pathToFileURL(options.config).toString())).default const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` - let channels = new Collection() + let channelList = new ChannelList({ channels: [] }) if (await storage.exists(outputFilepath)) { - channels = await parser.parse(outputFilepath) + channelList = await parser.parse(outputFilepath) } const args: { @@ -49,45 +58,31 @@ async function main() { if (isPromise(parsedChannels)) { parsedChannels = await parsedChannels } - parsedChannels = parsedChannels.map((channel: Channel) => { + parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => { channel.site = config.site return channel }) - let output = new Collection() - parsedChannels.forEach((channel: Channel) => { - const found: Channel | undefined = channels.first( - (_channel: Channel) => _channel.site_id == channel.site_id - ) + const newChannelList = new ChannelList({ channels: [] }) + parsedChannels.forEach((channel: epgGrabber.Channel) => { + if (!channel.site_id) return + + const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id) if (found) { channel.xmltv_id = found.xmltv_id channel.lang = found.lang } - output.add(channel) + newChannelList.add(channel) }) - output = output.orderBy([ - (channel: Channel) => channel.lang || '_', - (channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'), - (channel: Channel) => channel.site_id - ]) + newChannelList.sort() - const xml = new XML(output) - - await storage.save(outputFilepath, xml.toString()) + await storage.save(outputFilepath, newChannelList.toString()) logger.info(`File '${outputFilepath}' successfully saved`) } main() - -function isPromise(promise: object[] | Promise) { - return ( - !!promise && - typeof promise === 'object' && - typeof (promise as Promise).then === 'function' - ) -} diff --git a/scripts/commands/channels/validate.ts b/scripts/commands/channels/validate.ts index 2ad51148..43af23e5 100644 --- a/scripts/commands/channels/validate.ts +++ b/scripts/commands/channels/validate.ts @@ -1,11 +1,13 @@ -import { Storage, Collection, Dictionary, File } from '@freearhey/core' -import { ChannelsParser } from '../../core' -import { Channel, Feed } from '../../models' +import { ChannelsParser, DataLoader, DataProcessor } from '../../core' +import { DataProcessorData } from '../../types/dataProcessor' +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 chalk from 'chalk' 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) @@ -19,15 +21,14 @@ type ValidationError = { } async function main() { - const parser = new ChannelsParser({ storage: new Storage() }) - + const processor = new DataProcessor() const dataStorage = new Storage(DATA_DIR) - const channelsData = await dataStorage.json('channels.json') - const channels = new Collection(channelsData).map(data => new Channel(data)) - const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) - const feedsData = await dataStorage.json('feeds.json') - const feeds = new Collection(feedsData).map(data => new Feed(data)) - const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId()) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data) + const parser = new ChannelsParser({ + storage: new Storage() + }) let totalFiles = 0 let totalErrors = 0 @@ -38,11 +39,11 @@ async function main() { const file = new File(filepath) if (file.extension() !== 'xml') continue - const parsedChannels = await parser.parse(filepath) + const channelList: ChannelList = await parser.parse(filepath) const bufferBySiteId = new Dictionary() const errors: ValidationError[] = [] - parsedChannels.forEach((channel: epgGrabber.Channel) => { + channelList.channels.forEach((channel: epgGrabber.Channel) => { const bufferId: string = channel.site_id if (bufferBySiteId.missing(bufferId)) { bufferBySiteId.set(bufferId, true) diff --git a/scripts/commands/epg/grab.ts b/scripts/commands/epg/grab.ts index b350443c..ebf94e70 100644 --- a/scripts/commands/epg/grab.ts +++ b/scripts/commands/epg/grab.ts @@ -1,9 +1,10 @@ import { Logger, Timer, Storage, Collection } from '@freearhey/core' -import { Option, program } from 'commander' import { QueueCreator, Job, ChannelsParser } from '../../core' +import { Option, program } from 'commander' +import { SITES_DIR } from '../../constants' import { Channel } from 'epg-grabber' import path from 'path' -import { SITES_DIR } from '../../constants' +import { ChannelList } from '../../models' program .addOption(new Option('-s, --site ', 'Name of the site to parse')) @@ -31,7 +32,7 @@ program '--days ', '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') ) .addOption( @@ -87,31 +88,35 @@ async function main() { files = await storage.list(options.channels) } - let parsedChannels = new Collection() + let channels = new Collection() 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) { - parsedChannels = parsedChannels.filter((channel: Channel) => { + channels = channels.filter((channel: Channel) => { if (!options.lang || !channel.lang) return true return options.lang.includes(channel.lang) }) } - logger.info(` found ${parsedChannels.count()} channel(s)`) + + logger.info(` found ${channels.count()} channel(s)`) logger.info('run:') - runJob({ logger, parsedChannels }) + runJob({ logger, channels }) } main() -async function runJob({ logger, parsedChannels }: { logger: Logger; parsedChannels: Collection }) { +async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) { const timer = new Timer() timer.start() const queueCreator = new QueueCreator({ - parsedChannels, + channels, logger, options }) diff --git a/scripts/commands/sites/update.ts b/scripts/commands/sites/update.ts index a2cf5cd7..42db9acf 100644 --- a/scripts/commands/sites/update.ts +++ b/scripts/commands/sites/update.ts @@ -1,21 +1,25 @@ import { IssueLoader, HTMLTable, ChannelsParser } from '../../core' import { Logger, Storage, Collection } from '@freearhey/core' +import { ChannelList, Issue, Site } from '../../models' import { SITES_DIR, ROOT_DIR } from '../../constants' -import { Issue, Site } from '../../models' import { Channel } from 'epg-grabber' async function main() { - const logger = new Logger({ disabled: true }) - const loader = new IssueLoader() + const logger = new Logger({ level: -999 }) + const issueLoader = new IssueLoader() const sitesStorage = new Storage(SITES_DIR) - const channelsParser = new ChannelsParser({ storage: sitesStorage }) const sites = new Collection() + logger.info('loading channels...') + const channelsParser = new ChannelsParser({ + storage: sitesStorage + }) + logger.info('loading list of sites') const folders = await sitesStorage.list('*/') logger.info('loading issues...') - const issues = await loader.load() + const issues = await issueLoader.load() logger.info('putting the data together...') const brokenGuideReports = issues.filter(issue => @@ -33,19 +37,21 @@ async function main() { const files = await sitesStorage.list(`${domain}/*.channels.xml`) for (const filepath of files) { - const channels = await channelsParser.parse(filepath) + const channelList: ChannelList = await channelsParser.parse(filepath) - site.totalChannels += channels.count() - site.markedChannels += channels.filter((channel: Channel) => channel.xmltv_id).count() + site.totalChannels += channelList.channels.count() + site.markedChannels += channelList.channels + .filter((channel: Channel) => channel.xmltv_id) + .count() } sites.add(site) } logger.info('creating sites table...') - const data = new Collection() + const tableData = new Collection() sites.forEach((site: Site) => { - data.add([ + tableData.add([ { value: `${site.domain}` }, { value: site.totalChannels, align: 'right' }, { value: site.markedChannels, align: 'right' }, @@ -55,7 +61,7 @@ async function main() { }) logger.info('updating sites.md...') - const table = new HTMLTable(data.all(), [ + const table = new HTMLTable(tableData.all(), [ { name: 'Site', align: 'left' }, { name: 'Channels
(total / with xmltv-id)', colspan: 2, align: 'left' }, { name: 'Status', align: 'left' }, diff --git a/scripts/core/channelsParser.ts b/scripts/core/channelsParser.ts index d4630506..43a5f28b 100644 --- a/scripts/core/channelsParser.ts +++ b/scripts/core/channelsParser.ts @@ -1,5 +1,6 @@ import { parseChannels } from 'epg-grabber' -import { Storage, Collection } from '@freearhey/core' +import { Storage } from '@freearhey/core' +import { ChannelList } from '../models' type ChannelsParserProps = { storage: Storage @@ -12,13 +13,10 @@ export class ChannelsParser { this.storage = storage } - async parse(filepath: string) { - let parsedChannels = new Collection() - + async parse(filepath: string): Promise { const content = await this.storage.load(filepath) - const channels = parseChannels(content) - parsedChannels = parsedChannels.concat(new Collection(channels)) + const parsed = parseChannels(content) - return parsedChannels + return new ChannelList({ channels: parsed }) } } diff --git a/scripts/core/dataLoader.ts b/scripts/core/dataLoader.ts index 51348bba..3d817977 100644 --- a/scripts/core/dataLoader.ts +++ b/scripts/core/dataLoader.ts @@ -49,7 +49,8 @@ export class DataLoader { feeds, timezones, guides, - streams + streams, + logos ] = await Promise.all([ this.storage.json('countries.json'), this.storage.json('regions.json'), @@ -61,7 +62,8 @@ export class DataLoader { this.storage.json('feeds.json'), this.storage.json('timezones.json'), this.storage.json('guides.json'), - this.storage.json('streams.json') + this.storage.json('streams.json'), + this.storage.json('logos.json') ]) return { @@ -75,7 +77,8 @@ export class DataLoader { feeds, timezones, guides, - streams + streams, + logos } } diff --git a/scripts/core/dataProcessor.ts b/scripts/core/dataProcessor.ts index 372d8716..1f0252fc 100644 --- a/scripts/core/dataProcessor.ts +++ b/scripts/core/dataProcessor.ts @@ -1,6 +1,6 @@ +import { Channel, Feed, GuideChannel, Logo, Stream } from '../models' import { DataLoaderData } from '../types/dataLoader' import { Collection } from '@freearhey/core' -import { Channel, Feed, Guide, Stream } from '../models' export class DataProcessor { constructor() {} @@ -9,31 +9,48 @@ export class DataProcessor { let channels = new Collection(data.channels).map(data => new Channel(data)) const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) - const guides = new Collection(data.guides).map(data => new Guide(data)) - const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId()) + const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data)) + const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) => + channel.getStreamId() + ) const streams = new Collection(data.streams).map(data => new Stream(data)) 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) - .withGuides(guidesGroupedByStreamId) + .withGuideChannels(guideChannelsGroupedByStreamId) .withStreams(streamsGroupedById) .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) - channels = channels.map((channel: Channel) => channel.withFeeds(feedsGroupedByChannelId)) + channels = channels.map((channel: Channel) => + channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId) + ) return { + guideChannelsGroupedByStreamId, feedsGroupedByChannelId, - guidesGroupedByStreamId, + logosGroupedByChannelId, + logosGroupedByStreamId, streamsGroupedById, + feedsKeyByStreamId, channelsKeyById, + guideChannels, channels, streams, - guides, - feeds + feeds, + logos } } } diff --git a/scripts/core/guide.ts b/scripts/core/guide.ts deleted file mode 100644 index 924f79f3..00000000 --- a/scripts/core/guide.ts +++ /dev/null @@ -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) - } - } -} diff --git a/scripts/core/guideManager.ts b/scripts/core/guideManager.ts index b7244662..aee2f666 100644 --- a/scripts/core/guideManager.ts +++ b/scripts/core/guideManager.ts @@ -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 { Channel, Program } from 'epg-grabber' -import { Guide } from '.' +import { Channel, Feed, Guide } from '../models' +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 = { options: OptionValues @@ -12,7 +17,6 @@ type GuideManagerProps = { export class GuideManager { options: OptionValues - storage: Storage logger: Logger channels: Collection programs: Collection @@ -22,22 +26,51 @@ export class GuideManager { this.logger = logger this.channels = channels this.programs = programs - this.storage = new Storage() } async createGuides() { 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 - .orderBy([(channel: Channel) => channel.index, (channel: Channel) => channel.xmltv_id]) - .uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`) - .groupBy((channel: Channel) => { + .map((channel: epgGrabber.Channel) => { + if (channel.xmltv_id && !channel.icon) { + 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 || '' }) }) const groupedPrograms = this.programs - .orderBy([(program: Program) => program.channel, (program: Program) => program.start]) - .groupBy((program: Program) => { + .orderBy([ + (program: epgGrabber.Program) => program.channel, + (program: epgGrabber.Program) => program.start + ]) + .groupBy((program: epgGrabber.Program) => { const lang = program.titles && program.titles.length && program.titles[0].lang ? program.titles[0].lang @@ -51,11 +84,28 @@ export class GuideManager { filepath: groupKey, gzip: this.options.gzip, channels: new Collection(groupedChannels.get(groupKey)), - programs: new Collection(groupedPrograms.get(groupKey)), - logger: this.logger + programs: new Collection(groupedPrograms.get(groupKey)) }) - 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) } } } diff --git a/scripts/core/index.ts b/scripts/core/index.ts index f545c6c2..8d528fe7 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -4,7 +4,6 @@ export * from './configLoader' export * from './dataLoader' export * from './dataProcessor' export * from './grabber' -export * from './guide' export * from './guideManager' export * from './htmlTable' export * from './issueLoader' @@ -13,5 +12,3 @@ export * from './job' export * from './proxyParser' export * from './queue' export * from './queueCreator' -export * from './xml' -export * from './xmltv' diff --git a/scripts/core/issueLoader.ts b/scripts/core/issueLoader.ts index 4aa1cceb..eebd6c39 100644 --- a/scripts/core/issueLoader.ts +++ b/scripts/core/issueLoader.ts @@ -23,6 +23,7 @@ export class IssueLoader { repo: REPO, per_page: 100, labels, + state: 'open', headers: { 'X-GitHub-Api-Version': '2022-11-28' } diff --git a/scripts/core/queueCreator.ts b/scripts/core/queueCreator.ts index 8245813a..71213630 100644 --- a/scripts/core/queueCreator.ts +++ b/scripts/core/queueCreator.ts @@ -1,15 +1,14 @@ import { Storage, Collection, DateTime, Logger } from '@freearhey/core' -import { ChannelsParser, ConfigLoader, Queue } from './' import { SITES_DIR, DATA_DIR } from '../constants' +import { GrabOptions } from '../commands/epg/grab' +import { ConfigLoader, Queue } from './' import { SiteConfig } from 'epg-grabber' import path from 'path' -import { GrabOptions } from '../commands/epg/grab' -import { Channel } from '../models' type QueueCreatorProps = { logger: Logger options: GrabOptions - parsedChannels: Collection + channels: Collection } export class QueueCreator { @@ -17,44 +16,29 @@ export class QueueCreator { logger: Logger sitesStorage: Storage dataStorage: Storage - parser: ChannelsParser - parsedChannels: Collection + channels: Collection options: GrabOptions - constructor({ parsedChannels, logger, options }: QueueCreatorProps) { - this.parsedChannels = parsedChannels + constructor({ channels, logger, options }: QueueCreatorProps) { + this.channels = channels this.logger = logger this.sitesStorage = new Storage() this.dataStorage = new Storage(DATA_DIR) - this.parser = new ChannelsParser({ storage: new Storage() }) this.options = options this.configLoader = new ConfigLoader() } async create(): Promise { - const channelsContent = await this.dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new Channel(data)) - let index = 0 const queue = new Queue() - for (const channel of this.parsedChannels.all()) { + for (const channel of this.channels.all()) { channel.index = index++ if (!channel.site || !channel.site_id || !channel.name) continue const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) const config: SiteConfig = await this.configLoader.load(configPath) - 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 { + if (!channel.xmltv_id) { channel.xmltv_id = channel.site_id } diff --git a/scripts/core/xml.ts b/scripts/core/xml.ts deleted file mode 100644 index fd42f993..00000000 --- a/scripts/core/xml.ts +++ /dev/null @@ -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 = '\r\n\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 += ` ${escapeString(channel.name)}\r\n` - }) - - output += '\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, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\n|\r/g, ' ') - .replace(/ +/g, ' ') - .trim() -} diff --git a/scripts/core/xmltv.ts b/scripts/core/xmltv.ts deleted file mode 100644 index 5603c4e7..00000000 --- a/scripts/core/xmltv.ts +++ /dev/null @@ -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() - }) - } -} diff --git a/scripts/models/channel.ts b/scripts/models/channel.ts index 2fb734bd..fb62a3fc 100644 --- a/scripts/models/channel.ts +++ b/scripts/models/channel.ts @@ -1,26 +1,28 @@ import { ChannelData, ChannelSearchableData } from '../types/channel' import { Collection, Dictionary } from '@freearhey/core' -import { Stream, Guide, Feed } from './' +import { Stream, Feed, Logo, GuideChannel } from './' export class Channel { - id: string - name: string + id?: string + name?: string altNames?: Collection network?: string owners?: Collection - countryCode: string + countryCode?: string subdivisionCode?: string cityName?: string categoryIds?: Collection - isNSFW: boolean + isNSFW: boolean = false launched?: string closed?: string replacedBy?: string website?: string - logo?: string feeds?: Collection + logos: Collection = new Collection() + + constructor(data?: ChannelData) { + if (!data) return - constructor(data: ChannelData) { this.id = data.id this.name = data.name this.altNames = new Collection(data.alt_names) @@ -35,11 +37,16 @@ export class Channel { this.closed = data.closed || undefined this.replacedBy = data.replaced_by || undefined this.website = data.website || undefined - this.logo = data.logo } 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 } @@ -50,19 +57,19 @@ export class Channel { return this.feeds } - getGuides(): Collection { - let guides = new Collection() + getGuideChannels(): Collection { + let channels = new Collection() this.getFeeds().forEach((feed: Feed) => { - guides = guides.concat(feed.getGuides()) + channels = channels.concat(feed.getGuideChannels()) }) - return guides + return channels } - getGuideNames(): Collection { - return this.getGuides() - .map((guide: Guide) => guide.siteName) + getGuideChannelNames(): Collection { + return this.getGuideChannels() + .map((channel: GuideChannel) => channel.siteName) .uniq() } @@ -100,12 +107,56 @@ export class Channel { 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 { return { id: this.getId(), name: this.getName(), altNames: this.getAltNames().all(), - guideNames: this.getGuideNames().all(), + guideNames: this.getGuideChannelNames().all(), streamNames: this.getStreamNames().all(), feedFullNames: this.getFeedFullNames().all() } diff --git a/scripts/models/channelList.ts b/scripts/models/channelList.ts new file mode 100644 index 00000000..d312e71c --- /dev/null +++ b/scripts/models/channelList.ts @@ -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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\n|\r/g, ' ') + .replace(/ +/g, ' ') + .trim() + } + + let output = '\r\n\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 += ` ${displayName}\r\n` + }) + + output += '\r\n' + + return output + } +} diff --git a/scripts/models/feed.ts b/scripts/models/feed.ts index cb90510a..bb2c7020 100644 --- a/scripts/models/feed.ts +++ b/scripts/models/feed.ts @@ -1,6 +1,6 @@ import { Collection, Dictionary } from '@freearhey/core' import { FeedData } from '../types/feed' -import { Channel } from './channel' +import { Logo, Channel } from '.' export class Feed { channelId: string @@ -12,8 +12,9 @@ export class Feed { languageCodes: Collection timezoneIds: Collection videoFormat: string - guides?: Collection + guideChannels?: Collection streams?: Collection + logos: Collection = new Collection() constructor(data: FeedData) { this.channelId = data.channel @@ -42,20 +43,30 @@ export class Feed { return this } - withGuides(guidesGroupedByStreamId: Dictionary): this { - this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`)) + withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this { + this.guideChannels = new Collection( + guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`) + ) 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 } - getGuides(): Collection { - if (!this.guides) return new Collection() + withLogos(logosGroupedByStreamId: Dictionary): this { + 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 { @@ -73,4 +84,41 @@ export class Feed { getStreamId(): string { 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 || '' + } } diff --git a/scripts/models/guide.ts b/scripts/models/guide.ts index 349d6d5c..b6026743 100644 --- a/scripts/models/guide.ts +++ b/scripts/models/guide.ts @@ -1,35 +1,35 @@ -import type { GuideData } from '../types/guide' -import { uniqueId } from 'lodash' +import { Collection, DateTime } from '@freearhey/core' +import { generateXMLTV } from 'epg-grabber' + +type GuideData = { + channels: Collection + programs: Collection + filepath: string + gzip: boolean +} export class Guide { - channelId?: string - feedId?: string - siteDomain?: string - siteId?: string - siteName?: string - languageCode?: string + channels: Collection + programs: Collection + filepath: string + gzip: boolean - constructor(data?: GuideData) { - if (!data) return - - this.channelId = data.channel - this.feedId = data.feed - this.siteDomain = data.site - this.siteId = data.site_id - this.siteName = data.site_name - this.languageCode = data.lang + constructor({ channels, programs, filepath, gzip }: GuideData) { + this.channels = channels + this.programs = programs + this.filepath = filepath + this.gzip = gzip || false } - getUUID(): string { - if (!this.getStreamId() || !this.siteId) return uniqueId() + toString() { + const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), { + timezone: 'UTC' + }) - return this.getStreamId() + this.siteId - } - - getStreamId(): string | undefined { - if (!this.channelId) return undefined - if (!this.feedId) return this.channelId - - return `${this.channelId}@${this.feedId}` + return generateXMLTV({ + channels: this.channels.all(), + programs: this.programs.all(), + date: currDate.toJSON() + }) } } diff --git a/scripts/models/guideChannel.ts b/scripts/models/guideChannel.ts new file mode 100644 index 00000000..92aca912 --- /dev/null +++ b/scripts/models/guideChannel.ts @@ -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 || '' + } + } +} diff --git a/scripts/models/index.ts b/scripts/models/index.ts index 7602bede..38ab2027 100644 --- a/scripts/models/index.ts +++ b/scripts/models/index.ts @@ -1,6 +1,9 @@ -export * from './issue' -export * from './site' export * from './channel' export * from './feed' -export * from './stream' export * from './guide' +export * from './guideChannel' +export * from './issue' +export * from './logo' +export * from './site' +export * from './stream' +export * from './channelList' diff --git a/scripts/models/logo.ts b/scripts/models/logo.ts new file mode 100644 index 00000000..d864a3fb --- /dev/null +++ b/scripts/models/logo.ts @@ -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}` + } +} diff --git a/scripts/types/channel.d.ts b/scripts/types/channel.d.ts index d718c6b4..b1d2237c 100644 --- a/scripts/types/channel.d.ts +++ b/scripts/types/channel.d.ts @@ -15,7 +15,6 @@ export type ChannelData = { closed: string replaced_by: string website: string - logo: string } export type ChannelSearchableData = { diff --git a/scripts/types/dataLoader.d.ts b/scripts/types/dataLoader.d.ts index 41f21a96..135340e9 100644 --- a/scripts/types/dataLoader.d.ts +++ b/scripts/types/dataLoader.d.ts @@ -16,4 +16,5 @@ export type DataLoaderData = { timezones: object | object[] guides: object | object[] streams: object | object[] + logos: object | object[] } diff --git a/scripts/types/dataProcessor.d.ts b/scripts/types/dataProcessor.d.ts index e99fb47e..f158f16e 100644 --- a/scripts/types/dataProcessor.d.ts +++ b/scripts/types/dataProcessor.d.ts @@ -1,12 +1,16 @@ import { Collection, Dictionary } from '@freearhey/core' export type DataProcessorData = { + guideChannelsGroupedByStreamId: Dictionary feedsGroupedByChannelId: Dictionary - guidesGroupedByStreamId: Dictionary + logosGroupedByChannelId: Dictionary + logosGroupedByStreamId: Dictionary + feedsKeyByStreamId: Dictionary streamsGroupedById: Dictionary channelsKeyById: Dictionary + guideChannels: Collection channels: Collection streams: Collection - guides: Collection feeds: Collection + logos: Collection } diff --git a/scripts/types/logo.d.ts b/scripts/types/logo.d.ts new file mode 100644 index 00000000..c77f4799 --- /dev/null +++ b/scripts/types/logo.d.ts @@ -0,0 +1,9 @@ +export type LogoData = { + channel: string + feed: string | null + tags: string[] + width: number + height: number + format: string | null + url: string +}