Update scripts
This commit is contained in:
151
scripts/api.ts
Normal file
151
scripts/api.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
|
import { DATA_DIR } from './constants'
|
||||||
|
import cliProgress from 'cli-progress'
|
||||||
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
categoriesKeyById: new Dictionary<sdk.Models.Category>(),
|
||||||
|
countriesKeyByCode: new Dictionary<sdk.Models.Country>(),
|
||||||
|
subdivisionsKeyByCode: new Dictionary<sdk.Models.Subdivision>(),
|
||||||
|
citiesKeyByCode: new Dictionary<sdk.Models.City>(),
|
||||||
|
regionsKeyByCode: new Dictionary<sdk.Models.Region>(),
|
||||||
|
languagesKeyByCode: new Dictionary<sdk.Models.Language>(),
|
||||||
|
channelsKeyById: new Dictionary<sdk.Models.Channel>(),
|
||||||
|
feedsKeyByStreamId: new Dictionary<sdk.Models.Feed>(),
|
||||||
|
feedsGroupedByChannel: new Dictionary<sdk.Models.Feed[]>(),
|
||||||
|
blocklistRecordsGroupedByChannel: new Dictionary<sdk.Models.BlocklistRecord[]>(),
|
||||||
|
categories: new Collection<sdk.Models.Category>(),
|
||||||
|
countries: new Collection<sdk.Models.Country>(),
|
||||||
|
subdivisions: new Collection<sdk.Models.Subdivision>(),
|
||||||
|
cities: new Collection<sdk.Models.City>(),
|
||||||
|
regions: new Collection<sdk.Models.Region>()
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchIndex
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const dataManager = new sdk.DataManager({ dataDir: DATA_DIR })
|
||||||
|
await dataManager.loadFromDisk()
|
||||||
|
dataManager.processData()
|
||||||
|
|
||||||
|
const {
|
||||||
|
channels,
|
||||||
|
feeds,
|
||||||
|
categories,
|
||||||
|
languages,
|
||||||
|
countries,
|
||||||
|
subdivisions,
|
||||||
|
cities,
|
||||||
|
regions,
|
||||||
|
blocklist
|
||||||
|
} = dataManager.getProcessedData()
|
||||||
|
|
||||||
|
searchIndex = sdk.SearchEngine.createIndex<sdk.Models.Channel>(channels)
|
||||||
|
|
||||||
|
data.categoriesKeyById = categories.keyBy((category: sdk.Models.Category) => category.id)
|
||||||
|
data.countriesKeyByCode = countries.keyBy((country: sdk.Models.Country) => country.code)
|
||||||
|
data.subdivisionsKeyByCode = subdivisions.keyBy(
|
||||||
|
(subdivision: sdk.Models.Subdivision) => subdivision.code
|
||||||
|
)
|
||||||
|
data.citiesKeyByCode = cities.keyBy((city: sdk.Models.City) => city.code)
|
||||||
|
data.regionsKeyByCode = regions.keyBy((region: sdk.Models.Region) => region.code)
|
||||||
|
data.languagesKeyByCode = languages.keyBy((language: sdk.Models.Language) => language.code)
|
||||||
|
data.channelsKeyById = channels.keyBy((channel: sdk.Models.Channel) => channel.id)
|
||||||
|
data.feedsKeyByStreamId = feeds.keyBy((feed: sdk.Models.Feed) => feed.getStreamId())
|
||||||
|
data.feedsGroupedByChannel = feeds.groupBy((feed: sdk.Models.Feed) => feed.channel)
|
||||||
|
data.blocklistRecordsGroupedByChannel = blocklist.groupBy(
|
||||||
|
(blocklistRecord: sdk.Models.BlocklistRecord) => blocklistRecord.channel
|
||||||
|
)
|
||||||
|
data.categories = categories
|
||||||
|
data.countries = countries
|
||||||
|
data.subdivisions = subdivisions
|
||||||
|
data.cities = cities
|
||||||
|
data.regions = regions
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadData() {
|
||||||
|
function formatBytes(bytes: number) {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
'blocklist',
|
||||||
|
'categories',
|
||||||
|
'channels',
|
||||||
|
'cities',
|
||||||
|
'countries',
|
||||||
|
'feeds',
|
||||||
|
'guides',
|
||||||
|
'languages',
|
||||||
|
'logos',
|
||||||
|
'regions',
|
||||||
|
'streams',
|
||||||
|
'subdivisions',
|
||||||
|
'timezones'
|
||||||
|
]
|
||||||
|
|
||||||
|
const multiBar = new cliProgress.MultiBar({
|
||||||
|
stopOnComplete: true,
|
||||||
|
hideCursor: true,
|
||||||
|
forceRedraw: true,
|
||||||
|
barsize: 36,
|
||||||
|
format(options, params, payload) {
|
||||||
|
const filename = payload.filename.padEnd(18, ' ')
|
||||||
|
const barsize = options.barsize || 40
|
||||||
|
const percent = (params.progress * 100).toFixed(2)
|
||||||
|
const speed = payload.speed ? formatBytes(payload.speed) + '/s' : 'N/A'
|
||||||
|
const total = formatBytes(params.total)
|
||||||
|
const completeSize = Math.round(params.progress * barsize)
|
||||||
|
const incompleteSize = barsize - completeSize
|
||||||
|
const bar =
|
||||||
|
options.barCompleteString && options.barIncompleteString
|
||||||
|
? options.barCompleteString.substr(0, completeSize) +
|
||||||
|
options.barGlue +
|
||||||
|
options.barIncompleteString.substr(0, incompleteSize)
|
||||||
|
: '-'.repeat(barsize)
|
||||||
|
|
||||||
|
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataManager = new sdk.DataManager({ dataDir: DATA_DIR })
|
||||||
|
|
||||||
|
const requests: Promise<unknown>[] = []
|
||||||
|
for (const basename of files) {
|
||||||
|
const filename = `${basename}.json`
|
||||||
|
const progressBar = multiBar.create(0, 0, { filename })
|
||||||
|
const request = dataManager.downloadFileToDisk(basename, {
|
||||||
|
onDownloadProgress({ total, loaded, rate }) {
|
||||||
|
if (total) progressBar.setTotal(total)
|
||||||
|
progressBar.update(loaded, { speed: rate })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
requests.push(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(requests).catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchChannels(query: string): Collection<sdk.Models.Channel> {
|
||||||
|
if (!searchIndex) return new Collection<sdk.Models.Channel>()
|
||||||
|
|
||||||
|
const results = searchIndex.search(query)
|
||||||
|
|
||||||
|
const channels = new Collection<sdk.Models.Channel>()
|
||||||
|
|
||||||
|
new Collection<sdk.Types.ChannelSearchableData>(results).forEach(
|
||||||
|
(item: sdk.Types.ChannelSearchableData) => {
|
||||||
|
const channel = data.channelsKeyById.get(item.id)
|
||||||
|
if (channel) channels.add(channel)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
export { data, loadData, downloadData, searchChannels }
|
||||||
@@ -1,39 +1,31 @@
|
|||||||
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
import { API_DIR, STREAMS_DIR } from '../../constants'
|
||||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { API_DIR, STREAMS_DIR, DATA_DIR } from '../../constants'
|
import { PlaylistParser } from '../../core'
|
||||||
import type { DataLoaderData } from '../../types/dataLoader'
|
import { Logger } from '@freearhey/core'
|
||||||
import { Logger, Storage } from '@freearhey/core'
|
import { Stream } from '../../models'
|
||||||
import { Stream } from '../../models'
|
import { loadData } from '../../api'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
const processor = new DataProcessor()
|
await loadData()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
|
||||||
const dataLoader = new DataLoader({ storage: dataStorage })
|
logger.info('loading streams...')
|
||||||
const data: DataLoaderData = await dataLoader.load()
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
const parser = new PlaylistParser({
|
||||||
processor.process(data)
|
storage: streamsStorage
|
||||||
|
})
|
||||||
logger.info('loading streams...')
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
const parsed = await parser.parse(files)
|
||||||
const parser = new PlaylistParser({
|
const _streams = parsed
|
||||||
storage: streamsStorage,
|
.sortBy((stream: Stream) => stream.getId())
|
||||||
channelsKeyById,
|
.map((stream: Stream) => stream.toObject())
|
||||||
logosGroupedByStreamId,
|
logger.info(`found ${_streams.count()} streams`)
|
||||||
feedsGroupedByChannelId
|
|
||||||
})
|
logger.info('saving to .api/streams.json...')
|
||||||
const files = await streamsStorage.list('**/*.m3u')
|
const apiStorage = new Storage(API_DIR)
|
||||||
let streams = await parser.parse(files)
|
await apiStorage.save('streams.json', _streams.toJSON())
|
||||||
streams = streams
|
}
|
||||||
.orderBy((stream: Stream) => stream.getId())
|
|
||||||
.map((stream: Stream) => stream.toJSON())
|
main()
|
||||||
logger.info(`found ${streams.count()} streams`)
|
|
||||||
|
|
||||||
logger.info('saving to .api/streams.json...')
|
|
||||||
const apiStorage = new Storage(API_DIR)
|
|
||||||
await apiStorage.save('streams.json', streams.toJSON())
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,26 +1,7 @@
|
|||||||
import { DATA_DIR } from '../../constants'
|
import { downloadData } from '../../api'
|
||||||
import { Storage } from '@freearhey/core'
|
|
||||||
import { DataLoader } from '../../core'
|
async function main() {
|
||||||
|
await downloadData()
|
||||||
async function main() {
|
}
|
||||||
const storage = new Storage(DATA_DIR)
|
|
||||||
const loader = new DataLoader({ storage })
|
main()
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
loader.download('blocklist.json'),
|
|
||||||
loader.download('categories.json'),
|
|
||||||
loader.download('channels.json'),
|
|
||||||
loader.download('countries.json'),
|
|
||||||
loader.download('languages.json'),
|
|
||||||
loader.download('regions.json'),
|
|
||||||
loader.download('subdivisions.json'),
|
|
||||||
loader.download('feeds.json'),
|
|
||||||
loader.download('logos.json'),
|
|
||||||
loader.download('timezones.json'),
|
|
||||||
loader.download('guides.json'),
|
|
||||||
loader.download('streams.json'),
|
|
||||||
loader.download('cities.json')
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,217 +1,190 @@
|
|||||||
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
|
import { loadData, data, searchChannels } from '../../api'
|
||||||
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
import { Collection, Logger } from '@freearhey/core'
|
||||||
import type { ChannelSearchableData } from '../../types/channel'
|
import { select, input } from '@inquirer/prompts'
|
||||||
import { Channel, Feed, Playlist, Stream } from '../../models'
|
import { Playlist, Stream } from '../../models'
|
||||||
import { DataProcessorData } from '../../types/dataProcessor'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { DataLoaderData } from '../../types/dataLoader'
|
import { PlaylistParser } from '../../core'
|
||||||
import { select, input } from '@inquirer/prompts'
|
import nodeCleanup from 'node-cleanup'
|
||||||
import { DATA_DIR } from '../../constants'
|
import * as sdk from '@iptv-org/sdk'
|
||||||
import nodeCleanup from 'node-cleanup'
|
import { truncate } from '../../utils'
|
||||||
import sjs from '@freearhey/search-js'
|
import { Command } from 'commander'
|
||||||
import { Command } from 'commander'
|
import readline from 'readline'
|
||||||
import readline from 'readline'
|
|
||||||
|
type ChoiceValue = { type: string; value?: sdk.Models.Feed | sdk.Models.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 }
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
if (process.platform === 'win32') {
|
readline
|
||||||
readline
|
.createInterface({
|
||||||
.createInterface({
|
input: process.stdin,
|
||||||
input: process.stdin,
|
output: process.stdout
|
||||||
output: process.stdout
|
})
|
||||||
})
|
.on('SIGINT', function () {
|
||||||
.on('SIGINT', function () {
|
process.emit('SIGINT')
|
||||||
process.emit('SIGINT')
|
})
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
const program = new Command()
|
||||||
const program = new Command()
|
|
||||||
|
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
|
||||||
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
|
|
||||||
|
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 parsedStreams = new Collection<Stream>()
|
||||||
let parsedStreams = new Collection()
|
|
||||||
|
main(filepath)
|
||||||
main(filepath)
|
nodeCleanup(() => {
|
||||||
nodeCleanup(() => {
|
save(filepath)
|
||||||
save(filepath)
|
})
|
||||||
})
|
|
||||||
|
export default async function main(filepath: string) {
|
||||||
export default async function main(filepath: string) {
|
if (!(await storage.exists(filepath))) {
|
||||||
if (!(await storage.exists(filepath))) {
|
throw new Error(`File "${filepath}" does not exists`)
|
||||||
throw new Error(`File "${filepath}" does not exists`)
|
}
|
||||||
}
|
|
||||||
|
logger.info('loading data from api...')
|
||||||
logger.info('loading data from api...')
|
await loadData()
|
||||||
const processor = new DataProcessor()
|
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
logger.info('loading streams...')
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
const parser = new PlaylistParser({
|
||||||
const data: DataLoaderData = await loader.load()
|
storage
|
||||||
const {
|
})
|
||||||
channels,
|
parsedStreams = await parser.parseFile(filepath)
|
||||||
channelsKeyById,
|
const streamsWithoutId = parsedStreams.filter((stream: Stream) => !stream.tvgId)
|
||||||
feedsGroupedByChannelId,
|
|
||||||
logosGroupedByStreamId
|
logger.info(
|
||||||
}: DataProcessorData = processor.process(data)
|
`found ${parsedStreams.count()} streams (including ${streamsWithoutId.count()} without ID)`
|
||||||
|
)
|
||||||
logger.info('loading streams...')
|
|
||||||
const parser = new PlaylistParser({
|
logger.info('starting...\n')
|
||||||
storage,
|
|
||||||
feedsGroupedByChannelId,
|
for (const stream of streamsWithoutId.all()) {
|
||||||
logosGroupedByStreamId,
|
try {
|
||||||
channelsKeyById
|
stream.tvgId = await selectChannel(stream)
|
||||||
})
|
} catch (err) {
|
||||||
parsedStreams = await parser.parseFile(filepath)
|
logger.info(err.message)
|
||||||
const streamsWithoutId = parsedStreams.filter((stream: Stream) => !stream.id)
|
break
|
||||||
|
}
|
||||||
logger.info(
|
}
|
||||||
`found ${parsedStreams.count()} streams (including ${streamsWithoutId.count()} without ID)`
|
|
||||||
)
|
streamsWithoutId.forEach((stream: Stream) => {
|
||||||
|
if (stream.channel === '-') {
|
||||||
logger.info('creating search index...')
|
stream.channel = ''
|
||||||
const items = channels.map((channel: Channel) => channel.getSearchable()).all()
|
}
|
||||||
const searchIndex = sjs.createIndex(items, {
|
})
|
||||||
searchable: ['name', 'altNames', 'guideNames', 'streamTitles', 'feedFullNames']
|
}
|
||||||
})
|
|
||||||
|
async function selectChannel(stream: Stream): Promise<string> {
|
||||||
logger.info('starting...\n')
|
const query = escapeRegex(stream.title)
|
||||||
|
const similarChannels = searchChannels(query)
|
||||||
for (const stream of streamsWithoutId.all()) {
|
const url = truncate(stream.url, 50)
|
||||||
try {
|
|
||||||
stream.id = await selectChannel(stream, searchIndex, feedsGroupedByChannelId, channelsKeyById)
|
const selected: ChoiceValue = await select({
|
||||||
} catch (err) {
|
message: `Select channel ID for "${stream.title}" (${url}):`,
|
||||||
logger.info(err.message)
|
choices: getChannelChoises(similarChannels),
|
||||||
break
|
pageSize: 10
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
switch (selected.type) {
|
||||||
streamsWithoutId.forEach((stream: Stream) => {
|
case 'skip':
|
||||||
if (stream.id === '-') {
|
return '-'
|
||||||
stream.id = ''
|
case 'type': {
|
||||||
}
|
const typedChannelId = await input({ message: ' Channel ID:' })
|
||||||
})
|
if (!typedChannelId) return ''
|
||||||
}
|
const selectedFeedId = await selectFeed(typedChannelId)
|
||||||
|
if (selectedFeedId === '-') return typedChannelId
|
||||||
async function selectChannel(
|
return [typedChannelId, selectedFeedId].join('@')
|
||||||
stream: Stream,
|
}
|
||||||
searchIndex,
|
case 'channel': {
|
||||||
feedsGroupedByChannelId: Dictionary,
|
const selectedChannel = selected.value
|
||||||
channelsKeyById: Dictionary
|
if (!selectedChannel) return ''
|
||||||
): Promise<string> {
|
const selectedFeedId = await selectFeed(selectedChannel.id)
|
||||||
const query = escapeRegex(stream.getTitle())
|
if (selectedFeedId === '-') return selectedChannel.id
|
||||||
const similarChannels = searchIndex
|
return [selectedChannel.id, selectedFeedId].join('@')
|
||||||
.search(query)
|
}
|
||||||
.map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
|
}
|
||||||
|
|
||||||
const url = stream.url.length > 50 ? stream.url.slice(0, 50) + '...' : stream.url
|
return ''
|
||||||
|
}
|
||||||
const selected: ChoiceValue = await select({
|
|
||||||
message: `Select channel ID for "${stream.title}" (${url}):`,
|
async function selectFeed(channelId: string): Promise<string> {
|
||||||
choices: getChannelChoises(new Collection(similarChannels)),
|
const channelFeeds = new Collection(data.feedsGroupedByChannel.get(channelId))
|
||||||
pageSize: 10
|
const choices = getFeedChoises(channelFeeds)
|
||||||
})
|
|
||||||
|
const selected: ChoiceValue = await select({
|
||||||
switch (selected.type) {
|
message: `Select feed ID for "${channelId}":`,
|
||||||
case 'skip':
|
choices,
|
||||||
return '-'
|
pageSize: 10
|
||||||
case 'type': {
|
})
|
||||||
const typedChannelId = await input({ message: ' Channel ID:' })
|
|
||||||
if (!typedChannelId) return ''
|
switch (selected.type) {
|
||||||
const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId)
|
case 'skip':
|
||||||
if (selectedFeedId === '-') return typedChannelId
|
return '-'
|
||||||
return [typedChannelId, selectedFeedId].join('@')
|
case 'type':
|
||||||
}
|
return await input({ message: ' Feed ID:', default: 'SD' })
|
||||||
case 'channel': {
|
case 'feed':
|
||||||
const selectedChannel = selected.value
|
const selectedFeed = selected.value
|
||||||
if (!selectedChannel) return ''
|
if (!selectedFeed) return ''
|
||||||
const selectedFeedId = await selectFeed(selectedChannel.id, feedsGroupedByChannelId)
|
return selectedFeed.id
|
||||||
if (selectedFeedId === '-') return selectedChannel.id
|
}
|
||||||
return [selectedChannel.id, selectedFeedId].join('@')
|
|
||||||
}
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
function getChannelChoises(channels: Collection<sdk.Models.Channel>): Choice[] {
|
||||||
}
|
const choises: Choice[] = []
|
||||||
|
|
||||||
async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> {
|
channels.forEach((channel: sdk.Models.Channel) => {
|
||||||
const channelFeeds = new Collection(feedsGroupedByChannelId.get(channelId))
|
const names = new Collection([channel.name, ...channel.alt_names]).uniq().join(', ')
|
||||||
const choices = getFeedChoises(channelFeeds)
|
|
||||||
|
choises.push({
|
||||||
const selected: ChoiceValue = await select({
|
value: {
|
||||||
message: `Select feed ID for "${channelId}":`,
|
type: 'channel',
|
||||||
choices,
|
value: channel
|
||||||
pageSize: 10
|
},
|
||||||
})
|
name: `${channel.id} (${names})`,
|
||||||
|
short: `${channel.id}`
|
||||||
switch (selected.type) {
|
})
|
||||||
case 'skip':
|
})
|
||||||
return '-'
|
|
||||||
case 'type':
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
return await input({ message: ' Feed ID:', default: 'SD' })
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
case 'feed':
|
|
||||||
const selectedFeed = selected.value
|
return choises
|
||||||
if (!selectedFeed) return ''
|
}
|
||||||
return selectedFeed.id
|
|
||||||
}
|
function getFeedChoises(feeds: Collection<sdk.Models.Feed>): Choice[] {
|
||||||
|
const choises: Choice[] = []
|
||||||
return ''
|
|
||||||
}
|
feeds.forEach((feed: sdk.Models.Feed) => {
|
||||||
|
let name = `${feed.id} (${feed.name})`
|
||||||
function getChannelChoises(channels: Collection): Choice[] {
|
if (feed.is_main) name += ' [main]'
|
||||||
const choises: Choice[] = []
|
|
||||||
|
choises.push({
|
||||||
channels.forEach((channel: Channel) => {
|
value: {
|
||||||
const names = new Collection([channel.name, ...channel.altNames.all()]).uniq().join(', ')
|
type: 'feed',
|
||||||
|
value: feed
|
||||||
choises.push({
|
},
|
||||||
value: {
|
default: feed.is_main,
|
||||||
type: 'channel',
|
name,
|
||||||
value: channel
|
short: feed.id
|
||||||
},
|
})
|
||||||
name: `${channel.id} (${names})`,
|
})
|
||||||
short: `${channel.id}`
|
|
||||||
})
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
})
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
|
|
||||||
choises.push({ name: 'Type...', value: { type: 'type' } })
|
return choises
|
||||||
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
}
|
||||||
|
|
||||||
return choises
|
function save(filepath: string) {
|
||||||
}
|
if (!storage.existsSync(filepath)) return
|
||||||
|
const playlist = new Playlist(parsedStreams)
|
||||||
function getFeedChoises(feeds: Collection): Choice[] {
|
storage.saveSync(filepath, playlist.toString())
|
||||||
const choises: Choice[] = []
|
logger.info(`\nFile '${filepath}' successfully saved`)
|
||||||
|
}
|
||||||
feeds.forEach((feed: Feed) => {
|
|
||||||
let name = `${feed.id} (${feed.name})`
|
function escapeRegex(string: string) {
|
||||||
if (feed.isMain) name += ' [main]'
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
|
}
|
||||||
choises.push({
|
|
||||||
value: {
|
|
||||||
type: 'feed',
|
|
||||||
value: feed
|
|
||||||
},
|
|
||||||
default: feed.isMain,
|
|
||||||
name,
|
|
||||||
short: feed.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
choises.push({ name: 'Type...', value: { type: 'type' } })
|
|
||||||
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
|
||||||
|
|
||||||
return choises
|
|
||||||
}
|
|
||||||
|
|
||||||
function save(filepath: string) {
|
|
||||||
if (!storage.existsSync(filepath)) return
|
|
||||||
const playlist = new Playlist(parsedStreams)
|
|
||||||
storage.saveSync(filepath, playlist.toString())
|
|
||||||
logger.info(`\nFile '${filepath}' successfully saved`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegex(string: string) {
|
|
||||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,78 +1,84 @@
|
|||||||
import { Logger, Storage } from '@freearhey/core'
|
import { Collection, Logger } from '@freearhey/core'
|
||||||
import { STREAMS_DIR, DATA_DIR } from '../../constants'
|
import { Stream, Playlist } from '../../models'
|
||||||
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { Stream, Playlist } from '../../models'
|
import { STREAMS_DIR } from '../../constants'
|
||||||
import { program } from 'commander'
|
import { PlaylistParser } from '../../core'
|
||||||
import { DataLoaderData } from '../../types/dataLoader'
|
import { loadData } from '../../api'
|
||||||
import { DataProcessorData } from '../../types/dataProcessor'
|
import { program } from 'commander'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
program.argument('[filepath...]', 'Path to file to format').parse(process.argv)
|
program.argument('[filepath...]', 'Path to file to format').parse(process.argv)
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
const processor = new DataProcessor()
|
await loadData()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
logger.info('loading streams...')
|
||||||
const data: DataLoaderData = await loader.load()
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
const parser = new PlaylistParser({
|
||||||
processor.process(data)
|
storage: streamsStorage
|
||||||
|
})
|
||||||
logger.info('loading streams...')
|
let files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u')
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
files = files.map((filepath: string) => path.basename(filepath))
|
||||||
const parser = new PlaylistParser({
|
let streams = await parser.parse(files)
|
||||||
storage: streamsStorage,
|
|
||||||
channelsKeyById,
|
logger.info(`found ${streams.count()} streams`)
|
||||||
feedsGroupedByChannelId,
|
|
||||||
logosGroupedByStreamId
|
logger.info('normalizing links...')
|
||||||
})
|
streams = streams.map(stream => {
|
||||||
let files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u')
|
stream.normalizeURL()
|
||||||
files = files.map((filepath: string) => path.basename(filepath))
|
return stream
|
||||||
let streams = await parser.parse(files)
|
})
|
||||||
|
|
||||||
logger.info(`found ${streams.count()} streams`)
|
logger.info('removing duplicates...')
|
||||||
|
streams = streams.uniqBy(stream => stream.url)
|
||||||
logger.info('normalizing links...')
|
|
||||||
streams = streams.map(stream => {
|
logger.info('removing wrong id...')
|
||||||
stream.normalizeURL()
|
streams = streams.map((stream: Stream) => {
|
||||||
return stream
|
const channel = stream.getChannel()
|
||||||
})
|
if (channel) return stream
|
||||||
|
|
||||||
logger.info('removing duplicates...')
|
stream.tvgId = ''
|
||||||
streams = streams.uniqBy(stream => stream.url)
|
stream.channel = ''
|
||||||
|
stream.feed = ''
|
||||||
logger.info('removing wrong id...')
|
|
||||||
streams = streams.map((stream: Stream) => {
|
return stream
|
||||||
if (!stream.channel || channelsKeyById.missing(stream.channel.id)) {
|
})
|
||||||
stream.id = ''
|
|
||||||
}
|
logger.info('adding the missing feed id...')
|
||||||
|
streams = streams.map((stream: Stream) => {
|
||||||
return stream
|
const feed = stream.getFeed()
|
||||||
})
|
if (feed) {
|
||||||
|
stream.feed = feed.id
|
||||||
logger.info('sorting links...')
|
stream.tvgId = stream.getId()
|
||||||
streams = streams.orderBy(
|
}
|
||||||
[
|
|
||||||
(stream: Stream) => stream.title,
|
return stream
|
||||||
(stream: Stream) => stream.getVerticalResolution(),
|
})
|
||||||
(stream: Stream) => stream.getLabel(),
|
|
||||||
(stream: Stream) => stream.url
|
logger.info('sorting links...')
|
||||||
],
|
streams = streams.sortBy(
|
||||||
['asc', 'desc', 'asc', 'asc']
|
[
|
||||||
)
|
(stream: Stream) => stream.title,
|
||||||
|
(stream: Stream) => stream.getVerticalResolution(),
|
||||||
logger.info('saving...')
|
(stream: Stream) => stream.label,
|
||||||
const groupedStreams = streams.groupBy((stream: Stream) => stream.getFilepath())
|
(stream: Stream) => stream.url
|
||||||
for (const filepath of groupedStreams.keys()) {
|
],
|
||||||
const streams = groupedStreams.get(filepath) || []
|
['asc', 'desc', 'asc', 'asc']
|
||||||
|
)
|
||||||
if (!streams.length) return
|
|
||||||
|
logger.info('saving...')
|
||||||
const playlist = new Playlist(streams, { public: false })
|
const groupedStreams = streams.groupBy((stream: Stream) => stream.getFilepath())
|
||||||
await streamsStorage.save(filepath, playlist.toString())
|
for (const filepath of groupedStreams.keys()) {
|
||||||
}
|
const streams = new Collection(groupedStreams.get(filepath))
|
||||||
}
|
|
||||||
|
if (streams.isEmpty()) return
|
||||||
main()
|
|
||||||
|
const playlist = new Playlist(streams, { public: false })
|
||||||
|
await streamsStorage.save(filepath, playlist.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
|||||||
@@ -1,131 +1,115 @@
|
|||||||
import { PlaylistParser, DataProcessor, DataLoader } from '../../core'
|
import { LOGS_DIR, STREAMS_DIR } from '../../constants'
|
||||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { DATA_DIR, LOGS_DIR, STREAMS_DIR } from '../../constants'
|
import { PlaylistParser } from '../../core'
|
||||||
import type { DataLoaderData } from '../../types/dataLoader'
|
import { loadData, data } from '../../api'
|
||||||
import { Logger, Storage, File } from '@freearhey/core'
|
import { Logger } from '@freearhey/core'
|
||||||
import { Stream } from '../../models'
|
import uniqueId from 'lodash.uniqueid'
|
||||||
import uniqueId from 'lodash.uniqueid'
|
import { Stream } from '../../models'
|
||||||
import {
|
import {
|
||||||
IndexCategoryGenerator,
|
IndexCategoryGenerator,
|
||||||
IndexLanguageGenerator,
|
IndexLanguageGenerator,
|
||||||
IndexCountryGenerator,
|
IndexCountryGenerator,
|
||||||
SubdivisionsGenerator,
|
SubdivisionsGenerator,
|
||||||
CategoriesGenerator,
|
CategoriesGenerator,
|
||||||
CountriesGenerator,
|
CountriesGenerator,
|
||||||
LanguagesGenerator,
|
LanguagesGenerator,
|
||||||
RegionsGenerator,
|
RegionsGenerator,
|
||||||
SourcesGenerator,
|
SourcesGenerator,
|
||||||
CitiesGenerator,
|
CitiesGenerator,
|
||||||
IndexGenerator,
|
IndexGenerator,
|
||||||
RawGenerator
|
RawGenerator
|
||||||
} from '../../generators'
|
} from '../../generators'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const logFile = new File('generators.log')
|
const logFile = new File('generators.log')
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
const processor = new DataProcessor()
|
await loadData()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
logger.info('loading streams...')
|
||||||
const data: DataLoaderData = await loader.load()
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const {
|
const parser = new PlaylistParser({
|
||||||
feedsGroupedByChannelId,
|
storage: streamsStorage
|
||||||
logosGroupedByStreamId,
|
})
|
||||||
channelsKeyById,
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
subdivisions,
|
let streams = await parser.parse(files)
|
||||||
categories,
|
const totalStreams = streams.count()
|
||||||
countries,
|
logger.info(`found ${totalStreams} streams`)
|
||||||
regions,
|
|
||||||
cities
|
logger.info('generating raw/...')
|
||||||
}: DataProcessorData = processor.process(data)
|
await new RawGenerator({ streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('filtering streams...')
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
streams = streams.uniqBy((stream: Stream) => stream.getId() || uniqueId())
|
||||||
const parser = new PlaylistParser({
|
|
||||||
storage: streamsStorage,
|
logger.info('sorting streams...')
|
||||||
feedsGroupedByChannelId,
|
streams = streams.sortBy(
|
||||||
logosGroupedByStreamId,
|
[
|
||||||
channelsKeyById
|
(stream: Stream) => stream.getId(),
|
||||||
})
|
(stream: Stream) => stream.getVerticalResolution(),
|
||||||
const files = await streamsStorage.list('**/*.m3u')
|
(stream: Stream) => stream.label
|
||||||
let streams = await parser.parse(files)
|
],
|
||||||
const totalStreams = streams.count()
|
['asc', 'asc', 'desc']
|
||||||
logger.info(`found ${totalStreams} streams`)
|
)
|
||||||
|
|
||||||
logger.info('generating raw/...')
|
const { categories, countries, subdivisions, cities, regions } = data
|
||||||
await new RawGenerator({ streams, logFile }).generate()
|
|
||||||
|
logger.info('generating categories/...')
|
||||||
logger.info('filtering streams...')
|
await new CategoriesGenerator({ categories, streams, logFile }).generate()
|
||||||
streams = streams.uniqBy((stream: Stream) =>
|
|
||||||
stream.hasId() ? stream.getChannelId() + stream.getFeedId() : uniqueId()
|
logger.info('generating languages/...')
|
||||||
)
|
await new LanguagesGenerator({ streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('sorting streams...')
|
logger.info('generating countries/...')
|
||||||
streams = streams.orderBy(
|
await new CountriesGenerator({
|
||||||
[
|
countries,
|
||||||
(stream: Stream) => stream.getId(),
|
streams,
|
||||||
(stream: Stream) => stream.getVerticalResolution(),
|
logFile
|
||||||
(stream: Stream) => stream.getLabel()
|
}).generate()
|
||||||
],
|
|
||||||
['asc', 'asc', 'desc']
|
logger.info('generating subdivisions/...')
|
||||||
)
|
await new SubdivisionsGenerator({
|
||||||
|
subdivisions,
|
||||||
logger.info('generating categories/...')
|
streams,
|
||||||
await new CategoriesGenerator({ categories, streams, logFile }).generate()
|
logFile
|
||||||
|
}).generate()
|
||||||
logger.info('generating languages/...')
|
|
||||||
await new LanguagesGenerator({ streams, logFile }).generate()
|
logger.info('generating cities/...')
|
||||||
|
await new CitiesGenerator({
|
||||||
logger.info('generating countries/...')
|
cities,
|
||||||
await new CountriesGenerator({
|
streams,
|
||||||
countries,
|
logFile
|
||||||
streams,
|
}).generate()
|
||||||
logFile
|
|
||||||
}).generate()
|
logger.info('generating regions/...')
|
||||||
|
await new RegionsGenerator({
|
||||||
logger.info('generating subdivisions/...')
|
streams,
|
||||||
await new SubdivisionsGenerator({
|
regions,
|
||||||
subdivisions,
|
logFile
|
||||||
streams,
|
}).generate()
|
||||||
logFile
|
|
||||||
}).generate()
|
logger.info('generating sources/...')
|
||||||
|
await new SourcesGenerator({ streams, logFile }).generate()
|
||||||
logger.info('generating cities/...')
|
|
||||||
await new CitiesGenerator({
|
logger.info('generating index.m3u...')
|
||||||
cities,
|
await new IndexGenerator({ streams, logFile }).generate()
|
||||||
streams,
|
|
||||||
logFile
|
logger.info('generating index.category.m3u...')
|
||||||
}).generate()
|
await new IndexCategoryGenerator({ streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('generating regions/...')
|
logger.info('generating index.country.m3u...')
|
||||||
await new RegionsGenerator({
|
await new IndexCountryGenerator({
|
||||||
streams,
|
streams,
|
||||||
regions,
|
logFile
|
||||||
logFile
|
}).generate()
|
||||||
}).generate()
|
|
||||||
|
logger.info('generating index.language.m3u...')
|
||||||
logger.info('generating sources/...')
|
await new IndexLanguageGenerator({ streams, logFile }).generate()
|
||||||
await new SourcesGenerator({ streams, logFile }).generate()
|
|
||||||
|
logger.info('saving generators.log...')
|
||||||
logger.info('generating index.m3u...')
|
const logStorage = new Storage(LOGS_DIR)
|
||||||
await new IndexGenerator({ streams, logFile }).generate()
|
logStorage.saveFile(logFile)
|
||||||
|
}
|
||||||
logger.info('generating index.category.m3u...')
|
|
||||||
await new IndexCategoryGenerator({ streams, logFile }).generate()
|
main()
|
||||||
|
|
||||||
logger.info('generating index.country.m3u...')
|
|
||||||
await new IndexCountryGenerator({
|
|
||||||
streams,
|
|
||||||
logFile
|
|
||||||
}).generate()
|
|
||||||
|
|
||||||
logger.info('generating index.language.m3u...')
|
|
||||||
await new IndexLanguageGenerator({ streams, logFile }).generate()
|
|
||||||
|
|
||||||
logger.info('saving generators.log...')
|
|
||||||
const logStorage = new Storage(LOGS_DIR)
|
|
||||||
logStorage.saveFile(logFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,182 +1,177 @@
|
|||||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
import { PlaylistParser, StreamTester, CliTable } from '../../core'
|
||||||
import { ROOT_DIR, STREAMS_DIR, DATA_DIR } from '../../constants'
|
import type { TestResult } from '../../core/streamTester'
|
||||||
import { PlaylistParser, StreamTester, CliTable, DataProcessor, DataLoader } from '../../core'
|
import { ROOT_DIR, STREAMS_DIR } from '../../constants'
|
||||||
import type { TestResult } from '../../core/streamTester'
|
import { Logger, Collection } from '@freearhey/core'
|
||||||
import { Stream } from '../../models'
|
import { program, OptionValues } from 'commander'
|
||||||
import { program, OptionValues } from 'commander'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { eachLimit } from 'async-es'
|
import { Stream } from '../../models'
|
||||||
import chalk from 'chalk'
|
import { loadData } from '../../api'
|
||||||
import os from 'node:os'
|
import { eachLimit } from 'async'
|
||||||
import dns from 'node:dns'
|
import dns from 'node:dns'
|
||||||
import type { DataLoaderData } from '../../types/dataLoader'
|
import chalk from 'chalk'
|
||||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
import os from 'node:os'
|
||||||
|
import { truncate } from '../../utils'
|
||||||
const LIVE_UPDATE_INTERVAL = 5000
|
|
||||||
const LIVE_UPDATE_MAX_STREAMS = 100
|
const LIVE_UPDATE_INTERVAL = 5000
|
||||||
|
const LIVE_UPDATE_MAX_STREAMS = 100
|
||||||
let errors = 0
|
|
||||||
let warnings = 0
|
let errors = 0
|
||||||
const results: { [key: string]: string } = {}
|
let warnings = 0
|
||||||
let interval: string | number | NodeJS.Timeout | undefined
|
const results: { [key: string]: string } = {}
|
||||||
let streams = new Collection()
|
let interval: string | number | NodeJS.Timeout | undefined
|
||||||
let isLiveUpdateEnabled = true
|
let streams = new Collection<Stream>()
|
||||||
|
let isLiveUpdateEnabled = true
|
||||||
program
|
|
||||||
.argument('[filepath...]', 'Path to file to test')
|
program
|
||||||
.option(
|
.argument('[filepath...]', 'Path to file to test')
|
||||||
'-p, --parallel <number>',
|
.option(
|
||||||
'Batch size of streams to test concurrently',
|
'-p, --parallel <number>',
|
||||||
(value: string) => parseInt(value),
|
'Batch size of streams to test concurrently',
|
||||||
os.cpus().length
|
(value: string) => parseInt(value),
|
||||||
)
|
os.cpus().length
|
||||||
.option('-x, --proxy <url>', 'Use the specified proxy')
|
)
|
||||||
.option(
|
.option('-x, --proxy <url>', 'Use the specified proxy')
|
||||||
'-t, --timeout <number>',
|
.option(
|
||||||
'The number of milliseconds before the request will be aborted',
|
'-t, --timeout <number>',
|
||||||
(value: string) => parseInt(value),
|
'The number of milliseconds before the request will be aborted',
|
||||||
30000
|
(value: string) => parseInt(value),
|
||||||
)
|
30000
|
||||||
.parse(process.argv)
|
)
|
||||||
|
.parse(process.argv)
|
||||||
const options: OptionValues = program.opts()
|
|
||||||
|
const options: OptionValues = program.opts()
|
||||||
const logger = new Logger()
|
|
||||||
const tester = new StreamTester({ options })
|
const logger = new Logger()
|
||||||
|
const tester = new StreamTester({ options })
|
||||||
async function main() {
|
|
||||||
if (await isOffline()) {
|
async function main() {
|
||||||
logger.error(chalk.red('Internet connection is required for the script to work'))
|
if (await isOffline()) {
|
||||||
return
|
logger.error(chalk.red('Internet connection is required for the script to work'))
|
||||||
}
|
return
|
||||||
|
}
|
||||||
logger.info('loading data from api...')
|
|
||||||
const processor = new DataProcessor()
|
logger.info('loading data from api...')
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
await loadData()
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
|
||||||
const data: DataLoaderData = await loader.load()
|
logger.info('loading streams...')
|
||||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
const rootStorage = new Storage(ROOT_DIR)
|
||||||
processor.process(data)
|
const parser = new PlaylistParser({
|
||||||
|
storage: rootStorage
|
||||||
logger.info('loading streams...')
|
})
|
||||||
const rootStorage = new Storage(ROOT_DIR)
|
const files = program.args.length ? program.args : await rootStorage.list(`${STREAMS_DIR}/*.m3u`)
|
||||||
const parser = new PlaylistParser({
|
streams = await parser.parse(files)
|
||||||
storage: rootStorage,
|
|
||||||
channelsKeyById,
|
logger.info(`found ${streams.count()} streams`)
|
||||||
feedsGroupedByChannelId,
|
if (streams.count() > LIVE_UPDATE_MAX_STREAMS) isLiveUpdateEnabled = false
|
||||||
logosGroupedByStreamId
|
|
||||||
})
|
logger.info('starting...')
|
||||||
const files = program.args.length ? program.args : await rootStorage.list(`${STREAMS_DIR}/*.m3u`)
|
if (!isLiveUpdateEnabled) {
|
||||||
streams = await parser.parse(files)
|
drawTable()
|
||||||
|
interval = setInterval(() => {
|
||||||
logger.info(`found ${streams.count()} streams`)
|
drawTable()
|
||||||
if (streams.count() > LIVE_UPDATE_MAX_STREAMS) isLiveUpdateEnabled = false
|
}, LIVE_UPDATE_INTERVAL)
|
||||||
|
}
|
||||||
logger.info('starting...')
|
|
||||||
if (!isLiveUpdateEnabled) {
|
eachLimit(
|
||||||
drawTable()
|
streams.all(),
|
||||||
interval = setInterval(() => {
|
options.parallel,
|
||||||
drawTable()
|
async (stream: Stream) => {
|
||||||
}, LIVE_UPDATE_INTERVAL)
|
await runTest(stream)
|
||||||
}
|
|
||||||
|
if (isLiveUpdateEnabled) {
|
||||||
await eachLimit(
|
drawTable()
|
||||||
streams.all(),
|
}
|
||||||
options.parallel,
|
},
|
||||||
async (stream: Stream) => {
|
onFinish
|
||||||
await runTest(stream)
|
)
|
||||||
|
}
|
||||||
if (isLiveUpdateEnabled) {
|
|
||||||
drawTable()
|
main()
|
||||||
}
|
|
||||||
},
|
async function runTest(stream: Stream) {
|
||||||
onFinish
|
const key = stream.getUniqKey()
|
||||||
)
|
results[key] = chalk.white('LOADING...')
|
||||||
}
|
|
||||||
|
const result: TestResult = await tester.test(stream)
|
||||||
main()
|
|
||||||
|
let status = ''
|
||||||
async function runTest(stream: Stream) {
|
const errorStatusCodes = ['ENOTFOUND', 'HTTP_404_NOT_FOUND']
|
||||||
const key = stream.filepath + stream.getId() + stream.url
|
if (result.status.ok) status = chalk.green('OK')
|
||||||
results[key] = chalk.white('LOADING...')
|
else if (errorStatusCodes.includes(result.status.code)) {
|
||||||
|
status = chalk.red(result.status.code)
|
||||||
const result: TestResult = await tester.test(stream)
|
errors++
|
||||||
|
} else {
|
||||||
let status = ''
|
status = chalk.yellow(result.status.code)
|
||||||
const errorStatusCodes = ['ENOTFOUND', 'HTTP_404_NOT_FOUND']
|
warnings++
|
||||||
if (result.status.ok) status = chalk.green('OK')
|
}
|
||||||
else if (errorStatusCodes.includes(result.status.code)) {
|
|
||||||
status = chalk.red(result.status.code)
|
results[key] = status
|
||||||
errors++
|
}
|
||||||
} else {
|
|
||||||
status = chalk.yellow(result.status.code)
|
function drawTable() {
|
||||||
warnings++
|
process.stdout.write('\u001b[3J\u001b[1J')
|
||||||
}
|
console.clear()
|
||||||
|
|
||||||
results[key] = status
|
const streamsGrouped = streams.groupBy((stream: Stream) => stream.filepath)
|
||||||
}
|
for (const filepath of streamsGrouped.keys()) {
|
||||||
|
const streams: Stream[] = streamsGrouped.get(filepath) || []
|
||||||
function drawTable() {
|
|
||||||
process.stdout.write('\u001b[3J\u001b[1J')
|
const table = new CliTable({
|
||||||
console.clear()
|
columns: [
|
||||||
|
{ name: '', alignment: 'center', minLen: 3, maxLen: 3 },
|
||||||
const streamsGrouped = streams.groupBy((stream: Stream) => stream.filepath)
|
{ name: 'tvg-id', alignment: 'left', color: 'green', minLen: 25, maxLen: 25 },
|
||||||
for (const filepath of streamsGrouped.keys()) {
|
{ name: 'url', alignment: 'left', color: 'green', minLen: 100, maxLen: 100 },
|
||||||
const streams: Stream[] = streamsGrouped.get(filepath)
|
{ name: 'status', alignment: 'left', minLen: 25, maxLen: 25 }
|
||||||
|
]
|
||||||
const table = new CliTable({
|
})
|
||||||
columns: [
|
streams.forEach((stream: Stream, index: number) => {
|
||||||
{ name: '', alignment: 'center', minLen: 3, maxLen: 3 },
|
const key = stream.getUniqKey()
|
||||||
{ name: 'tvg-id', alignment: 'left', color: 'green', minLen: 25, maxLen: 25 },
|
const status = results[key] || chalk.gray('PENDING')
|
||||||
{ name: 'url', alignment: 'left', color: 'green', minLen: 100, maxLen: 100 },
|
const tvgId = stream.getTvgId()
|
||||||
{ name: 'status', alignment: 'left', minLen: 25, maxLen: 25 }
|
|
||||||
]
|
const row = {
|
||||||
})
|
'': index,
|
||||||
streams.forEach((stream: Stream, index: number) => {
|
'tvg-id': truncate(tvgId, 25),
|
||||||
const status = results[stream.filepath + stream.getId() + stream.url] || chalk.gray('PENDING')
|
url: truncate(stream.url, 100),
|
||||||
|
status
|
||||||
const row = {
|
}
|
||||||
'': index,
|
table.append(row)
|
||||||
'tvg-id': stream.getId().length > 25 ? stream.getId().slice(0, 22) + '...' : stream.getId(),
|
})
|
||||||
url: stream.url.length > 100 ? stream.url.slice(0, 97) + '...' : stream.url,
|
|
||||||
status
|
process.stdout.write(`\n${chalk.underline(filepath)}\n`)
|
||||||
}
|
|
||||||
table.append(row)
|
process.stdout.write(table.toString())
|
||||||
})
|
}
|
||||||
|
}
|
||||||
process.stdout.write(`\n${chalk.underline(filepath)}\n`)
|
|
||||||
|
function onFinish(error: Error) {
|
||||||
process.stdout.write(table.toString())
|
clearInterval(interval)
|
||||||
}
|
|
||||||
}
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
function onFinish(error: any) {
|
process.exit(1)
|
||||||
clearInterval(interval)
|
}
|
||||||
|
|
||||||
if (error) {
|
drawTable()
|
||||||
console.error(error)
|
|
||||||
process.exit(1)
|
if (errors > 0 || warnings > 0) {
|
||||||
}
|
console.log(
|
||||||
|
chalk.red(`\n${errors + warnings} problems (${errors} errors, ${warnings} warnings)`)
|
||||||
drawTable()
|
)
|
||||||
|
|
||||||
if (errors > 0 || warnings > 0) {
|
if (errors > 0) {
|
||||||
console.log(
|
process.exit(1)
|
||||||
chalk.red(`\n${errors + warnings} problems (${errors} errors, ${warnings} warnings)`)
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
if (errors > 0) {
|
process.exit(0)
|
||||||
process.exit(1)
|
}
|
||||||
}
|
|
||||||
}
|
async function isOffline() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
process.exit(0)
|
dns.lookup('info.cern.ch', err => {
|
||||||
}
|
if (err) resolve(true)
|
||||||
|
reject(false)
|
||||||
async function isOffline() {
|
})
|
||||||
return new Promise((resolve, reject) => {
|
}).catch(() => {})
|
||||||
dns.lookup('info.cern.ch', err => {
|
}
|
||||||
if (err) resolve(true)
|
|
||||||
reject(false)
|
|
||||||
})
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,194 +1,174 @@
|
|||||||
import { DataLoader, DataProcessor, IssueLoader, PlaylistParser } from '../../core'
|
import { IssueLoader, PlaylistParser } from '../../core'
|
||||||
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
import { Playlist, Issue, Stream } from '../../models'
|
||||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
import { loadData, data as apiData } from '../../api'
|
||||||
import { Stream, Playlist, Channel, Issue } from '../../models'
|
import { Logger, Collection } from '@freearhey/core'
|
||||||
import type { DataLoaderData } from '../../types/dataLoader'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
import { STREAMS_DIR } from '../../constants'
|
||||||
import { isURI } from '../../utils'
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
import { isURI } from '../../utils'
|
||||||
const processedIssues = new Collection()
|
|
||||||
|
const processedIssues = new Collection()
|
||||||
async function main() {
|
|
||||||
const logger = new Logger({ level: -999 })
|
async function main() {
|
||||||
const issueLoader = new IssueLoader()
|
const logger = new Logger({ level: -999 })
|
||||||
|
const issueLoader = new IssueLoader()
|
||||||
logger.info('loading issues...')
|
|
||||||
const issues = await issueLoader.load()
|
logger.info('loading issues...')
|
||||||
|
const issues = await issueLoader.load()
|
||||||
logger.info('loading data from api...')
|
|
||||||
const processor = new DataProcessor()
|
logger.info('loading data from api...')
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
await loadData()
|
||||||
const dataLoader = new DataLoader({ storage: dataStorage })
|
|
||||||
const data: DataLoaderData = await dataLoader.load()
|
logger.info('loading streams...')
|
||||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
processor.process(data)
|
const parser = new PlaylistParser({
|
||||||
|
storage: streamsStorage
|
||||||
logger.info('loading streams...')
|
})
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
const parser = new PlaylistParser({
|
const streams = await parser.parse(files)
|
||||||
storage: streamsStorage,
|
|
||||||
feedsGroupedByChannelId,
|
logger.info('removing streams...')
|
||||||
logosGroupedByStreamId,
|
await removeStreams({ streams, issues })
|
||||||
channelsKeyById
|
|
||||||
})
|
logger.info('edit stream description...')
|
||||||
const files = await streamsStorage.list('**/*.m3u')
|
await editStreams({
|
||||||
const streams = await parser.parse(files)
|
streams,
|
||||||
|
issues
|
||||||
logger.info('removing streams...')
|
})
|
||||||
await removeStreams({ streams, issues })
|
|
||||||
|
logger.info('add new streams...')
|
||||||
logger.info('edit stream description...')
|
await addStreams({
|
||||||
await editStreams({
|
streams,
|
||||||
streams,
|
issues
|
||||||
issues,
|
})
|
||||||
channelsKeyById,
|
|
||||||
feedsGroupedByChannelId
|
logger.info('saving...')
|
||||||
})
|
const groupedStreams = streams.groupBy((stream: Stream) => stream.getFilepath())
|
||||||
|
for (const filepath of groupedStreams.keys()) {
|
||||||
logger.info('add new streams...')
|
let streams = new Collection(groupedStreams.get(filepath))
|
||||||
await addStreams({
|
streams = streams.filter((stream: Stream) => stream.removed === false)
|
||||||
streams,
|
|
||||||
issues,
|
const playlist = new Playlist(streams, { public: false })
|
||||||
channelsKeyById,
|
await streamsStorage.save(filepath, playlist.toString())
|
||||||
feedsGroupedByChannelId
|
}
|
||||||
})
|
|
||||||
|
const output = processedIssues.map(issue_number => `closes #${issue_number}`).join(', ')
|
||||||
logger.info('saving...')
|
console.log(`OUTPUT=${output}`)
|
||||||
const groupedStreams = streams.groupBy((stream: Stream) => stream.getFilepath())
|
}
|
||||||
for (const filepath of groupedStreams.keys()) {
|
|
||||||
let streams = groupedStreams.get(filepath) || []
|
main()
|
||||||
streams = streams.filter((stream: Stream) => stream.removed === false)
|
|
||||||
|
async function removeStreams({
|
||||||
const playlist = new Playlist(streams, { public: false })
|
streams,
|
||||||
await streamsStorage.save(filepath, playlist.toString())
|
issues
|
||||||
}
|
}: {
|
||||||
|
streams: Collection<Stream>
|
||||||
const output = processedIssues.map(issue_number => `closes #${issue_number}`).join(', ')
|
issues: Collection<Issue>
|
||||||
console.log(`OUTPUT=${output}`)
|
}) {
|
||||||
}
|
const requests = issues.filter(
|
||||||
|
issue => issue.labels.includes('streams:remove') && issue.labels.includes('approved')
|
||||||
main()
|
)
|
||||||
|
|
||||||
async function removeStreams({ streams, issues }: { streams: Collection; issues: Collection }) {
|
requests.forEach((issue: Issue) => {
|
||||||
const requests = issues.filter(
|
const data = issue.data
|
||||||
issue => issue.labels.includes('streams:remove') && issue.labels.includes('approved')
|
if (data.missing('streamUrl')) return
|
||||||
)
|
|
||||||
requests.forEach((issue: Issue) => {
|
const streamUrls = data.getString('streamUrl') || ''
|
||||||
const data = issue.data
|
|
||||||
if (data.missing('streamUrl')) return
|
let changed = false
|
||||||
|
streamUrls
|
||||||
const streamUrls = data.getString('streamUrl') || ''
|
.split(/\r?\n/)
|
||||||
|
.filter(Boolean)
|
||||||
let changed = false
|
.forEach(link => {
|
||||||
streamUrls
|
const found: Stream = streams.first((_stream: Stream) => _stream.url === link.trim())
|
||||||
.split(/\r?\n/)
|
if (found) {
|
||||||
.filter(Boolean)
|
found.removed = true
|
||||||
.forEach(link => {
|
changed = true
|
||||||
const found: Stream = streams.first((_stream: Stream) => _stream.url === link.trim())
|
}
|
||||||
if (found) {
|
})
|
||||||
found.removed = true
|
|
||||||
changed = true
|
if (changed) processedIssues.add(issue.number)
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
if (changed) processedIssues.add(issue.number)
|
async function editStreams({
|
||||||
})
|
streams,
|
||||||
}
|
issues
|
||||||
|
}: {
|
||||||
async function editStreams({
|
streams: Collection<Stream>
|
||||||
streams,
|
issues: Collection<Issue>
|
||||||
issues,
|
}) {
|
||||||
channelsKeyById,
|
const requests = issues.filter(
|
||||||
feedsGroupedByChannelId
|
issue => issue.labels.includes('streams:edit') && issue.labels.includes('approved')
|
||||||
}: {
|
)
|
||||||
streams: Collection
|
requests.forEach((issue: Issue) => {
|
||||||
issues: Collection
|
const data = issue.data
|
||||||
channelsKeyById: Dictionary
|
|
||||||
feedsGroupedByChannelId: Dictionary
|
if (data.missing('streamUrl')) return
|
||||||
}) {
|
|
||||||
const requests = issues.filter(
|
const stream: Stream = streams.first(
|
||||||
issue => issue.labels.includes('streams:edit') && issue.labels.includes('approved')
|
(_stream: Stream) => _stream.url === data.getString('streamUrl')
|
||||||
)
|
)
|
||||||
requests.forEach((issue: Issue) => {
|
if (!stream) return
|
||||||
const data = issue.data
|
|
||||||
|
const streamId = data.getString('streamId') || ''
|
||||||
if (data.missing('streamUrl')) return
|
const [channelId, feedId] = streamId.split('@')
|
||||||
|
|
||||||
const stream: Stream = streams.first(
|
if (channelId) {
|
||||||
(_stream: Stream) => _stream.url === data.getString('streamUrl')
|
stream.channel = channelId
|
||||||
)
|
stream.feed = feedId
|
||||||
if (!stream) return
|
stream.updateTvgId().updateTitle().updateFilepath()
|
||||||
|
}
|
||||||
const streamId = data.getString('streamId') || ''
|
|
||||||
const [channelId, feedId] = streamId.split('@')
|
stream.updateWithIssue(data)
|
||||||
|
|
||||||
if (channelId) {
|
processedIssues.add(issue.number)
|
||||||
stream
|
})
|
||||||
.setChannelId(channelId)
|
}
|
||||||
.setFeedId(feedId)
|
|
||||||
.withChannel(channelsKeyById)
|
async function addStreams({
|
||||||
.withFeed(feedsGroupedByChannelId)
|
streams,
|
||||||
.updateId()
|
issues
|
||||||
.updateTitle()
|
}: {
|
||||||
.updateFilepath()
|
streams: Collection<Stream>
|
||||||
}
|
issues: Collection<Issue>
|
||||||
|
}) {
|
||||||
stream.update(data)
|
const requests = issues.filter(
|
||||||
|
issue => issue.labels.includes('streams:add') && issue.labels.includes('approved')
|
||||||
processedIssues.add(issue.number)
|
)
|
||||||
})
|
requests.forEach((issue: Issue) => {
|
||||||
}
|
const data = issue.data
|
||||||
|
if (data.missing('streamId') || data.missing('streamUrl')) return
|
||||||
async function addStreams({
|
if (streams.includes((_stream: Stream) => _stream.url === data.getString('streamUrl'))) return
|
||||||
streams,
|
const streamUrl = data.getString('streamUrl') || ''
|
||||||
issues,
|
if (!isURI(streamUrl)) return
|
||||||
channelsKeyById,
|
|
||||||
feedsGroupedByChannelId
|
const streamId = data.getString('streamId') || ''
|
||||||
}: {
|
const [channelId, feedId] = streamId.split('@')
|
||||||
streams: Collection
|
|
||||||
issues: Collection
|
const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId)
|
||||||
channelsKeyById: Dictionary
|
if (!channel) return
|
||||||
feedsGroupedByChannelId: Dictionary
|
|
||||||
}) {
|
const label = data.getString('label') || ''
|
||||||
const requests = issues.filter(
|
const quality = data.getString('quality') || null
|
||||||
issue => issue.labels.includes('streams:add') && issue.labels.includes('approved')
|
const httpUserAgent = data.getString('httpUserAgent') || null
|
||||||
)
|
const httpReferrer = data.getString('httpReferrer') || null
|
||||||
requests.forEach((issue: Issue) => {
|
const directives = data.getArray('directives') || []
|
||||||
const data = issue.data
|
|
||||||
if (data.missing('streamId') || data.missing('streamUrl')) return
|
const stream = new Stream({
|
||||||
if (streams.includes((_stream: Stream) => _stream.url === data.getString('streamUrl'))) return
|
channel: channelId,
|
||||||
const streamUrl = data.getString('streamUrl') || ''
|
feed: feedId,
|
||||||
if (!isURI(streamUrl)) return
|
title: channel.name,
|
||||||
|
url: streamUrl,
|
||||||
const streamId = data.getString('streamId') || ''
|
user_agent: httpUserAgent,
|
||||||
const [channelId, feedId] = streamId.split('@')
|
referrer: httpReferrer,
|
||||||
|
quality
|
||||||
const channel: Channel = channelsKeyById.get(channelId)
|
})
|
||||||
if (!channel) return
|
|
||||||
|
stream.label = label
|
||||||
const label = data.getString('label') || null
|
stream.setDirectives(directives).updateTitle().updateFilepath()
|
||||||
const quality = data.getString('quality') || null
|
|
||||||
const httpUserAgent = data.getString('httpUserAgent') || null
|
streams.add(stream)
|
||||||
const httpReferrer = data.getString('httpReferrer') || null
|
processedIssues.add(issue.number)
|
||||||
const directives = data.getArray('directives') || []
|
})
|
||||||
|
}
|
||||||
const stream = new Stream({
|
|
||||||
channelId,
|
|
||||||
feedId,
|
|
||||||
title: channel.name,
|
|
||||||
url: streamUrl,
|
|
||||||
userAgent: httpUserAgent,
|
|
||||||
referrer: httpReferrer,
|
|
||||||
directives,
|
|
||||||
quality,
|
|
||||||
label
|
|
||||||
})
|
|
||||||
.withChannel(channelsKeyById)
|
|
||||||
.withFeed(feedsGroupedByChannelId)
|
|
||||||
.updateTitle()
|
|
||||||
.updateFilepath()
|
|
||||||
|
|
||||||
streams.add(stream)
|
|
||||||
processedIssues.add(issue.number)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,129 +1,120 @@
|
|||||||
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
import { Logger, Collection, Dictionary } from '@freearhey/core'
|
||||||
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { DataProcessorData } from '../../types/dataProcessor'
|
import { PlaylistParser } from '../../core'
|
||||||
import { DATA_DIR, ROOT_DIR } from '../../constants'
|
import { data, loadData } from '../../api'
|
||||||
import { DataLoaderData } from '../../types/dataLoader'
|
import { ROOT_DIR } from '../../constants'
|
||||||
import { BlocklistRecord, Stream } from '../../models'
|
import { Stream } from '../../models'
|
||||||
import { program } from 'commander'
|
import * as sdk from '@iptv-org/sdk'
|
||||||
import chalk from 'chalk'
|
import { program } from 'commander'
|
||||||
|
import chalk from 'chalk'
|
||||||
program.argument('[filepath...]', 'Path to file to validate').parse(process.argv)
|
|
||||||
|
program.argument('[filepath...]', 'Path to file to validate').parse(process.argv)
|
||||||
type LogItem = {
|
|
||||||
type: string
|
type LogItem = {
|
||||||
line: number
|
type: string
|
||||||
message: string
|
line: number
|
||||||
}
|
message: string
|
||||||
|
}
|
||||||
async function main() {
|
|
||||||
const logger = new Logger()
|
async function main() {
|
||||||
|
const logger = new Logger()
|
||||||
logger.info('loading data from api...')
|
|
||||||
const processor = new DataProcessor()
|
logger.info('loading data from api...')
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
await loadData()
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
|
||||||
const data: DataLoaderData = await loader.load()
|
logger.info('loading streams...')
|
||||||
const {
|
const rootStorage = new Storage(ROOT_DIR)
|
||||||
channelsKeyById,
|
const parser = new PlaylistParser({
|
||||||
feedsGroupedByChannelId,
|
storage: rootStorage
|
||||||
logosGroupedByStreamId,
|
})
|
||||||
blocklistRecordsGroupedByChannelId
|
const files = program.args.length ? program.args : await rootStorage.list('streams/**/*.m3u')
|
||||||
}: DataProcessorData = processor.process(data)
|
const streams = await parser.parse(files)
|
||||||
|
logger.info(`found ${streams.count()} streams`)
|
||||||
logger.info('loading streams...')
|
|
||||||
const rootStorage = new Storage(ROOT_DIR)
|
let errors = new Collection()
|
||||||
const parser = new PlaylistParser({
|
let warnings = new Collection()
|
||||||
storage: rootStorage,
|
const streamsGroupedByFilepath = streams.groupBy((stream: Stream) => stream.getFilepath())
|
||||||
channelsKeyById,
|
for (const filepath of streamsGroupedByFilepath.keys()) {
|
||||||
feedsGroupedByChannelId,
|
const streams = streamsGroupedByFilepath.get(filepath)
|
||||||
logosGroupedByStreamId
|
if (!streams) continue
|
||||||
})
|
|
||||||
const files = program.args.length ? program.args : await rootStorage.list('streams/**/*.m3u')
|
const log = new Collection<LogItem>()
|
||||||
const streams = await parser.parse(files)
|
const buffer = new Dictionary<boolean>()
|
||||||
logger.info(`found ${streams.count()} streams`)
|
streams.forEach((stream: Stream) => {
|
||||||
|
if (stream.channel) {
|
||||||
let errors = new Collection()
|
const channel = data.channelsKeyById.get(stream.channel)
|
||||||
let warnings = new Collection()
|
if (!channel) {
|
||||||
const streamsGroupedByFilepath = streams.groupBy((stream: Stream) => stream.getFilepath())
|
log.add({
|
||||||
for (const filepath of streamsGroupedByFilepath.keys()) {
|
type: 'warning',
|
||||||
const streams = streamsGroupedByFilepath.get(filepath)
|
line: stream.getLine(),
|
||||||
if (!streams) continue
|
message: `"${stream.tvgId}" is not in the database`
|
||||||
|
})
|
||||||
const log = new Collection()
|
}
|
||||||
const buffer = new Dictionary()
|
}
|
||||||
streams.forEach((stream: Stream) => {
|
|
||||||
if (stream.channelId) {
|
const duplicate = stream.url && buffer.has(stream.url)
|
||||||
const channel = channelsKeyById.get(stream.channelId)
|
if (duplicate) {
|
||||||
if (!channel) {
|
log.add({
|
||||||
log.add({
|
type: 'warning',
|
||||||
type: 'warning',
|
line: stream.getLine(),
|
||||||
line: stream.getLine(),
|
message: `"${stream.url}" is already on the playlist`
|
||||||
message: `"${stream.id}" is not in the database`
|
})
|
||||||
})
|
} else {
|
||||||
}
|
buffer.set(stream.url, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicate = stream.url && buffer.has(stream.url)
|
if (stream.channel) {
|
||||||
if (duplicate) {
|
const blocklistRecords = new Collection(
|
||||||
log.add({
|
data.blocklistRecordsGroupedByChannel.get(stream.channel)
|
||||||
type: 'warning',
|
)
|
||||||
line: stream.getLine(),
|
|
||||||
message: `"${stream.url}" is already on the playlist`
|
blocklistRecords.forEach((blocklistRecord: sdk.Models.BlocklistRecord) => {
|
||||||
})
|
if (blocklistRecord.reason === 'dmca') {
|
||||||
} else {
|
log.add({
|
||||||
buffer.set(stream.url, true)
|
type: 'error',
|
||||||
}
|
line: stream.getLine(),
|
||||||
|
message: `"${blocklistRecord.channel}" is on the blocklist due to claims of copyright holders (${blocklistRecord.ref})`
|
||||||
const blocklistRecords = stream.channel
|
})
|
||||||
? new Collection(blocklistRecordsGroupedByChannelId.get(stream.channel.id))
|
} else if (blocklistRecord.reason === 'nsfw') {
|
||||||
: new Collection()
|
log.add({
|
||||||
|
type: 'error',
|
||||||
blocklistRecords.forEach((blocklistRecord: BlocklistRecord) => {
|
line: stream.getLine(),
|
||||||
if (blocklistRecord.reason === 'dmca') {
|
message: `"${blocklistRecord.channel}" is on the blocklist due to NSFW content (${blocklistRecord.ref})`
|
||||||
log.add({
|
})
|
||||||
type: 'error',
|
}
|
||||||
line: stream.getLine(),
|
})
|
||||||
message: `"${blocklistRecord.channelId}" is on the blocklist due to claims of copyright holders (${blocklistRecord.ref})`
|
}
|
||||||
})
|
})
|
||||||
} else if (blocklistRecord.reason === 'nsfw') {
|
|
||||||
log.add({
|
if (log.isNotEmpty()) {
|
||||||
type: 'error',
|
console.log(`\n${chalk.underline(filepath)}`)
|
||||||
line: stream.getLine(),
|
|
||||||
message: `"${blocklistRecord.channelId}" is on the blocklist due to NSFW content (${blocklistRecord.ref})`
|
log.forEach((logItem: LogItem) => {
|
||||||
})
|
const position = logItem.line.toString().padEnd(6, ' ')
|
||||||
}
|
const type = logItem.type.padEnd(9, ' ')
|
||||||
})
|
const status = logItem.type === 'error' ? chalk.red(type) : chalk.yellow(type)
|
||||||
})
|
|
||||||
|
console.log(` ${chalk.gray(position)}${status}${logItem.message}`)
|
||||||
if (log.notEmpty()) {
|
})
|
||||||
console.log(`\n${chalk.underline(filepath)}`)
|
|
||||||
|
errors = errors.concat(log.filter((logItem: LogItem) => logItem.type === 'error'))
|
||||||
log.forEach((logItem: LogItem) => {
|
warnings = warnings.concat(log.filter((logItem: LogItem) => logItem.type === 'warning'))
|
||||||
const position = logItem.line.toString().padEnd(6, ' ')
|
}
|
||||||
const type = logItem.type.padEnd(9, ' ')
|
}
|
||||||
const status = logItem.type === 'error' ? chalk.red(type) : chalk.yellow(type)
|
|
||||||
|
if (errors.count() || warnings.count()) {
|
||||||
console.log(` ${chalk.gray(position)}${status}${logItem.message}`)
|
console.log(
|
||||||
})
|
chalk.red(
|
||||||
|
`\n${
|
||||||
errors = errors.concat(log.filter((logItem: LogItem) => logItem.type === 'error'))
|
errors.count() + warnings.count()
|
||||||
warnings = warnings.concat(log.filter((logItem: LogItem) => logItem.type === 'warning'))
|
} problems (${errors.count()} errors, ${warnings.count()} warnings)`
|
||||||
}
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
if (errors.count() || warnings.count()) {
|
if (errors.count()) {
|
||||||
console.log(
|
process.exit(1)
|
||||||
chalk.red(
|
}
|
||||||
`\n${
|
}
|
||||||
errors.count() + warnings.count()
|
}
|
||||||
} problems (${errors.count()} errors, ${warnings.count()} warnings)`
|
|
||||||
)
|
main()
|
||||||
)
|
|
||||||
|
|
||||||
if (errors.count()) {
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,48 +1,30 @@
|
|||||||
import { CategoriesTable, CountriesTable, LanguagesTable, RegionsTable } from '../../tables'
|
import { CategoriesTable, CountriesTable, LanguagesTable, RegionsTable } from '../../tables'
|
||||||
import { DataLoader, DataProcessor, Markdown } from '../../core'
|
import { README_DIR, ROOT_DIR } from '../../constants'
|
||||||
import { DataProcessorData } from '../../types/dataProcessor'
|
import { Logger } from '@freearhey/core'
|
||||||
import { DataLoaderData } from '../../types/dataLoader'
|
import { Markdown } from '../../core'
|
||||||
import { README_DIR, DATA_DIR, ROOT_DIR } from '../../constants'
|
import { loadData } from '../../api'
|
||||||
import { Logger, Storage } from '@freearhey/core'
|
|
||||||
|
async function main() {
|
||||||
async function main() {
|
const logger = new Logger()
|
||||||
const logger = new Logger()
|
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
logger.info('loading data from api...')
|
||||||
const processor = new DataProcessor()
|
await loadData()
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
|
||||||
const data: DataLoaderData = await loader.load()
|
logger.info('creating category table...')
|
||||||
const {
|
await new CategoriesTable().create()
|
||||||
subdivisionsKeyByCode,
|
logger.info('creating language table...')
|
||||||
languagesKeyByCode,
|
await new LanguagesTable().create()
|
||||||
countriesKeyByCode,
|
logger.info('creating countires table...')
|
||||||
categoriesKeyById,
|
await new CountriesTable().create()
|
||||||
subdivisions,
|
logger.info('creating region table...')
|
||||||
countries,
|
await new RegionsTable().create()
|
||||||
regions,
|
|
||||||
cities
|
logger.info('updating playlists.md...')
|
||||||
}: DataProcessorData = processor.process(data)
|
const playlists = new Markdown({
|
||||||
|
build: `${ROOT_DIR}/PLAYLISTS.md`,
|
||||||
logger.info('creating category table...')
|
template: `${README_DIR}/template.md`
|
||||||
await new CategoriesTable({ categoriesKeyById }).make()
|
})
|
||||||
logger.info('creating language table...')
|
playlists.compile()
|
||||||
await new LanguagesTable({ languagesKeyByCode }).make()
|
}
|
||||||
logger.info('creating countires table...')
|
|
||||||
await new CountriesTable({
|
main()
|
||||||
countriesKeyByCode,
|
|
||||||
subdivisionsKeyByCode,
|
|
||||||
subdivisions,
|
|
||||||
countries,
|
|
||||||
cities
|
|
||||||
}).make()
|
|
||||||
logger.info('creating region table...')
|
|
||||||
await new RegionsTable({ regions }).make()
|
|
||||||
|
|
||||||
logger.info('updating playlists.md...')
|
|
||||||
const playlists = new Markdown({
|
|
||||||
build: `${ROOT_DIR}/PLAYLISTS.md`,
|
|
||||||
template: `${README_DIR}/template.md`
|
|
||||||
})
|
|
||||||
playlists.compile()
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,178 +1,159 @@
|
|||||||
import { DataLoader, DataProcessor, IssueLoader, PlaylistParser } from '../../core'
|
import { Logger, Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
import { IssueLoader, PlaylistParser } from '../../core'
|
||||||
import { DataProcessorData } from '../../types/dataProcessor'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
import { isURI, truncate } from '../../utils'
|
||||||
import { DataLoaderData } from '../../types/dataLoader'
|
import { STREAMS_DIR } from '../../constants'
|
||||||
import { Issue, Stream } from '../../models'
|
import { Issue, Stream } from '../../models'
|
||||||
import { isURI } from '../../utils'
|
import { data, loadData } from '../../api'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const issueLoader = new IssueLoader()
|
const issueLoader = new IssueLoader()
|
||||||
let report = new Collection()
|
let report = new Collection()
|
||||||
|
|
||||||
logger.info('loading issues...')
|
logger.info('loading issues...')
|
||||||
const issues = await issueLoader.load()
|
const issues = await issueLoader.load()
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
const processor = new DataProcessor()
|
await loadData()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
|
||||||
const dataLoader = new DataLoader({ storage: dataStorage })
|
logger.info('loading streams...')
|
||||||
const data: DataLoaderData = await dataLoader.load()
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const {
|
const parser = new PlaylistParser({
|
||||||
channelsKeyById,
|
storage: streamsStorage
|
||||||
feedsGroupedByChannelId,
|
})
|
||||||
logosGroupedByStreamId,
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
blocklistRecordsGroupedByChannelId
|
const streams = await parser.parse(files)
|
||||||
}: DataProcessorData = processor.process(data)
|
const streamsGroupedByUrl = streams.groupBy((stream: Stream) => stream.url)
|
||||||
|
const streamsGroupedByChannel = streams.groupBy((stream: Stream) => stream.channel)
|
||||||
logger.info('loading streams...')
|
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
|
||||||
const parser = new PlaylistParser({
|
logger.info('checking streams:remove requests...')
|
||||||
storage: streamsStorage,
|
const removeRequests = issues.filter(issue =>
|
||||||
channelsKeyById,
|
issue.labels.find((label: string) => label === 'streams:remove')
|
||||||
feedsGroupedByChannelId,
|
)
|
||||||
logosGroupedByStreamId
|
removeRequests.forEach((issue: Issue) => {
|
||||||
})
|
const streamUrls = issue.data.getArray('streamUrl') || []
|
||||||
const files = await streamsStorage.list('**/*.m3u')
|
|
||||||
const streams = await parser.parse(files)
|
if (!streamUrls.length) {
|
||||||
const streamsGroupedByUrl = streams.groupBy((stream: Stream) => stream.url)
|
const result = {
|
||||||
const streamsGroupedByChannelId = streams.groupBy((stream: Stream) => stream.channelId)
|
issueNumber: issue.number,
|
||||||
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
type: 'streams:remove',
|
||||||
|
streamId: undefined,
|
||||||
logger.info('checking streams:remove requests...')
|
streamUrl: undefined,
|
||||||
const removeRequests = issues.filter(issue =>
|
status: 'missing_link'
|
||||||
issue.labels.find((label: string) => label === 'streams:remove')
|
}
|
||||||
)
|
|
||||||
removeRequests.forEach((issue: Issue) => {
|
report.add(result)
|
||||||
const streamUrls = issue.data.getArray('streamUrl') || []
|
} else {
|
||||||
|
for (const streamUrl of streamUrls) {
|
||||||
if (!streamUrls.length) {
|
const result = {
|
||||||
const result = {
|
issueNumber: issue.number,
|
||||||
issueNumber: issue.number,
|
type: 'streams:remove',
|
||||||
type: 'streams:remove',
|
streamId: undefined,
|
||||||
streamId: undefined,
|
streamUrl: truncate(streamUrl),
|
||||||
streamUrl: undefined,
|
status: 'pending'
|
||||||
status: 'missing_link'
|
}
|
||||||
}
|
|
||||||
|
if (streamsGroupedByUrl.missing(streamUrl)) {
|
||||||
report.add(result)
|
result.status = 'wrong_link'
|
||||||
} else {
|
}
|
||||||
for (const streamUrl of streamUrls) {
|
|
||||||
const result = {
|
report.add(result)
|
||||||
issueNumber: issue.number,
|
}
|
||||||
type: 'streams:remove',
|
}
|
||||||
streamId: undefined,
|
})
|
||||||
streamUrl: truncate(streamUrl),
|
|
||||||
status: 'pending'
|
logger.info('checking streams:add requests...')
|
||||||
}
|
const addRequests = issues.filter(issue => issue.labels.includes('streams:add'))
|
||||||
|
const addRequestsBuffer = new Dictionary()
|
||||||
if (streamsGroupedByUrl.missing(streamUrl)) {
|
addRequests.forEach((issue: Issue) => {
|
||||||
result.status = 'wrong_link'
|
const streamId = issue.data.getString('streamId') || ''
|
||||||
}
|
const streamUrl = issue.data.getString('streamUrl') || ''
|
||||||
|
const [channelId] = streamId.split('@')
|
||||||
report.add(result)
|
|
||||||
}
|
const result = {
|
||||||
}
|
issueNumber: issue.number,
|
||||||
})
|
type: 'streams:add',
|
||||||
|
streamId: streamId || undefined,
|
||||||
logger.info('checking streams:add requests...')
|
streamUrl: truncate(streamUrl),
|
||||||
const addRequests = issues.filter(issue => issue.labels.includes('streams:add'))
|
status: 'pending'
|
||||||
const addRequestsBuffer = new Dictionary()
|
}
|
||||||
addRequests.forEach((issue: Issue) => {
|
|
||||||
const streamId = issue.data.getString('streamId') || ''
|
if (!channelId) result.status = 'missing_id'
|
||||||
const streamUrl = issue.data.getString('streamUrl') || ''
|
else if (!streamUrl) result.status = 'missing_link'
|
||||||
const [channelId] = streamId.split('@')
|
else if (!isURI(streamUrl)) result.status = 'invalid_link'
|
||||||
|
else if (data.blocklistRecordsGroupedByChannel.has(channelId)) result.status = 'blocked'
|
||||||
const result = {
|
else if (data.channelsKeyById.missing(channelId)) result.status = 'wrong_id'
|
||||||
issueNumber: issue.number,
|
else if (streamsGroupedByUrl.has(streamUrl)) result.status = 'on_playlist'
|
||||||
type: 'streams:add',
|
else if (addRequestsBuffer.has(streamUrl)) result.status = 'duplicate'
|
||||||
streamId: streamId || undefined,
|
else result.status = 'pending'
|
||||||
streamUrl: truncate(streamUrl),
|
|
||||||
status: 'pending'
|
addRequestsBuffer.set(streamUrl, true)
|
||||||
}
|
|
||||||
|
report.add(result)
|
||||||
if (!channelId) result.status = 'missing_id'
|
})
|
||||||
else if (!streamUrl) result.status = 'missing_link'
|
|
||||||
else if (!isURI(streamUrl)) result.status = 'invalid_link'
|
logger.info('checking streams:edit requests...')
|
||||||
else if (blocklistRecordsGroupedByChannelId.has(channelId)) result.status = 'blocked'
|
const editRequests = issues.filter(issue =>
|
||||||
else if (channelsKeyById.missing(channelId)) result.status = 'wrong_id'
|
issue.labels.find((label: string) => label === 'streams:edit')
|
||||||
else if (streamsGroupedByUrl.has(streamUrl)) result.status = 'on_playlist'
|
)
|
||||||
else if (addRequestsBuffer.has(streamUrl)) result.status = 'duplicate'
|
editRequests.forEach((issue: Issue) => {
|
||||||
else result.status = 'pending'
|
const streamId = issue.data.getString('streamId') || ''
|
||||||
|
const streamUrl = issue.data.getString('streamUrl') || ''
|
||||||
addRequestsBuffer.set(streamUrl, true)
|
const [channelId] = streamId.split('@')
|
||||||
|
|
||||||
report.add(result)
|
const result = {
|
||||||
})
|
issueNumber: issue.number,
|
||||||
|
type: 'streams:edit',
|
||||||
logger.info('checking streams:edit requests...')
|
streamId: streamId || undefined,
|
||||||
const editRequests = issues.filter(issue =>
|
streamUrl: truncate(streamUrl),
|
||||||
issue.labels.find((label: string) => label === 'streams:edit')
|
status: 'pending'
|
||||||
)
|
}
|
||||||
editRequests.forEach((issue: Issue) => {
|
|
||||||
const streamId = issue.data.getString('streamId') || ''
|
if (!streamUrl) result.status = 'missing_link'
|
||||||
const streamUrl = issue.data.getString('streamUrl') || ''
|
else if (streamsGroupedByUrl.missing(streamUrl)) result.status = 'invalid_link'
|
||||||
const [channelId] = streamId.split('@')
|
else if (channelId && data.channelsKeyById.missing(channelId)) result.status = 'invalid_id'
|
||||||
|
|
||||||
const result = {
|
report.add(result)
|
||||||
issueNumber: issue.number,
|
})
|
||||||
type: 'streams:edit',
|
|
||||||
streamId: streamId || undefined,
|
logger.info('checking channel search requests...')
|
||||||
streamUrl: truncate(streamUrl),
|
const channelSearchRequests = issues.filter(issue =>
|
||||||
status: 'pending'
|
issue.labels.find((label: string) => label === 'channel search')
|
||||||
}
|
)
|
||||||
|
const channelSearchRequestsBuffer = new Dictionary()
|
||||||
if (!streamUrl) result.status = 'missing_link'
|
channelSearchRequests.forEach((issue: Issue) => {
|
||||||
else if (streamsGroupedByUrl.missing(streamUrl)) result.status = 'invalid_link'
|
const streamId = issue.data.getString('channelId') || ''
|
||||||
else if (channelId && channelsKeyById.missing(channelId)) result.status = 'invalid_id'
|
const [channelId, feedId] = streamId.split('@')
|
||||||
|
|
||||||
report.add(result)
|
const result = {
|
||||||
})
|
issueNumber: issue.number,
|
||||||
|
type: 'channel search',
|
||||||
logger.info('checking channel search requests...')
|
streamId: streamId || undefined,
|
||||||
const channelSearchRequests = issues.filter(issue =>
|
streamUrl: undefined,
|
||||||
issue.labels.find((label: string) => label === 'channel search')
|
status: 'pending'
|
||||||
)
|
}
|
||||||
const channelSearchRequestsBuffer = new Dictionary()
|
|
||||||
channelSearchRequests.forEach((issue: Issue) => {
|
if (!channelId) result.status = 'missing_id'
|
||||||
const streamId = issue.data.getString('channelId') || ''
|
else if (data.channelsKeyById.missing(channelId)) result.status = 'invalid_id'
|
||||||
const [channelId, feedId] = streamId.split('@')
|
else if (channelSearchRequestsBuffer.has(streamId)) result.status = 'duplicate'
|
||||||
|
else if (data.blocklistRecordsGroupedByChannel.has(channelId)) result.status = 'blocked'
|
||||||
const result = {
|
else if (streamsGroupedById.has(streamId)) result.status = 'fulfilled'
|
||||||
issueNumber: issue.number,
|
else if (!feedId && streamsGroupedByChannel.has(channelId)) result.status = 'fulfilled'
|
||||||
type: 'channel search',
|
else {
|
||||||
streamId: streamId || undefined,
|
const channelData = data.channelsKeyById.get(channelId)
|
||||||
streamUrl: undefined,
|
if (channelData && channelData.isClosed()) result.status = 'closed'
|
||||||
status: 'pending'
|
}
|
||||||
}
|
|
||||||
|
channelSearchRequestsBuffer.set(streamId, true)
|
||||||
if (!channelId) result.status = 'missing_id'
|
|
||||||
else if (channelsKeyById.missing(channelId)) result.status = 'invalid_id'
|
report.add(result)
|
||||||
else if (channelSearchRequestsBuffer.has(streamId)) result.status = 'duplicate'
|
})
|
||||||
else if (blocklistRecordsGroupedByChannelId.has(channelId)) result.status = 'blocked'
|
|
||||||
else if (streamsGroupedById.has(streamId)) result.status = 'fulfilled'
|
report = report.sortBy(item => item.issueNumber).filter(item => item.status !== 'pending')
|
||||||
else if (!feedId && streamsGroupedByChannelId.has(channelId)) result.status = 'fulfilled'
|
|
||||||
else {
|
console.table(report.all())
|
||||||
const channelData = channelsKeyById.get(channelId)
|
}
|
||||||
if (channelData && channelData.isClosed) result.status = 'closed'
|
|
||||||
}
|
main()
|
||||||
|
|
||||||
channelSearchRequestsBuffer.set(streamId, true)
|
|
||||||
|
|
||||||
report.add(result)
|
|
||||||
})
|
|
||||||
|
|
||||||
report = report.orderBy(item => item.issueNumber).filter(item => item.status !== 'pending')
|
|
||||||
|
|
||||||
console.table(report.all())
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
||||||
function truncate(string: string, limit: number = 100) {
|
|
||||||
if (!string) return string
|
|
||||||
if (string.length < limit) return string
|
|
||||||
|
|
||||||
return string.slice(0, limit) + '...'
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
|
|
||||||
|
|
||||||
export class ApiClient {
|
|
||||||
instance: AxiosInstance
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.instance = axios.create({
|
|
||||||
baseURL: 'https://iptv-org.github.io/api',
|
|
||||||
responseType: 'stream'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
|
|
||||||
return this.instance.get(url, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import { Table } from 'console-table-printer'
|
import { ComplexOptions } from 'console-table-printer/dist/src/models/external-table'
|
||||||
import { ComplexOptions } from 'console-table-printer/dist/src/models/external-table'
|
import { Table } from 'console-table-printer'
|
||||||
|
|
||||||
export class CliTable {
|
export class CliTable {
|
||||||
table: Table
|
table: Table
|
||||||
|
|
||||||
constructor(options?: ComplexOptions | string[]) {
|
constructor(options?: ComplexOptions | string[]) {
|
||||||
this.table = new Table(options)
|
this.table = new Table(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
append(row) {
|
append(row) {
|
||||||
this.table.addRow(row)
|
this.table.addRow(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.table.printTable()
|
this.table.printTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return this.table.render()
|
return this.table.render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import { ApiClient } from './apiClient'
|
|
||||||
import { Storage } from '@freearhey/core'
|
|
||||||
import cliProgress, { MultiBar } from 'cli-progress'
|
|
||||||
import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader'
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
|
||||||
if (bytes === 0) return '0 B'
|
|
||||||
const k = 1024
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DataLoader {
|
|
||||||
client: ApiClient
|
|
||||||
storage: Storage
|
|
||||||
progressBar: MultiBar
|
|
||||||
|
|
||||||
constructor(props: DataLoaderProps) {
|
|
||||||
this.client = new ApiClient()
|
|
||||||
this.storage = props.storage
|
|
||||||
this.progressBar = new cliProgress.MultiBar({
|
|
||||||
stopOnComplete: true,
|
|
||||||
hideCursor: true,
|
|
||||||
forceRedraw: true,
|
|
||||||
barsize: 36,
|
|
||||||
format(options, params, payload) {
|
|
||||||
const filename = payload.filename.padEnd(18, ' ')
|
|
||||||
const barsize = options.barsize || 40
|
|
||||||
const percent = (params.progress * 100).toFixed(2)
|
|
||||||
const speed = payload.speed ? formatBytes(payload.speed) + '/s' : 'N/A'
|
|
||||||
const total = formatBytes(params.total)
|
|
||||||
const completeSize = Math.round(params.progress * barsize)
|
|
||||||
const incompleteSize = barsize - completeSize
|
|
||||||
const bar =
|
|
||||||
options.barCompleteString && options.barIncompleteString
|
|
||||||
? options.barCompleteString.substr(0, completeSize) +
|
|
||||||
options.barGlue +
|
|
||||||
options.barIncompleteString.substr(0, incompleteSize)
|
|
||||||
: '-'.repeat(barsize)
|
|
||||||
|
|
||||||
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async load(): Promise<DataLoaderData> {
|
|
||||||
const [
|
|
||||||
countries,
|
|
||||||
regions,
|
|
||||||
subdivisions,
|
|
||||||
languages,
|
|
||||||
categories,
|
|
||||||
blocklist,
|
|
||||||
channels,
|
|
||||||
feeds,
|
|
||||||
logos,
|
|
||||||
timezones,
|
|
||||||
guides,
|
|
||||||
streams,
|
|
||||||
cities
|
|
||||||
] = await Promise.all([
|
|
||||||
this.storage.json('countries.json'),
|
|
||||||
this.storage.json('regions.json'),
|
|
||||||
this.storage.json('subdivisions.json'),
|
|
||||||
this.storage.json('languages.json'),
|
|
||||||
this.storage.json('categories.json'),
|
|
||||||
this.storage.json('blocklist.json'),
|
|
||||||
this.storage.json('channels.json'),
|
|
||||||
this.storage.json('feeds.json'),
|
|
||||||
this.storage.json('logos.json'),
|
|
||||||
this.storage.json('timezones.json'),
|
|
||||||
this.storage.json('guides.json'),
|
|
||||||
this.storage.json('streams.json'),
|
|
||||||
this.storage.json('cities.json')
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
|
||||||
countries,
|
|
||||||
regions,
|
|
||||||
subdivisions,
|
|
||||||
languages,
|
|
||||||
categories,
|
|
||||||
blocklist,
|
|
||||||
channels,
|
|
||||||
feeds,
|
|
||||||
logos,
|
|
||||||
timezones,
|
|
||||||
guides,
|
|
||||||
streams,
|
|
||||||
cities
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async download(filename: string) {
|
|
||||||
if (!this.storage || !this.progressBar) return
|
|
||||||
|
|
||||||
const stream = await this.storage.createStream(filename)
|
|
||||||
const progressBar = this.progressBar.create(0, 0, { filename })
|
|
||||||
|
|
||||||
this.client
|
|
||||||
.get(filename, {
|
|
||||||
responseType: 'stream',
|
|
||||||
onDownloadProgress({ total, loaded, rate }) {
|
|
||||||
if (total) progressBar.setTotal(total)
|
|
||||||
progressBar.update(loaded, { speed: rate })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
response.data.pipe(stream)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import { DataProcessorData } from '../types/dataProcessor'
|
|
||||||
import { DataLoaderData } from '../types/dataLoader'
|
|
||||||
import { Collection } from '@freearhey/core'
|
|
||||||
import {
|
|
||||||
BlocklistRecord,
|
|
||||||
Subdivision,
|
|
||||||
Category,
|
|
||||||
Language,
|
|
||||||
Timezone,
|
|
||||||
Channel,
|
|
||||||
Country,
|
|
||||||
Region,
|
|
||||||
Stream,
|
|
||||||
Guide,
|
|
||||||
City,
|
|
||||||
Feed,
|
|
||||||
Logo
|
|
||||||
} from '../models'
|
|
||||||
|
|
||||||
export class DataProcessor {
|
|
||||||
process(data: DataLoaderData): DataProcessorData {
|
|
||||||
let regions = new Collection(data.regions).map(data => new Region(data))
|
|
||||||
let regionsKeyByCode = regions.keyBy((region: Region) => region.code)
|
|
||||||
|
|
||||||
const categories = new Collection(data.categories).map(data => new Category(data))
|
|
||||||
const categoriesKeyById = categories.keyBy((category: Category) => category.id)
|
|
||||||
|
|
||||||
const languages = new Collection(data.languages).map(data => new Language(data))
|
|
||||||
const languagesKeyByCode = languages.keyBy((language: Language) => language.code)
|
|
||||||
|
|
||||||
let subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data))
|
|
||||||
let subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
|
|
||||||
let subdivisionsGroupedByCountryCode = subdivisions.groupBy(
|
|
||||||
(subdivision: Subdivision) => subdivision.countryCode
|
|
||||||
)
|
|
||||||
|
|
||||||
let countries = new Collection(data.countries).map(data => new Country(data))
|
|
||||||
let countriesKeyByCode = countries.keyBy((country: Country) => country.code)
|
|
||||||
|
|
||||||
const cities = new Collection(data.cities).map(data =>
|
|
||||||
new City(data)
|
|
||||||
.withRegions(regions)
|
|
||||||
.withCountry(countriesKeyByCode)
|
|
||||||
.withSubdivision(subdivisionsKeyByCode)
|
|
||||||
)
|
|
||||||
const citiesKeyByCode = cities.keyBy((city: City) => city.code)
|
|
||||||
const citiesGroupedByCountryCode = cities.groupBy((city: City) => city.countryCode)
|
|
||||||
const citiesGroupedBySubdivisionCode = cities.groupBy((city: City) => city.subdivisionCode)
|
|
||||||
|
|
||||||
const timezones = new Collection(data.timezones).map(data =>
|
|
||||||
new Timezone(data).withCountries(countriesKeyByCode)
|
|
||||||
)
|
|
||||||
const timezonesKeyById = timezones.keyBy((timezone: Timezone) => timezone.id)
|
|
||||||
|
|
||||||
const blocklistRecords = new Collection(data.blocklist).map(data => new BlocklistRecord(data))
|
|
||||||
const blocklistRecordsGroupedByChannelId = blocklistRecords.groupBy(
|
|
||||||
(blocklistRecord: BlocklistRecord) => blocklistRecord.channelId
|
|
||||||
)
|
|
||||||
|
|
||||||
let channels = new Collection(data.channels).map(data => new Channel(data))
|
|
||||||
let channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
|
||||||
|
|
||||||
let feeds = new Collection(data.feeds).map(data => new Feed(data))
|
|
||||||
let feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
|
||||||
let feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
|
|
||||||
|
|
||||||
const logos = new Collection(data.logos).map(data => new Logo(data).withFeed(feedsGroupedById))
|
|
||||||
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
|
|
||||||
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
|
|
||||||
|
|
||||||
const streams = new Collection(data.streams).map(data =>
|
|
||||||
new Stream(data).withLogos(logosGroupedByStreamId)
|
|
||||||
)
|
|
||||||
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
|
||||||
|
|
||||||
const guides = new Collection(data.guides).map(data => new Guide(data))
|
|
||||||
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId())
|
|
||||||
|
|
||||||
regions = regions.map((region: Region) =>
|
|
||||||
region
|
|
||||||
.withCountries(countriesKeyByCode)
|
|
||||||
.withRegions(regions)
|
|
||||||
.withSubdivisions(subdivisions)
|
|
||||||
.withCities(cities)
|
|
||||||
)
|
|
||||||
regionsKeyByCode = regions.keyBy((region: Region) => region.code)
|
|
||||||
|
|
||||||
countries = countries.map((country: Country) =>
|
|
||||||
country
|
|
||||||
.withCities(citiesGroupedByCountryCode)
|
|
||||||
.withSubdivisions(subdivisionsGroupedByCountryCode)
|
|
||||||
.withRegions(regions)
|
|
||||||
.withLanguage(languagesKeyByCode)
|
|
||||||
)
|
|
||||||
countriesKeyByCode = countries.keyBy((country: Country) => country.code)
|
|
||||||
|
|
||||||
subdivisions = subdivisions.map((subdivision: Subdivision) =>
|
|
||||||
subdivision
|
|
||||||
.withCities(citiesGroupedBySubdivisionCode)
|
|
||||||
.withCountry(countriesKeyByCode)
|
|
||||||
.withRegions(regions)
|
|
||||||
.withParent(subdivisionsKeyByCode)
|
|
||||||
)
|
|
||||||
subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
|
|
||||||
subdivisionsGroupedByCountryCode = subdivisions.groupBy(
|
|
||||||
(subdivision: Subdivision) => subdivision.countryCode
|
|
||||||
)
|
|
||||||
|
|
||||||
channels = channels.map((channel: Channel) =>
|
|
||||||
channel
|
|
||||||
.withFeeds(feedsGroupedByChannelId)
|
|
||||||
.withLogos(logosGroupedByChannelId)
|
|
||||||
.withCategories(categoriesKeyById)
|
|
||||||
.withCountry(countriesKeyByCode)
|
|
||||||
.withSubdivision(subdivisionsKeyByCode)
|
|
||||||
.withCategories(categoriesKeyById)
|
|
||||||
)
|
|
||||||
channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
|
||||||
|
|
||||||
feeds = feeds.map((feed: Feed) =>
|
|
||||||
feed
|
|
||||||
.withChannel(channelsKeyById)
|
|
||||||
.withLanguages(languagesKeyByCode)
|
|
||||||
.withTimezones(timezonesKeyById)
|
|
||||||
.withBroadcastArea(
|
|
||||||
citiesKeyByCode,
|
|
||||||
subdivisionsKeyByCode,
|
|
||||||
countriesKeyByCode,
|
|
||||||
regionsKeyByCode
|
|
||||||
)
|
|
||||||
)
|
|
||||||
feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
|
||||||
feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
blocklistRecordsGroupedByChannelId,
|
|
||||||
subdivisionsGroupedByCountryCode,
|
|
||||||
feedsGroupedByChannelId,
|
|
||||||
guidesGroupedByStreamId,
|
|
||||||
logosGroupedByStreamId,
|
|
||||||
subdivisionsKeyByCode,
|
|
||||||
countriesKeyByCode,
|
|
||||||
languagesKeyByCode,
|
|
||||||
streamsGroupedById,
|
|
||||||
categoriesKeyById,
|
|
||||||
timezonesKeyById,
|
|
||||||
regionsKeyByCode,
|
|
||||||
blocklistRecords,
|
|
||||||
channelsKeyById,
|
|
||||||
citiesKeyByCode,
|
|
||||||
subdivisions,
|
|
||||||
categories,
|
|
||||||
countries,
|
|
||||||
languages,
|
|
||||||
timezones,
|
|
||||||
channels,
|
|
||||||
regions,
|
|
||||||
streams,
|
|
||||||
cities,
|
|
||||||
guides,
|
|
||||||
feeds,
|
|
||||||
logos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +1,50 @@
|
|||||||
type Column = {
|
import { Collection } from '@freearhey/core'
|
||||||
name: string
|
|
||||||
nowrap?: boolean
|
export type HTMLTableColumn = {
|
||||||
align?: string
|
name: string
|
||||||
}
|
nowrap?: boolean
|
||||||
|
align?: string
|
||||||
type DataItem = string[]
|
}
|
||||||
|
|
||||||
export class HTMLTable {
|
export type HTMLTableItem = string[]
|
||||||
data: DataItem[]
|
|
||||||
columns: Column[]
|
export class HTMLTable {
|
||||||
|
data: Collection<HTMLTableItem>
|
||||||
constructor(data: DataItem[], columns: Column[]) {
|
columns: Collection<HTMLTableColumn>
|
||||||
this.data = data
|
|
||||||
this.columns = columns
|
constructor(data: Collection<HTMLTableItem>, columns: Collection<HTMLTableColumn>) {
|
||||||
}
|
this.data = data
|
||||||
|
this.columns = columns
|
||||||
toString() {
|
}
|
||||||
let output = '<table>\r\n'
|
|
||||||
|
toString() {
|
||||||
output += ' <thead>\r\n <tr>'
|
let output = '<table>\r\n'
|
||||||
for (const column of this.columns) {
|
|
||||||
output += `<th align="left">${column.name}</th>`
|
output += ' <thead>\r\n <tr>'
|
||||||
}
|
this.columns.forEach((column: HTMLTableColumn) => {
|
||||||
output += '</tr>\r\n </thead>\r\n'
|
output += `<th align="left">${column.name}</th>`
|
||||||
|
})
|
||||||
output += ' <tbody>\r\n'
|
|
||||||
for (const item of this.data) {
|
output += '</tr>\r\n </thead>\r\n'
|
||||||
output += ' <tr>'
|
|
||||||
let i = 0
|
output += ' <tbody>\r\n'
|
||||||
for (const prop in item) {
|
this.data.forEach((item: HTMLTableItem) => {
|
||||||
const column = this.columns[i]
|
output += ' <tr>'
|
||||||
const nowrap = column.nowrap ? ' nowrap' : ''
|
let i = 0
|
||||||
const align = column.align ? ` align="${column.align}"` : ''
|
for (const prop in item) {
|
||||||
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
const column = this.columns.all()[i]
|
||||||
i++
|
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||||
}
|
const align = column.align ? ` align="${column.align}"` : ''
|
||||||
output += '</tr>\r\n'
|
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
||||||
}
|
i++
|
||||||
output += ' </tbody>\r\n'
|
}
|
||||||
|
output += '</tr>\r\n'
|
||||||
output += '</table>'
|
})
|
||||||
|
|
||||||
return output
|
output += ' </tbody>\r\n'
|
||||||
}
|
|
||||||
}
|
output += '</table>'
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
export * from './apiClient'
|
export * from './cliTable'
|
||||||
export * from './cliTable'
|
export * from './htmlTable'
|
||||||
export * from './dataProcessor'
|
export * from './issueData'
|
||||||
export * from './dataLoader'
|
export * from './issueLoader'
|
||||||
export * from './htmlTable'
|
export * from './issueParser'
|
||||||
export * from './issueData'
|
export * from './logParser'
|
||||||
export * from './issueLoader'
|
export * from './markdown'
|
||||||
export * from './issueParser'
|
export * from './numberParser'
|
||||||
export * from './logParser'
|
export * from './playlistParser'
|
||||||
export * from './markdown'
|
export * from './proxyParser'
|
||||||
export * from './numberParser'
|
export * from './streamTester'
|
||||||
export * from './playlistParser'
|
|
||||||
export * from './proxyParser'
|
|
||||||
export * from './streamTester'
|
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
|
|
||||||
export class IssueData {
|
export class IssueData {
|
||||||
_data: Dictionary
|
_data: Dictionary<string>
|
||||||
constructor(data: Dictionary) {
|
constructor(data: Dictionary<string>) {
|
||||||
this._data = data
|
this._data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
has(key: string): boolean {
|
has(key: string): boolean {
|
||||||
return this._data.has(key)
|
return this._data.has(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
missing(key: string): boolean {
|
missing(key: string): boolean {
|
||||||
return this._data.missing(key) || this._data.get(key) === undefined
|
return this._data.missing(key) || this._data.get(key) === undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
getBoolean(key: string): boolean {
|
getBoolean(key: string): boolean {
|
||||||
return Boolean(this._data.get(key))
|
return Boolean(this._data.get(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
getString(key: string): string | undefined {
|
getString(key: string): string | undefined {
|
||||||
const deleteSymbol = '~'
|
const deleteSymbol = '~'
|
||||||
|
|
||||||
return this._data.get(key) === deleteSymbol ? '' : this._data.get(key)
|
return this._data.get(key) === deleteSymbol ? '' : this._data.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
getArray(key: string): string[] | undefined {
|
getArray(key: string): string[] | undefined {
|
||||||
const deleteSymbol = '~'
|
const deleteSymbol = '~'
|
||||||
|
|
||||||
if (this._data.missing(key)) return undefined
|
if (this._data.missing(key)) return undefined
|
||||||
|
|
||||||
return this._data.get(key) === deleteSymbol ? [] : this._data.get(key).split('\r\n')
|
const value = this._data.get(key)
|
||||||
}
|
|
||||||
}
|
return !value || value === deleteSymbol ? [] : value.split('\r\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
||||||
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
||||||
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
import { TESTING, OWNER, REPO } from '../constants'
|
||||||
import { Octokit } from '@octokit/core'
|
import { Collection } from '@freearhey/core'
|
||||||
import { IssueParser } from './'
|
import { Octokit } from '@octokit/core'
|
||||||
import { TESTING, OWNER, REPO } from '../constants'
|
import { IssueParser } from './'
|
||||||
|
|
||||||
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
||||||
const octokit = new CustomOctokit()
|
const octokit = new CustomOctokit()
|
||||||
|
|
||||||
export class IssueLoader {
|
export class IssueLoader {
|
||||||
async load(props?: { labels: string | string[] }) {
|
async load(props?: { labels: string | string[] }) {
|
||||||
let labels = ''
|
let labels = ''
|
||||||
if (props && props.labels) {
|
if (props && props.labels) {
|
||||||
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
|
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
|
||||||
}
|
}
|
||||||
let issues: object[] = []
|
let issues: object[] = []
|
||||||
if (TESTING) {
|
if (TESTING) {
|
||||||
issues = (await import('../../tests/__data__/input/issues.js')).default
|
issues = (await import('../../tests/__data__/input/issues.js')).default
|
||||||
} else {
|
} else {
|
||||||
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||||
owner: OWNER,
|
owner: OWNER,
|
||||||
repo: REPO,
|
repo: REPO,
|
||||||
per_page: 100,
|
per_page: 100,
|
||||||
labels,
|
labels,
|
||||||
status: 'open',
|
status: 'open',
|
||||||
headers: {
|
headers: {
|
||||||
'X-GitHub-Api-Version': '2022-11-28'
|
'X-GitHub-Api-Version': '2022-11-28'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = new IssueParser()
|
const parser = new IssueParser()
|
||||||
|
|
||||||
return new Collection(issues).map(parser.parse)
|
return new Collection(issues).map(parser.parse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import { Issue } from '../models'
|
import { IssueData } from './issueData'
|
||||||
import { IssueData } from './issueData'
|
import { Issue } from '../models'
|
||||||
|
|
||||||
const FIELDS = new Dictionary({
|
const FIELDS = new Dictionary({
|
||||||
'Stream ID': 'streamId',
|
'Stream ID': 'streamId',
|
||||||
'Channel ID': 'channelId',
|
'Channel ID': 'channelId',
|
||||||
'Feed ID': 'feedId',
|
'Feed ID': 'feedId',
|
||||||
'Stream URL': 'streamUrl',
|
'Stream URL': 'streamUrl',
|
||||||
'New Stream URL': 'newStreamUrl',
|
'New Stream URL': 'newStreamUrl',
|
||||||
Label: 'label',
|
Label: 'label',
|
||||||
Quality: 'quality',
|
Quality: 'quality',
|
||||||
'HTTP User-Agent': 'httpUserAgent',
|
'HTTP User-Agent': 'httpUserAgent',
|
||||||
'HTTP User Agent': 'httpUserAgent',
|
'HTTP User Agent': 'httpUserAgent',
|
||||||
'HTTP Referrer': 'httpReferrer',
|
'HTTP Referrer': 'httpReferrer',
|
||||||
'What happened to the stream?': 'reason',
|
'What happened to the stream?': 'reason',
|
||||||
Reason: 'reason',
|
Reason: 'reason',
|
||||||
Notes: 'notes',
|
Notes: 'notes',
|
||||||
Directives: 'directives'
|
Directives: 'directives'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class IssueParser {
|
export class IssueParser {
|
||||||
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
||||||
const fields = typeof issue.body === 'string' ? issue.body.split('###') : []
|
const fields = typeof issue.body === 'string' ? issue.body.split('###') : []
|
||||||
|
|
||||||
const data = new Dictionary()
|
const data = new Dictionary<string>()
|
||||||
fields.forEach((field: string) => {
|
fields.forEach((field: string) => {
|
||||||
const parsed = typeof field === 'string' ? field.split(/\r?\n/).filter(Boolean) : []
|
const parsed = typeof field === 'string' ? field.split(/\r?\n/).filter(Boolean) : []
|
||||||
let _label = parsed.shift()
|
let _label = parsed.shift()
|
||||||
_label = _label ? _label.replace(/ \(optional\)| \(required\)/, '').trim() : ''
|
_label = _label ? _label.replace(/ \(optional\)| \(required\)/, '').trim() : ''
|
||||||
let _value = parsed.join('\r\n')
|
let _value = parsed.join('\r\n')
|
||||||
_value = _value ? _value.trim() : ''
|
_value = _value ? _value.trim() : ''
|
||||||
|
|
||||||
if (!_label || !_value) return data
|
if (!_label || !_value) return data
|
||||||
|
|
||||||
const id: string = FIELDS.get(_label)
|
const id = FIELDS.get(_label)
|
||||||
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
||||||
|
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
data.set(id, value)
|
data.set(id, value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const labels = issue.labels.map(label => label.name)
|
const labels = issue.labels.map(label => label.name)
|
||||||
|
|
||||||
return new Issue({ number: issue.number, labels, data: new IssueData(data) })
|
return new Issue({ number: issue.number, labels, data: new IssueData(data) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import fs from 'fs'
|
import path from 'path'
|
||||||
import path from 'path'
|
import fs from 'fs'
|
||||||
|
|
||||||
type MarkdownConfig = {
|
type MarkdownConfig = {
|
||||||
build: string
|
build: string
|
||||||
template: string
|
template: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Markdown {
|
export class Markdown {
|
||||||
build: string
|
build: string
|
||||||
template: string
|
template: string
|
||||||
|
|
||||||
constructor(config: MarkdownConfig) {
|
constructor(config: MarkdownConfig) {
|
||||||
this.build = config.build
|
this.build = config.build
|
||||||
this.template = config.template
|
this.template = config.template
|
||||||
}
|
}
|
||||||
|
|
||||||
compile() {
|
compile() {
|
||||||
const workingDir = process.cwd()
|
const workingDir = process.cwd()
|
||||||
|
|
||||||
const templatePath = path.resolve(workingDir, this.template)
|
const templatePath = path.resolve(workingDir, this.template)
|
||||||
const template = fs.readFileSync(templatePath, 'utf8')
|
const template = fs.readFileSync(templatePath, 'utf8')
|
||||||
const processedContent = this.processIncludes(template, workingDir)
|
const processedContent = this.processIncludes(template, workingDir)
|
||||||
|
|
||||||
if (this.build) {
|
if (this.build) {
|
||||||
const outputPath = path.resolve(workingDir, this.build)
|
const outputPath = path.resolve(workingDir, this.build)
|
||||||
fs.writeFileSync(outputPath, processedContent, 'utf8')
|
fs.writeFileSync(outputPath, processedContent, 'utf8')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processIncludes(template: string, baseDir: string): string {
|
private processIncludes(template: string, baseDir: string): string {
|
||||||
const includeRegex = /#include\s+"([^"]+)"/g
|
const includeRegex = /#include\s+"([^"]+)"/g
|
||||||
|
|
||||||
return template.replace(includeRegex, (match, includePath) => {
|
return template.replace(includeRegex, (match, includePath) => {
|
||||||
try {
|
try {
|
||||||
const fullPath = path.resolve(baseDir, includePath)
|
const fullPath = path.resolve(baseDir, includePath)
|
||||||
const includeContent = fs.readFileSync(fullPath, 'utf8')
|
const includeContent = fs.readFileSync(fullPath, 'utf8')
|
||||||
return this.processIncludes(includeContent, baseDir)
|
return this.processIncludes(includeContent, baseDir)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Warning: Could not include file ${includePath}: ${error}`)
|
console.warn(`Warning: Could not include file ${includePath}: ${error}`)
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,43 @@
|
|||||||
import { Collection, Storage, Dictionary } from '@freearhey/core'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import parser from 'iptv-playlist-parser'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Stream } from '../models'
|
import parser from 'iptv-playlist-parser'
|
||||||
|
import { Stream } from '../models'
|
||||||
type PlaylistPareserProps = {
|
|
||||||
storage: Storage
|
type PlaylistPareserProps = {
|
||||||
feedsGroupedByChannelId: Dictionary
|
storage: Storage
|
||||||
logosGroupedByStreamId: Dictionary
|
}
|
||||||
channelsKeyById: Dictionary
|
|
||||||
}
|
export class PlaylistParser {
|
||||||
|
storage: Storage
|
||||||
export class PlaylistParser {
|
|
||||||
storage: Storage
|
constructor({ storage }: PlaylistPareserProps) {
|
||||||
feedsGroupedByChannelId: Dictionary
|
this.storage = storage
|
||||||
logosGroupedByStreamId: Dictionary
|
}
|
||||||
channelsKeyById: Dictionary
|
|
||||||
|
async parse(files: string[]): Promise<Collection<Stream>> {
|
||||||
constructor({
|
const parsed = new Collection<Stream>()
|
||||||
storage,
|
|
||||||
feedsGroupedByChannelId,
|
for (const filepath of files) {
|
||||||
logosGroupedByStreamId,
|
if (!this.storage.existsSync(filepath)) continue
|
||||||
channelsKeyById
|
const _parsed: Collection<Stream> = await this.parseFile(filepath)
|
||||||
}: PlaylistPareserProps) {
|
parsed.concat(_parsed)
|
||||||
this.storage = storage
|
}
|
||||||
this.feedsGroupedByChannelId = feedsGroupedByChannelId
|
|
||||||
this.logosGroupedByStreamId = logosGroupedByStreamId
|
return parsed
|
||||||
this.channelsKeyById = channelsKeyById
|
}
|
||||||
}
|
|
||||||
|
async parseFile(filepath: string): Promise<Collection<Stream>> {
|
||||||
async parse(files: string[]): Promise<Collection> {
|
const content = await this.storage.load(filepath)
|
||||||
let streams = new Collection()
|
const parsed: parser.Playlist = parser.parse(content)
|
||||||
|
|
||||||
for (const filepath of files) {
|
const streams = new Collection<Stream>()
|
||||||
if (!this.storage.existsSync(filepath)) continue
|
parsed.items.forEach((data: parser.PlaylistItem) => {
|
||||||
|
const stream = Stream.fromPlaylistItem(data)
|
||||||
const _streams: Collection = await this.parseFile(filepath)
|
stream.filepath = filepath
|
||||||
streams = streams.concat(_streams)
|
|
||||||
}
|
streams.add(stream)
|
||||||
|
})
|
||||||
return streams
|
|
||||||
}
|
return streams
|
||||||
|
}
|
||||||
async parseFile(filepath: string): Promise<Collection> {
|
}
|
||||||
const content = await this.storage.load(filepath)
|
|
||||||
const parsed: parser.Playlist = parser.parse(content)
|
|
||||||
|
|
||||||
const streams = new Collection(parsed.items).map((data: parser.PlaylistItem) => {
|
|
||||||
const stream = new Stream()
|
|
||||||
.fromPlaylistItem(data)
|
|
||||||
.withFeed(this.feedsGroupedByChannelId)
|
|
||||||
.withChannel(this.channelsKeyById)
|
|
||||||
.withLogos(this.logosGroupedByStreamId)
|
|
||||||
.setFilepath(filepath)
|
|
||||||
|
|
||||||
return stream
|
|
||||||
})
|
|
||||||
|
|
||||||
return streams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,117 +1,125 @@
|
|||||||
import { Stream } from '../models'
|
import axios, { AxiosInstance, AxiosProxyConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
import { TESTING } from '../constants'
|
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||||
import mediaInfoFactory from 'mediainfo.js'
|
import { ProxyParser } from './proxyParser.js'
|
||||||
import axios, { AxiosInstance, AxiosProxyConfig, AxiosRequestConfig } from 'axios'
|
import mediaInfoFactory from 'mediainfo.js'
|
||||||
import { ProxyParser } from './proxyParser.js'
|
import { OptionValues } from 'commander'
|
||||||
import { OptionValues } from 'commander'
|
import { TESTING } from '../constants'
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
import { Stream } from '../models'
|
||||||
|
|
||||||
export type TestResult = {
|
export type StreamTesterResult = {
|
||||||
status: {
|
status: {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StreamTesterProps = {
|
export type StreamTesterError = {
|
||||||
options: OptionValues
|
name: string
|
||||||
}
|
code?: string
|
||||||
|
cause?: Error & { code?: string }
|
||||||
export class StreamTester {
|
response?: AxiosResponse
|
||||||
client: AxiosInstance
|
}
|
||||||
options: OptionValues
|
|
||||||
|
export type StreamTesterProps = {
|
||||||
constructor({ options }: StreamTesterProps) {
|
options: OptionValues
|
||||||
const proxyParser = new ProxyParser()
|
}
|
||||||
let request: AxiosRequestConfig = {
|
|
||||||
responseType: 'arraybuffer'
|
export class StreamTester {
|
||||||
}
|
client: AxiosInstance
|
||||||
|
options: OptionValues
|
||||||
if (options.proxy !== undefined) {
|
|
||||||
const proxy = proxyParser.parse(options.proxy) as AxiosProxyConfig
|
constructor({ options }: StreamTesterProps) {
|
||||||
|
const proxyParser = new ProxyParser()
|
||||||
if (
|
let request: AxiosRequestConfig = {
|
||||||
proxy.protocol &&
|
responseType: 'arraybuffer'
|
||||||
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
}
|
||||||
) {
|
|
||||||
const socksProxyAgent = new SocksProxyAgent(options.proxy)
|
if (options.proxy !== undefined) {
|
||||||
|
const proxy = proxyParser.parse(options.proxy) as AxiosProxyConfig
|
||||||
request = { ...request, ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } }
|
|
||||||
} else {
|
if (
|
||||||
request = { ...request, ...{ proxy } }
|
proxy.protocol &&
|
||||||
}
|
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
||||||
}
|
) {
|
||||||
|
const socksProxyAgent = new SocksProxyAgent(options.proxy)
|
||||||
this.client = axios.create(request)
|
|
||||||
this.options = options
|
request = { ...request, ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } }
|
||||||
}
|
} else {
|
||||||
|
request = { ...request, ...{ proxy } }
|
||||||
async test(stream: Stream): Promise<TestResult> {
|
}
|
||||||
if (TESTING) {
|
}
|
||||||
const results = (await import('../../tests/__data__/input/playlist_test/results.js')).default
|
|
||||||
|
this.client = axios.create(request)
|
||||||
return results[stream.url as keyof typeof results]
|
this.options = options
|
||||||
} else {
|
}
|
||||||
try {
|
|
||||||
const res = await this.client(stream.url, {
|
async test(stream: Stream): Promise<StreamTesterResult> {
|
||||||
signal: AbortSignal.timeout(this.options.timeout),
|
if (TESTING) {
|
||||||
headers: {
|
const results = (await import('../../tests/__data__/input/playlist_test/results.js')).default
|
||||||
'User-Agent': stream.getUserAgent() || 'Mozilla/5.0',
|
|
||||||
Referer: stream.getReferrer()
|
return results[stream.url as keyof typeof results]
|
||||||
}
|
} else {
|
||||||
})
|
try {
|
||||||
|
const res = await this.client(stream.url, {
|
||||||
const mediainfo = await mediaInfoFactory({ format: 'object' })
|
signal: AbortSignal.timeout(this.options.timeout),
|
||||||
const buffer = await res.data
|
headers: {
|
||||||
const result = await mediainfo.analyzeData(
|
'User-Agent': stream.user_agent || 'Mozilla/5.0',
|
||||||
() => buffer.byteLength,
|
Referer: stream.referrer
|
||||||
(size: any, offset: number | undefined) =>
|
}
|
||||||
Buffer.from(buffer).subarray(offset, offset + size)
|
})
|
||||||
)
|
|
||||||
|
const mediainfo = await mediaInfoFactory({ format: 'object' })
|
||||||
if (result && result.media && result.media.track.length > 0) {
|
const buffer = await res.data
|
||||||
return {
|
const result = await mediainfo.analyzeData(
|
||||||
status: {
|
() => buffer.byteLength,
|
||||||
ok: true,
|
(size: number, offset: number) => Buffer.from(buffer).subarray(offset, offset + size)
|
||||||
code: 'OK'
|
)
|
||||||
}
|
|
||||||
}
|
if (result && result.media && result.media.track.length > 0) {
|
||||||
} else {
|
return {
|
||||||
return {
|
status: {
|
||||||
status: {
|
ok: true,
|
||||||
ok: false,
|
code: 'OK'
|
||||||
code: 'NO_VIDEO'
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
return {
|
||||||
} catch (error: any) {
|
status: {
|
||||||
let code = 'UNKNOWN_ERROR'
|
ok: false,
|
||||||
if (error.name === 'CanceledError') {
|
code: 'NO_VIDEO'
|
||||||
code = 'TIMEOUT'
|
}
|
||||||
} else if (error.name === 'AxiosError') {
|
}
|
||||||
if (error.response) {
|
}
|
||||||
const status = error.response?.status
|
} catch (err: unknown) {
|
||||||
const statusText = error.response?.statusText.toUpperCase().replace(/\s+/, '_')
|
const error = err as StreamTesterError
|
||||||
code = `HTTP_${status}_${statusText}`
|
|
||||||
} else {
|
let code = 'UNKNOWN_ERROR'
|
||||||
code = `AXIOS_${error.code}`
|
if (error.name === 'CanceledError') {
|
||||||
}
|
code = 'TIMEOUT'
|
||||||
} else if (error.cause) {
|
} else if (error.name === 'AxiosError') {
|
||||||
const cause = error.cause as Error & { code?: string }
|
if (error.response) {
|
||||||
if (cause.code) {
|
const status = error.response?.status
|
||||||
code = cause.code
|
const statusText = error.response?.statusText.toUpperCase().replace(/\s+/, '_')
|
||||||
} else {
|
code = `HTTP_${status}_${statusText}`
|
||||||
code = cause.name
|
} else {
|
||||||
}
|
code = `AXIOS_${error.code}`
|
||||||
}
|
}
|
||||||
|
} else if (error.cause) {
|
||||||
return {
|
const cause = error.cause
|
||||||
status: {
|
if (cause.code) {
|
||||||
ok: false,
|
code = cause.code
|
||||||
code
|
} else {
|
||||||
}
|
code = cause.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
return {
|
||||||
}
|
status: {
|
||||||
|
ok: false,
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,54 +1,60 @@
|
|||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Stream, Category, Playlist } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Generator } from './generator'
|
import { Stream, Playlist } from '../models'
|
||||||
|
import { Generator } from './generator'
|
||||||
type CategoriesGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
categories: Collection
|
type CategoriesGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
categories: Collection<sdk.Models.Category>
|
||||||
|
logFile: File
|
||||||
export class CategoriesGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
categories: Collection
|
export class CategoriesGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
categories: Collection<sdk.Models.Category>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, categories, logFile }: CategoriesGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.categories = categories
|
constructor({ streams, categories, logFile }: CategoriesGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.categories = categories
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate() {
|
}
|
||||||
const streams = this.streams.orderBy([(stream: Stream) => stream.getTitle()])
|
|
||||||
|
async generate() {
|
||||||
this.categories.forEach(async (category: Category) => {
|
const streams = this.streams.sortBy([(stream: Stream) => stream.title])
|
||||||
const categoryStreams = streams
|
|
||||||
.filter((stream: Stream) => stream.hasCategory(category))
|
this.categories.forEach(async (category: sdk.Models.Category) => {
|
||||||
.map((stream: Stream) => {
|
const categoryStreams = streams
|
||||||
const groupTitle = stream.getCategoryNames().join(';')
|
.filter((stream: Stream) => stream.hasCategory(category))
|
||||||
if (groupTitle) stream.groupTitle = groupTitle
|
.map((stream: Stream) => {
|
||||||
|
const groupTitle = stream
|
||||||
return stream
|
.getCategories()
|
||||||
})
|
.map(category => category.name)
|
||||||
|
.sort()
|
||||||
const playlist = new Playlist(categoryStreams, { public: true })
|
.join(';')
|
||||||
const filepath = `categories/${category.id}.m3u`
|
if (groupTitle) stream.groupTitle = groupTitle
|
||||||
await this.storage.save(filepath, playlist.toString())
|
|
||||||
this.logFile.append(
|
return stream
|
||||||
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL
|
})
|
||||||
)
|
|
||||||
})
|
const playlist = new Playlist(categoryStreams, { public: true })
|
||||||
|
const filepath = `categories/${category.id}.m3u`
|
||||||
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasCategories())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
const playlist = new Playlist(undefinedStreams, { public: true })
|
this.logFile.append(
|
||||||
const filepath = 'categories/undefined.m3u'
|
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL
|
||||||
await this.storage.save(filepath, playlist.toString())
|
)
|
||||||
this.logFile.append(
|
})
|
||||||
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL
|
|
||||||
)
|
const undefinedStreams = streams.filter((stream: Stream) => stream.getCategories().isEmpty())
|
||||||
}
|
const playlist = new Playlist(undefinedStreams, { public: true })
|
||||||
}
|
const filepath = 'categories/undefined.m3u'
|
||||||
|
await this.storage.save(filepath, playlist.toString())
|
||||||
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +1,54 @@
|
|||||||
import { City, Stream, Playlist } from '../models'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type CitiesGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
cities: Collection
|
type CitiesGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
cities: Collection<sdk.Models.City>
|
||||||
|
logFile: File
|
||||||
export class CitiesGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
cities: Collection
|
export class CitiesGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
cities: Collection<sdk.Models.City>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, cities, logFile }: CitiesGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.cities = cities
|
constructor({ streams, cities, logFile }: CitiesGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.cities = cities
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
const streams = this.streams
|
|
||||||
.orderBy((stream: Stream) => stream.getTitle())
|
async generate(): Promise<void> {
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
const streams = this.streams
|
||||||
|
.sortBy((stream: Stream) => stream.title)
|
||||||
this.cities.forEach(async (city: City) => {
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
const cityStreams = streams.filter((stream: Stream) => stream.isBroadcastInCity(city))
|
|
||||||
|
const streamsGroupedByCityCode = {}
|
||||||
if (cityStreams.isEmpty()) return
|
streams.forEach((stream: Stream) => {
|
||||||
|
stream.getBroadcastCities().forEach((city: sdk.Models.City) => {
|
||||||
const playlist = new Playlist(cityStreams, { public: true })
|
if (streamsGroupedByCityCode[city.code]) {
|
||||||
const filepath = `cities/${city.code.toLowerCase()}.m3u`
|
streamsGroupedByCityCode[city.code].add(stream)
|
||||||
await this.storage.save(filepath, playlist.toString())
|
} else {
|
||||||
this.logFile.append(
|
streamsGroupedByCityCode[city.code] = new Collection<Stream>([stream])
|
||||||
JSON.stringify({ type: 'city', filepath, count: playlist.streams.count() }) + EOL
|
}
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
for (const cityCode in streamsGroupedByCityCode) {
|
||||||
|
const cityStreams = streamsGroupedByCityCode[cityCode]
|
||||||
|
|
||||||
|
const playlist = new Playlist(cityStreams, { public: true })
|
||||||
|
const filepath = `cities/${cityCode.toLowerCase()}.m3u`
|
||||||
|
await this.storage.save(filepath, playlist.toString())
|
||||||
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'city', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,68 +1,80 @@
|
|||||||
import { Country, Stream, Playlist } from '../models'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type CountriesGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
countries: Collection
|
type CountriesGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
countries: Collection<sdk.Models.Country>
|
||||||
|
logFile: File
|
||||||
export class CountriesGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
countries: Collection
|
export class CountriesGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
countries: Collection<sdk.Models.Country>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, countries, logFile }: CountriesGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.countries = countries
|
constructor({ streams, countries, logFile }: CountriesGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.countries = countries
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
const streams = this.streams
|
|
||||||
.orderBy((stream: Stream) => stream.getTitle())
|
async generate(): Promise<void> {
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
const streams = this.streams
|
||||||
|
.sortBy((stream: Stream) => stream.title)
|
||||||
this.countries.forEach(async (country: Country) => {
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
const countryStreams = streams.filter((stream: Stream) =>
|
|
||||||
stream.isBroadcastInCountry(country)
|
const streamsGroupedByCountryCode = {}
|
||||||
)
|
streams.forEach((stream: Stream) => {
|
||||||
if (countryStreams.isEmpty()) return
|
stream.getBroadcastCountries().forEach((country: sdk.Models.Country) => {
|
||||||
|
if (streamsGroupedByCountryCode[country.code]) {
|
||||||
const playlist = new Playlist(countryStreams, { public: true })
|
streamsGroupedByCountryCode[country.code].add(stream)
|
||||||
const filepath = `countries/${country.code.toLowerCase()}.m3u`
|
} else {
|
||||||
await this.storage.save(filepath, playlist.toString())
|
streamsGroupedByCountryCode[country.code] = new Collection<Stream>([stream])
|
||||||
this.logFile.append(
|
}
|
||||||
JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL
|
})
|
||||||
)
|
})
|
||||||
})
|
|
||||||
|
for (const countryCode in streamsGroupedByCountryCode) {
|
||||||
const internationalStreams = streams.filter((stream: Stream) => stream.isInternational())
|
const countryStreams = streamsGroupedByCountryCode[countryCode]
|
||||||
const internationalPlaylist = new Playlist(internationalStreams, { public: true })
|
|
||||||
const internationalFilepath = 'countries/int.m3u'
|
const playlist = new Playlist(countryStreams, { public: true })
|
||||||
await this.storage.save(internationalFilepath, internationalPlaylist.toString())
|
const filepath = `countries/${countryCode.toLowerCase()}.m3u`
|
||||||
this.logFile.append(
|
await this.storage.save(filepath, playlist.toString())
|
||||||
JSON.stringify({
|
this.logFile.append(
|
||||||
type: 'country',
|
JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL
|
||||||
filepath: internationalFilepath,
|
)
|
||||||
count: internationalPlaylist.streams.count()
|
}
|
||||||
}) + EOL
|
|
||||||
)
|
const internationalStreams = streams.filter((stream: Stream) => stream.isInternational())
|
||||||
|
const internationalPlaylist = new Playlist(internationalStreams, { public: true })
|
||||||
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
|
const internationalFilepath = 'countries/int.m3u'
|
||||||
const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
|
await this.storage.save(internationalFilepath, internationalPlaylist.toString())
|
||||||
const undefinedFilepath = 'countries/undefined.m3u'
|
this.logFile.append(
|
||||||
await this.storage.save(undefinedFilepath, undefinedPlaylist.toString())
|
JSON.stringify({
|
||||||
this.logFile.append(
|
type: 'country',
|
||||||
JSON.stringify({
|
filepath: internationalFilepath,
|
||||||
type: 'country',
|
count: internationalPlaylist.streams.count()
|
||||||
filepath: undefinedFilepath,
|
}) + EOL
|
||||||
count: undefinedPlaylist.streams.count()
|
)
|
||||||
}) + EOL
|
|
||||||
)
|
const undefinedStreams = streams.filter((stream: Stream) =>
|
||||||
}
|
stream.getBroadcastAreaCodes().isEmpty()
|
||||||
}
|
)
|
||||||
|
const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
|
||||||
|
const undefinedFilepath = 'countries/undefined.m3u'
|
||||||
|
await this.storage.save(undefinedFilepath, undefinedPlaylist.toString())
|
||||||
|
this.logFile.append(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'country',
|
||||||
|
filepath: undefinedFilepath,
|
||||||
|
count: undefinedPlaylist.streams.count()
|
||||||
|
}) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
export * from './categoriesGenerator'
|
export * from './categoriesGenerator'
|
||||||
export * from './citiesGenerator'
|
export * from './citiesGenerator'
|
||||||
export * from './countriesGenerator'
|
export * from './countriesGenerator'
|
||||||
export * from './indexCategoryGenerator'
|
export * from './indexCategoryGenerator'
|
||||||
export * from './indexCountryGenerator'
|
export * from './indexCountryGenerator'
|
||||||
export * from './indexGenerator'
|
export * from './indexGenerator'
|
||||||
export * from './indexLanguageGenerator'
|
export * from './indexLanguageGenerator'
|
||||||
export * from './indexNsfwGenerator'
|
export * from './languagesGenerator'
|
||||||
export * from './languagesGenerator'
|
export * from './rawGenerator'
|
||||||
export * from './rawGenerator'
|
export * from './regionsGenerator'
|
||||||
export * from './regionsGenerator'
|
export * from './sourcesGenerator'
|
||||||
export * from './sourcesGenerator'
|
export * from './subdivisionsGenerator'
|
||||||
export * from './subdivisionsGenerator'
|
|
||||||
|
|||||||
@@ -1,55 +1,56 @@
|
|||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Stream, Playlist, Category } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type IndexCategoryGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
logFile: File
|
type IndexCategoryGeneratorProps = {
|
||||||
}
|
streams: Collection<Stream>
|
||||||
|
logFile: File
|
||||||
export class IndexCategoryGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
storage: Storage
|
export class IndexCategoryGenerator implements Generator {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, logFile }: IndexCategoryGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
constructor({ streams, logFile }: IndexCategoryGeneratorProps) {
|
||||||
this.logFile = logFile
|
this.streams = streams.clone()
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
const streams = this.streams
|
|
||||||
.orderBy(stream => stream.getTitle())
|
async generate(): Promise<void> {
|
||||||
.filter(stream => stream.isSFW())
|
const streams = this.streams.sortBy(stream => stream.title).filter(stream => stream.isSFW())
|
||||||
|
|
||||||
let groupedStreams = new Collection()
|
let groupedStreams = new Collection<Stream>()
|
||||||
streams.forEach((stream: Stream) => {
|
streams.forEach((stream: Stream) => {
|
||||||
if (!stream.hasCategories()) {
|
const streamCategories = stream.getCategories()
|
||||||
const streamClone = stream.clone()
|
if (streamCategories.isEmpty()) {
|
||||||
streamClone.groupTitle = 'Undefined'
|
const streamClone = stream.clone()
|
||||||
groupedStreams.add(streamClone)
|
streamClone.groupTitle = 'Undefined'
|
||||||
return
|
groupedStreams.add(streamClone)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
stream.getCategories().forEach((category: Category) => {
|
|
||||||
const streamClone = stream.clone()
|
streamCategories.forEach((category: sdk.Models.Category) => {
|
||||||
streamClone.groupTitle = category.name
|
const streamClone = stream.clone()
|
||||||
groupedStreams.push(streamClone)
|
streamClone.groupTitle = category.name
|
||||||
})
|
groupedStreams.add(streamClone)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
groupedStreams = groupedStreams.orderBy(stream => {
|
|
||||||
if (stream.groupTitle === 'Undefined') return 'ZZ'
|
groupedStreams = groupedStreams.sortBy(stream => {
|
||||||
return stream.groupTitle
|
if (stream.groupTitle === 'Undefined') return 'ZZ'
|
||||||
})
|
return stream.groupTitle
|
||||||
|
})
|
||||||
const playlist = new Playlist(groupedStreams, { public: true })
|
|
||||||
const filepath = 'index.category.m3u'
|
const playlist = new Playlist(groupedStreams, { public: true })
|
||||||
await this.storage.save(filepath, playlist.toString())
|
const filepath = 'index.category.m3u'
|
||||||
this.logFile.append(
|
await this.storage.save(filepath, playlist.toString())
|
||||||
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
this.logFile.append(
|
||||||
)
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,63 +1,67 @@
|
|||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Stream, Playlist, Country } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type IndexCountryGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
logFile: File
|
type IndexCountryGeneratorProps = {
|
||||||
}
|
streams: Collection<Stream>
|
||||||
|
logFile: File
|
||||||
export class IndexCountryGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
storage: Storage
|
export class IndexCountryGenerator implements Generator {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, logFile }: IndexCountryGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
constructor({ streams, logFile }: IndexCountryGeneratorProps) {
|
||||||
this.logFile = logFile
|
this.streams = streams.clone()
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
let groupedStreams = new Collection()
|
|
||||||
|
async generate(): Promise<void> {
|
||||||
this.streams
|
let groupedStreams = new Collection<Stream>()
|
||||||
.orderBy((stream: Stream) => stream.getTitle())
|
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
this.streams
|
||||||
.forEach((stream: Stream) => {
|
.sortBy((stream: Stream) => stream.title)
|
||||||
if (!stream.hasBroadcastArea()) {
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
const streamClone = stream.clone()
|
.forEach((stream: Stream) => {
|
||||||
streamClone.groupTitle = 'Undefined'
|
const broadcastAreaCountries = stream.getBroadcastCountries()
|
||||||
groupedStreams.add(streamClone)
|
|
||||||
return
|
if (stream.getBroadcastAreaCodes().isEmpty()) {
|
||||||
}
|
const streamClone = stream.clone()
|
||||||
|
streamClone.groupTitle = 'Undefined'
|
||||||
stream.getBroadcastCountries().forEach((country: Country) => {
|
groupedStreams.add(streamClone)
|
||||||
const streamClone = stream.clone()
|
return
|
||||||
streamClone.groupTitle = country.name
|
}
|
||||||
groupedStreams.add(streamClone)
|
|
||||||
})
|
broadcastAreaCountries.forEach((country: sdk.Models.Country) => {
|
||||||
|
const streamClone = stream.clone()
|
||||||
if (stream.isInternational()) {
|
streamClone.groupTitle = country.name
|
||||||
const streamClone = stream.clone()
|
groupedStreams.add(streamClone)
|
||||||
streamClone.groupTitle = 'International'
|
})
|
||||||
groupedStreams.add(streamClone)
|
|
||||||
}
|
if (stream.isInternational()) {
|
||||||
})
|
const streamClone = stream.clone()
|
||||||
|
streamClone.groupTitle = 'International'
|
||||||
groupedStreams = groupedStreams.orderBy((stream: Stream) => {
|
groupedStreams.add(streamClone)
|
||||||
if (stream.groupTitle === 'International') return 'ZZ'
|
}
|
||||||
if (stream.groupTitle === 'Undefined') return 'ZZZ'
|
})
|
||||||
|
|
||||||
return stream.groupTitle
|
groupedStreams = groupedStreams.sortBy((stream: Stream) => {
|
||||||
})
|
if (stream.groupTitle === 'International') return 'ZZ'
|
||||||
|
if (stream.groupTitle === 'Undefined') return 'ZZZ'
|
||||||
const playlist = new Playlist(groupedStreams, { public: true })
|
|
||||||
const filepath = 'index.country.m3u'
|
return stream.groupTitle
|
||||||
await this.storage.save(filepath, playlist.toString())
|
})
|
||||||
this.logFile.append(
|
|
||||||
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
const playlist = new Playlist(groupedStreams, { public: true })
|
||||||
)
|
const filepath = 'index.country.m3u'
|
||||||
}
|
await this.storage.save(filepath, playlist.toString())
|
||||||
}
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
import { Collection, File, Storage } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Stream, Playlist } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type IndexGeneratorProps = {
|
|
||||||
streams: Collection
|
type IndexGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
logFile: File
|
||||||
|
}
|
||||||
export class IndexGenerator implements Generator {
|
|
||||||
streams: Collection
|
export class IndexGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
storage: Storage
|
||||||
|
logFile: File
|
||||||
constructor({ streams, logFile }: IndexGeneratorProps) {
|
|
||||||
this.streams = streams.clone()
|
constructor({ streams, logFile }: IndexGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
}
|
this.logFile = logFile
|
||||||
|
}
|
||||||
async generate(): Promise<void> {
|
|
||||||
const sfwStreams = this.streams
|
async generate(): Promise<void> {
|
||||||
.orderBy(stream => stream.getTitle())
|
const sfwStreams = this.streams
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
.sortBy(stream => stream.title)
|
||||||
.map((stream: Stream) => {
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
const groupTitle = stream.getCategoryNames().join(';')
|
.map((stream: Stream) => {
|
||||||
if (groupTitle) stream.groupTitle = groupTitle
|
const groupTitle = stream
|
||||||
|
.getCategories()
|
||||||
return stream
|
.map(category => category.name)
|
||||||
})
|
.sort()
|
||||||
|
.join(';')
|
||||||
const playlist = new Playlist(sfwStreams, { public: true })
|
if (groupTitle) stream.groupTitle = groupTitle
|
||||||
const filepath = 'index.m3u'
|
|
||||||
await this.storage.save(filepath, playlist.toString())
|
return stream
|
||||||
this.logFile.append(
|
})
|
||||||
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
|
||||||
)
|
const playlist = new Playlist(sfwStreams, { public: true })
|
||||||
}
|
const filepath = 'index.m3u'
|
||||||
}
|
await this.storage.save(filepath, playlist.toString())
|
||||||
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,54 +1,57 @@
|
|||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Stream, Playlist, Language } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type IndexLanguageGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
logFile: File
|
type IndexLanguageGeneratorProps = {
|
||||||
}
|
streams: Collection<Stream>
|
||||||
|
logFile: File
|
||||||
export class IndexLanguageGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
storage: Storage
|
export class IndexLanguageGenerator implements Generator {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, logFile }: IndexLanguageGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
constructor({ streams, logFile }: IndexLanguageGeneratorProps) {
|
||||||
this.logFile = logFile
|
this.streams = streams.clone()
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
let groupedStreams = new Collection()
|
|
||||||
this.streams
|
async generate(): Promise<void> {
|
||||||
.orderBy((stream: Stream) => stream.getTitle())
|
let groupedStreams = new Collection<Stream>()
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
this.streams
|
||||||
.forEach((stream: Stream) => {
|
.sortBy((stream: Stream) => stream.title)
|
||||||
if (!stream.hasLanguages()) {
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
const streamClone = stream.clone()
|
.forEach((stream: Stream) => {
|
||||||
streamClone.groupTitle = 'Undefined'
|
const streamLanguages = stream.getLanguages()
|
||||||
groupedStreams.add(streamClone)
|
if (streamLanguages.isEmpty()) {
|
||||||
return
|
const streamClone = stream.clone()
|
||||||
}
|
streamClone.groupTitle = 'Undefined'
|
||||||
|
groupedStreams.add(streamClone)
|
||||||
stream.getLanguages().forEach((language: Language) => {
|
return
|
||||||
const streamClone = stream.clone()
|
}
|
||||||
streamClone.groupTitle = language.name
|
|
||||||
groupedStreams.add(streamClone)
|
streamLanguages.forEach((language: sdk.Models.Language) => {
|
||||||
})
|
const streamClone = stream.clone()
|
||||||
})
|
streamClone.groupTitle = language.name
|
||||||
|
groupedStreams.add(streamClone)
|
||||||
groupedStreams = groupedStreams.orderBy((stream: Stream) => {
|
})
|
||||||
if (stream.groupTitle === 'Undefined') return 'ZZ'
|
})
|
||||||
return stream.groupTitle
|
|
||||||
})
|
groupedStreams = groupedStreams.sortBy((stream: Stream) => {
|
||||||
|
if (stream.groupTitle === 'Undefined') return 'ZZ'
|
||||||
const playlist = new Playlist(groupedStreams, { public: true })
|
return stream.groupTitle
|
||||||
const filepath = 'index.language.m3u'
|
})
|
||||||
await this.storage.save(filepath, playlist.toString())
|
|
||||||
this.logFile.append(
|
const playlist = new Playlist(groupedStreams, { public: true })
|
||||||
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
const filepath = 'index.language.m3u'
|
||||||
)
|
await this.storage.save(filepath, playlist.toString())
|
||||||
}
|
this.logFile.append(
|
||||||
}
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Collection, File, Storage } from '@freearhey/core'
|
|
||||||
import { Stream, Playlist } from '../models'
|
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
|
||||||
import { Generator } from './generator'
|
|
||||||
|
|
||||||
type IndexNsfwGeneratorProps = {
|
|
||||||
streams: Collection
|
|
||||||
logFile: File
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IndexNsfwGenerator implements Generator {
|
|
||||||
streams: Collection
|
|
||||||
storage: Storage
|
|
||||||
logFile: File
|
|
||||||
|
|
||||||
constructor({ streams, logFile }: IndexNsfwGeneratorProps) {
|
|
||||||
this.streams = streams.clone()
|
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
|
||||||
this.logFile = logFile
|
|
||||||
}
|
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
|
||||||
const allStreams = this.streams.orderBy((stream: Stream) => stream.getTitle())
|
|
||||||
|
|
||||||
const playlist = new Playlist(allStreams, { public: true })
|
|
||||||
const filepath = 'index.nsfw.m3u'
|
|
||||||
await this.storage.save(filepath, playlist.toString())
|
|
||||||
this.logFile.append(
|
|
||||||
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,58 @@
|
|||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Playlist, Language, Stream } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Playlist, Stream } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type LanguagesGeneratorProps = { streams: Collection; logFile: File }
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
|
||||||
export class LanguagesGenerator implements Generator {
|
type LanguagesGeneratorProps = { streams: Collection<Stream>; logFile: File }
|
||||||
streams: Collection
|
|
||||||
storage: Storage
|
export class LanguagesGenerator implements Generator {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, logFile }: LanguagesGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
constructor({ streams, logFile }: LanguagesGeneratorProps) {
|
||||||
this.logFile = logFile
|
this.streams = streams.clone()
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
const streams = this.streams
|
|
||||||
.orderBy((stream: Stream) => stream.getTitle())
|
async generate(): Promise<void> {
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
const streams: Collection<Stream> = this.streams
|
||||||
|
.sortBy((stream: Stream) => stream.title)
|
||||||
let languages = new Collection()
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
streams.forEach((stream: Stream) => {
|
|
||||||
languages = languages.concat(stream.getLanguages())
|
const languages = new Collection<sdk.Models.Language>()
|
||||||
})
|
streams.forEach((stream: Stream) => {
|
||||||
|
languages.concat(stream.getLanguages())
|
||||||
languages
|
})
|
||||||
.filter(Boolean)
|
|
||||||
.uniqBy((language: Language) => language.code)
|
languages
|
||||||
.orderBy((language: Language) => language.name)
|
.filter(Boolean)
|
||||||
.forEach(async (language: Language) => {
|
.uniqBy((language: sdk.Models.Language) => language.code)
|
||||||
const languageStreams = streams.filter((stream: Stream) => stream.hasLanguage(language))
|
.sortBy((language: sdk.Models.Language) => language.name)
|
||||||
|
.forEach(async (language: sdk.Models.Language) => {
|
||||||
if (languageStreams.isEmpty()) return
|
const languageStreams = streams.filter((stream: Stream) => stream.hasLanguage(language))
|
||||||
|
|
||||||
const playlist = new Playlist(languageStreams, { public: true })
|
if (languageStreams.isEmpty()) return
|
||||||
const filepath = `languages/${language.code}.m3u`
|
|
||||||
await this.storage.save(filepath, playlist.toString())
|
const playlist = new Playlist(languageStreams, { public: true })
|
||||||
this.logFile.append(
|
const filepath = `languages/${language.code}.m3u`
|
||||||
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL
|
await this.storage.save(filepath, playlist.toString())
|
||||||
)
|
this.logFile.append(
|
||||||
})
|
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasLanguages())
|
})
|
||||||
|
|
||||||
if (undefinedStreams.isEmpty()) return
|
const undefinedStreams = streams.filter((stream: Stream) => stream.getLanguages().isEmpty())
|
||||||
|
if (undefinedStreams.isEmpty()) return
|
||||||
const playlist = new Playlist(undefinedStreams, { public: true })
|
|
||||||
const filepath = 'languages/undefined.m3u'
|
const playlist = new Playlist(undefinedStreams, { public: true })
|
||||||
await this.storage.save(filepath, playlist.toString())
|
const filepath = 'languages/undefined.m3u'
|
||||||
this.logFile.append(
|
await this.storage.save(filepath, playlist.toString())
|
||||||
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL
|
this.logFile.append(
|
||||||
)
|
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Stream, Playlist } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type RawGeneratorProps = {
|
|
||||||
streams: Collection
|
type RawGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
logFile: File
|
||||||
|
}
|
||||||
export class RawGenerator implements Generator {
|
|
||||||
streams: Collection
|
export class RawGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
storage: Storage
|
||||||
|
logFile: File
|
||||||
constructor({ streams, logFile }: RawGeneratorProps) {
|
|
||||||
this.streams = streams.clone()
|
constructor({ streams, logFile }: RawGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
}
|
this.logFile = logFile
|
||||||
|
}
|
||||||
async generate() {
|
|
||||||
const files = this.streams.groupBy((stream: Stream) => stream.getFilename())
|
async generate() {
|
||||||
|
const files = this.streams.groupBy((stream: Stream) => stream.getFilename())
|
||||||
for (const filename of files.keys()) {
|
|
||||||
const streams = new Collection(files.get(filename)).map((stream: Stream) => {
|
for (const filename of files.keys()) {
|
||||||
const groupTitle = stream.getCategoryNames().join(';')
|
const streams = new Collection(files.get(filename)).map((stream: Stream) => {
|
||||||
if (groupTitle) stream.groupTitle = groupTitle
|
const groupTitle = stream
|
||||||
|
.getCategories()
|
||||||
return stream
|
.map(category => category.name)
|
||||||
})
|
.sort()
|
||||||
const playlist = new Playlist(streams, { public: true })
|
.join(';')
|
||||||
const filepath = `raw/${filename}`
|
if (groupTitle) stream.groupTitle = groupTitle
|
||||||
await this.storage.save(filepath, playlist.toString())
|
|
||||||
this.logFile.append(
|
return stream
|
||||||
JSON.stringify({ type: 'raw', filepath, count: playlist.streams.count() }) + EOL
|
})
|
||||||
)
|
const playlist = new Playlist(streams, { public: true })
|
||||||
}
|
const filepath = `raw/${filename}`
|
||||||
}
|
await this.storage.save(filepath, playlist.toString())
|
||||||
}
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'raw', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +1,54 @@
|
|||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Playlist, Region, Stream } from '../models'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Playlist, Stream } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type RegionsGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
regions: Collection
|
type RegionsGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
regions: Collection<sdk.Models.Region>
|
||||||
|
logFile: File
|
||||||
export class RegionsGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
regions: Collection
|
export class RegionsGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
regions: Collection<sdk.Models.Region>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, regions, logFile }: RegionsGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.regions = regions
|
constructor({ streams, regions, logFile }: RegionsGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.regions = regions
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
const streams = this.streams
|
|
||||||
.orderBy((stream: Stream) => stream.getTitle())
|
async generate(): Promise<void> {
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
const streams = this.streams
|
||||||
|
.sortBy((stream: Stream) => stream.title)
|
||||||
this.regions.forEach(async (region: Region) => {
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
const regionStreams = streams.filter((stream: Stream) => stream.isBroadcastInRegion(region))
|
|
||||||
|
const streamsGroupedByRegionCode = {}
|
||||||
const playlist = new Playlist(regionStreams, { public: true })
|
streams.forEach((stream: Stream) => {
|
||||||
const filepath = `regions/${region.code.toLowerCase()}.m3u`
|
stream.getBroadcastRegions().forEach((region: sdk.Models.Region) => {
|
||||||
await this.storage.save(filepath, playlist.toString())
|
if (streamsGroupedByRegionCode[region.code]) {
|
||||||
this.logFile.append(
|
streamsGroupedByRegionCode[region.code].add(stream)
|
||||||
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
|
} else {
|
||||||
)
|
streamsGroupedByRegionCode[region.code] = new Collection<Stream>([stream])
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
|
for (const regionCode in streamsGroupedByRegionCode) {
|
||||||
|
const regionStreams = streamsGroupedByRegionCode[regionCode]
|
||||||
|
|
||||||
|
const playlist = new Playlist(regionStreams, { public: true })
|
||||||
|
const filepath = `regions/${regionCode.toLowerCase()}.m3u`
|
||||||
|
await this.storage.save(filepath, playlist.toString())
|
||||||
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +1,49 @@
|
|||||||
import { Collection, Storage, File, type Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Stream, Playlist } from '../models'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { Generator } from './generator'
|
import { Stream, Playlist } from '../models'
|
||||||
|
import { Generator } from './generator'
|
||||||
type SourcesGeneratorProps = {
|
|
||||||
streams: Collection
|
type SourcesGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
logFile: File
|
||||||
|
}
|
||||||
export class SourcesGenerator implements Generator {
|
|
||||||
streams: Collection
|
export class SourcesGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
storage: Storage
|
||||||
|
logFile: File
|
||||||
constructor({ streams, logFile }: SourcesGeneratorProps) {
|
|
||||||
this.streams = streams.clone()
|
constructor({ streams, logFile }: SourcesGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
}
|
this.logFile = logFile
|
||||||
|
}
|
||||||
async generate() {
|
|
||||||
const files: Dictionary = this.streams.groupBy((stream: Stream) => stream.getFilename())
|
async generate() {
|
||||||
|
const files: Dictionary<Stream[]> = this.streams.groupBy((stream: Stream) =>
|
||||||
for (const filename of files.keys()) {
|
stream.getFilename()
|
||||||
if (!filename) continue
|
)
|
||||||
|
|
||||||
let streams = new Collection(files.get(filename))
|
for (const filename of files.keys()) {
|
||||||
streams = streams.map((stream: Stream) => {
|
if (!filename) continue
|
||||||
const groupTitle = stream.getCategoryNames().join(';')
|
|
||||||
if (groupTitle) stream.groupTitle = groupTitle
|
const streams = new Collection<Stream>(files.get(filename)).map((stream: Stream) => {
|
||||||
|
const groupTitle = stream
|
||||||
return stream
|
.getCategories()
|
||||||
})
|
.map(category => category.name)
|
||||||
const playlist = new Playlist(streams, { public: true })
|
.sort()
|
||||||
const filepath = `sources/${filename}`
|
.join(';')
|
||||||
await this.storage.save(filepath, playlist.toString())
|
if (groupTitle) stream.groupTitle = groupTitle
|
||||||
this.logFile.append(
|
|
||||||
JSON.stringify({ type: 'source', filepath, count: playlist.streams.count() }) + EOL
|
return stream
|
||||||
)
|
})
|
||||||
}
|
const playlist = new Playlist(streams, { public: true })
|
||||||
}
|
const filepath = `sources/${filename}`
|
||||||
}
|
await this.storage.save(filepath, playlist.toString())
|
||||||
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'source', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,45 +1,54 @@
|
|||||||
import { Subdivision, Stream, Playlist } from '../models'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { Collection, Storage, File } from '@freearhey/core'
|
import { PUBLIC_DIR, EOL } from '../constants'
|
||||||
import { PUBLIC_DIR, EOL } from '../constants'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Generator } from './generator'
|
||||||
type SubdivisionsGeneratorProps = {
|
import * as sdk from '@iptv-org/sdk'
|
||||||
streams: Collection
|
|
||||||
subdivisions: Collection
|
type SubdivisionsGeneratorProps = {
|
||||||
logFile: File
|
streams: Collection<Stream>
|
||||||
}
|
subdivisions: Collection<sdk.Models.Subdivision>
|
||||||
|
logFile: File
|
||||||
export class SubdivisionsGenerator implements Generator {
|
}
|
||||||
streams: Collection
|
|
||||||
subdivisions: Collection
|
export class SubdivisionsGenerator implements Generator {
|
||||||
storage: Storage
|
streams: Collection<Stream>
|
||||||
logFile: File
|
subdivisions: Collection<sdk.Models.Subdivision>
|
||||||
|
storage: Storage
|
||||||
constructor({ streams, subdivisions, logFile }: SubdivisionsGeneratorProps) {
|
logFile: File
|
||||||
this.streams = streams.clone()
|
|
||||||
this.subdivisions = subdivisions
|
constructor({ streams, subdivisions, logFile }: SubdivisionsGeneratorProps) {
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.streams = streams.clone()
|
||||||
this.logFile = logFile
|
this.subdivisions = subdivisions
|
||||||
}
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
|
this.logFile = logFile
|
||||||
async generate(): Promise<void> {
|
}
|
||||||
const streams = this.streams
|
|
||||||
.orderBy((stream: Stream) => stream.getTitle())
|
async generate(): Promise<void> {
|
||||||
.filter((stream: Stream) => stream.isSFW())
|
const streams = this.streams
|
||||||
|
.sortBy((stream: Stream) => stream.title)
|
||||||
this.subdivisions.forEach(async (subdivision: Subdivision) => {
|
.filter((stream: Stream) => stream.isSFW())
|
||||||
const subdivisionStreams = streams.filter((stream: Stream) =>
|
|
||||||
stream.isBroadcastInSubdivision(subdivision)
|
const streamsGroupedBySubdivisionCode = {}
|
||||||
)
|
streams.forEach((stream: Stream) => {
|
||||||
|
stream.getBroadcastSubdivisions().forEach((subdivision: sdk.Models.Subdivision) => {
|
||||||
if (subdivisionStreams.isEmpty()) return
|
if (streamsGroupedBySubdivisionCode[subdivision.code]) {
|
||||||
|
streamsGroupedBySubdivisionCode[subdivision.code].add(stream)
|
||||||
const playlist = new Playlist(subdivisionStreams, { public: true })
|
} else {
|
||||||
const filepath = `subdivisions/${subdivision.code.toLowerCase()}.m3u`
|
streamsGroupedBySubdivisionCode[subdivision.code] = new Collection<Stream>([stream])
|
||||||
await this.storage.save(filepath, playlist.toString())
|
}
|
||||||
this.logFile.append(
|
})
|
||||||
JSON.stringify({ type: 'subdivision', filepath, count: playlist.streams.count() }) + EOL
|
})
|
||||||
)
|
|
||||||
})
|
for (const subdivisionCode in streamsGroupedBySubdivisionCode) {
|
||||||
}
|
const subdivisionStreams = streamsGroupedBySubdivisionCode[subdivisionCode]
|
||||||
}
|
|
||||||
|
const playlist = new Playlist(subdivisionStreams, { public: true })
|
||||||
|
const filepath = `subdivisions/${subdivisionCode.toLowerCase()}.m3u`
|
||||||
|
await this.storage.save(filepath, playlist.toString())
|
||||||
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'subdivision', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { BlocklistRecordData } from '../types/blocklistRecord'
|
|
||||||
|
|
||||||
export class BlocklistRecord {
|
|
||||||
channelId: string
|
|
||||||
reason: string
|
|
||||||
ref: string
|
|
||||||
|
|
||||||
constructor(data?: BlocklistRecordData) {
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
this.channelId = data.channel
|
|
||||||
this.reason = data.reason
|
|
||||||
this.ref = data.ref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
import { City, Subdivision, Region, Country } from './'
|
|
||||||
|
|
||||||
export class BroadcastArea {
|
|
||||||
codes: Collection
|
|
||||||
citiesIncluded: Collection
|
|
||||||
subdivisionsIncluded: Collection
|
|
||||||
countriesIncluded: Collection
|
|
||||||
regionsIncluded: Collection
|
|
||||||
|
|
||||||
constructor(codes: Collection) {
|
|
||||||
this.codes = codes
|
|
||||||
}
|
|
||||||
|
|
||||||
withLocations(
|
|
||||||
citiesKeyByCode: Dictionary,
|
|
||||||
subdivisionsKeyByCode: Dictionary,
|
|
||||||
countriesKeyByCode: Dictionary,
|
|
||||||
regionsKeyByCode: Dictionary
|
|
||||||
): this {
|
|
||||||
const citiesIncluded = new Collection()
|
|
||||||
const subdivisionsIncluded = new Collection()
|
|
||||||
const countriesIncluded = new Collection()
|
|
||||||
let regionsIncluded = new Collection()
|
|
||||||
|
|
||||||
this.codes.forEach((value: string) => {
|
|
||||||
const [type, code] = value.split('/')
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'ct': {
|
|
||||||
const city: City = citiesKeyByCode.get(code)
|
|
||||||
if (!city) return
|
|
||||||
citiesIncluded.add(city)
|
|
||||||
if (city.subdivision) subdivisionsIncluded.add(city.subdivision)
|
|
||||||
if (city.subdivision && city.subdivision.parent)
|
|
||||||
subdivisionsIncluded.add(city.subdivision.parent)
|
|
||||||
if (city.country) countriesIncluded.add(city.country)
|
|
||||||
regionsIncluded = regionsIncluded.concat(city.getRegions())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 's': {
|
|
||||||
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
|
|
||||||
if (!subdivision) return
|
|
||||||
subdivisionsIncluded.add(subdivision)
|
|
||||||
if (subdivision.country) countriesIncluded.add(subdivision.country)
|
|
||||||
regionsIncluded = regionsIncluded.concat(subdivision.getRegions())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'c': {
|
|
||||||
const country: Country = countriesKeyByCode.get(code)
|
|
||||||
if (!country) return
|
|
||||||
countriesIncluded.add(country)
|
|
||||||
regionsIncluded = regionsIncluded.concat(country.getRegions())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'r': {
|
|
||||||
const region: Region = regionsKeyByCode.get(code)
|
|
||||||
if (!region) return
|
|
||||||
regionsIncluded = regionsIncluded.concat(region.getRegions())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.citiesIncluded = citiesIncluded.uniqBy((city: City) => city.code)
|
|
||||||
this.subdivisionsIncluded = subdivisionsIncluded.uniqBy(
|
|
||||||
(subdivision: Subdivision) => subdivision.code
|
|
||||||
)
|
|
||||||
this.countriesIncluded = countriesIncluded.uniqBy((country: Country) => country.code)
|
|
||||||
this.regionsIncluded = regionsIncluded.uniqBy((region: Region) => region.code)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountries(): Collection {
|
|
||||||
return this.countriesIncluded || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubdivisions(): Collection {
|
|
||||||
return this.subdivisionsIncluded || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
getCities(): Collection {
|
|
||||||
return this.citiesIncluded || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegions(): Collection {
|
|
||||||
return this.regionsIncluded || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
includesCountry(country: Country): boolean {
|
|
||||||
return this.getCountries().includes((_country: Country) => _country.code === country.code)
|
|
||||||
}
|
|
||||||
|
|
||||||
includesSubdivision(subdivision: Subdivision): boolean {
|
|
||||||
return this.getSubdivisions().includes(
|
|
||||||
(_subdivision: Subdivision) => _subdivision.code === subdivision.code
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
includesRegion(region: Region): boolean {
|
|
||||||
return this.getRegions().includes((_region: Region) => _region.code === region.code)
|
|
||||||
}
|
|
||||||
|
|
||||||
includesCity(city: City): boolean {
|
|
||||||
return this.getCities().includes((_city: City) => _city.code === city.code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { CategoryData, CategorySerializedData } from '../types/category'
|
|
||||||
|
|
||||||
export class Category {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
|
|
||||||
constructor(data: CategoryData) {
|
|
||||||
this.id = data.id
|
|
||||||
this.name = data.name
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): CategorySerializedData {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
import { Category, Country, Feed, Guide, Logo, Stream, Subdivision } from './index'
|
|
||||||
import type { ChannelData, ChannelSearchableData, ChannelSerializedData } from '../types/channel'
|
|
||||||
|
|
||||||
export class Channel {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
altNames: Collection
|
|
||||||
network?: string
|
|
||||||
owners: Collection
|
|
||||||
countryCode: string
|
|
||||||
country?: Country
|
|
||||||
subdivisionCode?: string
|
|
||||||
subdivision?: Subdivision
|
|
||||||
cityName?: string
|
|
||||||
categoryIds: Collection
|
|
||||||
categories: Collection = new Collection()
|
|
||||||
isNSFW: boolean
|
|
||||||
launched?: string
|
|
||||||
closed?: string
|
|
||||||
replacedBy?: string
|
|
||||||
isClosed: boolean
|
|
||||||
website?: string
|
|
||||||
feeds?: Collection
|
|
||||||
logos: Collection = new Collection()
|
|
||||||
|
|
||||||
constructor(data?: ChannelData) {
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
this.id = data.id
|
|
||||||
this.name = data.name
|
|
||||||
this.altNames = new Collection(data.alt_names)
|
|
||||||
this.network = data.network || undefined
|
|
||||||
this.owners = new Collection(data.owners)
|
|
||||||
this.countryCode = data.country
|
|
||||||
this.subdivisionCode = data.subdivision || undefined
|
|
||||||
this.cityName = data.city || undefined
|
|
||||||
this.categoryIds = new Collection(data.categories)
|
|
||||||
this.isNSFW = data.is_nsfw
|
|
||||||
this.launched = data.launched || undefined
|
|
||||||
this.closed = data.closed || undefined
|
|
||||||
this.replacedBy = data.replaced_by || undefined
|
|
||||||
this.website = data.website || undefined
|
|
||||||
this.isClosed = !!data.closed || !!data.replaced_by
|
|
||||||
}
|
|
||||||
|
|
||||||
withSubdivision(subdivisionsKeyByCode: Dictionary): this {
|
|
||||||
if (!this.subdivisionCode) return this
|
|
||||||
|
|
||||||
this.subdivision = subdivisionsKeyByCode.get(this.subdivisionCode)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withCountry(countriesKeyByCode: Dictionary): this {
|
|
||||||
this.country = countriesKeyByCode.get(this.countryCode)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withCategories(categoriesKeyById: Dictionary): this {
|
|
||||||
this.categories = this.categoryIds
|
|
||||||
.map((id: string) => categoriesKeyById.get(id))
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountry(): Country | undefined {
|
|
||||||
return this.country
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubdivision(): Subdivision | undefined {
|
|
||||||
return this.subdivision
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategories(): Collection {
|
|
||||||
return this.categories || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
hasCategories(): boolean {
|
|
||||||
return !!this.categories && this.categories.notEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
hasCategory(category: Category): boolean {
|
|
||||||
return (
|
|
||||||
!!this.categories &&
|
|
||||||
this.categories.includes((_category: Category) => _category.id === category.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
getStreamTitles(): Collection {
|
|
||||||
return this.getStreams()
|
|
||||||
.map((stream: Stream) => stream.getTitle())
|
|
||||||
.uniq()
|
|
||||||
}
|
|
||||||
|
|
||||||
getFeedFullNames(): Collection {
|
|
||||||
return this.getFeeds()
|
|
||||||
.map((feed: Feed) => feed.getFullName())
|
|
||||||
.uniq()
|
|
||||||
}
|
|
||||||
|
|
||||||
isSFW(): boolean {
|
|
||||||
return this.isNSFW === false
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = { 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
getSearchable(): ChannelSearchableData {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
altNames: this.altNames.all(),
|
|
||||||
guideNames: this.getGuideNames().all(),
|
|
||||||
streamTitles: this.getStreamTitles().all(),
|
|
||||||
feedFullNames: this.getFeedFullNames().all()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): ChannelSerializedData {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
altNames: this.altNames.all(),
|
|
||||||
network: this.network,
|
|
||||||
owners: this.owners.all(),
|
|
||||||
countryCode: this.countryCode,
|
|
||||||
country: this.country ? this.country.serialize() : undefined,
|
|
||||||
subdivisionCode: this.subdivisionCode,
|
|
||||||
subdivision: this.subdivision ? this.subdivision.serialize() : undefined,
|
|
||||||
cityName: this.cityName,
|
|
||||||
categoryIds: this.categoryIds.all(),
|
|
||||||
categories: this.categories.map((category: Category) => category.serialize()).all(),
|
|
||||||
isNSFW: this.isNSFW,
|
|
||||||
launched: this.launched,
|
|
||||||
closed: this.closed,
|
|
||||||
replacedBy: this.replacedBy,
|
|
||||||
website: this.website
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(data: ChannelSerializedData): this {
|
|
||||||
this.id = data.id
|
|
||||||
this.name = data.name
|
|
||||||
this.altNames = new Collection(data.altNames)
|
|
||||||
this.network = data.network
|
|
||||||
this.owners = new Collection(data.owners)
|
|
||||||
this.countryCode = data.countryCode
|
|
||||||
this.country = data.country ? new Country().deserialize(data.country) : undefined
|
|
||||||
this.subdivisionCode = data.subdivisionCode
|
|
||||||
this.cityName = data.cityName
|
|
||||||
this.categoryIds = new Collection(data.categoryIds)
|
|
||||||
this.isNSFW = data.isNSFW
|
|
||||||
this.launched = data.launched
|
|
||||||
this.closed = data.closed
|
|
||||||
this.replacedBy = data.replacedBy
|
|
||||||
this.website = data.website
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
import { Country, Region, Subdivision } from '.'
|
|
||||||
import type { CityData, CitySerializedData } from '../types/city'
|
|
||||||
|
|
||||||
export class City {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
countryCode: string
|
|
||||||
country?: Country
|
|
||||||
subdivisionCode?: string
|
|
||||||
subdivision?: Subdivision
|
|
||||||
wikidataId: string
|
|
||||||
regions?: Collection
|
|
||||||
|
|
||||||
constructor(data?: CityData) {
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.countryCode = data.country
|
|
||||||
this.subdivisionCode = data.subdivision || undefined
|
|
||||||
this.wikidataId = data.wikidata_id
|
|
||||||
}
|
|
||||||
|
|
||||||
withCountry(countriesKeyByCode: Dictionary): this {
|
|
||||||
this.country = countriesKeyByCode.get(this.countryCode)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withSubdivision(subdivisionsKeyByCode: Dictionary): this {
|
|
||||||
if (!this.subdivisionCode) return this
|
|
||||||
|
|
||||||
this.subdivision = subdivisionsKeyByCode.get(this.subdivisionCode)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withRegions(regions: Collection): this {
|
|
||||||
this.regions = regions.filter((region: Region) =>
|
|
||||||
region.countryCodes.includes(this.countryCode)
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegions(): Collection {
|
|
||||||
if (!this.regions) return new Collection()
|
|
||||||
|
|
||||||
return this.regions
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): CitySerializedData {
|
|
||||||
return {
|
|
||||||
code: this.code,
|
|
||||||
name: this.name,
|
|
||||||
countryCode: this.countryCode,
|
|
||||||
country: this.country ? this.country.serialize() : undefined,
|
|
||||||
subdivisionCode: this.subdivisionCode || null,
|
|
||||||
subdivision: this.subdivision ? this.subdivision.serialize() : undefined,
|
|
||||||
wikidataId: this.wikidataId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(data: CitySerializedData): this {
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.countryCode = data.countryCode
|
|
||||||
this.country = data.country ? new Country().deserialize(data.country) : undefined
|
|
||||||
this.subdivisionCode = data.subdivisionCode || undefined
|
|
||||||
this.subdivision = data.subdivision
|
|
||||||
? new Subdivision().deserialize(data.subdivision)
|
|
||||||
: undefined
|
|
||||||
this.wikidataId = data.wikidataId
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
import { Region, Language, Subdivision } from '.'
|
|
||||||
import type { CountryData, CountrySerializedData } from '../types/country'
|
|
||||||
import { SubdivisionSerializedData } from '../types/subdivision'
|
|
||||||
import { RegionSerializedData } from '../types/region'
|
|
||||||
|
|
||||||
export class Country {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
flag: string
|
|
||||||
languageCode: string
|
|
||||||
language?: Language
|
|
||||||
subdivisions?: Collection
|
|
||||||
regions?: Collection
|
|
||||||
cities?: Collection
|
|
||||||
|
|
||||||
constructor(data?: CountryData) {
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.flag = data.flag
|
|
||||||
this.languageCode = data.lang
|
|
||||||
}
|
|
||||||
|
|
||||||
withSubdivisions(subdivisionsGroupedByCountryCode: Dictionary): this {
|
|
||||||
this.subdivisions = new Collection(subdivisionsGroupedByCountryCode.get(this.code))
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withRegions(regions: Collection): this {
|
|
||||||
this.regions = regions.filter((region: Region) => region.includesCountryCode(this.code))
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withCities(citiesGroupedByCountryCode: Dictionary): this {
|
|
||||||
this.cities = new Collection(citiesGroupedByCountryCode.get(this.code))
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withLanguage(languagesKeyByCode: Dictionary): this {
|
|
||||||
this.language = languagesKeyByCode.get(this.languageCode)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getLanguage(): Language | undefined {
|
|
||||||
return this.language
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegions(): Collection {
|
|
||||||
return this.regions || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubdivisions(): Collection {
|
|
||||||
return this.subdivisions || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
getCities(): Collection {
|
|
||||||
return this.cities || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): CountrySerializedData {
|
|
||||||
return {
|
|
||||||
code: this.code,
|
|
||||||
name: this.name,
|
|
||||||
flag: this.flag,
|
|
||||||
languageCode: this.languageCode,
|
|
||||||
language: this.language ? this.language.serialize() : null,
|
|
||||||
subdivisions: this.subdivisions
|
|
||||||
? this.subdivisions.map((subdivision: Subdivision) => subdivision.serialize()).all()
|
|
||||||
: [],
|
|
||||||
regions: this.regions ? this.regions.map((region: Region) => region.serialize()).all() : []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(data: CountrySerializedData): this {
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.flag = data.flag
|
|
||||||
this.languageCode = data.languageCode
|
|
||||||
this.language = data.language ? new Language().deserialize(data.language) : undefined
|
|
||||||
this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) =>
|
|
||||||
new Subdivision().deserialize(data)
|
|
||||||
)
|
|
||||||
this.regions = new Collection(data.regions).map((data: RegionSerializedData) =>
|
|
||||||
new Region().deserialize(data)
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import { Country, Language, Region, Channel, Subdivision, BroadcastArea, City } from './index'
|
|
||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
import type { FeedData } from '../types/feed'
|
|
||||||
|
|
||||||
export class Feed {
|
|
||||||
channelId: string
|
|
||||||
channel?: Channel
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
isMain: boolean
|
|
||||||
broadcastAreaCodes: Collection
|
|
||||||
broadcastArea?: BroadcastArea
|
|
||||||
languageCodes: Collection
|
|
||||||
languages?: Collection
|
|
||||||
timezoneIds: Collection
|
|
||||||
timezones?: Collection
|
|
||||||
videoFormat: string
|
|
||||||
guides?: Collection
|
|
||||||
streams?: Collection
|
|
||||||
|
|
||||||
constructor(data: FeedData) {
|
|
||||||
this.channelId = data.channel
|
|
||||||
this.id = data.id
|
|
||||||
this.name = data.name
|
|
||||||
this.isMain = data.is_main
|
|
||||||
this.broadcastAreaCodes = new Collection(data.broadcast_area)
|
|
||||||
this.languageCodes = new Collection(data.languages)
|
|
||||||
this.timezoneIds = new Collection(data.timezones)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
withLanguages(languagesKeyByCode: Dictionary): this {
|
|
||||||
this.languages = this.languageCodes
|
|
||||||
.map((code: string) => languagesKeyByCode.get(code))
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withTimezones(timezonesKeyById: Dictionary): this {
|
|
||||||
this.timezones = this.timezoneIds.map((id: string) => timezonesKeyById.get(id)).filter(Boolean)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withBroadcastArea(
|
|
||||||
citiesKeyByCode: Dictionary,
|
|
||||||
subdivisionsKeyByCode: Dictionary,
|
|
||||||
countriesKeyByCode: Dictionary,
|
|
||||||
regionsKeyByCode: Dictionary
|
|
||||||
): this {
|
|
||||||
this.broadcastArea = new BroadcastArea(this.broadcastAreaCodes).withLocations(
|
|
||||||
citiesKeyByCode,
|
|
||||||
subdivisionsKeyByCode,
|
|
||||||
countriesKeyByCode,
|
|
||||||
regionsKeyByCode
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
hasBroadcastArea(): boolean {
|
|
||||||
return !!this.broadcastArea
|
|
||||||
}
|
|
||||||
|
|
||||||
getBroadcastCountries(): Collection {
|
|
||||||
if (!this.broadcastArea) return new Collection()
|
|
||||||
|
|
||||||
return this.broadcastArea.getCountries()
|
|
||||||
}
|
|
||||||
|
|
||||||
getBroadcastRegions(): Collection {
|
|
||||||
if (!this.broadcastArea) return new Collection()
|
|
||||||
|
|
||||||
return this.broadcastArea.getRegions()
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimezones(): Collection {
|
|
||||||
return this.timezones || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
getLanguages(): Collection {
|
|
||||||
return this.languages || new Collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
hasLanguages(): boolean {
|
|
||||||
return !!this.languages && this.languages.notEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
hasLanguage(language: Language): boolean {
|
|
||||||
return (
|
|
||||||
!!this.languages &&
|
|
||||||
this.languages.includes((_language: Language) => _language.code === language.code)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
isBroadcastInCity(city: City): boolean {
|
|
||||||
if (!this.broadcastArea) return false
|
|
||||||
|
|
||||||
return this.broadcastArea.includesCity(city)
|
|
||||||
}
|
|
||||||
|
|
||||||
isBroadcastInSubdivision(subdivision: Subdivision): boolean {
|
|
||||||
if (!this.broadcastArea) return false
|
|
||||||
|
|
||||||
return this.broadcastArea.includesSubdivision(subdivision)
|
|
||||||
}
|
|
||||||
|
|
||||||
isBroadcastInCountry(country: Country): boolean {
|
|
||||||
if (!this.broadcastArea) return false
|
|
||||||
|
|
||||||
return this.broadcastArea.includesCountry(country)
|
|
||||||
}
|
|
||||||
|
|
||||||
isBroadcastInRegion(region: Region): boolean {
|
|
||||||
if (!this.broadcastArea) return false
|
|
||||||
|
|
||||||
return this.broadcastArea.includesRegion(region)
|
|
||||||
}
|
|
||||||
|
|
||||||
isInternational(): boolean {
|
|
||||||
if (!this.broadcastArea) return false
|
|
||||||
|
|
||||||
return this.broadcastArea.codes.join(',').includes('r/')
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import type { GuideData, GuideSerializedData } from '../types/guide'
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return this.getStreamId() + this.siteId
|
|
||||||
}
|
|
||||||
|
|
||||||
getStreamId(): string | undefined {
|
|
||||||
if (!this.channelId) return undefined
|
|
||||||
if (!this.feedId) return this.channelId
|
|
||||||
|
|
||||||
return `${this.channelId}@${this.feedId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): GuideSerializedData {
|
|
||||||
return {
|
|
||||||
channelId: this.channelId,
|
|
||||||
feedId: this.feedId,
|
|
||||||
siteDomain: this.siteDomain,
|
|
||||||
siteId: this.siteId,
|
|
||||||
siteName: this.siteName,
|
|
||||||
languageCode: this.languageCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(data: GuideSerializedData): this {
|
|
||||||
this.channelId = data.channelId
|
|
||||||
this.feedId = data.feedId
|
|
||||||
this.siteDomain = data.siteDomain
|
|
||||||
this.siteId = data.siteId
|
|
||||||
this.siteName = data.siteName
|
|
||||||
this.languageCode = data.languageCode
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
export * from './blocklistRecord'
|
export * from './issue'
|
||||||
export * from './broadcastArea'
|
export * from './playlist'
|
||||||
export * from './category'
|
export * from './stream'
|
||||||
export * from './channel'
|
|
||||||
export * from './city'
|
|
||||||
export * from './country'
|
|
||||||
export * from './feed'
|
|
||||||
export * from './guide'
|
|
||||||
export * from './issue'
|
|
||||||
export * from './language'
|
|
||||||
export * from './logo'
|
|
||||||
export * from './playlist'
|
|
||||||
export * from './region'
|
|
||||||
export * from './stream'
|
|
||||||
export * from './subdivision'
|
|
||||||
export * from './timezone'
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { LanguageData, LanguageSerializedData } from '../types/language'
|
|
||||||
|
|
||||||
export class Language {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
|
|
||||||
constructor(data?: LanguageData) {
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): LanguageSerializedData {
|
|
||||||
return {
|
|
||||||
code: this.code,
|
|
||||||
name: this.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(data: LanguageSerializedData): this {
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
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(feedsKeyById: Dictionary): this {
|
|
||||||
if (!this.feedId) return this
|
|
||||||
|
|
||||||
this.feed = feedsKeyById.get(this.feedId)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getStreamId(): string {
|
|
||||||
if (!this.feedId) return this.channelId
|
|
||||||
|
|
||||||
return `${this.channelId}@${this.feedId}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Stream } from '../models'
|
import { Stream } from '../models'
|
||||||
|
|
||||||
type PlaylistOptions = {
|
type PlaylistOptions = {
|
||||||
public: boolean
|
public: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Playlist {
|
export class Playlist {
|
||||||
streams: Collection
|
streams: Collection<Stream>
|
||||||
options: {
|
options: {
|
||||||
public: boolean
|
public: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(streams: Collection, options?: PlaylistOptions) {
|
constructor(streams: Collection<Stream>, options?: PlaylistOptions) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.options = options || { public: false }
|
this.options = options || { public: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
let output = '#EXTM3U\r\n'
|
let output = '#EXTM3U\r\n'
|
||||||
|
|
||||||
this.streams.forEach((stream: Stream) => {
|
this.streams.forEach((stream: Stream) => {
|
||||||
output += stream.toString(this.options) + '\r\n'
|
output += stream.toString(this.options) + '\r\n'
|
||||||
})
|
})
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
import { City, Country, Subdivision } from '.'
|
|
||||||
import type { RegionData, RegionSerializedData } from '../types/region'
|
|
||||||
import { CountrySerializedData } from '../types/country'
|
|
||||||
import { SubdivisionSerializedData } from '../types/subdivision'
|
|
||||||
import { CitySerializedData } from '../types/city'
|
|
||||||
|
|
||||||
export class Region {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
countryCodes: Collection
|
|
||||||
countries?: Collection
|
|
||||||
subdivisions?: Collection
|
|
||||||
cities?: Collection
|
|
||||||
regions?: Collection
|
|
||||||
|
|
||||||
constructor(data?: RegionData) {
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.countryCodes = new Collection(data.countries)
|
|
||||||
}
|
|
||||||
|
|
||||||
withCountries(countriesKeyByCode: Dictionary): this {
|
|
||||||
this.countries = this.countryCodes.map((code: string) => countriesKeyByCode.get(code))
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withSubdivisions(subdivisions: Collection): this {
|
|
||||||
this.subdivisions = subdivisions.filter(
|
|
||||||
(subdivision: Subdivision) => this.countryCodes.indexOf(subdivision.countryCode) > -1
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withCities(cities: Collection): this {
|
|
||||||
this.cities = cities.filter((city: City) => this.countryCodes.indexOf(city.countryCode) > -1)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withRegions(regions: Collection): this {
|
|
||||||
this.regions = regions.filter(
|
|
||||||
(region: Region) => !region.countryCodes.intersects(this.countryCodes).isEmpty()
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubdivisions(): Collection {
|
|
||||||
if (!this.subdivisions) return new Collection()
|
|
||||||
|
|
||||||
return this.subdivisions
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountries(): Collection {
|
|
||||||
if (!this.countries) return new Collection()
|
|
||||||
|
|
||||||
return this.countries
|
|
||||||
}
|
|
||||||
|
|
||||||
getCities(): Collection {
|
|
||||||
if (!this.cities) return new Collection()
|
|
||||||
|
|
||||||
return this.cities
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegions(): Collection {
|
|
||||||
if (!this.regions) return new Collection()
|
|
||||||
|
|
||||||
return this.regions
|
|
||||||
}
|
|
||||||
|
|
||||||
includesCountryCode(code: string): boolean {
|
|
||||||
return this.countryCodes.includes((countryCode: string) => countryCode === code)
|
|
||||||
}
|
|
||||||
|
|
||||||
isWorldwide(): boolean {
|
|
||||||
return ['INT', 'WW'].includes(this.code)
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): RegionSerializedData {
|
|
||||||
return {
|
|
||||||
code: this.code,
|
|
||||||
name: this.name,
|
|
||||||
countryCodes: this.countryCodes.all(),
|
|
||||||
countries: this.getCountries()
|
|
||||||
.map((country: Country) => country.serialize())
|
|
||||||
.all(),
|
|
||||||
subdivisions: this.getSubdivisions()
|
|
||||||
.map((subdivision: Subdivision) => subdivision.serialize())
|
|
||||||
.all(),
|
|
||||||
cities: this.getCities()
|
|
||||||
.map((city: City) => city.serialize())
|
|
||||||
.all()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(data: RegionSerializedData): this {
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.countryCodes = new Collection(data.countryCodes)
|
|
||||||
this.countries = new Collection(data.countries).map((data: CountrySerializedData) =>
|
|
||||||
new Country().deserialize(data)
|
|
||||||
)
|
|
||||||
this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) =>
|
|
||||||
new Subdivision().deserialize(data)
|
|
||||||
)
|
|
||||||
this.cities = new Collection(data.cities).map((data: CitySerializedData) =>
|
|
||||||
new City().deserialize(data)
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,474 +1,461 @@
|
|||||||
import {
|
import { Collection } from '@freearhey/core'
|
||||||
Feed,
|
import parser from 'iptv-playlist-parser'
|
||||||
Channel,
|
import { normalizeURL } from '../utils'
|
||||||
Category,
|
import * as sdk from '@iptv-org/sdk'
|
||||||
Region,
|
import { IssueData } from '../core'
|
||||||
Subdivision,
|
import { data } from '../api'
|
||||||
Country,
|
import path from 'node:path'
|
||||||
Language,
|
|
||||||
Logo,
|
export class Stream extends sdk.Models.Stream {
|
||||||
City
|
directives: Collection<string>
|
||||||
} from './index'
|
filepath?: string
|
||||||
import { URL, Collection, Dictionary } from '@freearhey/core'
|
line?: number
|
||||||
import type { StreamData } from '../types/stream'
|
groupTitle: string = 'Undefined'
|
||||||
import parser from 'iptv-playlist-parser'
|
removed: boolean = false
|
||||||
import { IssueData } from '../core'
|
tvgId?: string
|
||||||
import path from 'node:path'
|
label: string | null
|
||||||
|
|
||||||
export class Stream {
|
updateWithIssue(issueData: IssueData): this {
|
||||||
title: string
|
const data = {
|
||||||
url: string
|
label: issueData.getString('label'),
|
||||||
id?: string
|
quality: issueData.getString('quality'),
|
||||||
channelId?: string
|
httpUserAgent: issueData.getString('httpUserAgent'),
|
||||||
channel?: Channel
|
httpReferrer: issueData.getString('httpReferrer'),
|
||||||
feedId?: string
|
newStreamUrl: issueData.getString('newStreamUrl'),
|
||||||
feed?: Feed
|
directives: issueData.getArray('directives')
|
||||||
logos: Collection = new Collection()
|
}
|
||||||
filepath?: string
|
|
||||||
line?: number
|
if (data.label !== undefined) this.label = data.label
|
||||||
label?: string
|
if (data.quality !== undefined) this.quality = data.quality
|
||||||
verticalResolution?: number
|
if (data.httpUserAgent !== undefined) this.user_agent = data.httpUserAgent
|
||||||
isInterlaced?: boolean
|
if (data.httpReferrer !== undefined) this.referrer = data.httpReferrer
|
||||||
referrer?: string
|
if (data.newStreamUrl !== undefined) this.url = data.newStreamUrl
|
||||||
userAgent?: string
|
if (data.directives !== undefined) this.setDirectives(data.directives)
|
||||||
groupTitle: string = 'Undefined'
|
|
||||||
removed: boolean = false
|
return this
|
||||||
directives: Collection = new Collection()
|
}
|
||||||
|
|
||||||
constructor(data?: StreamData) {
|
static fromPlaylistItem(data: parser.PlaylistItem): Stream {
|
||||||
if (!data) return
|
function escapeRegExp(text) {
|
||||||
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
|
||||||
const id =
|
}
|
||||||
data.channelId && data.feedId ? [data.channelId, data.feedId].join('@') : data.channelId
|
|
||||||
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
|
function parseName(name: string): {
|
||||||
|
title: string
|
||||||
this.id = id || undefined
|
label: string
|
||||||
this.channelId = data.channelId || undefined
|
quality: string
|
||||||
this.feedId = data.feedId || undefined
|
} {
|
||||||
this.title = data.title || ''
|
let title = name
|
||||||
this.url = data.url
|
const [, label] = title.match(/ \[(.*)\]$/) || [null, '']
|
||||||
this.referrer = data.referrer || undefined
|
title = title.replace(new RegExp(` \\[${escapeRegExp(label)}\\]$`), '')
|
||||||
this.userAgent = data.userAgent || undefined
|
const [, quality] = title.match(/ \(([0-9]+[p|i])\)$/) || [null, '']
|
||||||
this.verticalResolution = verticalResolution || undefined
|
title = title.replace(new RegExp(` \\(${quality}\\)$`), '')
|
||||||
this.isInterlaced = isInterlaced || undefined
|
|
||||||
this.label = data.label || undefined
|
return { title, label, quality }
|
||||||
this.directives = new Collection(data.directives)
|
}
|
||||||
}
|
|
||||||
|
function parseDirectives(string: string): Collection<string> {
|
||||||
update(issueData: IssueData): this {
|
const directives = new Collection<string>()
|
||||||
const data = {
|
|
||||||
label: issueData.getString('label'),
|
if (!string) return directives
|
||||||
quality: issueData.getString('quality'),
|
|
||||||
httpUserAgent: issueData.getString('httpUserAgent'),
|
const supportedDirectives = ['#EXTVLCOPT', '#KODIPROP']
|
||||||
httpReferrer: issueData.getString('httpReferrer'),
|
const lines = string.split('\r\n')
|
||||||
newStreamUrl: issueData.getString('newStreamUrl'),
|
const regex = new RegExp(`^${supportedDirectives.join('|')}`, 'i')
|
||||||
directives: issueData.getArray('directives')
|
|
||||||
}
|
lines.forEach((line: string) => {
|
||||||
|
if (regex.test(line)) {
|
||||||
if (data.label !== undefined) this.label = data.label
|
directives.add(line.trim())
|
||||||
if (data.quality !== undefined) this.setQuality(data.quality)
|
}
|
||||||
if (data.httpUserAgent !== undefined) this.userAgent = data.httpUserAgent
|
})
|
||||||
if (data.httpReferrer !== undefined) this.referrer = data.httpReferrer
|
|
||||||
if (data.newStreamUrl !== undefined) this.url = data.newStreamUrl
|
return directives
|
||||||
if (data.directives !== undefined) this.directives = new Collection(data.directives)
|
}
|
||||||
|
|
||||||
return this
|
if (!data.name) throw new Error('"name" property is required')
|
||||||
}
|
if (!data.url) throw new Error('"url" property is required')
|
||||||
|
|
||||||
fromPlaylistItem(data: parser.PlaylistItem): this {
|
const [channelId, feedId] = data.tvg.id.split('@')
|
||||||
function parseName(name: string): {
|
const { title, label, quality } = parseName(data.name)
|
||||||
title: string
|
|
||||||
label: string
|
const stream = new Stream({
|
||||||
quality: string
|
channel: channelId || null,
|
||||||
} {
|
feed: feedId || null,
|
||||||
let title = name
|
title: title,
|
||||||
const [, label] = title.match(/ \[(.*)\]$/) || [null, '']
|
quality: quality || null,
|
||||||
title = title.replace(new RegExp(` \\[${escapeRegExp(label)}\\]$`), '')
|
url: data.url,
|
||||||
const [, quality] = title.match(/ \(([0-9]+p)\)$/) || [null, '']
|
referrer: data.http.referrer || null,
|
||||||
title = title.replace(new RegExp(` \\(${quality}\\)$`), '')
|
user_agent: data.http['user-agent'] || null
|
||||||
|
})
|
||||||
return { title, label, quality }
|
|
||||||
}
|
stream.tvgId = data.tvg.id
|
||||||
|
stream.line = data.line
|
||||||
function parseDirectives(string: string) {
|
stream.label = label || null
|
||||||
const directives = new Collection()
|
stream.directives = parseDirectives(data.raw)
|
||||||
|
|
||||||
if (!string) return directives
|
return stream
|
||||||
|
}
|
||||||
const supportedDirectives = ['#EXTVLCOPT', '#KODIPROP']
|
|
||||||
const lines = string.split('\r\n')
|
isSFW(): boolean {
|
||||||
const regex = new RegExp(`^${supportedDirectives.join('|')}`, 'i')
|
const channel = this.getChannel()
|
||||||
|
|
||||||
lines.forEach((line: string) => {
|
if (!channel) return true
|
||||||
if (regex.test(line)) {
|
|
||||||
directives.add(line.trim())
|
return !channel.is_nsfw
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
getUniqKey(): string {
|
||||||
return directives
|
const filepath = this.getFilepath()
|
||||||
}
|
const tvgId = this.getTvgId()
|
||||||
|
|
||||||
if (!data.name) throw new Error('"name" property is required')
|
return filepath + tvgId + this.url
|
||||||
if (!data.url) throw new Error('"url" property is required')
|
}
|
||||||
|
|
||||||
const [channelId, feedId] = data.tvg.id.split('@')
|
getVerticalResolution(): number {
|
||||||
const { title, label, quality } = parseName(data.name)
|
if (!this.quality) return 0
|
||||||
const { verticalResolution, isInterlaced } = parseQuality(quality)
|
|
||||||
|
const [, verticalResolutionString] = this.quality.match(/^(\d+)/) || ['', '0']
|
||||||
this.id = data.tvg.id || undefined
|
|
||||||
this.feedId = feedId || undefined
|
return parseInt(verticalResolutionString)
|
||||||
this.channelId = channelId || undefined
|
}
|
||||||
this.line = data.line
|
|
||||||
this.label = label || undefined
|
getBroadcastCountries(): Collection<sdk.Models.Country> {
|
||||||
this.title = title
|
const countries = new Collection<sdk.Models.Country>()
|
||||||
this.verticalResolution = verticalResolution || undefined
|
|
||||||
this.isInterlaced = isInterlaced || undefined
|
const feed = this.getFeed()
|
||||||
this.url = data.url
|
if (!feed) return countries
|
||||||
this.referrer = data.http.referrer || undefined
|
|
||||||
this.userAgent = data.http['user-agent'] || undefined
|
feed
|
||||||
this.directives = parseDirectives(data.raw)
|
.getBroadcastArea()
|
||||||
|
.getLocations()
|
||||||
return this
|
.forEach((location: sdk.Models.BroadcastAreaLocation) => {
|
||||||
}
|
let country: sdk.Models.Country | undefined
|
||||||
|
switch (location.type) {
|
||||||
withChannel(channelsKeyById: Dictionary): this {
|
case 'country': {
|
||||||
if (!this.channelId) return this
|
country = data.countriesKeyByCode.get(location.code)
|
||||||
|
break
|
||||||
this.channel = channelsKeyById.get(this.channelId)
|
}
|
||||||
|
case 'subdivision': {
|
||||||
return this
|
const subdivision = data.subdivisionsKeyByCode.get(location.code)
|
||||||
}
|
if (!subdivision) break
|
||||||
|
country = data.countriesKeyByCode.get(subdivision.country)
|
||||||
withFeed(feedsGroupedByChannelId: Dictionary): this {
|
break
|
||||||
if (!this.channelId) return this
|
}
|
||||||
|
case 'city': {
|
||||||
const channelFeeds = feedsGroupedByChannelId.get(this.channelId) || []
|
const city = data.citiesKeyByCode.get(location.code)
|
||||||
if (this.feedId) this.feed = channelFeeds.find((feed: Feed) => feed.id === this.feedId)
|
if (!city) break
|
||||||
if (!this.feedId && !this.feed) this.feed = channelFeeds.find((feed: Feed) => feed.isMain)
|
country = data.countriesKeyByCode.get(city.country)
|
||||||
|
break
|
||||||
return this
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withLogos(logosGroupedByStreamId: Dictionary): this {
|
if (country) countries.add(country)
|
||||||
if (this.id) this.logos = new Collection(logosGroupedByStreamId.get(this.id))
|
})
|
||||||
|
|
||||||
return this
|
return countries.uniqBy((country: sdk.Models.Country) => country.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
setId(id: string): this {
|
getBroadcastSubdivisions(): Collection<sdk.Models.Subdivision> {
|
||||||
this.id = id
|
const subdivisions = new Collection<sdk.Models.Subdivision>()
|
||||||
|
|
||||||
return this
|
const feed = this.getFeed()
|
||||||
}
|
if (!feed) return subdivisions
|
||||||
|
|
||||||
setChannelId(channelId: string): this {
|
feed
|
||||||
this.channelId = channelId
|
.getBroadcastArea()
|
||||||
|
.getLocations()
|
||||||
return this
|
.forEach((location: sdk.Models.BroadcastAreaLocation) => {
|
||||||
}
|
switch (location.type) {
|
||||||
|
case 'subdivision': {
|
||||||
setFeedId(feedId: string | undefined): this {
|
const subdivision = data.subdivisionsKeyByCode.get(location.code)
|
||||||
this.feedId = feedId
|
if (!subdivision) break
|
||||||
|
subdivisions.add(subdivision)
|
||||||
return this
|
if (!subdivision.parent) break
|
||||||
}
|
const parentSubdivision = data.subdivisionsKeyByCode.get(subdivision.parent)
|
||||||
|
if (!parentSubdivision) break
|
||||||
setQuality(quality: string): this {
|
subdivisions.add(parentSubdivision)
|
||||||
const { verticalResolution, isInterlaced } = parseQuality(quality)
|
break
|
||||||
|
}
|
||||||
this.verticalResolution = verticalResolution || undefined
|
case 'city': {
|
||||||
this.isInterlaced = isInterlaced || undefined
|
const city = data.citiesKeyByCode.get(location.code)
|
||||||
|
if (!city || !city.subdivision) break
|
||||||
return this
|
const subdivision = data.subdivisionsKeyByCode.get(city.subdivision)
|
||||||
}
|
if (!subdivision) break
|
||||||
|
subdivisions.add(subdivision)
|
||||||
getLine(): number {
|
if (!subdivision.parent) break
|
||||||
return this.line || -1
|
const parentSubdivision = data.subdivisionsKeyByCode.get(subdivision.parent)
|
||||||
}
|
if (!parentSubdivision) break
|
||||||
|
subdivisions.add(parentSubdivision)
|
||||||
getFilename(): string {
|
break
|
||||||
if (!this.filepath) return ''
|
}
|
||||||
|
}
|
||||||
return path.basename(this.filepath)
|
})
|
||||||
}
|
|
||||||
|
return subdivisions.uniqBy((subdivision: sdk.Models.Subdivision) => subdivision.code)
|
||||||
setFilepath(filepath: string): this {
|
}
|
||||||
this.filepath = filepath
|
|
||||||
|
getBroadcastCities(): Collection<sdk.Models.City> {
|
||||||
return this
|
const cities = new Collection<sdk.Models.City>()
|
||||||
}
|
|
||||||
|
const feed = this.getFeed()
|
||||||
updateFilepath(): this {
|
if (!feed) return cities
|
||||||
if (!this.channel) return this
|
|
||||||
|
feed
|
||||||
this.filepath = `${this.channel.countryCode.toLowerCase()}.m3u`
|
.getBroadcastArea()
|
||||||
|
.getLocations()
|
||||||
return this
|
.forEach((location: sdk.Models.BroadcastAreaLocation) => {
|
||||||
}
|
if (location.type !== 'city') return
|
||||||
|
|
||||||
getChannelId(): string {
|
const city = data.citiesKeyByCode.get(location.code)
|
||||||
return this.channelId || ''
|
|
||||||
}
|
if (city) cities.add(city)
|
||||||
|
})
|
||||||
getFeedId(): string {
|
|
||||||
if (this.feedId) return this.feedId
|
return cities.uniqBy((city: sdk.Models.City) => city.code)
|
||||||
if (this.feed) return this.feed.id
|
}
|
||||||
return ''
|
|
||||||
}
|
getBroadcastRegions(): Collection<sdk.Models.Region> {
|
||||||
|
const regions = new Collection<sdk.Models.Region>()
|
||||||
getFilepath(): string {
|
|
||||||
return this.filepath || ''
|
const feed = this.getFeed()
|
||||||
}
|
if (!feed) return regions
|
||||||
|
|
||||||
getReferrer(): string {
|
feed
|
||||||
return this.referrer || ''
|
.getBroadcastArea()
|
||||||
}
|
.getLocations()
|
||||||
|
.forEach((location: sdk.Models.BroadcastAreaLocation) => {
|
||||||
getUserAgent(): string {
|
switch (location.type) {
|
||||||
return this.userAgent || ''
|
case 'region': {
|
||||||
}
|
const region = data.regionsKeyByCode.get(location.code)
|
||||||
|
if (!region) break
|
||||||
getQuality(): string {
|
regions.add(region)
|
||||||
if (!this.verticalResolution) return ''
|
|
||||||
|
const relatedRegions = data.regions.filter((_region: sdk.Models.Region) =>
|
||||||
let quality = this.verticalResolution.toString()
|
new Collection<string>(_region.countries)
|
||||||
|
.intersects(new Collection<string>(region.countries))
|
||||||
if (this.isInterlaced) quality += 'i'
|
.isNotEmpty()
|
||||||
else quality += 'p'
|
)
|
||||||
|
regions.concat(relatedRegions)
|
||||||
return quality
|
break
|
||||||
}
|
}
|
||||||
|
case 'country': {
|
||||||
hasId(): boolean {
|
const country = data.countriesKeyByCode.get(location.code)
|
||||||
return !!this.id
|
if (!country) break
|
||||||
}
|
const countryRegions = data.regions.filter((_region: sdk.Models.Region) =>
|
||||||
|
new Collection<string>(_region.countries).includes(
|
||||||
hasQuality(): boolean {
|
(code: string) => code === country.code
|
||||||
return !!this.verticalResolution
|
)
|
||||||
}
|
)
|
||||||
|
regions.concat(countryRegions)
|
||||||
getVerticalResolution(): number {
|
break
|
||||||
if (!this.hasQuality()) return 0
|
}
|
||||||
|
case 'subdivision': {
|
||||||
return parseInt(this.getQuality().replace(/p|i/, ''))
|
const subdivision = data.subdivisionsKeyByCode.get(location.code)
|
||||||
}
|
if (!subdivision) break
|
||||||
|
const subdivisionRegions = data.regions.filter((_region: sdk.Models.Region) =>
|
||||||
updateTitle(): this {
|
new Collection<string>(_region.countries).includes(
|
||||||
if (!this.channel) return this
|
(code: string) => code === subdivision.country
|
||||||
|
)
|
||||||
this.title = this.channel.name
|
)
|
||||||
if (this.feed && !this.feed.isMain) {
|
regions.concat(subdivisionRegions)
|
||||||
this.title += ` ${this.feed.name}`
|
break
|
||||||
}
|
}
|
||||||
|
case 'city': {
|
||||||
return this
|
const city = data.citiesKeyByCode.get(location.code)
|
||||||
}
|
if (!city) break
|
||||||
|
const cityRegions = data.regions.filter((_region: sdk.Models.Region) =>
|
||||||
updateId(): this {
|
new Collection<string>(_region.countries).includes(
|
||||||
if (!this.channel) return this
|
(code: string) => code === city.country
|
||||||
if (this.feed) {
|
)
|
||||||
this.id = `${this.channel.id}@${this.feed.id}`
|
)
|
||||||
} else {
|
regions.concat(cityRegions)
|
||||||
this.id = this.channel.id
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return this
|
})
|
||||||
}
|
|
||||||
|
return regions.uniqBy((region: sdk.Models.Region) => region.code)
|
||||||
normalizeURL() {
|
}
|
||||||
const url = new URL(this.url)
|
|
||||||
|
isInternational(): boolean {
|
||||||
this.url = url.normalize().toString()
|
const feed = this.getFeed()
|
||||||
}
|
if (!feed) return false
|
||||||
|
|
||||||
clone(): Stream {
|
const broadcastAreaCodes = feed.getBroadcastArea().codes
|
||||||
return Object.assign(Object.create(Object.getPrototypeOf(this)), this)
|
if (broadcastAreaCodes.join(';').includes('r/')) return true
|
||||||
}
|
if (broadcastAreaCodes.filter(code => code.includes('c/')).length > 1) return true
|
||||||
|
|
||||||
hasChannel() {
|
return false
|
||||||
return !!this.channel
|
}
|
||||||
}
|
|
||||||
|
hasCategory(category: sdk.Models.Category): boolean {
|
||||||
getBroadcastRegions(): Collection {
|
const channel = this.getChannel()
|
||||||
return this.feed ? this.feed.getBroadcastRegions() : new Collection()
|
|
||||||
}
|
if (!channel) return false
|
||||||
|
|
||||||
getBroadcastCountries(): Collection {
|
const found = channel.categories.find((id: string) => id === category.id)
|
||||||
return this.feed ? this.feed.getBroadcastCountries() : new Collection()
|
|
||||||
}
|
return !!found
|
||||||
|
}
|
||||||
hasBroadcastArea(): boolean {
|
|
||||||
return this.feed ? this.feed.hasBroadcastArea() : false
|
hasLanguage(language: sdk.Models.Language): boolean {
|
||||||
}
|
const found = this.getLanguages().find(
|
||||||
|
(_language: sdk.Models.Language) => _language.code === language.code
|
||||||
isSFW(): boolean {
|
)
|
||||||
return this.channel ? this.channel.isSFW() : true
|
|
||||||
}
|
return !!found
|
||||||
|
}
|
||||||
hasCategories(): boolean {
|
|
||||||
return this.channel ? this.channel.hasCategories() : false
|
setDirectives(directives: string[]): this {
|
||||||
}
|
this.directives = new Collection(directives).filter((directive: string) =>
|
||||||
|
/^(#KODIPROP|#EXTVLCOPT)/.test(directive)
|
||||||
hasCategory(category: Category): boolean {
|
)
|
||||||
return this.channel ? this.channel.hasCategory(category) : false
|
|
||||||
}
|
return this
|
||||||
|
}
|
||||||
getCategoryNames(): string[] {
|
|
||||||
return this.getCategories()
|
updateTvgId(): this {
|
||||||
.map((category: Category) => category.name)
|
if (!this.channel) return this
|
||||||
.sort()
|
if (this.feed) {
|
||||||
.all()
|
this.tvgId = `${this.channel}@${this.feed}`
|
||||||
}
|
} else {
|
||||||
|
this.tvgId = this.channel
|
||||||
getCategories(): Collection {
|
}
|
||||||
return this.channel ? this.channel.getCategories() : new Collection()
|
|
||||||
}
|
return this
|
||||||
|
}
|
||||||
getLanguages(): Collection {
|
|
||||||
return this.feed ? this.feed.getLanguages() : new Collection()
|
updateFilepath(): this {
|
||||||
}
|
const channel = this.getChannel()
|
||||||
|
if (!channel) return this
|
||||||
hasLanguages() {
|
|
||||||
return this.feed ? this.feed.hasLanguages() : false
|
this.filepath = `${channel.country.toLowerCase()}.m3u`
|
||||||
}
|
|
||||||
|
return this
|
||||||
hasLanguage(language: Language) {
|
}
|
||||||
return this.feed ? this.feed.hasLanguage(language) : false
|
|
||||||
}
|
updateTitle(): this {
|
||||||
|
const channel = this.getChannel()
|
||||||
getBroadcastAreaCodes(): Collection {
|
|
||||||
return this.feed ? this.feed.broadcastAreaCodes : new Collection()
|
if (!channel) return this
|
||||||
}
|
|
||||||
|
const feed = this.getFeed()
|
||||||
isBroadcastInCity(city: City): boolean {
|
|
||||||
return this.feed ? this.feed.isBroadcastInCity(city) : false
|
this.title = channel.name
|
||||||
}
|
if (feed && !feed.is_main) {
|
||||||
|
this.title += ` ${feed.name}`
|
||||||
isBroadcastInSubdivision(subdivision: Subdivision): boolean {
|
}
|
||||||
return this.feed ? this.feed.isBroadcastInSubdivision(subdivision) : false
|
|
||||||
}
|
return this
|
||||||
|
}
|
||||||
isBroadcastInCountry(country: Country): boolean {
|
|
||||||
return this.feed ? this.feed.isBroadcastInCountry(country) : false
|
normalizeURL() {
|
||||||
}
|
this.url = normalizeURL(this.url)
|
||||||
|
}
|
||||||
isBroadcastInRegion(region: Region): boolean {
|
|
||||||
return this.feed ? this.feed.isBroadcastInRegion(region) : false
|
getLogos(): Collection<sdk.Models.Logo> {
|
||||||
}
|
const logos = super.getLogos()
|
||||||
|
|
||||||
isInternational(): boolean {
|
if (logos.isEmpty()) return new Collection()
|
||||||
return this.feed ? this.feed.isInternational() : false
|
|
||||||
}
|
function format(logo: sdk.Models.Logo): number {
|
||||||
|
const levelByFormat = { SVG: 0, PNG: 3, APNG: 1, WebP: 1, AVIF: 1, JPEG: 2, GIF: 1 }
|
||||||
getLogos(): Collection {
|
|
||||||
function format(logo: Logo): number {
|
return logo.format ? levelByFormat[logo.format] : 0
|
||||||
const levelByFormat = { SVG: 0, PNG: 3, APNG: 1, WebP: 1, AVIF: 1, JPEG: 2, GIF: 1 }
|
}
|
||||||
|
|
||||||
return logo.format ? levelByFormat[logo.format] : 0
|
function size(logo: sdk.Models.Logo): number {
|
||||||
}
|
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
||||||
|
}
|
||||||
function size(logo: Logo): number {
|
|
||||||
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
return logos.sortBy([format, size], ['desc', 'asc'], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
|
getFilepath(): string {
|
||||||
}
|
return this.filepath || ''
|
||||||
|
}
|
||||||
getLogo(): Logo | undefined {
|
|
||||||
return this.getLogos().first()
|
getFilename(): string {
|
||||||
}
|
return path.basename(this.getFilepath())
|
||||||
|
}
|
||||||
hasLogo(): boolean {
|
|
||||||
return this.getLogos().notEmpty()
|
getLine(): number {
|
||||||
}
|
return this.line || -1
|
||||||
|
}
|
||||||
getLogoUrl(): string {
|
|
||||||
let logo: Logo | undefined
|
getTvgId(): string {
|
||||||
|
if (this.tvgId) return this.tvgId
|
||||||
if (this.hasLogo()) logo = this.getLogo()
|
|
||||||
else logo = this?.channel?.getLogo()
|
return this.getId()
|
||||||
|
}
|
||||||
return logo ? logo.url : ''
|
|
||||||
}
|
getTvgLogo(): string {
|
||||||
|
const logo = this.getLogos().first()
|
||||||
getTitle(): string {
|
|
||||||
return this.title || ''
|
return logo ? logo.url : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getFullTitle(): string {
|
getFullTitle(): string {
|
||||||
let title = `${this.getTitle()}`
|
let title = `${this.title}`
|
||||||
|
|
||||||
if (this.getQuality()) {
|
if (this.quality) {
|
||||||
title += ` (${this.getQuality()})`
|
title += ` (${this.quality})`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.label) {
|
if (this.label) {
|
||||||
title += ` [${this.label}]`
|
title += ` [${this.label}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabel(): string {
|
toString(options: { public?: boolean } = {}) {
|
||||||
return this.label || ''
|
options = { ...{ public: false }, ...options }
|
||||||
}
|
|
||||||
|
let output = `#EXTINF:-1 tvg-id="${this.getTvgId()}"`
|
||||||
getId(): string {
|
|
||||||
return this.id || ''
|
if (options.public) {
|
||||||
}
|
output += ` tvg-logo="${this.getTvgLogo()}" group-title="${this.groupTitle}"`
|
||||||
|
}
|
||||||
toJSON() {
|
|
||||||
return {
|
if (this.referrer) {
|
||||||
channel: this.channelId || null,
|
output += ` http-referrer="${this.referrer}"`
|
||||||
feed: this.feedId || null,
|
}
|
||||||
title: this.title,
|
|
||||||
url: this.url,
|
if (this.user_agent) {
|
||||||
referrer: this.referrer || null,
|
output += ` http-user-agent="${this.user_agent}"`
|
||||||
user_agent: this.userAgent || null,
|
}
|
||||||
quality: this.getQuality() || null
|
|
||||||
}
|
output += `,${this.getFullTitle()}`
|
||||||
}
|
|
||||||
|
this.directives.forEach((prop: string) => {
|
||||||
toString(options: { public: boolean }) {
|
output += `\r\n${prop}`
|
||||||
let output = `#EXTINF:-1 tvg-id="${this.getId()}"`
|
})
|
||||||
|
|
||||||
if (options.public) {
|
output += `\r\n${this.url}`
|
||||||
output += ` tvg-logo="${this.getLogoUrl()}" group-title="${this.groupTitle}"`
|
|
||||||
}
|
return output
|
||||||
|
}
|
||||||
if (this.referrer) {
|
|
||||||
output += ` http-referrer="${this.referrer}"`
|
toObject(): sdk.Types.StreamData {
|
||||||
}
|
let feedId = this.feed
|
||||||
|
if (!feedId) {
|
||||||
if (this.userAgent) {
|
const feed = this.getFeed()
|
||||||
output += ` http-user-agent="${this.userAgent}"`
|
if (feed) feedId = feed.id
|
||||||
}
|
}
|
||||||
|
|
||||||
output += `,${this.getFullTitle()}`
|
return {
|
||||||
|
channel: this.channel,
|
||||||
this.directives.forEach((prop: string) => {
|
feed: feedId,
|
||||||
output += `\r\n${prop}`
|
title: this.title,
|
||||||
})
|
url: this.url,
|
||||||
|
quality: this.quality,
|
||||||
output += `\r\n${this.url}`
|
user_agent: this.user_agent,
|
||||||
|
referrer: this.referrer
|
||||||
return output
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
clone(): Stream {
|
||||||
function escapeRegExp(text) {
|
return Object.assign(Object.create(Object.getPrototypeOf(this)), this)
|
||||||
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import { SubdivisionData, SubdivisionSerializedData } from '../types/subdivision'
|
|
||||||
import { Dictionary, Collection } from '@freearhey/core'
|
|
||||||
import { Country, Region } from '.'
|
|
||||||
|
|
||||||
export class Subdivision {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
countryCode: string
|
|
||||||
country?: Country
|
|
||||||
parentCode?: string
|
|
||||||
parent?: Subdivision
|
|
||||||
regions?: Collection
|
|
||||||
cities?: Collection
|
|
||||||
|
|
||||||
constructor(data?: SubdivisionData) {
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.countryCode = data.country
|
|
||||||
this.parentCode = data.parent || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
withCountry(countriesKeyByCode: Dictionary): this {
|
|
||||||
this.country = countriesKeyByCode.get(this.countryCode)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withRegions(regions: Collection): this {
|
|
||||||
this.regions = regions.filter((region: Region) =>
|
|
||||||
region.countryCodes.includes(this.countryCode)
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withCities(citiesGroupedBySubdivisionCode: Dictionary): this {
|
|
||||||
this.cities = new Collection(citiesGroupedBySubdivisionCode.get(this.code))
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
withParent(subdivisionsKeyByCode: Dictionary): this {
|
|
||||||
if (!this.parentCode) return this
|
|
||||||
|
|
||||||
this.parent = subdivisionsKeyByCode.get(this.parentCode)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegions(): Collection {
|
|
||||||
if (!this.regions) return new Collection()
|
|
||||||
|
|
||||||
return this.regions
|
|
||||||
}
|
|
||||||
|
|
||||||
getCities(): Collection {
|
|
||||||
if (!this.cities) return new Collection()
|
|
||||||
|
|
||||||
return this.cities
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): SubdivisionSerializedData {
|
|
||||||
return {
|
|
||||||
code: this.code,
|
|
||||||
name: this.name,
|
|
||||||
countryCode: this.countryCode,
|
|
||||||
country: this.country ? this.country.serialize() : undefined,
|
|
||||||
parentCode: this.parentCode || null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(data: SubdivisionSerializedData): this {
|
|
||||||
this.code = data.code
|
|
||||||
this.name = data.name
|
|
||||||
this.countryCode = data.countryCode
|
|
||||||
this.country = data.country ? new Country().deserialize(data.country) : undefined
|
|
||||||
this.parentCode = data.parentCode || undefined
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
|
|
||||||
type TimezoneData = {
|
|
||||||
id: string
|
|
||||||
utc_offset: string
|
|
||||||
countries: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Timezone {
|
|
||||||
id: string
|
|
||||||
utcOffset: string
|
|
||||||
countryCodes: Collection
|
|
||||||
countries?: Collection
|
|
||||||
|
|
||||||
constructor(data: TimezoneData) {
|
|
||||||
this.id = data.id
|
|
||||||
this.utcOffset = data.utc_offset
|
|
||||||
this.countryCodes = new Collection(data.countries)
|
|
||||||
}
|
|
||||||
|
|
||||||
withCountries(countriesKeyByCode: Dictionary): this {
|
|
||||||
this.countries = this.countryCodes.map((code: string) => countriesKeyByCode.get(code))
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountries(): Collection {
|
|
||||||
return this.countries || new Collection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,63 @@
|
|||||||
import { Storage, Collection, File, Dictionary } from '@freearhey/core'
|
import { HTMLTable, HTMLTableItem, LogParser, LogItem, HTMLTableColumn } from '../core'
|
||||||
import { HTMLTable, LogParser, LogItem } from '../core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { LOGS_DIR, README_DIR } from '../constants'
|
import { LOGS_DIR, README_DIR } from '../constants'
|
||||||
import { Category } from '../models'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Table } from './table'
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
import { Table } from './table'
|
||||||
type CategoriesTableProps = {
|
import { data } from '../api'
|
||||||
categoriesKeyById: Dictionary
|
|
||||||
}
|
export class CategoriesTable implements Table {
|
||||||
|
async create() {
|
||||||
export class CategoriesTable implements Table {
|
const parser = new LogParser()
|
||||||
categoriesKeyById: Dictionary
|
const logsStorage = new Storage(LOGS_DIR)
|
||||||
|
const generatorsLog = await logsStorage.load('generators.log')
|
||||||
constructor({ categoriesKeyById }: CategoriesTableProps) {
|
|
||||||
this.categoriesKeyById = categoriesKeyById
|
let items = new Collection<HTMLTableItem>()
|
||||||
}
|
parser
|
||||||
|
.parse(generatorsLog)
|
||||||
async make() {
|
.filter((logItem: LogItem) => logItem.type === 'category')
|
||||||
const parser = new LogParser()
|
.forEach((logItem: LogItem) => {
|
||||||
const logsStorage = new Storage(LOGS_DIR)
|
if (logItem.filepath.includes('undefined')) {
|
||||||
const generatorsLog = await logsStorage.load('generators.log')
|
items.add([
|
||||||
|
'ZZ',
|
||||||
let items = new Collection()
|
'Undefined',
|
||||||
parser
|
logItem.count.toString(),
|
||||||
.parse(generatorsLog)
|
`<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>`
|
||||||
.filter((logItem: LogItem) => logItem.type === 'category')
|
])
|
||||||
.forEach((logItem: LogItem) => {
|
|
||||||
const file = new File(logItem.filepath)
|
return
|
||||||
const categoryId = file.name()
|
}
|
||||||
const category: Category = this.categoriesKeyById.get(categoryId)
|
|
||||||
|
const file = new File(logItem.filepath)
|
||||||
items.add([
|
const categoryId = file.name()
|
||||||
category ? category.name : 'ZZ',
|
const category: sdk.Models.Category | undefined = data.categoriesKeyById.get(categoryId)
|
||||||
category ? category.name : 'Undefined',
|
|
||||||
logItem.count,
|
if (!category) return
|
||||||
`<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>`
|
|
||||||
])
|
items.add([
|
||||||
})
|
category.name,
|
||||||
|
category.name,
|
||||||
items = items
|
logItem.count.toString(),
|
||||||
.orderBy(item => item[0])
|
`<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>`
|
||||||
.map(item => {
|
])
|
||||||
item.shift()
|
})
|
||||||
return item
|
|
||||||
})
|
items = items
|
||||||
|
.sortBy(item => item[0])
|
||||||
const table = new HTMLTable(items.all(), [
|
.map(item => {
|
||||||
{ name: 'Category' },
|
item.shift()
|
||||||
{ name: 'Channels', align: 'right' },
|
return item
|
||||||
{ name: 'Playlist', nowrap: true }
|
})
|
||||||
])
|
|
||||||
|
const columns = new Collection<HTMLTableColumn>([
|
||||||
const readmeStorage = new Storage(README_DIR)
|
{ name: 'Category' },
|
||||||
await readmeStorage.save('_categories.md', table.toString())
|
{ name: 'Channels', align: 'right' },
|
||||||
}
|
{ name: 'Playlist', nowrap: true }
|
||||||
}
|
])
|
||||||
|
|
||||||
|
const table = new HTMLTable(items, columns)
|
||||||
|
|
||||||
|
const readmeStorage = new Storage(README_DIR)
|
||||||
|
await readmeStorage.save('_categories.md', table.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,189 +1,176 @@
|
|||||||
import { Storage, Collection, Dictionary } from '@freearhey/core'
|
import { LOGS_DIR, README_DIR } from '../constants'
|
||||||
import { City, Country, Subdivision } from '../models'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { LOGS_DIR, README_DIR } from '../constants'
|
import { Collection } from '@freearhey/core'
|
||||||
import { LogParser, LogItem } from '../core'
|
import { LogParser, LogItem } from '../core'
|
||||||
import { Table } from './table'
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
import { Table } from './table'
|
||||||
type CountriesTableProps = {
|
import { data } from '../api'
|
||||||
countriesKeyByCode: Dictionary
|
|
||||||
subdivisionsKeyByCode: Dictionary
|
type ListItem = {
|
||||||
countries: Collection
|
index: string
|
||||||
subdivisions: Collection
|
count: number
|
||||||
cities: Collection
|
link: string
|
||||||
}
|
name: string
|
||||||
|
children: Collection<ListItem>
|
||||||
export class CountriesTable implements Table {
|
}
|
||||||
countriesKeyByCode: Dictionary
|
|
||||||
subdivisionsKeyByCode: Dictionary
|
export class CountriesTable implements Table {
|
||||||
countries: Collection
|
async create() {
|
||||||
subdivisions: Collection
|
const parser = new LogParser()
|
||||||
cities: Collection
|
const logsStorage = new Storage(LOGS_DIR)
|
||||||
|
const generatorsLog = await logsStorage.load('generators.log')
|
||||||
constructor({
|
const parsed = parser.parse(generatorsLog)
|
||||||
countriesKeyByCode,
|
const logCountries = parsed.filter((logItem: LogItem) => logItem.type === 'country')
|
||||||
subdivisionsKeyByCode,
|
const logSubdivisions = parsed.filter((logItem: LogItem) => logItem.type === 'subdivision')
|
||||||
countries,
|
const logCities = parsed.filter((logItem: LogItem) => logItem.type === 'city')
|
||||||
subdivisions,
|
|
||||||
cities
|
let items = new Collection()
|
||||||
}: CountriesTableProps) {
|
data.countries.forEach((country: sdk.Models.Country) => {
|
||||||
this.countriesKeyByCode = countriesKeyByCode
|
const countryCode = country.code
|
||||||
this.subdivisionsKeyByCode = subdivisionsKeyByCode
|
const countriesLogItem = logCountries.find(
|
||||||
this.countries = countries
|
(logItem: LogItem) => logItem.filepath === `countries/${countryCode.toLowerCase()}.m3u`
|
||||||
this.subdivisions = subdivisions
|
)
|
||||||
this.cities = cities
|
|
||||||
}
|
const countryItem: ListItem = {
|
||||||
|
index: country.name,
|
||||||
async make() {
|
count: 0,
|
||||||
const parser = new LogParser()
|
link: `https://iptv-org.github.io/iptv/countries/${countryCode.toLowerCase()}.m3u`,
|
||||||
const logsStorage = new Storage(LOGS_DIR)
|
name: `${country.flag} ${country.name}`,
|
||||||
const generatorsLog = await logsStorage.load('generators.log')
|
children: new Collection()
|
||||||
const parsed = parser.parse(generatorsLog)
|
}
|
||||||
const logCountries = parsed.filter((logItem: LogItem) => logItem.type === 'country')
|
|
||||||
const logSubdivisions = parsed.filter((logItem: LogItem) => logItem.type === 'subdivision')
|
if (countriesLogItem) {
|
||||||
const logCities = parsed.filter((logItem: LogItem) => logItem.type === 'city')
|
countryItem.count = countriesLogItem.count
|
||||||
|
}
|
||||||
let items = new Collection()
|
|
||||||
this.countries.forEach((country: Country) => {
|
const countrySubdivisions = data.subdivisions.filter(
|
||||||
const countriesLogItem = logCountries.find(
|
(subdivision: sdk.Models.Subdivision) => subdivision.country === countryCode
|
||||||
(logItem: LogItem) => logItem.filepath === `countries/${country.code.toLowerCase()}.m3u`
|
)
|
||||||
)
|
const countryCities = data.cities.filter(
|
||||||
|
(city: sdk.Models.City) => city.country === countryCode
|
||||||
const countryItem = {
|
)
|
||||||
index: country.name,
|
if (countrySubdivisions.isNotEmpty()) {
|
||||||
count: 0,
|
data.subdivisions.forEach((subdivision: sdk.Models.Subdivision) => {
|
||||||
link: `https://iptv-org.github.io/iptv/countries/${country.code.toLowerCase()}.m3u`,
|
if (subdivision.country !== countryCode) return
|
||||||
name: `${country.flag} ${country.name}`,
|
|
||||||
children: new Collection()
|
const subdivisionCode = subdivision.code
|
||||||
}
|
const subdivisionCities = countryCities.filter(
|
||||||
|
(city: sdk.Models.City) =>
|
||||||
if (countriesLogItem) {
|
(city.subdivision && city.subdivision === subdivisionCode) ||
|
||||||
countryItem.count = countriesLogItem.count
|
city.country === subdivision.country
|
||||||
}
|
)
|
||||||
|
const subdivisionsLogItem = logSubdivisions.find(
|
||||||
const countrySubdivisions = this.subdivisions.filter(
|
(logItem: LogItem) =>
|
||||||
(subdivision: Subdivision) => subdivision.countryCode === country.code
|
logItem.filepath === `subdivisions/${subdivisionCode.toLowerCase()}.m3u`
|
||||||
)
|
)
|
||||||
const countryCities = this.cities.filter((city: City) => city.countryCode === country.code)
|
|
||||||
if (countrySubdivisions.notEmpty()) {
|
const subdivisionItem: ListItem = {
|
||||||
this.subdivisions.forEach((subdivision: Subdivision) => {
|
index: subdivision.name,
|
||||||
if (subdivision.countryCode !== country.code) return
|
name: subdivision.name,
|
||||||
const subdivisionCities = countryCities.filter(
|
count: 0,
|
||||||
(city: City) =>
|
link: `https://iptv-org.github.io/iptv/subdivisions/${subdivisionCode.toLowerCase()}.m3u`,
|
||||||
(city.subdivisionCode && city.subdivisionCode === subdivision.code) ||
|
children: new Collection<ListItem>()
|
||||||
city.countryCode === subdivision.countryCode
|
}
|
||||||
)
|
|
||||||
const subdivisionsLogItem = logSubdivisions.find(
|
if (subdivisionsLogItem) {
|
||||||
(logItem: LogItem) =>
|
subdivisionItem.count = subdivisionsLogItem.count
|
||||||
logItem.filepath === `subdivisions/${subdivision.code.toLowerCase()}.m3u`
|
}
|
||||||
)
|
|
||||||
|
subdivisionCities.forEach((city: sdk.Models.City) => {
|
||||||
const subdivisionItem = {
|
if (city.country !== countryCode || city.subdivision !== subdivisionCode) return
|
||||||
index: subdivision.name,
|
const citiesLogItem = logCities.find(
|
||||||
name: subdivision.name,
|
(logItem: LogItem) => logItem.filepath === `cities/${city.code.toLowerCase()}.m3u`
|
||||||
count: 0,
|
)
|
||||||
link: `https://iptv-org.github.io/iptv/subdivisions/${subdivision.code.toLowerCase()}.m3u`,
|
|
||||||
children: new Collection()
|
if (!citiesLogItem) return
|
||||||
}
|
|
||||||
|
subdivisionItem.children.add({
|
||||||
if (subdivisionsLogItem) {
|
index: city.name,
|
||||||
subdivisionItem.count = subdivisionsLogItem.count
|
name: city.name,
|
||||||
}
|
count: citiesLogItem.count,
|
||||||
|
link: `https://iptv-org.github.io/iptv/${citiesLogItem.filepath}`,
|
||||||
subdivisionCities.forEach((city: City) => {
|
children: new Collection<ListItem>()
|
||||||
if (city.countryCode !== country.code || city.subdivisionCode !== subdivision.code)
|
})
|
||||||
return
|
})
|
||||||
const citiesLogItem = logCities.find(
|
|
||||||
(logItem: LogItem) => logItem.filepath === `cities/${city.code.toLowerCase()}.m3u`
|
if (subdivisionItem.count > 0 || subdivisionItem.children.isNotEmpty()) {
|
||||||
)
|
countryItem.children.add(subdivisionItem)
|
||||||
|
}
|
||||||
if (!citiesLogItem) return
|
})
|
||||||
|
} else if (countryCities.isNotEmpty()) {
|
||||||
subdivisionItem.children.add({
|
countryCities.forEach((city: sdk.Models.City) => {
|
||||||
index: city.name,
|
const citiesLogItem = logCities.find(
|
||||||
name: city.name,
|
(logItem: LogItem) => logItem.filepath === `cities/${city.code.toLowerCase()}.m3u`
|
||||||
count: citiesLogItem.count,
|
)
|
||||||
link: `https://iptv-org.github.io/iptv/${citiesLogItem.filepath}`
|
|
||||||
})
|
if (!citiesLogItem) return
|
||||||
})
|
|
||||||
|
countryItem.children.add({
|
||||||
if (subdivisionItem.count > 0 || subdivisionItem.children.notEmpty()) {
|
index: city.name,
|
||||||
countryItem.children.add(subdivisionItem)
|
name: city.name,
|
||||||
}
|
count: citiesLogItem.count,
|
||||||
})
|
link: `https://iptv-org.github.io/iptv/${citiesLogItem.filepath}`,
|
||||||
} else if (countryCities.notEmpty()) {
|
children: new Collection()
|
||||||
countryCities.forEach((city: City) => {
|
})
|
||||||
const citiesLogItem = logCities.find(
|
})
|
||||||
(logItem: LogItem) => logItem.filepath === `cities/${city.code.toLowerCase()}.m3u`
|
}
|
||||||
)
|
|
||||||
|
if (countryItem.count > 0 || countryItem.children.isNotEmpty()) {
|
||||||
if (!citiesLogItem) return
|
items.add(countryItem)
|
||||||
|
}
|
||||||
countryItem.children.add({
|
})
|
||||||
index: city.name,
|
|
||||||
name: city.name,
|
const internationalLogItem = logCountries.find(
|
||||||
count: citiesLogItem.count,
|
(logItem: LogItem) => logItem.filepath === 'countries/int.m3u'
|
||||||
link: `https://iptv-org.github.io/iptv/${citiesLogItem.filepath}`,
|
)
|
||||||
children: new Collection()
|
|
||||||
})
|
if (internationalLogItem) {
|
||||||
})
|
items.add({
|
||||||
}
|
index: 'ZZ',
|
||||||
|
name: '🌐 International',
|
||||||
if (countryItem.count > 0 || countryItem.children.notEmpty()) {
|
count: internationalLogItem.count,
|
||||||
items.add(countryItem)
|
link: `https://iptv-org.github.io/iptv/${internationalLogItem.filepath}`,
|
||||||
}
|
children: new Collection()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
const internationalLogItem = logCountries.find(
|
|
||||||
(logItem: LogItem) => logItem.filepath === 'countries/int.m3u'
|
const undefinedLogItem = logCountries.find(
|
||||||
)
|
(logItem: LogItem) => logItem.filepath === 'countries/undefined.m3u'
|
||||||
|
)
|
||||||
if (internationalLogItem) {
|
|
||||||
items.push({
|
if (undefinedLogItem) {
|
||||||
index: 'ZZ',
|
items.add({
|
||||||
name: '🌐 International',
|
index: 'ZZZ',
|
||||||
count: internationalLogItem.count,
|
name: 'Undefined',
|
||||||
link: `https://iptv-org.github.io/iptv/${internationalLogItem.filepath}`,
|
count: undefinedLogItem.count,
|
||||||
children: new Collection()
|
link: `https://iptv-org.github.io/iptv/${undefinedLogItem.filepath}`,
|
||||||
})
|
children: new Collection()
|
||||||
}
|
})
|
||||||
|
}
|
||||||
const undefinedLogItem = logCountries.find(
|
|
||||||
(logItem: LogItem) => logItem.filepath === 'countries/undefined.m3u'
|
items = items.sortBy(item => item.index)
|
||||||
)
|
|
||||||
|
const output = items
|
||||||
if (undefinedLogItem) {
|
.map((item: ListItem) => {
|
||||||
items.push({
|
let row = `- ${item.name} <code>${item.link}</code>`
|
||||||
index: 'ZZZ',
|
|
||||||
name: 'Undefined',
|
item.children
|
||||||
count: undefinedLogItem.count,
|
.sortBy((item: ListItem) => item.index)
|
||||||
link: `https://iptv-org.github.io/iptv/${undefinedLogItem.filepath}`,
|
.forEach((item: ListItem) => {
|
||||||
children: new Collection()
|
row += `\r\n - ${item.name} <code>${item.link}</code>`
|
||||||
})
|
|
||||||
}
|
item.children
|
||||||
|
.sortBy((item: ListItem) => item.index)
|
||||||
items = items.orderBy(item => item.index)
|
.forEach((item: ListItem) => {
|
||||||
|
row += `\r\n - ${item.name} <code>${item.link}</code>`
|
||||||
const output = items
|
})
|
||||||
.map(item => {
|
})
|
||||||
let row = `- ${item.name} <code>${item.link}</code>`
|
|
||||||
|
return row
|
||||||
item.children
|
})
|
||||||
.orderBy(item => item.index)
|
.join('\r\n')
|
||||||
.forEach(item => {
|
|
||||||
row += `\r\n - ${item.name} <code>${item.link}</code>`
|
const readmeStorage = new Storage(README_DIR)
|
||||||
|
await readmeStorage.save('_countries.md', output)
|
||||||
item.children
|
}
|
||||||
.orderBy(item => item.index)
|
}
|
||||||
.forEach(item => {
|
|
||||||
row += `\r\n - ${item.name} <code>${item.link}</code>`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return row
|
|
||||||
})
|
|
||||||
.join('\r\n')
|
|
||||||
|
|
||||||
const readmeStorage = new Storage(README_DIR)
|
|
||||||
await readmeStorage.save('_countries.md', output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,56 +1,63 @@
|
|||||||
import { Storage, Collection, File, Dictionary } from '@freearhey/core'
|
import { HTMLTable, LogParser, LogItem, HTMLTableColumn, HTMLTableItem } from '../core'
|
||||||
import { HTMLTable, LogParser, LogItem } from '../core'
|
import { Storage, File } from '@freearhey/storage-js'
|
||||||
import { LOGS_DIR, README_DIR } from '../constants'
|
import { LOGS_DIR, README_DIR } from '../constants'
|
||||||
import { Language } from '../models'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Table } from './table'
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
import { Table } from './table'
|
||||||
type LanguagesTableProps = {
|
import { data } from '../api'
|
||||||
languagesKeyByCode: Dictionary
|
|
||||||
}
|
export class LanguagesTable implements Table {
|
||||||
|
async create() {
|
||||||
export class LanguagesTable implements Table {
|
const parser = new LogParser()
|
||||||
languagesKeyByCode: Dictionary
|
const logsStorage = new Storage(LOGS_DIR)
|
||||||
|
const generatorsLog = await logsStorage.load('generators.log')
|
||||||
constructor({ languagesKeyByCode }: LanguagesTableProps) {
|
|
||||||
this.languagesKeyByCode = languagesKeyByCode
|
let items = new Collection<HTMLTableItem>()
|
||||||
}
|
parser
|
||||||
|
.parse(generatorsLog)
|
||||||
async make() {
|
.filter((logItem: LogItem) => logItem.type === 'language')
|
||||||
const parser = new LogParser()
|
.forEach((logItem: LogItem) => {
|
||||||
const logsStorage = new Storage(LOGS_DIR)
|
if (logItem.filepath.includes('undefined')) {
|
||||||
const generatorsLog = await logsStorage.load('generators.log')
|
items.add([
|
||||||
|
'ZZ',
|
||||||
let data = new Collection()
|
'Undefined',
|
||||||
parser
|
logItem.count.toString(),
|
||||||
.parse(generatorsLog)
|
`<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>`
|
||||||
.filter((logItem: LogItem) => logItem.type === 'language')
|
])
|
||||||
.forEach((logItem: LogItem) => {
|
|
||||||
const file = new File(logItem.filepath)
|
return
|
||||||
const languageCode = file.name()
|
}
|
||||||
const language: Language = this.languagesKeyByCode.get(languageCode)
|
|
||||||
|
const file = new File(logItem.filepath)
|
||||||
data.add([
|
const languageCode = file.name()
|
||||||
language ? language.name : 'ZZ',
|
const language: sdk.Models.Language | undefined = data.languagesKeyByCode.get(languageCode)
|
||||||
language ? language.name : 'Undefined',
|
|
||||||
logItem.count,
|
if (!language) return
|
||||||
`<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>`
|
|
||||||
])
|
items.add([
|
||||||
})
|
language.name,
|
||||||
|
language.name,
|
||||||
data = data
|
logItem.count.toString(),
|
||||||
.orderBy(item => item[0])
|
`<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>`
|
||||||
.map(item => {
|
])
|
||||||
item.shift()
|
})
|
||||||
return item
|
|
||||||
})
|
items = items
|
||||||
|
.sortBy(item => item[0])
|
||||||
const table = new HTMLTable(data.all(), [
|
.map(item => {
|
||||||
{ name: 'Language', align: 'left' },
|
item.shift()
|
||||||
{ name: 'Channels', align: 'right' },
|
return item
|
||||||
{ name: 'Playlist', align: 'left', nowrap: true }
|
})
|
||||||
])
|
|
||||||
|
const columns = new Collection<HTMLTableColumn>([
|
||||||
const readmeStorage = new Storage(README_DIR)
|
{ name: 'Language', align: 'left' },
|
||||||
await readmeStorage.save('_languages.md', table.toString())
|
{ name: 'Channels', align: 'right' },
|
||||||
}
|
{ name: 'Playlist', align: 'left', nowrap: true }
|
||||||
}
|
])
|
||||||
|
|
||||||
|
const table = new HTMLTable(items, columns)
|
||||||
|
|
||||||
|
const readmeStorage = new Storage(README_DIR)
|
||||||
|
await readmeStorage.save('_languages.md', table.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,52 +1,49 @@
|
|||||||
import { Storage, Collection } from '@freearhey/core'
|
import { LOGS_DIR, README_DIR } from '../constants'
|
||||||
import { LogParser, LogItem } from '../core'
|
import { Storage } from '@freearhey/storage-js'
|
||||||
import { LOGS_DIR, README_DIR } from '../constants'
|
import { LogParser, LogItem } from '../core'
|
||||||
import { Region } from '../models'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Table } from './table'
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
import { Table } from './table'
|
||||||
type RegionsTableProps = {
|
import { data } from '../api'
|
||||||
regions: Collection
|
|
||||||
}
|
type ListItem = {
|
||||||
|
name: string
|
||||||
export class RegionsTable implements Table {
|
count: number
|
||||||
regions: Collection
|
link: string
|
||||||
|
}
|
||||||
constructor({ regions }: RegionsTableProps) {
|
|
||||||
this.regions = regions
|
export class RegionsTable implements Table {
|
||||||
}
|
async create() {
|
||||||
|
const parser = new LogParser()
|
||||||
async make() {
|
const logsStorage = new Storage(LOGS_DIR)
|
||||||
const parser = new LogParser()
|
const generatorsLog = await logsStorage.load('generators.log')
|
||||||
const logsStorage = new Storage(LOGS_DIR)
|
const parsed = parser.parse(generatorsLog)
|
||||||
const generatorsLog = await logsStorage.load('generators.log')
|
const logRegions = parsed.filter((logItem: LogItem) => logItem.type === 'region')
|
||||||
const parsed = parser.parse(generatorsLog)
|
|
||||||
const logRegions = parsed.filter((logItem: LogItem) => logItem.type === 'region')
|
let items = new Collection<ListItem>()
|
||||||
|
data.regions.forEach((region: sdk.Models.Region) => {
|
||||||
let items = new Collection()
|
const logItem = logRegions.find(
|
||||||
this.regions.forEach((region: Region) => {
|
(logItem: LogItem) => logItem.filepath === `regions/${region.code.toLowerCase()}.m3u`
|
||||||
const logItem = logRegions.find(
|
)
|
||||||
(logItem: LogItem) => logItem.filepath === `regions/${region.code.toLowerCase()}.m3u`
|
|
||||||
)
|
if (!logItem) return
|
||||||
|
|
||||||
if (!logItem) return
|
items.add({
|
||||||
|
name: region.name,
|
||||||
items.add({
|
count: logItem.count,
|
||||||
index: region.name,
|
link: `https://iptv-org.github.io/iptv/${logItem.filepath}`
|
||||||
name: region.name,
|
})
|
||||||
count: logItem.count,
|
})
|
||||||
link: `https://iptv-org.github.io/iptv/${logItem.filepath}`
|
|
||||||
})
|
items = items.sortBy(item => item.name)
|
||||||
})
|
|
||||||
|
const output = items
|
||||||
items = items.orderBy(item => item.index)
|
.map(item => {
|
||||||
|
return `- ${item.name} <code>${item.link}</code>`
|
||||||
const output = items
|
})
|
||||||
.map(item => {
|
.join('\r\n')
|
||||||
return `- ${item.name} <code>${item.link}</code>`
|
|
||||||
})
|
const readmeStorage = new Storage(README_DIR)
|
||||||
.join('\r\n')
|
await readmeStorage.save('_regions.md', output)
|
||||||
|
}
|
||||||
const readmeStorage = new Storage(README_DIR)
|
}
|
||||||
await readmeStorage.save('_regions.md', output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface Table {
|
export interface Table {
|
||||||
make(): void
|
create(): void
|
||||||
}
|
}
|
||||||
|
|||||||
5
scripts/types/blocklistRecord.d.ts
vendored
5
scripts/types/blocklistRecord.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
export type BlocklistRecordData = {
|
|
||||||
channel: string
|
|
||||||
reason: string
|
|
||||||
ref: string
|
|
||||||
}
|
|
||||||
9
scripts/types/category.d.ts
vendored
9
scripts/types/category.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
export type CategorySerializedData = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CategoryData = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
50
scripts/types/channel.d.ts
vendored
50
scripts/types/channel.d.ts
vendored
@@ -1,50 +0,0 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
|
||||||
import type { CountrySerializedData } from './country'
|
|
||||||
import type { SubdivisionSerializedData } from './subdivision'
|
|
||||||
import type { CategorySerializedData } from './category'
|
|
||||||
|
|
||||||
export type ChannelSerializedData = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
altNames: string[]
|
|
||||||
network?: string
|
|
||||||
owners: string[]
|
|
||||||
countryCode: string
|
|
||||||
country?: CountrySerializedData
|
|
||||||
subdivisionCode?: string
|
|
||||||
subdivision?: SubdivisionSerializedData
|
|
||||||
cityName?: string
|
|
||||||
categoryIds: string[]
|
|
||||||
categories?: CategorySerializedData[]
|
|
||||||
isNSFW: boolean
|
|
||||||
launched?: string
|
|
||||||
closed?: string
|
|
||||||
replacedBy?: string
|
|
||||||
website?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChannelSearchableData = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
altNames: string[]
|
|
||||||
guideNames: string[]
|
|
||||||
streamTitles: string[]
|
|
||||||
feedFullNames: string[]
|
|
||||||
}
|
|
||||||
20
scripts/types/city.d.ts
vendored
20
scripts/types/city.d.ts
vendored
@@ -1,20 +0,0 @@
|
|||||||
import { CountrySerializedData } from './country'
|
|
||||||
import { SubdivisionSerializedData } from './subdivision'
|
|
||||||
|
|
||||||
export type CitySerializedData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
countryCode: string
|
|
||||||
country?: CountrySerializedData
|
|
||||||
subdivisionCode: string | null
|
|
||||||
subdivision?: SubdivisionSerializedData
|
|
||||||
wikidataId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CityData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
country: string
|
|
||||||
subdivision: string | null
|
|
||||||
wikidata_id: string
|
|
||||||
}
|
|
||||||
20
scripts/types/country.d.ts
vendored
20
scripts/types/country.d.ts
vendored
@@ -1,20 +0,0 @@
|
|||||||
import type { LanguageSerializedData } from './language'
|
|
||||||
import type { SubdivisionSerializedData } from './subdivision'
|
|
||||||
import type { RegionSerializedData } from './region'
|
|
||||||
|
|
||||||
export type CountrySerializedData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
flag: string
|
|
||||||
languageCode: string
|
|
||||||
language: LanguageSerializedData | null
|
|
||||||
subdivisions: SubdivisionSerializedData[]
|
|
||||||
regions: RegionSerializedData[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CountryData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
lang: string
|
|
||||||
flag: string
|
|
||||||
}
|
|
||||||
21
scripts/types/dataLoader.d.ts
vendored
21
scripts/types/dataLoader.d.ts
vendored
@@ -1,21 +0,0 @@
|
|||||||
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[]
|
|
||||||
logos: object | object[]
|
|
||||||
timezones: object | object[]
|
|
||||||
guides: object | object[]
|
|
||||||
streams: object | object[]
|
|
||||||
cities: object | object[]
|
|
||||||
}
|
|
||||||
31
scripts/types/dataProcessor.d.ts
vendored
31
scripts/types/dataProcessor.d.ts
vendored
@@ -1,31 +0,0 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
|
||||||
|
|
||||||
export type DataProcessorData = {
|
|
||||||
blocklistRecordsGroupedByChannelId: Dictionary
|
|
||||||
subdivisionsGroupedByCountryCode: Dictionary
|
|
||||||
feedsGroupedByChannelId: Dictionary
|
|
||||||
guidesGroupedByStreamId: Dictionary
|
|
||||||
logosGroupedByStreamId: Dictionary
|
|
||||||
subdivisionsKeyByCode: Dictionary
|
|
||||||
countriesKeyByCode: Dictionary
|
|
||||||
languagesKeyByCode: Dictionary
|
|
||||||
streamsGroupedById: Dictionary
|
|
||||||
categoriesKeyById: Dictionary
|
|
||||||
timezonesKeyById: Dictionary
|
|
||||||
regionsKeyByCode: Dictionary
|
|
||||||
blocklistRecords: Collection
|
|
||||||
channelsKeyById: Dictionary
|
|
||||||
citiesKeyByCode: Dictionary
|
|
||||||
subdivisions: Collection
|
|
||||||
categories: Collection
|
|
||||||
countries: Collection
|
|
||||||
languages: Collection
|
|
||||||
timezones: Collection
|
|
||||||
channels: Collection
|
|
||||||
regions: Collection
|
|
||||||
streams: Collection
|
|
||||||
cities: Collection
|
|
||||||
guides: Collection
|
|
||||||
feeds: Collection
|
|
||||||
logos: Collection
|
|
||||||
}
|
|
||||||
10
scripts/types/feed.d.ts
vendored
10
scripts/types/feed.d.ts
vendored
@@ -1,10 +0,0 @@
|
|||||||
export type FeedData = {
|
|
||||||
channel: string
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
is_main: boolean
|
|
||||||
broadcast_area: string[]
|
|
||||||
languages: string[]
|
|
||||||
timezones: string[]
|
|
||||||
video_format: string
|
|
||||||
}
|
|
||||||
17
scripts/types/guide.d.ts
vendored
17
scripts/types/guide.d.ts
vendored
@@ -1,17 +0,0 @@
|
|||||||
export type GuideSerializedData = {
|
|
||||||
channelId?: string
|
|
||||||
feedId?: string
|
|
||||||
siteDomain: string
|
|
||||||
siteId: string
|
|
||||||
siteName: string
|
|
||||||
languageCode: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GuideData = {
|
|
||||||
channel: string
|
|
||||||
feed: string
|
|
||||||
site: string
|
|
||||||
site_id: string
|
|
||||||
site_name: string
|
|
||||||
lang: string
|
|
||||||
}
|
|
||||||
9
scripts/types/language.d.ts
vendored
9
scripts/types/language.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
export type LanguageSerializedData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LanguageData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
9
scripts/types/logo.d.ts
vendored
9
scripts/types/logo.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
export type LogoData = {
|
|
||||||
channel: string
|
|
||||||
feed: string | null
|
|
||||||
tags: string[]
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
format: string | null
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
18
scripts/types/region.d.ts
vendored
18
scripts/types/region.d.ts
vendored
@@ -1,18 +0,0 @@
|
|||||||
import { CitySerializedData } from './city'
|
|
||||||
import { CountrySerializedData } from './country'
|
|
||||||
import { SubdivisionSerializedData } from './subdivision'
|
|
||||||
|
|
||||||
export type RegionSerializedData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
countryCodes: string[]
|
|
||||||
countries?: CountrySerializedData[]
|
|
||||||
subdivisions?: SubdivisionSerializedData[]
|
|
||||||
cities?: CitySerializedData[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RegionData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
countries: string[]
|
|
||||||
}
|
|
||||||
11
scripts/types/stream.d.ts
vendored
11
scripts/types/stream.d.ts
vendored
@@ -1,11 +0,0 @@
|
|||||||
export type StreamData = {
|
|
||||||
channelId: string | null
|
|
||||||
feedId: string | null
|
|
||||||
title: string | null
|
|
||||||
url: string
|
|
||||||
referrer: string | null
|
|
||||||
userAgent: string | null
|
|
||||||
quality: string | null
|
|
||||||
label: string | null
|
|
||||||
directives: string[]
|
|
||||||
}
|
|
||||||
16
scripts/types/subdivision.d.ts
vendored
16
scripts/types/subdivision.d.ts
vendored
@@ -1,16 +0,0 @@
|
|||||||
import { CountrySerializedData } from './country'
|
|
||||||
|
|
||||||
export type SubdivisionSerializedData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
countryCode: string
|
|
||||||
country?: CountrySerializedData
|
|
||||||
parentCode: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SubdivisionData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
country: string
|
|
||||||
parent: string | null
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
export function isURI(string: string): boolean {
|
import normalizeUrl from 'normalize-url'
|
||||||
try {
|
|
||||||
new URL(string)
|
export function isURI(string: string): boolean {
|
||||||
return true
|
try {
|
||||||
} catch {
|
new URL(string)
|
||||||
return false
|
return true
|
||||||
}
|
} catch {
|
||||||
}
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeURL(url: string): string {
|
||||||
|
const normalized = normalizeUrl(url, { stripWWW: false })
|
||||||
|
|
||||||
|
return decodeURIComponent(normalized).replace(/\s/g, '+').toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncate(string: string, limit: number = 100) {
|
||||||
|
if (!string) return string
|
||||||
|
if (string.length < limit) return string
|
||||||
|
|
||||||
|
return string.slice(0, limit - 3) + '...'
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user