mirror of https://github.com/iptv-org/iptv
Update scripts
This commit is contained in:
parent
925fdfbff0
commit
f35681d173
|
@ -13,13 +13,15 @@ async function main() {
|
|||
const dataStorage = new Storage(DATA_DIR)
|
||||
const dataLoader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await dataLoader.load()
|
||||
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
||||
processor.process(data)
|
||||
|
||||
logger.info('loading streams...')
|
||||
const streamsStorage = new Storage(STREAMS_DIR)
|
||||
const parser = new PlaylistParser({
|
||||
storage: streamsStorage,
|
||||
channelsKeyById,
|
||||
logosGroupedByStreamId,
|
||||
feedsGroupedByChannelId
|
||||
})
|
||||
const files = await streamsStorage.list('**/*.m3u')
|
||||
|
|
|
@ -15,6 +15,7 @@ async function main() {
|
|||
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')
|
||||
|
|
|
@ -49,11 +49,20 @@ export default async function main(filepath: string) {
|
|||
const dataStorage = new Storage(DATA_DIR)
|
||||
const loader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await loader.load()
|
||||
const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
|
||||
processor.process(data)
|
||||
const {
|
||||
channels,
|
||||
channelsKeyById,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId
|
||||
}: DataProcessorData = processor.process(data)
|
||||
|
||||
logger.info('loading streams...')
|
||||
const parser = new PlaylistParser({ storage, feedsGroupedByChannelId, channelsKeyById })
|
||||
const parser = new PlaylistParser({
|
||||
storage,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
channelsKeyById
|
||||
})
|
||||
parsedStreams = await parser.parseFile(filepath)
|
||||
const streamsWithoutId = parsedStreams.filter((stream: Stream) => !stream.id)
|
||||
|
||||
|
|
|
@ -16,14 +16,16 @@ async function main() {
|
|||
const dataStorage = new Storage(DATA_DIR)
|
||||
const loader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await loader.load()
|
||||
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
||||
processor.process(data)
|
||||
|
||||
logger.info('loading streams...')
|
||||
const streamsStorage = new Storage(STREAMS_DIR)
|
||||
const parser = new PlaylistParser({
|
||||
storage: streamsStorage,
|
||||
channelsKeyById,
|
||||
feedsGroupedByChannelId
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId
|
||||
})
|
||||
const files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u')
|
||||
let streams = await parser.parse(files)
|
||||
|
|
|
@ -14,7 +14,8 @@ import {
|
|||
CountriesGenerator,
|
||||
LanguagesGenerator,
|
||||
RegionsGenerator,
|
||||
IndexGenerator
|
||||
IndexGenerator,
|
||||
SourcesGenerator
|
||||
} from '../../generators'
|
||||
|
||||
async function main() {
|
||||
|
@ -28,6 +29,7 @@ async function main() {
|
|||
const data: DataLoaderData = await loader.load()
|
||||
const {
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
channelsKeyById,
|
||||
categories,
|
||||
countries,
|
||||
|
@ -39,15 +41,18 @@ async function main() {
|
|||
const parser = new PlaylistParser({
|
||||
storage: streamsStorage,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
channelsKeyById
|
||||
})
|
||||
const files = await streamsStorage.list('**/*.m3u')
|
||||
let streams = await parser.parse(files)
|
||||
const totalStreams = streams.count()
|
||||
logger.info(`found ${totalStreams} streams`)
|
||||
|
||||
logger.info('filtering streams...')
|
||||
streams = streams.uniqBy((stream: Stream) =>
|
||||
stream.hasId() ? stream.getChannelId() + stream.getFeedId() : uniqueId()
|
||||
)
|
||||
logger.info(`found ${totalStreams} streams (including ${streams.count()} unique)`)
|
||||
|
||||
logger.info('sorting streams...')
|
||||
streams = streams.orderBy(
|
||||
|
@ -79,6 +84,9 @@ async function main() {
|
|||
logFile
|
||||
}).generate()
|
||||
|
||||
logger.info('generating sources/...')
|
||||
await new SourcesGenerator({ streams, logFile }).generate()
|
||||
|
||||
logger.info('generating index.m3u...')
|
||||
await new IndexGenerator({ streams, logFile }).generate()
|
||||
|
||||
|
|
|
@ -61,14 +61,16 @@ async function main() {
|
|||
const dataStorage = new Storage(DATA_DIR)
|
||||
const loader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await loader.load()
|
||||
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
||||
processor.process(data)
|
||||
|
||||
logger.info('loading streams...')
|
||||
const rootStorage = new Storage(ROOT_DIR)
|
||||
const parser = new PlaylistParser({
|
||||
storage: rootStorage,
|
||||
channelsKeyById,
|
||||
feedsGroupedByChannelId
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId
|
||||
})
|
||||
const files = program.args.length ? program.args : await rootStorage.list(`${STREAMS_DIR}/*.m3u`)
|
||||
streams = await parser.parse(files)
|
||||
|
|
|
@ -20,13 +20,15 @@ async function main() {
|
|||
const dataStorage = new Storage(DATA_DIR)
|
||||
const dataLoader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await dataLoader.load()
|
||||
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||
const { channelsKeyById, feedsGroupedByChannelId, logosGroupedByStreamId }: DataProcessorData =
|
||||
processor.process(data)
|
||||
|
||||
logger.info('loading streams...')
|
||||
const streamsStorage = new Storage(STREAMS_DIR)
|
||||
const parser = new PlaylistParser({
|
||||
storage: streamsStorage,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
channelsKeyById
|
||||
})
|
||||
const files = await streamsStorage.list('**/*.m3u')
|
||||
|
@ -168,6 +170,7 @@ async function addStreams({
|
|||
const quality = data.getString('quality') || null
|
||||
const httpUserAgent = data.getString('httpUserAgent') || null
|
||||
const httpReferrer = data.getString('httpReferrer') || null
|
||||
const directives = data.getArray('directives') || []
|
||||
|
||||
const stream = new Stream({
|
||||
channel: channelId,
|
||||
|
@ -176,6 +179,7 @@ async function addStreams({
|
|||
url: streamUrl,
|
||||
user_agent: httpUserAgent,
|
||||
referrer: httpReferrer,
|
||||
directives,
|
||||
quality,
|
||||
label
|
||||
})
|
||||
|
|
|
@ -26,6 +26,7 @@ async function main() {
|
|||
const {
|
||||
channelsKeyById,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
blocklistRecordsGroupedByChannelId
|
||||
}: DataProcessorData = processor.process(data)
|
||||
|
||||
|
@ -34,7 +35,8 @@ async function main() {
|
|||
const parser = new PlaylistParser({
|
||||
storage: rootStorage,
|
||||
channelsKeyById,
|
||||
feedsGroupedByChannelId
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId
|
||||
})
|
||||
const files = program.args.length ? program.args : await rootStorage.list('streams/**/*.m3u')
|
||||
const streams = await parser.parse(files)
|
||||
|
|
|
@ -21,6 +21,7 @@ async function main() {
|
|||
const {
|
||||
channelsKeyById,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
blocklistRecordsGroupedByChannelId
|
||||
}: DataProcessorData = processor.process(data)
|
||||
|
||||
|
@ -29,7 +30,8 @@ async function main() {
|
|||
const parser = new PlaylistParser({
|
||||
storage: streamsStorage,
|
||||
channelsKeyById,
|
||||
feedsGroupedByChannelId
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId
|
||||
})
|
||||
const files = await streamsStorage.list('**/*.m3u')
|
||||
const streams = await parser.parse(files)
|
||||
|
@ -151,7 +153,7 @@ async function main() {
|
|||
else if (!feedId && streamsGroupedByChannelId.has(channelId)) result.status = 'fulfilled'
|
||||
else {
|
||||
const channelData = channelsKeyById.get(channelId)
|
||||
if (channelData.length && channelData[0].closed) result.status = 'closed'
|
||||
if (channelData && channelData.isClosed) result.status = 'closed'
|
||||
}
|
||||
|
||||
channelSearchRequestsBuffer.set(streamId, true)
|
||||
|
|
|
@ -47,6 +47,7 @@ export class DataLoader {
|
|||
blocklist,
|
||||
channels,
|
||||
feeds,
|
||||
logos,
|
||||
timezones,
|
||||
guides,
|
||||
streams
|
||||
|
@ -59,6 +60,7 @@ export class DataLoader {
|
|||
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')
|
||||
|
@ -73,6 +75,7 @@ export class DataLoader {
|
|||
blocklist,
|
||||
channels,
|
||||
feeds,
|
||||
logos,
|
||||
timezones,
|
||||
guides,
|
||||
streams
|
||||
|
|
|
@ -11,7 +11,8 @@ import {
|
|||
Region,
|
||||
Stream,
|
||||
Guide,
|
||||
Feed
|
||||
Feed,
|
||||
Logo
|
||||
} from '../models'
|
||||
|
||||
export class DataProcessor {
|
||||
|
@ -21,6 +22,9 @@ export class DataProcessor {
|
|||
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)
|
||||
|
||||
const subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data))
|
||||
const subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
|
||||
const subdivisionsGroupedByCountryCode = subdivisions.groupBy(
|
||||
|
@ -30,20 +34,6 @@ export class DataProcessor {
|
|||
let regions = new Collection(data.regions).map(data => new Region(data))
|
||||
const regionsKeyByCode = regions.keyBy((region: Region) => region.code)
|
||||
|
||||
const blocklistRecords = new Collection(data.blocklist).map(data => new BlocklistRecord(data))
|
||||
const blocklistRecordsGroupedByChannelId = blocklistRecords.groupBy(
|
||||
(blocklistRecord: BlocklistRecord) => blocklistRecord.channelId
|
||||
)
|
||||
|
||||
const streams = new Collection(data.streams).map(data => new Stream(data))
|
||||
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())
|
||||
|
||||
const languages = new Collection(data.languages).map(data => new Language(data))
|
||||
const languagesKeyByCode = languages.keyBy((language: Language) => language.code)
|
||||
|
||||
const countries = new Collection(data.countries).map(data =>
|
||||
new Country(data)
|
||||
.withRegions(regions)
|
||||
|
@ -52,13 +42,16 @@ export class DataProcessor {
|
|||
)
|
||||
const countriesKeyByCode = countries.keyBy((country: Country) => country.code)
|
||||
|
||||
regions = regions.map((region: Region) => region.withCountries(countriesKeyByCode))
|
||||
|
||||
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)
|
||||
.withCategories(categoriesKeyById)
|
||||
|
@ -66,6 +59,7 @@ export class DataProcessor {
|
|||
.withSubdivision(subdivisionsKeyByCode)
|
||||
.withCategories(categoriesKeyById)
|
||||
)
|
||||
|
||||
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||
|
||||
const feeds = new Collection(data.feeds).map(data =>
|
||||
|
@ -78,14 +72,32 @@ export class DataProcessor {
|
|||
.withBroadcastSubdivisions(subdivisionsKeyByCode)
|
||||
)
|
||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
||||
const feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
|
||||
|
||||
channels = channels.map((channel: Channel) => channel.withFeeds(feedsGroupedByChannelId))
|
||||
let 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))
|
||||
|
||||
channels = channels.map((channel: Channel) =>
|
||||
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
|
||||
)
|
||||
|
||||
return {
|
||||
blocklistRecordsGroupedByChannelId,
|
||||
subdivisionsGroupedByCountryCode,
|
||||
feedsGroupedByChannelId,
|
||||
guidesGroupedByStreamId,
|
||||
logosGroupedByStreamId,
|
||||
subdivisionsKeyByCode,
|
||||
countriesKeyByCode,
|
||||
languagesKeyByCode,
|
||||
|
@ -104,7 +116,8 @@ export class DataProcessor {
|
|||
regions,
|
||||
streams,
|
||||
guides,
|
||||
feeds
|
||||
feeds,
|
||||
logos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,9 +24,11 @@ export class IssueData {
|
|||
return this._data.get(key) === deleteSymbol ? '' : this._data.get(key)
|
||||
}
|
||||
|
||||
getArray(key: string): string[] {
|
||||
getArray(key: string): string[] | undefined {
|
||||
const deleteSymbol = '~'
|
||||
|
||||
if (this._data.missing(key)) return undefined
|
||||
|
||||
return this._data.get(key) === deleteSymbol ? [] : this._data.get(key).split('\r\n')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export class IssueLoader {
|
|||
}
|
||||
let issues: object[] = []
|
||||
if (TESTING) {
|
||||
issues = (await import('../../tests/__data__/input/playlist_update/issues.js')).default
|
||||
issues = (await import('../../tests/__data__/input/issues.js')).default
|
||||
} else {
|
||||
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||
owner: OWNER,
|
||||
|
|
|
@ -16,7 +16,8 @@ const FIELDS = new Dictionary({
|
|||
'HTTP Referrer': 'httpReferrer',
|
||||
'What happened to the stream?': 'reason',
|
||||
Reason: 'reason',
|
||||
Notes: 'notes'
|
||||
Notes: 'notes',
|
||||
Directives: 'directives'
|
||||
})
|
||||
|
||||
export class IssueParser {
|
||||
|
|
|
@ -5,17 +5,25 @@ import { Stream } from '../models'
|
|||
type PlaylistPareserProps = {
|
||||
storage: Storage
|
||||
feedsGroupedByChannelId: Dictionary
|
||||
logosGroupedByStreamId: Dictionary
|
||||
channelsKeyById: Dictionary
|
||||
}
|
||||
|
||||
export class PlaylistParser {
|
||||
storage: Storage
|
||||
feedsGroupedByChannelId: Dictionary
|
||||
logosGroupedByStreamId: Dictionary
|
||||
channelsKeyById: Dictionary
|
||||
|
||||
constructor({ storage, feedsGroupedByChannelId, channelsKeyById }: PlaylistPareserProps) {
|
||||
constructor({
|
||||
storage,
|
||||
feedsGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
channelsKeyById
|
||||
}: PlaylistPareserProps) {
|
||||
this.storage = storage
|
||||
this.feedsGroupedByChannelId = feedsGroupedByChannelId
|
||||
this.logosGroupedByStreamId = logosGroupedByStreamId
|
||||
this.channelsKeyById = channelsKeyById
|
||||
}
|
||||
|
||||
|
@ -41,6 +49,7 @@ export class PlaylistParser {
|
|||
.fromPlaylistItem(data)
|
||||
.withFeed(this.feedsGroupedByChannelId)
|
||||
.withChannel(this.channelsKeyById)
|
||||
.withLogos(this.logosGroupedByStreamId)
|
||||
.setFilepath(filepath)
|
||||
|
||||
return stream
|
||||
|
|
|
@ -17,7 +17,7 @@ export class CategoriesGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, categories, logFile }: CategoriesGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.categories = categories
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
|
@ -30,7 +30,8 @@ export class CategoriesGenerator implements Generator {
|
|||
const categoryStreams = streams
|
||||
.filter((stream: Stream) => stream.hasCategory(category))
|
||||
.map((stream: Stream) => {
|
||||
stream.groupTitle = stream.getCategoryNames().join(';')
|
||||
const groupTitle = stream.getCategoryNames().join(';')
|
||||
if (groupTitle) stream.groupTitle = groupTitle
|
||||
|
||||
return stream
|
||||
})
|
||||
|
|
|
@ -17,7 +17,7 @@ export class CountriesGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, countries, logFile }: CountriesGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.countries = countries
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
export * from './categoriesGenerator'
|
||||
export * from './countriesGenerator'
|
||||
export * from './languagesGenerator'
|
||||
export * from './regionsGenerator'
|
||||
export * from './indexGenerator'
|
||||
export * from './indexNsfwGenerator'
|
||||
export * from './indexCategoryGenerator'
|
||||
export * from './indexCountryGenerator'
|
||||
export * from './indexGenerator'
|
||||
export * from './indexLanguageGenerator'
|
||||
export * from './indexNsfwGenerator'
|
||||
export * from './indexRegionGenerator'
|
||||
export * from './languagesGenerator'
|
||||
export * from './regionsGenerator'
|
||||
export * from './sourcesGenerator'
|
||||
|
|
|
@ -15,7 +15,7 @@ export class IndexCategoryGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, logFile }: IndexCategoryGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export class IndexCountryGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, logFile }: IndexCountryGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export class IndexGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, logFile }: IndexGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
}
|
||||
|
@ -24,6 +24,12 @@ export class IndexGenerator implements Generator {
|
|||
const sfwStreams = this.streams
|
||||
.orderBy(stream => stream.getTitle())
|
||||
.filter((stream: Stream) => stream.isSFW())
|
||||
.map((stream: Stream) => {
|
||||
const groupTitle = stream.getCategoryNames().join(';')
|
||||
if (groupTitle) stream.groupTitle = groupTitle
|
||||
|
||||
return stream
|
||||
})
|
||||
|
||||
const playlist = new Playlist(sfwStreams, { public: true })
|
||||
const filepath = 'index.m3u'
|
||||
|
|
|
@ -15,7 +15,7 @@ export class IndexLanguageGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, logFile }: IndexLanguageGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export class IndexNsfwGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, logFile }: IndexNsfwGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export class IndexRegionGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, regions, logFile }: IndexRegionGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.regions = regions
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
|
|
|
@ -12,7 +12,7 @@ export class LanguagesGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, logFile }: LanguagesGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export class RegionsGenerator implements Generator {
|
|||
logFile: File
|
||||
|
||||
constructor({ streams, regions, logFile }: RegionsGeneratorProps) {
|
||||
this.streams = streams
|
||||
this.streams = streams.clone()
|
||||
this.regions = regions
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { Collection, Storage, File, type Dictionary } from '@freearhey/core'
|
||||
import { Stream, Playlist } from '../models'
|
||||
import { PUBLIC_DIR } from '../constants'
|
||||
import { Generator } from './generator'
|
||||
import { EOL } from 'node:os'
|
||||
|
||||
type SourcesGeneratorProps = {
|
||||
streams: Collection
|
||||
logFile: File
|
||||
}
|
||||
|
||||
export class SourcesGenerator implements Generator {
|
||||
streams: Collection
|
||||
storage: Storage
|
||||
logFile: File
|
||||
|
||||
constructor({ streams, logFile }: SourcesGeneratorProps) {
|
||||
this.streams = streams.clone()
|
||||
this.storage = new Storage(PUBLIC_DIR)
|
||||
this.logFile = logFile
|
||||
}
|
||||
|
||||
async generate() {
|
||||
const files: Dictionary = this.streams.groupBy((stream: Stream) => stream.getFilename())
|
||||
|
||||
for (let filename of files.keys()) {
|
||||
if (!filename) continue
|
||||
|
||||
let streams = new Collection(files.get(filename))
|
||||
streams = streams.map((stream: Stream) => {
|
||||
const groupTitle = stream.getCategoryNames().join(';')
|
||||
if (groupTitle) stream.groupTitle = groupTitle
|
||||
|
||||
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,5 +1,5 @@
|
|||
import { Collection, Dictionary } from '@freearhey/core'
|
||||
import { Category, Country, Feed, Guide, Stream, Subdivision } from './index'
|
||||
import { Category, Country, Feed, Guide, Logo, Stream, Subdivision } from './index'
|
||||
import type { ChannelData, ChannelSearchableData, ChannelSerializedData } from '../types/channel'
|
||||
|
||||
export class Channel {
|
||||
|
@ -19,9 +19,10 @@ export class Channel {
|
|||
launched?: string
|
||||
closed?: string
|
||||
replacedBy?: string
|
||||
isClosed: boolean
|
||||
website?: string
|
||||
logo: string
|
||||
feeds?: Collection
|
||||
logos: Collection = new Collection()
|
||||
|
||||
constructor(data?: ChannelData) {
|
||||
if (!data) return
|
||||
|
@ -40,7 +41,7 @@ export class Channel {
|
|||
this.closed = data.closed || undefined
|
||||
this.replacedBy = data.replaced_by || undefined
|
||||
this.website = data.website || undefined
|
||||
this.logo = data.logo
|
||||
this.isClosed = !!data.closed || !!data.replaced_by
|
||||
}
|
||||
|
||||
withSubdivision(subdivisionsKeyByCode: Dictionary): this {
|
||||
|
@ -71,6 +72,12 @@ export class Channel {
|
|||
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
|
||||
}
|
||||
|
@ -142,6 +149,35 @@ export class Channel {
|
|||
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,
|
||||
|
@ -171,8 +207,7 @@ export class Channel {
|
|||
launched: this.launched,
|
||||
closed: this.closed,
|
||||
replacedBy: this.replacedBy,
|
||||
website: this.website,
|
||||
logo: this.logo
|
||||
website: this.website
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,7 +227,6 @@ export class Channel {
|
|||
this.closed = data.closed
|
||||
this.replacedBy = data.replacedBy
|
||||
this.website = data.website
|
||||
this.logo = data.logo
|
||||
|
||||
return this
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export * from './feed'
|
|||
export * from './guide'
|
||||
export * from './issue'
|
||||
export * from './language'
|
||||
export * from './logo'
|
||||
export * from './playlist'
|
||||
export * from './region'
|
||||
export * from './stream'
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
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,8 +1,9 @@
|
|||
import { Feed, Channel, Category, Region, Subdivision, Country, Language } from './index'
|
||||
import { Feed, Channel, Category, Region, Subdivision, Country, Language, Logo } from './index'
|
||||
import { URL, Collection, Dictionary } from '@freearhey/core'
|
||||
import type { StreamData } from '../types/stream'
|
||||
import parser from 'iptv-playlist-parser'
|
||||
import { IssueData } from '../core'
|
||||
import path from 'node:path'
|
||||
|
||||
export class Stream {
|
||||
name?: string
|
||||
|
@ -12,6 +13,7 @@ export class Stream {
|
|||
channel?: Channel
|
||||
feedId?: string
|
||||
feed?: Feed
|
||||
logos: Collection = new Collection()
|
||||
filepath?: string
|
||||
line?: number
|
||||
label?: string
|
||||
|
@ -21,6 +23,7 @@ export class Stream {
|
|||
userAgent?: string
|
||||
groupTitle: string = 'Undefined'
|
||||
removed: boolean = false
|
||||
directives: Collection = new Collection()
|
||||
|
||||
constructor(data?: StreamData) {
|
||||
if (!data) return
|
||||
|
@ -38,6 +41,7 @@ export class Stream {
|
|||
this.verticalResolution = verticalResolution || undefined
|
||||
this.isInterlaced = isInterlaced || undefined
|
||||
this.label = data.label || undefined
|
||||
this.directives = new Collection(data.directives)
|
||||
}
|
||||
|
||||
update(issueData: IssueData): this {
|
||||
|
@ -46,7 +50,8 @@ export class Stream {
|
|||
quality: issueData.getString('quality'),
|
||||
httpUserAgent: issueData.getString('httpUserAgent'),
|
||||
httpReferrer: issueData.getString('httpReferrer'),
|
||||
newStreamUrl: issueData.getString('newStreamUrl')
|
||||
newStreamUrl: issueData.getString('newStreamUrl'),
|
||||
directives: issueData.getArray('directives')
|
||||
}
|
||||
|
||||
if (data.label !== undefined) this.label = data.label
|
||||
|
@ -54,11 +59,43 @@ export class Stream {
|
|||
if (data.httpUserAgent !== undefined) this.userAgent = data.httpUserAgent
|
||||
if (data.httpReferrer !== undefined) this.referrer = data.httpReferrer
|
||||
if (data.newStreamUrl !== undefined) this.url = data.newStreamUrl
|
||||
if (data.directives !== undefined) this.directives = new Collection(data.directives)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fromPlaylistItem(data: parser.PlaylistItem): this {
|
||||
function parseTitle(title: string): {
|
||||
name: string
|
||||
label: string
|
||||
quality: string
|
||||
} {
|
||||
const [, label] = title.match(/ \[(.*)\]$/) || [null, '']
|
||||
title = title.replace(new RegExp(` \\[${escapeRegExp(label)}\\]$`), '')
|
||||
const [, quality] = title.match(/ \(([0-9]+p)\)$/) || [null, '']
|
||||
title = title.replace(new RegExp(` \\(${quality}\\)$`), '')
|
||||
|
||||
return { name: title, label, quality }
|
||||
}
|
||||
|
||||
function parseDirectives(string: string) {
|
||||
let directives = new Collection()
|
||||
|
||||
if (!string) return directives
|
||||
|
||||
const supportedDirectives = ['#EXTVLCOPT', '#KODIPROP']
|
||||
const lines = string.split('\r\n')
|
||||
const regex = new RegExp(`^${supportedDirectives.join('|')}`, 'i')
|
||||
|
||||
lines.forEach((line: string) => {
|
||||
if (regex.test(line)) {
|
||||
directives.add(line.trim())
|
||||
}
|
||||
})
|
||||
|
||||
return directives
|
||||
}
|
||||
|
||||
if (!data.name) throw new Error('"name" property is required')
|
||||
if (!data.url) throw new Error('"url" property is required')
|
||||
|
||||
|
@ -77,6 +114,7 @@ export class Stream {
|
|||
this.url = data.url
|
||||
this.referrer = data.http.referrer || undefined
|
||||
this.userAgent = data.http['user-agent'] || undefined
|
||||
this.directives = parseDirectives(data.raw)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -99,6 +137,12 @@ export class Stream {
|
|||
return this
|
||||
}
|
||||
|
||||
withLogos(logosGroupedByStreamId: Dictionary): this {
|
||||
if (this.id) this.logos = new Collection(logosGroupedByStreamId.get(this.id))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setId(id: string): this {
|
||||
this.id = id
|
||||
|
||||
|
@ -130,6 +174,12 @@ export class Stream {
|
|||
return this.line || -1
|
||||
}
|
||||
|
||||
getFilename(): string {
|
||||
if (!this.filepath) return ''
|
||||
|
||||
return path.basename(this.filepath)
|
||||
}
|
||||
|
||||
setFilepath(filepath: string): this {
|
||||
this.filepath = filepath
|
||||
|
||||
|
@ -294,8 +344,35 @@ export class Stream {
|
|||
return this.feed ? this.feed.isInternational() : false
|
||||
}
|
||||
|
||||
getLogo(): string {
|
||||
return this?.channel?.logo || ''
|
||||
getLogos(): Collection {
|
||||
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([format, size], ['desc', 'asc'], false)
|
||||
}
|
||||
|
||||
getLogo(): Logo | undefined {
|
||||
return this.getLogos().first()
|
||||
}
|
||||
|
||||
hasLogo(): boolean {
|
||||
return this.getLogos().notEmpty()
|
||||
}
|
||||
|
||||
getLogoUrl(): string {
|
||||
let logo: Logo | undefined
|
||||
|
||||
if (this.hasLogo()) logo = this.getLogo()
|
||||
else logo = this?.channel?.getLogo()
|
||||
|
||||
return logo ? logo.url : ''
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
|
@ -339,7 +416,7 @@ export class Stream {
|
|||
let output = `#EXTINF:-1 tvg-id="${this.getId()}"`
|
||||
|
||||
if (options.public) {
|
||||
output += ` tvg-logo="${this.getLogo()}" group-title="${this.groupTitle}"`
|
||||
output += ` tvg-logo="${this.getLogoUrl()}" group-title="${this.groupTitle}"`
|
||||
}
|
||||
|
||||
if (this.referrer) {
|
||||
|
@ -352,13 +429,9 @@ export class Stream {
|
|||
|
||||
output += `,${this.getTitle()}`
|
||||
|
||||
if (this.referrer) {
|
||||
output += `\r\n#EXTVLCOPT:http-referrer=${this.referrer}`
|
||||
}
|
||||
|
||||
if (this.userAgent) {
|
||||
output += `\r\n#EXTVLCOPT:http-user-agent=${this.userAgent}`
|
||||
}
|
||||
this.directives.forEach((prop: string) => {
|
||||
output += `\r\n${prop}`
|
||||
})
|
||||
|
||||
output += `\r\n${this.url}`
|
||||
|
||||
|
@ -366,19 +439,6 @@ export class Stream {
|
|||
}
|
||||
}
|
||||
|
||||
function parseTitle(title: string): {
|
||||
name: string
|
||||
label: string
|
||||
quality: string
|
||||
} {
|
||||
const [, label] = title.match(/ \[(.*)\]$/) || [null, '']
|
||||
title = title.replace(new RegExp(` \\[${escapeRegExp(label)}\\]$`), '')
|
||||
const [, quality] = title.match(/ \(([0-9]+p)\)$/) || [null, '']
|
||||
title = title.replace(new RegExp(` \\(${quality}\\)$`), '')
|
||||
|
||||
return { name: title, label, quality }
|
||||
}
|
||||
|
||||
function escapeRegExp(text) {
|
||||
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ export type ChannelSerializedData = {
|
|||
closed?: string
|
||||
replacedBy?: string
|
||||
website?: string
|
||||
logo: string
|
||||
}
|
||||
|
||||
export type ChannelData = {
|
||||
|
@ -39,7 +38,6 @@ export type ChannelData = {
|
|||
closed: string
|
||||
replaced_by: string
|
||||
website: string
|
||||
logo: string
|
||||
}
|
||||
|
||||
export type ChannelSearchableData = {
|
||||
|
|
|
@ -13,6 +13,7 @@ export type DataLoaderData = {
|
|||
blocklist: object | object[]
|
||||
channels: object | object[]
|
||||
feeds: object | object[]
|
||||
logos: object | object[]
|
||||
timezones: object | object[]
|
||||
guides: object | object[]
|
||||
streams: object | object[]
|
||||
|
|
|
@ -5,6 +5,7 @@ export type DataProcessorData = {
|
|||
subdivisionsGroupedByCountryCode: Dictionary
|
||||
feedsGroupedByChannelId: Dictionary
|
||||
guidesGroupedByStreamId: Dictionary
|
||||
logosGroupedByStreamId: Dictionary
|
||||
subdivisionsKeyByCode: Dictionary
|
||||
countriesKeyByCode: Dictionary
|
||||
languagesKeyByCode: Dictionary
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export type LogoData = {
|
||||
channel: string
|
||||
feed: string | null
|
||||
tags: string[]
|
||||
width: number
|
||||
height: number
|
||||
format: string | null
|
||||
url: string
|
||||
}
|
|
@ -7,4 +7,5 @@ export type StreamData = {
|
|||
user_agent: string | null
|
||||
quality: string | null
|
||||
label: string | null
|
||||
directives: string[]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue