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