Fix errors found by linter

This commit is contained in:
freearhey 2023-09-22 06:22:47 +03:00
parent efcae8e3b5
commit 21efb82055
24 changed files with 632 additions and 629 deletions

View file

@ -19,19 +19,19 @@ export class HTMLTable {
let output = '<table>\n' let output = '<table>\n'
output += ' <thead>\n <tr>' output += ' <thead>\n <tr>'
for (let column of this.columns) { for (const column of this.columns) {
output += `<th align="left">${column.name}</th>` output += `<th align="left">${column.name}</th>`
} }
output += '</tr>\n </thead>\n' output += '</tr>\n </thead>\n'
output += ' <tbody>\n' output += ' <tbody>\n'
for (let item of this.data) { for (const item of this.data) {
output += ' <tr>' output += ' <tr>'
let i = 0 let i = 0
for (let prop in item) { for (const prop in item) {
const column = this.columns[i] const column = this.columns[i]
let nowrap = column.nowrap ? ` nowrap` : '' const nowrap = column.nowrap ? ' nowrap' : ''
let align = column.align ? ` align="${column.align}"` : '' const align = column.align ? ` align="${column.align}"` : ''
output += `<td${align}${nowrap}>${item[prop]}</td>` output += `<td${align}${nowrap}>${item[prop]}</td>`
i++ i++
} }

View file

@ -11,7 +11,7 @@ const octokit = new CustomOctokit()
export class IssueLoader { export class IssueLoader {
async load({ labels }: { labels: string[] | string }) { async load({ labels }: { labels: string[] | string }) {
labels = Array.isArray(labels) ? labels.join(',') : labels labels = Array.isArray(labels) ? labels.join(',') : labels
let issues: any[] = [] let issues: object[] = []
if (TESTING) { if (TESTING) {
switch (labels) { switch (labels) {
case 'streams:add': case 'streams:add':

View file

@ -1,6 +1,5 @@
import { Dictionary } from '@freearhey/core' import { Dictionary } from '@freearhey/core'
import { Issue } from '../models' import { Issue } from '../models'
import _ from 'lodash'
const FIELDS = new Dictionary({ const FIELDS = new Dictionary({
'Channel ID': 'channel_id', 'Channel ID': 'channel_id',
@ -21,7 +20,7 @@ const FIELDS = new Dictionary({
}) })
export class IssueParser { export class IssueParser {
parse(issue: any): Issue { parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
const fields = issue.body.split('###') const fields = issue.body.split('###')
const data = new Dictionary() const data = new Dictionary()

View file

@ -4,7 +4,7 @@ export type LogItem = {
} }
export class LogParser { export class LogParser {
parse(content: string): any[] { parse(content: string): LogItem[] {
if (!content) return [] if (!content) return []
const lines = content.split('\n') const lines = content.split('\n')

View file

@ -14,7 +14,7 @@ export class PlaylistParser {
async parse(files: string[]): Promise<Collection> { async parse(files: string[]): Promise<Collection> {
let streams = new Collection() let streams = new Collection()
for (let filepath of files) { for (const filepath of files) {
const relativeFilepath = filepath.replace(path.normalize(STREAMS_DIR), '') const relativeFilepath = filepath.replace(path.normalize(STREAMS_DIR), '')
const _streams: Collection = await this.parseFile(relativeFilepath) const _streams: Collection = await this.parseFile(relativeFilepath)
streams = streams.concat(_streams) streams = streams.concat(_streams)

View file

@ -1,55 +1,53 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Stream, Category, Playlist } from '../models' import { Stream, Category, Playlist } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type CategoriesGeneratorProps = { type CategoriesGeneratorProps = {
streams: Collection streams: Collection
categories: Collection categories: Collection
logger: Logger logger: Logger
} }
export class CategoriesGenerator implements Generator { export class CategoriesGenerator implements Generator {
streams: Collection streams: Collection
categories: Collection categories: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, categories, logger }: CategoriesGeneratorProps) { constructor({ streams, categories, logger }: CategoriesGeneratorProps) {
this.streams = streams this.streams = streams
this.categories = categories this.categories = categories
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate() { async generate() {
const streams = this.streams.orderBy([(stream: Stream) => stream.getTitle()]) const streams = this.streams.orderBy([(stream: Stream) => stream.getTitle()])
this.categories.forEach(async (category: Category) => { this.categories.forEach(async (category: Category) => {
let categoryStreams = streams const categoryStreams = streams
.filter((stream: Stream) => stream.hasCategory(category)) .filter((stream: Stream) => stream.hasCategory(category))
.map((stream: Stream) => { .map((stream: Stream) => {
const groupTitle = stream.categories const streamCategories = stream.categories
? stream.categories .map((category: Category) => category.name)
.map((category: Category) => category.name) .sort()
.sort() const groupTitle = stream.categories ? streamCategories.join(';') : ''
.join(';') stream.groupTitle = groupTitle
: ''
stream.groupTitle = groupTitle return stream
})
return stream
}) const playlist = new Playlist(categoryStreams, { public: true })
const filepath = `categories/${category.id}.m3u`
const playlist = new Playlist(categoryStreams, { public: true }) await this.storage.save(filepath, playlist.toString())
const filepath = `categories/${category.id}.m3u` this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
await this.storage.save(filepath, playlist.toString()) })
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
}) const undefinedStreams = streams.filter((stream: Stream) => stream.noCategories())
const playlist = new Playlist(undefinedStreams, { public: true })
const undefinedStreams = streams.filter((stream: Stream) => stream.noCategories()) const filepath = 'categories/undefined.m3u'
const playlist = new Playlist(undefinedStreams, { public: true }) await this.storage.save(filepath, playlist.toString())
const filepath = `categories/undefined.m3u` this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
await this.storage.save(filepath, playlist.toString()) }
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) }
}
}

View file

@ -1,85 +1,85 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Country, Region, Subdivision, Stream, Playlist } from '../models' import { Country, Region, Subdivision, Stream, Playlist } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type CountriesGeneratorProps = { type CountriesGeneratorProps = {
streams: Collection streams: Collection
regions: Collection regions: Collection
subdivisions: Collection subdivisions: Collection
countries: Collection countries: Collection
logger: Logger logger: Logger
} }
export class CountriesGenerator implements Generator { export class CountriesGenerator implements Generator {
streams: Collection streams: Collection
countries: Collection countries: Collection
regions: Collection regions: Collection
subdivisions: Collection subdivisions: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, countries, regions, subdivisions, logger }: CountriesGeneratorProps) { constructor({ streams, countries, regions, subdivisions, logger }: CountriesGeneratorProps) {
this.streams = streams this.streams = streams
this.countries = countries this.countries = countries
this.regions = regions this.regions = regions
this.subdivisions = subdivisions this.subdivisions = subdivisions
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
let streams = this.streams const streams = this.streams
.orderBy([stream => stream.getTitle()]) .orderBy([stream => stream.getTitle()])
.filter((stream: Stream) => stream.isSFW()) .filter((stream: Stream) => stream.isSFW())
let regions = this.regions.filter((region: Region) => region.code !== 'INT') const regions = this.regions.filter((region: Region) => region.code !== 'INT')
this.countries.forEach(async (country: Country) => { this.countries.forEach(async (country: Country) => {
const countrySubdivisions = this.subdivisions.filter( const countrySubdivisions = this.subdivisions.filter(
(subdivision: Subdivision) => subdivision.country === country.code (subdivision: Subdivision) => subdivision.country === country.code
) )
const countrySubdivisionsCodes = countrySubdivisions.map( const countrySubdivisionsCodes = countrySubdivisions.map(
(subdivision: Subdivision) => `s/${subdivision.code}` (subdivision: Subdivision) => `s/${subdivision.code}`
) )
const countryAreaCodes = regions const countryAreaCodes = regions
.filter((region: Region) => region.countries.includes(country.code)) .filter((region: Region) => region.countries.includes(country.code))
.map((region: Region) => `r/${region.code}`) .map((region: Region) => `r/${region.code}`)
.concat(countrySubdivisionsCodes) .concat(countrySubdivisionsCodes)
.add(`c/${country.code}`) .add(`c/${country.code}`)
const countryStreams = streams.filter(stream => const countryStreams = streams.filter(stream =>
stream.broadcastArea.intersects(countryAreaCodes) stream.broadcastArea.intersects(countryAreaCodes)
) )
if (countryStreams.isEmpty()) return if (countryStreams.isEmpty()) return
const playlist = new Playlist(countryStreams, { public: true }) const playlist = new Playlist(countryStreams, { public: true })
const filepath = `countries/${country.code.toLowerCase()}.m3u` const filepath = `countries/${country.code.toLowerCase()}.m3u`
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
countrySubdivisions.forEach(async (subdivision: Subdivision) => { countrySubdivisions.forEach(async (subdivision: Subdivision) => {
const subdivisionStreams = streams.filter(stream => const subdivisionStreams = streams.filter(stream =>
stream.broadcastArea.includes(`s/${subdivision.code}`) stream.broadcastArea.includes(`s/${subdivision.code}`)
) )
if (subdivisionStreams.isEmpty()) return if (subdivisionStreams.isEmpty()) return
const playlist = new Playlist(subdivisionStreams, { public: true }) const playlist = new Playlist(subdivisionStreams, { public: true })
const filepath = `subdivisions/${subdivision.code.toLowerCase()}.m3u` const filepath = `subdivisions/${subdivision.code.toLowerCase()}.m3u`
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
}) })
}) })
const internationalStreams = streams.filter(stream => stream.isInternational()) const internationalStreams = streams.filter(stream => stream.isInternational())
if (internationalStreams.notEmpty()) { if (internationalStreams.notEmpty()) {
const playlist = new Playlist(internationalStreams, { public: true }) const playlist = new Playlist(internationalStreams, { public: true })
const filepath = `countries/int.m3u` const filepath = 'countries/int.m3u'
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} }
} }
} }

View file

@ -1,10 +1,10 @@
export * from './categoriesGenerator' export * from './categoriesGenerator'
export * from './countriesGenerator' export * from './countriesGenerator'
export * from './languagesGenerator' export * from './languagesGenerator'
export * from './regionsGenerator' export * from './regionsGenerator'
export * from './indexGenerator' export * from './indexGenerator'
export * from './indexNsfwGenerator' export * from './indexNsfwGenerator'
export * from './indexCategoryGenerator' export * from './indexCategoryGenerator'
export * from './indexCountryGenerator' export * from './indexCountryGenerator'
export * from './indexLanguageGenerator' export * from './indexLanguageGenerator'
export * from './indexRegionGenerator' export * from './indexRegionGenerator'

View file

@ -1,53 +1,53 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Stream, Playlist, Category } from '../models' import { Stream, Playlist, Category } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type IndexCategoryGeneratorProps = { type IndexCategoryGeneratorProps = {
streams: Collection streams: Collection
logger: Logger logger: Logger
} }
export class IndexCategoryGenerator implements Generator { export class IndexCategoryGenerator implements Generator {
streams: Collection streams: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, logger }: IndexCategoryGeneratorProps) { constructor({ streams, logger }: IndexCategoryGeneratorProps) {
this.streams = streams this.streams = streams
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
const streams = this.streams const streams = this.streams
.orderBy(stream => stream.getTitle()) .orderBy(stream => stream.getTitle())
.filter(stream => stream.isSFW()) .filter(stream => stream.isSFW())
let groupedStreams = new Collection() let groupedStreams = new Collection()
streams.forEach((stream: Stream) => { streams.forEach((stream: Stream) => {
if (stream.noCategories()) { if (stream.noCategories()) {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = 'Undefined' streamClone.groupTitle = 'Undefined'
groupedStreams.add(streamClone) groupedStreams.add(streamClone)
return return
} }
stream.categories.forEach((category: Category) => { stream.categories.forEach((category: Category) => {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = category.name streamClone.groupTitle = category.name
groupedStreams.push(streamClone) groupedStreams.push(streamClone)
}) })
}) })
groupedStreams = groupedStreams.orderBy(stream => { groupedStreams = groupedStreams.orderBy(stream => {
if (stream.groupTitle === 'Undefined') return 'ZZ' if (stream.groupTitle === 'Undefined') return 'ZZ'
return stream.groupTitle return stream.groupTitle
}) })
const playlist = new Playlist(groupedStreams, { public: true }) const playlist = new Playlist(groupedStreams, { public: true })
const filepath = 'index.category.m3u' const filepath = 'index.category.m3u'
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} }
} }

View file

@ -1,104 +1,104 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Stream, Playlist, Country, Subdivision, Region } from '../models' import { Stream, Playlist, Country, Subdivision, Region } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type IndexCountryGeneratorProps = { type IndexCountryGeneratorProps = {
streams: Collection streams: Collection
regions: Collection regions: Collection
countries: Collection countries: Collection
subdivisions: Collection subdivisions: Collection
logger: Logger logger: Logger
} }
export class IndexCountryGenerator implements Generator { export class IndexCountryGenerator implements Generator {
streams: Collection streams: Collection
countries: Collection countries: Collection
regions: Collection regions: Collection
subdivisions: Collection subdivisions: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, regions, countries, subdivisions, logger }: IndexCountryGeneratorProps) { constructor({ streams, regions, countries, subdivisions, logger }: IndexCountryGeneratorProps) {
this.streams = streams this.streams = streams
this.countries = countries this.countries = countries
this.regions = regions this.regions = regions
this.subdivisions = subdivisions this.subdivisions = subdivisions
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
let groupedStreams = new Collection() let groupedStreams = new Collection()
this.streams this.streams
.orderBy(stream => stream.getTitle()) .orderBy(stream => stream.getTitle())
.filter(stream => stream.isSFW()) .filter(stream => stream.isSFW())
.forEach(stream => { .forEach(stream => {
if (stream.noBroadcastArea()) { if (stream.noBroadcastArea()) {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = 'Undefined' streamClone.groupTitle = 'Undefined'
groupedStreams.add(streamClone) groupedStreams.add(streamClone)
return return
} }
if (stream.isInternational()) { if (stream.isInternational()) {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = 'International' streamClone.groupTitle = 'International'
groupedStreams.add(streamClone) groupedStreams.add(streamClone)
} }
this.getStreamBroadcastCountries(stream).forEach((country: Country) => { this.getStreamBroadcastCountries(stream).forEach((country: Country) => {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = country.name streamClone.groupTitle = country.name
groupedStreams.add(streamClone) groupedStreams.add(streamClone)
}) })
}) })
groupedStreams = groupedStreams.orderBy((stream: Stream) => { groupedStreams = groupedStreams.orderBy((stream: Stream) => {
if (stream.groupTitle === 'International') return 'ZZ' if (stream.groupTitle === 'International') return 'ZZ'
if (stream.groupTitle === 'Undefined') return 'ZZZ' if (stream.groupTitle === 'Undefined') return 'ZZZ'
return stream.groupTitle return stream.groupTitle
}) })
const playlist = new Playlist(groupedStreams, { public: true }) const playlist = new Playlist(groupedStreams, { public: true })
const filepath = 'index.country.m3u' const filepath = 'index.country.m3u'
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} }
getStreamBroadcastCountries(stream: Stream) { getStreamBroadcastCountries(stream: Stream) {
const groupedRegions = this.regions.keyBy((region: Region) => region.code) const groupedRegions = this.regions.keyBy((region: Region) => region.code)
const groupedCountries = this.countries.keyBy((country: Country) => country.code) const groupedCountries = this.countries.keyBy((country: Country) => country.code)
const groupedSubdivisions = this.subdivisions.keyBy( const groupedSubdivisions = this.subdivisions.keyBy(
(subdivision: Subdivision) => subdivision.code (subdivision: Subdivision) => subdivision.code
) )
let broadcastCountries = new Collection() let broadcastCountries = new Collection()
stream.broadcastArea.forEach(broadcastAreaCode => { stream.broadcastArea.forEach(broadcastAreaCode => {
const [type, code] = broadcastAreaCode.split('/') const [type, code] = broadcastAreaCode.split('/')
switch (type) { switch (type) {
case 'c': case 'c':
broadcastCountries.add(code) broadcastCountries.add(code)
break break
case 'r': case 'r':
if (code !== 'INT' && groupedRegions.has(code)) { if (code !== 'INT' && groupedRegions.has(code)) {
broadcastCountries = broadcastCountries.concat(groupedRegions.get(code).countries) broadcastCountries = broadcastCountries.concat(groupedRegions.get(code).countries)
} }
break break
case 's': case 's':
if (groupedSubdivisions.has(code)) { if (groupedSubdivisions.has(code)) {
broadcastCountries.add(groupedSubdivisions.get(code).country) broadcastCountries.add(groupedSubdivisions.get(code).country)
} }
break break
} }
}) })
return broadcastCountries return broadcastCountries
.uniq() .uniq()
.map(code => groupedCountries.get(code)) .map(code => groupedCountries.get(code))
.filter(Boolean) .filter(Boolean)
} }
} }

View file

@ -1,32 +1,32 @@
import { Collection, Logger, Storage } from '@freearhey/core' import { Collection, Logger, Storage } from '@freearhey/core'
import { Stream, Playlist } from '../models' import { Stream, Playlist } from '../models'
import { Generator } from './generator' import { Generator } from './generator'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type IndexGeneratorProps = { type IndexGeneratorProps = {
streams: Collection streams: Collection
logger: Logger logger: Logger
} }
export class IndexGenerator implements Generator { export class IndexGenerator implements Generator {
streams: Collection streams: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, logger }: IndexGeneratorProps) { constructor({ streams, logger }: IndexGeneratorProps) {
this.streams = streams this.streams = streams
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
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())
const playlist = new Playlist(sfwStreams, { public: true }) const playlist = new Playlist(sfwStreams, { public: true })
const filepath = 'index.m3u' const filepath = 'index.m3u'
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} }
} }

View file

@ -1,52 +1,52 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Stream, Playlist, Language } from '../models' import { Stream, Playlist, Language } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type IndexLanguageGeneratorProps = { type IndexLanguageGeneratorProps = {
streams: Collection streams: Collection
logger: Logger logger: Logger
} }
export class IndexLanguageGenerator implements Generator { export class IndexLanguageGenerator implements Generator {
streams: Collection streams: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, logger }: IndexLanguageGeneratorProps) { constructor({ streams, logger }: IndexLanguageGeneratorProps) {
this.streams = streams this.streams = streams
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
let groupedStreams = new Collection() let groupedStreams = new Collection()
this.streams this.streams
.orderBy(stream => stream.getTitle()) .orderBy(stream => stream.getTitle())
.filter(stream => stream.isSFW()) .filter(stream => stream.isSFW())
.forEach(stream => { .forEach(stream => {
if (stream.noLanguages()) { if (stream.noLanguages()) {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = 'Undefined' streamClone.groupTitle = 'Undefined'
groupedStreams.add(streamClone) groupedStreams.add(streamClone)
return return
} }
stream.languages.forEach((language: Language) => { stream.languages.forEach((language: Language) => {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = language.name streamClone.groupTitle = language.name
groupedStreams.add(streamClone) groupedStreams.add(streamClone)
}) })
}) })
groupedStreams = groupedStreams.orderBy((stream: Stream) => { groupedStreams = groupedStreams.orderBy((stream: Stream) => {
if (stream.groupTitle === 'Undefined') return 'ZZ' if (stream.groupTitle === 'Undefined') return 'ZZ'
return stream.groupTitle return stream.groupTitle
}) })
const playlist = new Playlist(groupedStreams, { public: true }) const playlist = new Playlist(groupedStreams, { public: true })
const filepath = 'index.language.m3u' const filepath = 'index.language.m3u'
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} }
} }

View file

@ -1,30 +1,30 @@
import { Collection, Logger, Storage } from '@freearhey/core' import { Collection, Logger, Storage } from '@freearhey/core'
import { Stream, Playlist } from '../models' import { Stream, Playlist } from '../models'
import { Generator } from './generator' import { Generator } from './generator'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type IndexNsfwGeneratorProps = { type IndexNsfwGeneratorProps = {
streams: Collection streams: Collection
logger: Logger logger: Logger
} }
export class IndexNsfwGenerator implements Generator { export class IndexNsfwGenerator implements Generator {
streams: Collection streams: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, logger }: IndexNsfwGeneratorProps) { constructor({ streams, logger }: IndexNsfwGeneratorProps) {
this.streams = streams this.streams = streams
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
const allStreams = this.streams.orderBy((stream: Stream) => stream.getTitle()) const allStreams = this.streams.orderBy((stream: Stream) => stream.getTitle())
const playlist = new Playlist(allStreams, { public: true }) const playlist = new Playlist(allStreams, { public: true })
const filepath = 'index.nsfw.m3u' const filepath = 'index.nsfw.m3u'
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} }
} }

View file

@ -1,83 +1,83 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Stream, Playlist, Region } from '../models' import { Stream, Playlist, Region } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type IndexRegionGeneratorProps = { type IndexRegionGeneratorProps = {
streams: Collection streams: Collection
regions: Collection regions: Collection
logger: Logger logger: Logger
} }
export class IndexRegionGenerator implements Generator { export class IndexRegionGenerator implements Generator {
streams: Collection streams: Collection
regions: Collection regions: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, regions, logger }: IndexRegionGeneratorProps) { constructor({ streams, regions, logger }: IndexRegionGeneratorProps) {
this.streams = streams this.streams = streams
this.regions = regions this.regions = regions
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
let groupedStreams = new Collection() let groupedStreams = new Collection()
this.streams this.streams
.orderBy((stream: Stream) => stream.getTitle()) .orderBy((stream: Stream) => stream.getTitle())
.filter((stream: Stream) => stream.isSFW()) .filter((stream: Stream) => stream.isSFW())
.forEach((stream: Stream) => { .forEach((stream: Stream) => {
if (stream.noBroadcastArea()) { if (stream.noBroadcastArea()) {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = 'Undefined' streamClone.groupTitle = 'Undefined'
groupedStreams.push(streamClone) groupedStreams.push(streamClone)
return return
} }
this.getStreamRegions(stream).forEach((region: Region) => { this.getStreamRegions(stream).forEach((region: Region) => {
const streamClone = stream.clone() const streamClone = stream.clone()
streamClone.groupTitle = region.name streamClone.groupTitle = region.name
groupedStreams.push(streamClone) groupedStreams.push(streamClone)
}) })
}) })
groupedStreams = groupedStreams.orderBy((stream: Stream) => { groupedStreams = groupedStreams.orderBy((stream: Stream) => {
if (stream.groupTitle === 'Undefined') return 'ZZ' if (stream.groupTitle === 'Undefined') return 'ZZ'
return stream.groupTitle return stream.groupTitle
}) })
const playlist = new Playlist(groupedStreams, { public: true }) const playlist = new Playlist(groupedStreams, { public: true })
const filepath = 'index.region.m3u' const filepath = 'index.region.m3u'
await this.storage.save(filepath, playlist.toString()) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} }
getStreamRegions(stream: Stream) { getStreamRegions(stream: Stream) {
let streamRegions = new Collection() let streamRegions = new Collection()
stream.broadcastArea.forEach(broadcastAreaCode => { stream.broadcastArea.forEach(broadcastAreaCode => {
const [type, code] = broadcastAreaCode.split('/') const [type, code] = broadcastAreaCode.split('/')
switch (type) { switch (type) {
case 'r': case 'r':
const groupedRegions = this.regions.keyBy((region: Region) => region.code) const groupedRegions = this.regions.keyBy((region: Region) => region.code)
streamRegions.add(groupedRegions.get(code)) streamRegions.add(groupedRegions.get(code))
break break
case 's': case 's':
const [countryCode] = code.split('-') const [countryCode] = code.split('-')
const subdivisionRegions = this.regions.filter((region: Region) => const subdivisionRegions = this.regions.filter((region: Region) =>
region.countries.includes(countryCode) region.countries.includes(countryCode)
) )
streamRegions = streamRegions.concat(subdivisionRegions) streamRegions = streamRegions.concat(subdivisionRegions)
break break
case 'c': case 'c':
const countryRegions = this.regions.filter((region: Region) => const countryRegions = this.regions.filter((region: Region) =>
region.countries.includes(code) region.countries.includes(code)
) )
streamRegions = streamRegions.concat(countryRegions) streamRegions = streamRegions.concat(countryRegions)
break break
} }
}) })
return streamRegions return streamRegions
} }
} }

View file

@ -1,50 +1,52 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Playlist, Language, Stream } from '../models' import { Playlist, Language, Stream } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type LanguagesGeneratorProps = { streams: Collection; logger: Logger } type LanguagesGeneratorProps = { streams: Collection; logger: Logger }
export class LanguagesGenerator implements Generator { export class LanguagesGenerator implements Generator {
streams: Collection streams: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, logger }: LanguagesGeneratorProps) { constructor({ streams, logger }: LanguagesGeneratorProps) {
this.streams = streams this.streams = streams
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
let streams = this.streams.orderBy(stream => stream.getTitle()).filter(stream => stream.isSFW()) const streams = this.streams
.orderBy(stream => stream.getTitle())
let languages = new Collection() .filter(stream => stream.isSFW())
streams.forEach((stream: Stream) => {
languages = languages.concat(stream.languages) let languages = new Collection()
}) streams.forEach((stream: Stream) => {
languages = languages.concat(stream.languages)
languages })
.uniqBy((language: Language) => language.code)
.orderBy((language: Language) => language.name) languages
.forEach(async (language: Language) => { .uniqBy((language: Language) => language.code)
const languageStreams = streams.filter(stream => stream.hasLanguage(language)) .orderBy((language: Language) => language.name)
.forEach(async (language: Language) => {
if (languageStreams.isEmpty()) return const languageStreams = streams.filter(stream => stream.hasLanguage(language))
const playlist = new Playlist(languageStreams, { public: true }) if (languageStreams.isEmpty()) return
const filepath = `languages/${language.code}.m3u`
await this.storage.save(filepath, playlist.toString()) const playlist = new Playlist(languageStreams, { public: true })
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) const filepath = `languages/${language.code}.m3u`
}) await this.storage.save(filepath, playlist.toString())
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
const undefinedStreams = streams.filter(stream => stream.noLanguages()) })
if (undefinedStreams.isEmpty()) return const undefinedStreams = streams.filter(stream => stream.noLanguages())
const playlist = new Playlist(undefinedStreams, { public: true }) if (undefinedStreams.isEmpty()) return
const filepath = 'languages/undefined.m3u'
await this.storage.save(filepath, playlist.toString()) const playlist = new Playlist(undefinedStreams, { public: true })
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) const filepath = 'languages/undefined.m3u'
} await this.storage.save(filepath, playlist.toString())
} this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
}
}

View file

@ -1,51 +1,53 @@
import { Generator } from './generator' import { Generator } from './generator'
import { Collection, Storage, Logger } from '@freearhey/core' import { Collection, Storage, Logger } from '@freearhey/core'
import { Playlist, Subdivision, Region } from '../models' import { Playlist, Subdivision, Region } from '../models'
import { PUBLIC_DIR } from '../constants' import { PUBLIC_DIR } from '../constants'
type RegionsGeneratorProps = { type RegionsGeneratorProps = {
streams: Collection streams: Collection
regions: Collection regions: Collection
subdivisions: Collection subdivisions: Collection
logger: Logger logger: Logger
} }
export class RegionsGenerator implements Generator { export class RegionsGenerator implements Generator {
streams: Collection streams: Collection
regions: Collection regions: Collection
subdivisions: Collection subdivisions: Collection
storage: Storage storage: Storage
logger: Logger logger: Logger
constructor({ streams, regions, subdivisions, logger }: RegionsGeneratorProps) { constructor({ streams, regions, subdivisions, logger }: RegionsGeneratorProps) {
this.streams = streams this.streams = streams
this.regions = regions this.regions = regions
this.subdivisions = subdivisions this.subdivisions = subdivisions
this.storage = new Storage(PUBLIC_DIR) this.storage = new Storage(PUBLIC_DIR)
this.logger = logger this.logger = logger
} }
async generate(): Promise<void> { async generate(): Promise<void> {
let streams = this.streams.orderBy(stream => stream.getTitle()).filter(stream => stream.isSFW()) const streams = this.streams
.orderBy(stream => stream.getTitle())
this.regions.forEach(async (region: Region) => { .filter(stream => stream.isSFW())
if (region.code === 'INT') return
this.regions.forEach(async (region: Region) => {
const regionSubdivisionsCodes = this.subdivisions if (region.code === 'INT') return
.filter((subdivision: Subdivision) => region.countries.indexOf(subdivision.country) > -1)
.map((subdivision: Subdivision) => `s/${subdivision.code}`) const regionSubdivisionsCodes = this.subdivisions
.filter((subdivision: Subdivision) => region.countries.indexOf(subdivision.country) > -1)
const regionCodes = region.countries .map((subdivision: Subdivision) => `s/${subdivision.code}`)
.map((code: string) => `c/${code}`)
.concat(regionSubdivisionsCodes) const regionCodes = region.countries
.add(`r/${region.code}`) .map((code: string) => `c/${code}`)
.concat(regionSubdivisionsCodes)
const regionStreams = streams.filter(stream => stream.broadcastArea.intersects(regionCodes)) .add(`r/${region.code}`)
const playlist = new Playlist(regionStreams, { public: true }) const regionStreams = streams.filter(stream => stream.broadcastArea.intersects(regionCodes))
const filepath = `regions/${region.code.toLowerCase()}.m3u`
await this.storage.save(filepath, playlist.toString()) const playlist = new Playlist(regionStreams, { public: true })
this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() })) const filepath = `regions/${region.code.toLowerCase()}.m3u`
}) await this.storage.save(filepath, playlist.toString())
} this.logger.info(JSON.stringify({ filepath, count: playlist.streams.count() }))
} })
}
}

View file

@ -17,10 +17,10 @@ export class Playlist {
} }
toString() { toString() {
let output = `#EXTM3U\n` let output = '#EXTM3U\n'
this.streams.forEach((stream: Stream) => { this.streams.forEach((stream: Stream) => {
output += stream.toString(this.options) + `\n` output += stream.toString(this.options) + '\n'
}) })
return output return output

View file

@ -48,7 +48,7 @@ export class CountryTable implements Table {
} else if (countryCode === 'INT') { } else if (countryCode === 'INT') {
data.add([ data.add([
'ZZ', 'ZZ',
`🌍 International`, '🌍 International',
logItem.count, logItem.count,
`<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>` `<code>https://iptv-org.github.io/iptv/${logItem.filepath}</code>`
]) ])

View file

@ -4,14 +4,14 @@ import fs from 'fs-extra'
beforeEach(() => { beforeEach(() => {
fs.emptyDirSync('tests/__data__/output') fs.emptyDirSync('tests/__data__/output')
const stdout = execSync( execSync(
'STREAMS_DIR=tests/__data__/input/streams_generate API_DIR=tests/__data__/output/.api npm run api:generate', 'STREAMS_DIR=tests/__data__/input/streams_generate API_DIR=tests/__data__/output/.api npm run api:generate',
{ encoding: 'utf8' } { encoding: 'utf8' }
) )
}) })
it('can create streams.json', () => { it('can create streams.json', () => {
expect(content(`output/.api/streams.json`)).toMatchObject(content(`expected/.api/streams.json`)) expect(content('output/.api/streams.json')).toMatchObject(content('expected/.api/streams.json'))
}) })
function content(filepath: string) { function content(filepath: string) {

View file

@ -8,7 +8,7 @@ beforeEach(() => {
}) })
it('can format playlists', () => { it('can format playlists', () => {
const stdout = execSync('STREAMS_DIR=tests/__data__/output/streams npm run playlist:format', { execSync('STREAMS_DIR=tests/__data__/output/streams npm run playlist:format', {
encoding: 'utf8' encoding: 'utf8'
}) })

View file

@ -5,7 +5,7 @@ import * as glob from 'glob'
beforeEach(() => { beforeEach(() => {
fs.emptyDirSync('tests/__data__/output') fs.emptyDirSync('tests/__data__/output')
const stdout = execSync( execSync(
'STREAMS_DIR=tests/__data__/input/streams_generate DATA_DIR=tests/__data__/input/data PUBLIC_DIR=tests/__data__/output/.gh-pages LOGS_DIR=tests/__data__/output/logs npm run playlist:generate', 'STREAMS_DIR=tests/__data__/input/streams_generate DATA_DIR=tests/__data__/input/data PUBLIC_DIR=tests/__data__/output/.gh-pages LOGS_DIR=tests/__data__/output/logs npm run playlist:generate',
{ encoding: 'utf8' } { encoding: 'utf8' }
) )
@ -20,8 +20,8 @@ it('can generate playlists and logs', () => {
expect(content(`output/${filepath}`), filepath).toBe(content(`expected/${filepath}`)) expect(content(`output/${filepath}`), filepath).toBe(content(`expected/${filepath}`))
}) })
expect(content(`output/logs/generators.log`).split('\n').sort()).toStrictEqual( expect(content('output/logs/generators.log').split('\n').sort()).toStrictEqual(
content(`expected/logs/generators.log`).split('\n').sort() content('expected/logs/generators.log').split('\n').sort()
) )
}) })

View file

@ -26,7 +26,7 @@ it('can format playlists', () => {
}) })
expect(stdout).toBe( expect(stdout).toBe(
`OUTPUT=closes #14151, closes #14140, closes #14139, closes #14110, closes #14179, closes #14178\n` 'OUTPUT=closes #14151, closes #14140, closes #14139, closes #14110, closes #14179, closes #14178\n'
) )
}) })

View file

@ -10,11 +10,13 @@ it('show an error if channel name in the blocklist', () => {
) )
console.log(stdout) console.log(stdout)
process.exit(1) process.exit(1)
} catch (error: any) { } catch (error: unknown) {
// @ts-ignore
expect(error.status).toBe(1) expect(error.status).toBe(1)
expect( expect(
// @ts-ignore
error.stdout.includes( error.stdout.includes(
`us_blocked.m3u\n 2 error "Fox Sports 2 Asia (Thai)" is on the blocklist due to claims of copyright holders (https://github.com/iptv-org/iptv/issues/0000)\n\n1 problems (1 errors, 0 warnings)\n` 'us_blocked.m3u\n 2 error "Fox Sports 2 Asia (Thai)" is on the blocklist due to claims of copyright holders (https://github.com/iptv-org/iptv/issues/0000)\n\n1 problems (1 errors, 0 warnings)\n'
) )
).toBe(true) ).toBe(true)
} }
@ -30,7 +32,7 @@ it('show a warning if channel has wrong id', () => {
expect( expect(
stdout.includes( stdout.includes(
`wrong_id.m3u\n 2 warning "qib22lAq1L.us" is not in the database\n\n1 problems (0 errors, 1 warnings)\n` 'wrong_id.m3u\n 2 warning "qib22lAq1L.us" is not in the database\n\n1 problems (0 errors, 1 warnings)\n'
) )
).toBe(true) ).toBe(true)
}) })

View file

@ -14,7 +14,7 @@ beforeEach(() => {
'tests/__data__/output/.readme/template.md' 'tests/__data__/output/.readme/template.md'
) )
const stdout = execSync( execSync(
'DATA_DIR=tests/__data__/input/data LOGS_DIR=tests/__data__/input/logs README_DIR=tests/__data__/output/.readme npm run readme:update', 'DATA_DIR=tests/__data__/input/data LOGS_DIR=tests/__data__/input/logs README_DIR=tests/__data__/output/.readme npm run readme:update',
{ encoding: 'utf8' } { encoding: 'utf8' }
) )