Update scripts

This commit is contained in:
freearhey 2025-04-19 02:06:15 +03:00
parent d681fcb3d5
commit 8e363d0e83
19 changed files with 562 additions and 141 deletions

View file

@ -1,19 +1,24 @@
import { Logger } from '@freearhey/core' import { DATA_DIR } from '../../constants'
import { ApiClient } from '../../core' import { Storage } from '@freearhey/core'
import { DataLoader } from '../../core'
async function main() { async function main() {
const logger = new Logger() const storage = new Storage(DATA_DIR)
const client = new ApiClient({ logger }) const loader = new DataLoader({ storage })
const requests = [ await Promise.all([
client.download('channels.json'), loader.download('blocklist.json'),
client.download('feeds.json'), loader.download('categories.json'),
client.download('countries.json'), loader.download('channels.json'),
client.download('regions.json'), loader.download('countries.json'),
client.download('subdivisions.json') loader.download('languages.json'),
] loader.download('regions.json'),
loader.download('subdivisions.json'),
await Promise.all(requests) loader.download('feeds.json'),
loader.download('timezones.json'),
loader.download('guides.json'),
loader.download('streams.json')
])
} }
main() main()

View file

@ -4,13 +4,17 @@ import { ChannelsParser, XML } from '../../core'
import { Channel, Feed } from '../../models' 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 epgGrabber from 'epg-grabber'
import { Command } from 'commander' import { Command } from 'commander'
import readline from 'readline' import readline from 'readline'
import Fuse from 'fuse.js' 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 } type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean }
if (process.platform === 'win32') { if (process.platform === 'win32') {
readline readline
@ -42,31 +46,48 @@ export default async function main(filepath: string) {
throw new Error(`File "${filepath}" does not exists`) throw new Error(`File "${filepath}" does not exists`)
} }
logger.info('loading data from api...')
const processor = new DataProcessor()
const dataStorage = new Storage(DATA_DIR)
const loader = new DataLoader({ storage: dataStorage })
const data: DataLoaderData = await loader.load()
const { feedsGroupedByChannelId, channels, channelsKeyById }: DataProcessorData =
processor.process(data)
logger.info('loading channels...')
const parser = new ChannelsParser({ storage }) const parser = new ChannelsParser({ storage })
parsedChannels = await parser.parse(filepath) parsedChannels = await parser.parse(filepath)
const parsedChannelsWithoutId = parsedChannels.filter(
(channel: epgGrabber.Channel) => !channel.xmltv_id
)
const dataStorage = new Storage(DATA_DIR) logger.info(
const channelsData = await dataStorage.json('channels.json') `found ${parsedChannels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
const channels = new Collection(channelsData).map(data => new Channel(data)) )
const feedsData = await dataStorage.json('feeds.json')
const feeds = new Collection(feedsData).map(data => new Feed(data))
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
const searchIndex: Fuse<Channel> = new Fuse(channels.all(), { logger.info('creating search index...')
keys: ['name', 'alt_names'], const items = channels.map((channel: Channel) => channel.getSearchable()).all()
threshold: 0.4 const searchIndex = sjs.createIndex(items, {
searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames']
}) })
for (const channel of parsedChannels.all()) { logger.info('starting...\n')
if (channel.xmltv_id) continue
for (const parsedChannel of parsedChannelsWithoutId.all()) {
try { try {
channel.xmltv_id = await selectChannel(channel, searchIndex, feedsGroupedByChannelId) parsedChannel.xmltv_id = await selectChannel(
} catch { parsedChannel,
searchIndex,
feedsGroupedByChannelId,
channelsKeyById
)
} catch (err) {
logger.info(err.message)
break break
} }
} }
parsedChannels.forEach((channel: epgGrabber.Channel) => { parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => {
if (channel.xmltv_id === '-') { if (channel.xmltv_id === '-') {
channel.xmltv_id = '' channel.xmltv_id = ''
} }
@ -75,12 +96,14 @@ export default async function main(filepath: string) {
async function selectChannel( async function selectChannel(
channel: epgGrabber.Channel, channel: epgGrabber.Channel,
searchIndex: Fuse<Channel>, searchIndex,
feedsGroupedByChannelId: Dictionary feedsGroupedByChannelId: Dictionary,
channelsKeyById: Dictionary
): Promise<string> { ): Promise<string> {
const query = escapeRegex(channel.name)
const similarChannels = searchIndex const similarChannels = searchIndex
.search(channel.name) .search(query)
.map((result: { item: Channel }) => result.item) .map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
const selected: ChoiceValue = await select({ const selected: ChoiceValue = await select({
message: `Select channel ID for "${channel.name}" (${channel.site_id}):`, message: `Select channel ID for "${channel.name}" (${channel.site_id}):`,
@ -93,13 +116,16 @@ async function selectChannel(
return '-' return '-'
case 'type': { case 'type': {
const typedChannelId = await input({ message: ' Channel ID:' }) const typedChannelId = await input({ message: ' Channel ID:' })
const typedFeedId = await input({ message: ' Feed ID:', default: 'SD' }) if (!typedChannelId) return ''
return [typedChannelId, typedFeedId].join('@') const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId)
if (selectedFeedId === '-') return typedChannelId
return [typedChannelId, selectedFeedId].join('@')
} }
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
return [selectedChannel.id, selectedFeedId].join('@') return [selectedChannel.id, selectedFeedId].join('@')
} }
} }
@ -108,18 +134,22 @@ async function selectChannel(
} }
async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> { async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> {
const channelFeeds = feedsGroupedByChannelId.get(channelId) || [] const channelFeeds = feedsGroupedByChannelId.has(channelId)
if (channelFeeds.length <= 1) return '' ? new Collection(feedsGroupedByChannelId.get(channelId))
: new Collection()
const choices = getFeedChoises(channelFeeds)
const selected: ChoiceValue = await select({ const selected: ChoiceValue = await select({
message: `Select feed ID for "${channelId}":`, message: `Select feed ID for "${channelId}":`,
choices: getFeedChoises(channelFeeds), choices,
pageSize: 10 pageSize: 10
}) })
switch (selected.type) { switch (selected.type) {
case 'skip':
return '-'
case 'type': case 'type':
return await input({ message: ' Feed ID:' }) return await input({ message: ' Feed ID:', default: 'SD' })
case 'feed': case 'feed':
const selectedFeed = selected.value const selectedFeed = selected.value
if (!selectedFeed) return '' if (!selectedFeed) return ''
@ -133,7 +163,7 @@ function getChannelChoises(channels: Collection): Choice[] {
const choises: Choice[] = [] const choises: Choice[] = []
channels.forEach((channel: Channel) => { channels.forEach((channel: Channel) => {
const names = [channel.name, ...channel.altNames.all()].join(', ') const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ')
choises.push({ choises.push({
value: { value: {
@ -163,12 +193,14 @@ function getFeedChoises(feeds: Collection): Choice[] {
type: 'feed', type: 'feed',
value: feed value: feed
}, },
default: feed.isMain,
name, name,
short: feed.id short: feed.id
}) })
}) })
choises.push({ name: 'Type...', value: { type: 'type' } }) choises.push({ name: 'Type...', value: { type: 'type' } })
choises.push({ name: 'Skip', value: { type: 'skip' } })
return choises return choises
} }
@ -179,3 +211,7 @@ function save(filepath: string) {
storage.saveSync(filepath, xml.toString()) storage.saveSync(filepath, xml.toString())
logger.info(`\nFile '${filepath}' successfully saved`) logger.info(`\nFile '${filepath}' successfully saved`)
} }
function escapeRegex(string: string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
}

View file

@ -14,7 +14,7 @@ program
) )
) )
.addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml')) .addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml'))
.addOption(new Option('-l, --lang <code>', 'Filter channels by language (ISO 639-1 code)')) .addOption(new Option('-l, --lang <codes>', 'Filter channels by languages (ISO 639-1 codes)'))
.addOption( .addOption(
new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env( new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env(
'TIMEOUT' 'TIMEOUT'
@ -90,7 +90,11 @@ async function main() {
parsedChannels = parsedChannels.concat(await parser.parse(filepath)) parsedChannels = parsedChannels.concat(await parser.parse(filepath))
} }
if (options.lang) { if (options.lang) {
parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang) parsedChannels = parsedChannels.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 ${parsedChannels.count()} channel(s)`)

View file

@ -1,59 +1,16 @@
import { Logger, Storage } from '@freearhey/core' import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios'
import cliProgress, { MultiBar } from 'cli-progress'
import numeral from 'numeral'
export class ApiClient { export class ApiClient {
progressBar: MultiBar instance: AxiosInstance
client: AxiosInstance
storage: Storage
logger: Logger
constructor({ logger }: { logger: Logger }) { constructor() {
this.logger = logger this.instance = axios.create({
this.client = axios.create({ baseURL: 'https://iptv-org.github.io/api',
responseType: 'stream' responseType: 'stream'
}) })
this.storage = new Storage()
this.progressBar = new cliProgress.MultiBar({
stopOnComplete: true,
hideCursor: true,
forceRedraw: true,
barsize: 36,
format(options, params, payload) {
const filename = payload.filename.padEnd(18, ' ')
const barsize = options.barsize || 40
const percent = (params.progress * 100).toFixed(2)
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
const total = numeral(params.total).format('0.0 b')
const completeSize = Math.round(params.progress * barsize)
const incompleteSize = barsize - completeSize
const bar =
options.barCompleteString && options.barIncompleteString
? options.barCompleteString.substr(0, completeSize) +
options.barGlue +
options.barIncompleteString.substr(0, incompleteSize)
: '-'.repeat(barsize)
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
}
})
} }
async download(filename: string) { get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
const stream = await this.storage.createStream(`temp/data/${filename}`) return this.instance.get(url, options)
const bar = this.progressBar.create(0, 0, { filename })
this.client
.get(`https://iptv-org.github.io/api/${filename}`, {
onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) {
if (total) bar.setTotal(total)
bar.update(loaded, { speed: rate })
}
})
.then((response: AxiosResponse) => {
response.data.pipe(stream)
})
} }
} }

100
scripts/core/dataLoader.ts Normal file
View file

@ -0,0 +1,100 @@
import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader'
import cliProgress, { MultiBar } from 'cli-progress'
import { Storage } from '@freearhey/core'
import { ApiClient } from './apiClient'
import numeral from 'numeral'
export class DataLoader {
client: ApiClient
storage: Storage
progressBar: MultiBar
constructor(props: DataLoaderProps) {
this.client = new ApiClient()
this.storage = props.storage
this.progressBar = new cliProgress.MultiBar({
stopOnComplete: true,
hideCursor: true,
forceRedraw: true,
barsize: 36,
format(options, params, payload) {
const filename = payload.filename.padEnd(18, ' ')
const barsize = options.barsize || 40
const percent = (params.progress * 100).toFixed(2)
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
const total = numeral(params.total).format('0.0 b')
const completeSize = Math.round(params.progress * barsize)
const incompleteSize = barsize - completeSize
const bar =
options.barCompleteString && options.barIncompleteString
? options.barCompleteString.substr(0, completeSize) +
options.barGlue +
options.barIncompleteString.substr(0, incompleteSize)
: '-'.repeat(barsize)
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
}
})
}
async load(): Promise<DataLoaderData> {
const [
countries,
regions,
subdivisions,
languages,
categories,
blocklist,
channels,
feeds,
timezones,
guides,
streams
] = await Promise.all([
this.storage.json('countries.json'),
this.storage.json('regions.json'),
this.storage.json('subdivisions.json'),
this.storage.json('languages.json'),
this.storage.json('categories.json'),
this.storage.json('blocklist.json'),
this.storage.json('channels.json'),
this.storage.json('feeds.json'),
this.storage.json('timezones.json'),
this.storage.json('guides.json'),
this.storage.json('streams.json')
])
return {
countries,
regions,
subdivisions,
languages,
categories,
blocklist,
channels,
feeds,
timezones,
guides,
streams
}
}
async download(filename: string) {
if (!this.storage || !this.progressBar) return
const stream = await this.storage.createStream(filename)
const progressBar = this.progressBar.create(0, 0, { filename })
this.client
.get(filename, {
responseType: 'stream',
onDownloadProgress({ total, loaded, rate }) {
if (total) progressBar.setTotal(total)
progressBar.update(loaded, { speed: rate })
}
})
.then(response => {
response.data.pipe(stream)
})
}
}

View file

@ -0,0 +1,39 @@
import { DataLoaderData } from '../types/dataLoader'
import { Collection } from '@freearhey/core'
import { Channel, Feed, Guide, Stream } from '../models'
export class DataProcessor {
constructor() {}
process(data: DataLoaderData) {
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 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 =>
new Feed(data)
.withGuides(guidesGroupedByStreamId)
.withStreams(streamsGroupedById)
.withChannel(channelsKeyById)
)
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
channels = channels.map((channel: Channel) => channel.withFeeds(feedsGroupedByChannelId))
return {
feedsGroupedByChannelId,
guidesGroupedByStreamId,
streamsGroupedById,
channelsKeyById,
channels,
streams,
guides,
feeds
}
}
}

View file

@ -1,15 +1,17 @@
export * from './xml'
export * from './channelsParser'
export * from './xmltv'
export * from './configLoader'
export * from './grabber'
export * from './job'
export * from './queue'
export * from './guideManager'
export * from './guide'
export * from './apiClient' export * from './apiClient'
export * from './queueCreator' export * from './channelsParser'
export * from './configLoader'
export * from './dataLoader'
export * from './dataProcessor'
export * from './grabber'
export * from './guide'
export * from './guideManager'
export * from './htmlTable'
export * from './issueLoader' export * from './issueLoader'
export * from './issueParser' export * from './issueParser'
export * from './htmlTable' export * from './job'
export * from './proxyParser' export * from './proxyParser'
export * from './queue'
export * from './queueCreator'
export * from './xml'
export * from './xmltv'

View file

@ -38,7 +38,6 @@ export class QueueCreator {
const queue = new Queue() const queue = new Queue()
for (const channel of this.parsedChannels.all()) { for (const channel of this.parsedChannels.all()) {
if (!channel.site || !channel.site_id || !channel.name) continue if (!channel.site || !channel.site_id || !channel.name) continue
if (this.options.lang && channel.lang !== this.options.lang) 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)

View file

@ -1,40 +1,24 @@
import { Collection } from '@freearhey/core' import { ChannelData, ChannelSearchableData } from '../types/channel'
import { Collection, Dictionary } from '@freearhey/core'
type ChannelData = { import { Stream, Guide, Feed } from './'
id: string
name: string
alt_names: string[]
network: string
owners: Collection
country: string
subdivision: string
city: string
categories: Collection
is_nsfw: boolean
launched: string
closed: string
replaced_by: string
website: string
logo: string
}
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
categories?: Collection
isNSFW: boolean isNSFW: boolean
launched?: string launched?: string
closed?: string closed?: string
replacedBy?: string replacedBy?: string
website?: string website?: string
logo: string logo?: string
feeds?: Collection
constructor(data: ChannelData) { constructor(data: ChannelData) {
this.id = data.id this.id = data.id
@ -53,4 +37,77 @@ export class Channel {
this.website = data.website || undefined this.website = data.website || undefined
this.logo = data.logo this.logo = data.logo
} }
withFeeds(feedsGroupedByChannelId: Dictionary): this {
this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
return this
}
getFeeds(): Collection {
if (!this.feeds) return new Collection()
return this.feeds
}
getGuides(): Collection {
let guides = new Collection()
this.getFeeds().forEach((feed: Feed) => {
guides = guides.concat(feed.getGuides())
})
return guides
}
getGuideNames(): Collection {
return this.getGuides()
.map((guide: Guide) => guide.siteName)
.uniq()
}
getStreams(): Collection {
let streams = new Collection()
this.getFeeds().forEach((feed: Feed) => {
streams = streams.concat(feed.getStreams())
})
return streams
}
getStreamNames(): Collection {
return this.getStreams()
.map((stream: Stream) => stream.getName())
.uniq()
}
getFeedFullNames(): Collection {
return this.getFeeds()
.map((feed: Feed) => feed.getFullName())
.uniq()
}
getName(): string {
return this.name || ''
}
getId(): string {
return this.id || ''
}
getAltNames(): Collection {
return this.altNames || new Collection()
}
getSearchable(): ChannelSearchableData {
return {
id: this.getId(),
name: this.getName(),
altNames: this.getAltNames().all(),
guideNames: this.getGuideNames().all(),
streamNames: this.getStreamNames().all(),
feedFullNames: this.getFeedFullNames().all()
}
}
} }

View file

@ -1,18 +1,10 @@
import { Collection } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
import { FeedData } from '../types/feed'
type FeedData = { import { Channel } from './channel'
channel: string
id: string
name: string
is_main: boolean
broadcast_area: Collection
languages: Collection
timezones: Collection
video_format: string
}
export class Feed { export class Feed {
channelId: string channelId: string
channel?: Channel
id: string id: string
name: string name: string
isMain: boolean isMain: boolean
@ -20,6 +12,8 @@ export class Feed {
languageCodes: Collection languageCodes: Collection
timezoneIds: Collection timezoneIds: Collection
videoFormat: string videoFormat: string
guides?: Collection
streams?: Collection
constructor(data: FeedData) { constructor(data: FeedData) {
this.channelId = data.channel this.channelId = data.channel
@ -31,4 +25,48 @@ export class Feed {
this.timezoneIds = new Collection(data.timezones) this.timezoneIds = new Collection(data.timezones)
this.videoFormat = data.video_format this.videoFormat = data.video_format
} }
withChannel(channelsKeyById: Dictionary): this {
this.channel = channelsKeyById.get(this.channelId)
return this
}
withStreams(streamsGroupedById: Dictionary): this {
this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`))
if (this.isMain) {
this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId)))
}
return this
}
withGuides(guidesGroupedByStreamId: Dictionary): this {
this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`))
if (this.isMain) {
this.guides = this.guides.concat(new Collection(guidesGroupedByStreamId.get(this.channelId)))
}
return this
}
getGuides(): Collection {
if (!this.guides) return new Collection()
return this.guides
}
getStreams(): Collection {
if (!this.streams) return new Collection()
return this.streams
}
getFullName(): string {
if (!this.channel) return ''
return `${this.channel.name} ${this.name}`
}
} }

35
scripts/models/guide.ts Normal file
View file

@ -0,0 +1,35 @@
import type { GuideData } from '../types/guide'
import { uniqueId } from 'lodash'
export class Guide {
channelId?: string
feedId?: string
siteDomain?: string
siteId?: string
siteName?: string
languageCode?: string
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
}
getUUID(): string {
if (!this.getStreamId() || !this.siteId) return uniqueId()
return this.getStreamId() + this.siteId
}
getStreamId(): string | undefined {
if (!this.channelId) return undefined
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
}

View file

@ -2,3 +2,5 @@ export * from './issue'
export * from './site' export * from './site'
export * from './channel' export * from './channel'
export * from './feed' export * from './feed'
export * from './stream'
export * from './guide'

58
scripts/models/stream.ts Normal file
View file

@ -0,0 +1,58 @@
import type { StreamData } from '../types/stream'
import { Feed, Channel } from './index'
export class Stream {
name?: string
url: string
id?: string
channelId?: string
channel?: Channel
feedId?: string
feed?: Feed
filepath?: string
line?: number
label?: string
verticalResolution?: number
isInterlaced?: boolean
referrer?: string
userAgent?: string
groupTitle: string = 'Undefined'
removed: boolean = false
constructor(data: StreamData) {
const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
this.id = id || undefined
this.channelId = data.channel || undefined
this.feedId = data.feed || undefined
this.name = data.name || undefined
this.url = data.url
this.referrer = data.referrer || undefined
this.userAgent = data.user_agent || undefined
this.verticalResolution = verticalResolution || undefined
this.isInterlaced = isInterlaced || undefined
this.label = data.label || undefined
}
getId(): string {
return this.id || ''
}
getName(): string {
return this.name || ''
}
}
function parseQuality(quality: string | null): {
verticalResolution: number | null
isInterlaced: boolean | null
} {
if (!quality) return { verticalResolution: null, isInterlaced: null }
const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
const isInterlaced = /i$/i.test(quality)
let verticalResolution = 0
if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString)
return { verticalResolution, isInterlaced }
}

28
scripts/types/channel.d.ts vendored Normal file
View file

@ -0,0 +1,28 @@
import { Collection } from '@freearhey/core'
export type ChannelData = {
id: string
name: string
alt_names: string[]
network: string
owners: Collection
country: string
subdivision: string
city: string
categories: Collection
is_nsfw: boolean
launched: string
closed: string
replaced_by: string
website: string
logo: string
}
export type ChannelSearchableData = {
id: string
name: string
altNames: string[]
guideNames: string[]
streamNames: string[]
feedFullNames: string[]
}

19
scripts/types/dataLoader.d.ts vendored Normal file
View file

@ -0,0 +1,19 @@
import { Storage } from '@freearhey/core'
export type DataLoaderProps = {
storage: Storage
}
export type DataLoaderData = {
countries: object | object[]
regions: object | object[]
subdivisions: object | object[]
languages: object | object[]
categories: object | object[]
blocklist: object | object[]
channels: object | object[]
feeds: object | object[]
timezones: object | object[]
guides: object | object[]
streams: object | object[]
}

12
scripts/types/dataProcessor.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
import { Collection, Dictionary } from '@freearhey/core'
export type DataProcessorData = {
feedsGroupedByChannelId: Dictionary
guidesGroupedByStreamId: Dictionary
streamsGroupedById: Dictionary
channelsKeyById: Dictionary
channels: Collection
streams: Collection
guides: Collection
feeds: Collection
}

12
scripts/types/feed.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
import { Collection } from '@freearhey/core'
export type FeedData = {
channel: string
id: string
name: string
is_main: boolean
broadcast_area: Collection
languages: Collection
timezones: Collection
video_format: string
}

8
scripts/types/guide.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
export type GuideData = {
channel: string
feed: string
site: string
site_id: string
site_name: string
lang: string
}

10
scripts/types/stream.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
export type StreamData = {
channel: string | null
feed: string | null
name?: string
url: string
referrer: string | null
user_agent: string | null
quality: string | null
label: string | null
}