diff --git a/scripts/api.ts b/scripts/api.ts index 35cec92794..03f3ae3d37 100644 --- a/scripts/api.ts +++ b/scripts/api.ts @@ -14,6 +14,7 @@ const data = { feedsKeyByStreamId: new Dictionary(), feedsGroupedByChannel: new Dictionary(), blocklistRecordsGroupedByChannel: new Dictionary(), + guidesGroupedByStreamId: new Dictionary(), categories: new Collection(), countries: new Collection(), subdivisions: new Collection(), @@ -37,7 +38,8 @@ async function loadData() { subdivisions, cities, regions, - blocklist + blocklist, + guides } = dataManager.getProcessedData() const searchableData = channels.map((channel: sdk.Models.Channel) => channel.getSearchable()) @@ -57,6 +59,9 @@ async function loadData() { data.blocklistRecordsGroupedByChannel = blocklist.groupBy( (blocklistRecord: sdk.Models.BlocklistRecord) => blocklistRecord.channel ) + data.guidesGroupedByStreamId = guides + .filter((guide: sdk.Models.Guide) => !!guide.sources.length) + .groupBy((guide: sdk.Models.Guide) => guide.getStreamId()) data.categories = categories data.countries = countries data.subdivisions = subdivisions diff --git a/scripts/commands/playlist/format.ts b/scripts/commands/playlist/format.ts index 20425d1b09..a7c69ee013 100644 --- a/scripts/commands/playlist/format.ts +++ b/scripts/commands/playlist/format.ts @@ -5,8 +5,8 @@ import { Storage } from '@freearhey/storage-js' import { STREAMS_DIR } from '../../constants' import { PlaylistParser } from '../../core' import { getStreamInfo } from '../../utils' +import { loadData, data } from '../../api' import cliProgress from 'cli-progress' -import { loadData } from '../../api' import { eachLimit } from 'async' import path from 'node:path' import os from 'node:os' @@ -44,6 +44,10 @@ async function main() { let files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u') files = files.map((filepath: string) => path.basename(filepath)) let streams = await parser.parse(files) + streams = streams.map((stream: Stream) => { + stream.setGuides(data.guidesGroupedByStreamId.get(stream.getId())) + return stream + }) logger.info(`found ${streams.count()} streams`) diff --git a/scripts/commands/playlist/generate.ts b/scripts/commands/playlist/generate.ts index 16876411a8..4a2dc14eb5 100644 --- a/scripts/commands/playlist/generate.ts +++ b/scripts/commands/playlist/generate.ts @@ -34,6 +34,10 @@ async function main() { }) const files = await streamsStorage.list('**/*.m3u') let streams = await parser.parse(files) + streams = streams.map((stream: Stream) => { + stream.setGuides(data.guidesGroupedByStreamId.get(stream.getId())) + return stream + }) const totalStreams = streams.count() logger.info(`found ${totalStreams} streams`) @@ -62,32 +66,16 @@ async function main() { await new LanguagesGenerator({ streams, logFile }).generate() logger.info('generating countries/...') - await new CountriesGenerator({ - countries, - streams, - logFile - }).generate() + await new CountriesGenerator({ countries, streams, logFile }).generate() logger.info('generating subdivisions/...') - await new SubdivisionsGenerator({ - subdivisions, - streams, - logFile - }).generate() + await new SubdivisionsGenerator({ subdivisions, streams, logFile }).generate() logger.info('generating cities/...') - await new CitiesGenerator({ - cities, - streams, - logFile - }).generate() + await new CitiesGenerator({ cities, streams, logFile }).generate() logger.info('generating regions/...') - await new RegionsGenerator({ - streams, - regions, - logFile - }).generate() + await new RegionsGenerator({ streams, regions, logFile }).generate() logger.info('generating sources/...') await new SourcesGenerator({ streams, logFile }).generate() @@ -99,10 +87,7 @@ async function main() { await new IndexCategoryGenerator({ streams, logFile }).generate() logger.info('generating index.country.m3u...') - await new IndexCountryGenerator({ - streams, - logFile - }).generate() + await new IndexCountryGenerator({ streams, logFile }).generate() logger.info('generating index.language.m3u...') await new IndexLanguageGenerator({ streams, logFile }).generate() diff --git a/scripts/commands/playlist/update.ts b/scripts/commands/playlist/update.ts index 81db4dce69..f0477ed84e 100644 --- a/scripts/commands/playlist/update.ts +++ b/scripts/commands/playlist/update.ts @@ -81,6 +81,10 @@ async function loadStreams() { const files = await streamsStorage.list('**/*.m3u') streams = await parser.parse(files) + streams = streams.map((stream: Stream) => { + stream.setGuides(apiData.guidesGroupedByStreamId.get(stream.getId())) + return stream + }) } async function processIssues(issues: Collection) { @@ -132,7 +136,7 @@ async function removeStream(issue: Issue) { if (changed) { processedIssues.add(issue) } else { - log.error(`None of the URLs specified in the request were found in the playlists`) + log.error('None of the URLs specified in the request were found in the playlists') skippedIssues.add(issue) } } @@ -162,6 +166,8 @@ async function editStream(issue: Issue) { stream.updateWithIssue(data) + stream.setGuides(apiData.guidesGroupedByStreamId.get(stream.getId())) + const errors = new Collection() errors.concat(stream.validate()) if (errors.isNotEmpty()) { @@ -260,6 +266,8 @@ async function addStream(issue: Issue) { stream.updateTitle().updateFilepath() + stream.setGuides(apiData.guidesGroupedByStreamId.get(stream.getId())) + streams.add(stream) const errors = new Collection() diff --git a/scripts/models/playlist.ts b/scripts/models/playlist.ts index f6a2b25d60..472ebc1474 100644 --- a/scripts/models/playlist.ts +++ b/scripts/models/playlist.ts @@ -1,26 +1,65 @@ import { Collection } from '@freearhey/core' +import * as sdk from '@iptv-org/sdk' import { Stream } from '../models' type PlaylistOptions = { - public: boolean + public?: boolean } export class Playlist { streams: Collection - options: { - public: boolean - } + public: boolean constructor(streams: Collection, options?: PlaylistOptions) { this.streams = streams - this.options = options || { public: false } + this.public = options?.public === true + } + + getGuides(): Collection { + const guides = new Collection() + + this.streams.forEach(stream => { + guides.concat(stream.getGuides()) + }) + + return guides + } + + getGuideUrls(): Collection { + function byFormat(source: sdk.Types.GuideSource) { + if (source.format === 'GZIP') return 2 + if (source.format === 'XML') return 1 + return 0 + } + + const urls = new Collection() + + this.getGuides().forEach((guide: sdk.Models.Guide) => { + const sources = new Collection(guide.sources) + + const source = sources + .filter((source: sdk.Types.GuideSource) => ['GZIP', 'XML'].includes(source.format)) + .sortBy([byFormat], ['desc']) + .first() + + if (source) urls.add(source.url) + }) + + return urls.uniq() } toString() { - let output = '#EXTM3U\r\n' + let output = '#EXTM3U' + + const guideUrls = this.getGuideUrls() + if (guideUrls.count()) { + output += ` x-tvg-url="${guideUrls.join(',')}"` + } + + output += '\r\n' this.streams.forEach((stream: Stream) => { - output += stream.toString(this.options) + '\r\n' + output += stream.toString({ public: this.public }) + '\r\n' }) return output diff --git a/scripts/models/stream.ts b/scripts/models/stream.ts index 61dace58cf..173f4554f5 100644 --- a/scripts/models/stream.ts +++ b/scripts/models/stream.ts @@ -13,6 +13,15 @@ export class Stream extends sdk.Models.Stream { removed: boolean = false tvgId?: string statusCode?: string + guides = new Collection() + + setGuides(guides?: sdk.Models.Guide[]) { + this.guides = new Collection(guides) + } + + getGuides(): Collection { + return this.guides + } validate(): Collection { const errors = new Collection() @@ -80,12 +89,12 @@ export class Stream extends sdk.Models.Stream { quality: quality || null, url: data.url, referrer: data.http.referrer || null, - user_agent: data.http['user-agent'] || null + user_agent: data.http['user-agent'] || null, + label: label || null }) stream.tvgId = data.tvg.id stream.line = data.line - stream.label = label || null return stream }