Update scripts

This commit is contained in:
freearhey
2025-08-23 17:47:03 +03:00
parent f665512bfc
commit 63262842e7
21 changed files with 417 additions and 245 deletions

View File

@@ -18,7 +18,8 @@ async function main() {
loader.download('logos.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'),
loader.download('cities.json')
]) ])
} }

View File

@@ -57,7 +57,8 @@ export class DataLoader {
logos, logos,
timezones, timezones,
guides, guides,
streams streams,
cities
] = await Promise.all([ ] = await Promise.all([
this.storage.json('countries.json'), this.storage.json('countries.json'),
this.storage.json('regions.json'), this.storage.json('regions.json'),
@@ -70,7 +71,8 @@ export class DataLoader {
this.storage.json('logos.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'),
this.storage.json('cities.json')
]) ])
return { return {
@@ -85,7 +87,8 @@ export class DataLoader {
logos, logos,
timezones, timezones,
guides, guides,
streams streams,
cities
} }
} }

View File

@@ -1,3 +1,4 @@
import { DataProcessorData } from '../types/dataProcessor'
import { DataLoaderData } from '../types/dataLoader' import { DataLoaderData } from '../types/dataLoader'
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
import { import {
@@ -11,14 +12,16 @@ import {
Region, Region,
Stream, Stream,
Guide, Guide,
City,
Feed, Feed,
Logo Logo
} from '../models' } from '../models'
export class DataProcessor { export class DataProcessor {
constructor() {} process(data: DataLoaderData): DataProcessorData {
let regions = new Collection(data.regions).map(data => new Region(data))
let regionsKeyByCode = regions.keyBy((region: Region) => region.code)
process(data: DataLoaderData) {
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)
@@ -26,25 +29,23 @@ export class DataProcessor {
const languagesKeyByCode = languages.keyBy((language: Language) => language.code) const languagesKeyByCode = languages.keyBy((language: Language) => language.code)
let subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data)) let subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data))
const subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code) let subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
const subdivisionsGroupedByCountryCode = subdivisions.groupBy( let subdivisionsGroupedByCountryCode = subdivisions.groupBy(
(subdivision: Subdivision) => subdivision.countryCode (subdivision: Subdivision) => subdivision.countryCode
) )
let regions = new Collection(data.regions).map(data => new Region(data)) let countries = new Collection(data.countries).map(data => new Country(data))
const regionsKeyByCode = regions.keyBy((region: Region) => region.code) let countriesKeyByCode = countries.keyBy((country: Country) => country.code)
const countries = new Collection(data.countries).map(data => const cities = new Collection(data.cities).map(data =>
new Country(data) new City(data)
.withRegions(regions) .withRegions(regions)
.withLanguage(languagesKeyByCode) .withCountry(countriesKeyByCode)
.withSubdivisions(subdivisionsGroupedByCountryCode) .withSubdivision(subdivisionsKeyByCode)
)
const countriesKeyByCode = countries.keyBy((country: Country) => country.code)
subdivisions = subdivisions.map((subdivision: Subdivision) =>
subdivision.withCountry(countriesKeyByCode)
) )
const citiesKeyByCode = cities.keyBy((city: City) => city.code)
const citiesGroupedByCountryCode = cities.groupBy((city: City) => city.countryCode)
const citiesGroupedBySubdivisionCode = cities.groupBy((city: City) => city.subdivisionCode)
const timezones = new Collection(data.timezones).map(data => const timezones = new Collection(data.timezones).map(data =>
new Timezone(data).withCountries(countriesKeyByCode) new Timezone(data).withCountries(countriesKeyByCode)
@@ -56,27 +57,12 @@ export class DataProcessor {
(blocklistRecord: BlocklistRecord) => blocklistRecord.channelId (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) let channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
.withCategories(categoriesKeyById)
.withCountry(countriesKeyByCode)
.withSubdivision(subdivisionsKeyByCode)
.withCategories(categoriesKeyById)
)
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) let feeds = new Collection(data.feeds).map(data => new Feed(data))
let feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
const feeds = new Collection(data.feeds).map(data => let feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
new Feed(data)
.withChannel(channelsKeyById)
.withLanguages(languagesKeyByCode)
.withTimezones(timezonesKeyById)
.withBroadcastCountries(countriesKeyByCode, regionsKeyByCode, subdivisionsKeyByCode)
.withBroadcastRegions(regions)
.withBroadcastSubdivisions(subdivisionsKeyByCode)
)
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
const feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
const logos = new Collection(data.logos).map(data => new Logo(data).withFeed(feedsGroupedById)) const logos = new Collection(data.logos).map(data => new Logo(data).withFeed(feedsGroupedById))
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId) const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
@@ -90,11 +76,60 @@ export class DataProcessor {
const guides = new Collection(data.guides).map(data => new Guide(data)) const guides = new Collection(data.guides).map(data => new Guide(data))
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId()) const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId())
regions = regions.map((region: Region) => region.withCountries(countriesKeyByCode)) regions = regions.map((region: Region) =>
region
.withCountries(countriesKeyByCode)
.withRegions(regions)
.withSubdivisions(subdivisions)
.withCities(cities)
)
regionsKeyByCode = regions.keyBy((region: Region) => region.code)
countries = countries.map((country: Country) =>
country
.withCities(citiesGroupedByCountryCode)
.withSubdivisions(subdivisionsGroupedByCountryCode)
.withRegions(regions)
.withLanguage(languagesKeyByCode)
)
countriesKeyByCode = countries.keyBy((country: Country) => country.code)
subdivisions = subdivisions.map((subdivision: Subdivision) =>
subdivision
.withCities(citiesGroupedBySubdivisionCode)
.withCountry(countriesKeyByCode)
.withRegions(regions)
)
subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
subdivisionsGroupedByCountryCode = subdivisions.groupBy(
(subdivision: Subdivision) => subdivision.countryCode
)
channels = channels.map((channel: Channel) => channels = channels.map((channel: Channel) =>
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId) channel
.withFeeds(feedsGroupedByChannelId)
.withLogos(logosGroupedByChannelId)
.withCategories(categoriesKeyById)
.withCountry(countriesKeyByCode)
.withSubdivision(subdivisionsKeyByCode)
.withCategories(categoriesKeyById)
) )
channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
feeds = feeds.map((feed: Feed) =>
feed
.withChannel(channelsKeyById)
.withLanguages(languagesKeyByCode)
.withTimezones(timezonesKeyById)
.withBroadcastArea(
citiesKeyByCode,
subdivisionsKeyByCode,
countriesKeyByCode,
regionsKeyByCode
)
)
feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
return { return {
blocklistRecordsGroupedByChannelId, blocklistRecordsGroupedByChannelId,
@@ -111,6 +146,7 @@ export class DataProcessor {
regionsKeyByCode, regionsKeyByCode,
blocklistRecords, blocklistRecords,
channelsKeyById, channelsKeyById,
citiesKeyByCode,
subdivisions, subdivisions,
categories, categories,
countries, countries,
@@ -119,6 +155,7 @@ export class DataProcessor {
channels, channels,
regions, regions,
streams, streams,
cities,
guides, guides,
feeds, feeds,
logos logos

View File

@@ -40,17 +40,5 @@ export class CountriesGenerator implements Generator {
JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL
) )
}) })
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
const undefinedFilepath = 'countries/undefined.m3u'
await this.storage.save(undefinedFilepath, undefinedPlaylist.toString())
this.logFile.append(
JSON.stringify({
type: 'country',
filepath: undefinedFilepath,
count: undefinedPlaylist.streams.count()
}) + EOL
)
} }
} }

View File

@@ -26,18 +26,7 @@ export class IndexCountryGenerator implements Generator {
.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.hasBroadcastArea()) { if (!stream.hasBroadcastArea()) return
const streamClone = stream.clone()
streamClone.groupTitle = 'Undefined'
groupedStreams.add(streamClone)
return
}
if (stream.isInternational()) {
const streamClone = stream.clone()
streamClone.groupTitle = 'International'
groupedStreams.add(streamClone)
}
stream.getBroadcastCountries().forEach((country: Country) => { stream.getBroadcastCountries().forEach((country: Country) => {
const streamClone = stream.clone() const streamClone = stream.clone()
@@ -46,12 +35,7 @@ export class IndexCountryGenerator implements Generator {
}) })
}) })
groupedStreams = groupedStreams.orderBy((stream: Stream) => { groupedStreams = groupedStreams.orderBy((stream: Stream) => stream.groupTitle)
if (stream.groupTitle === 'International') return 'ZZ'
if (stream.groupTitle === 'Undefined') return 'ZZZ'
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'

View File

@@ -28,19 +28,7 @@ export class IndexRegionGenerator implements Generator {
.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.isInternational()) { if (!stream.hasBroadcastArea()) return
const streamClone = stream.clone()
streamClone.groupTitle = 'International'
groupedStreams.push(streamClone)
return
}
if (!stream.hasBroadcastArea()) {
const streamClone = stream.clone()
streamClone.groupTitle = 'Undefined'
groupedStreams.push(streamClone)
return
}
stream.getBroadcastRegions().forEach((region: Region) => { stream.getBroadcastRegions().forEach((region: Region) => {
const streamClone = stream.clone() const streamClone = stream.clone()
@@ -49,11 +37,7 @@ export class IndexRegionGenerator implements Generator {
}) })
}) })
groupedStreams = groupedStreams.orderBy((stream: Stream) => { groupedStreams = groupedStreams.orderBy((stream: Stream) => stream.groupTitle)
if (stream.groupTitle === 'International') return 'ZZ'
if (stream.groupTitle === 'Undefined') return 'ZZZ'
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'

View File

@@ -28,8 +28,6 @@ export class RegionsGenerator implements Generator {
.filter((stream: Stream) => stream.isSFW()) .filter((stream: Stream) => stream.isSFW())
this.regions.forEach(async (region: Region) => { this.regions.forEach(async (region: Region) => {
if (region.isWorldwide()) return
const regionStreams = streams.filter((stream: Stream) => stream.isBroadcastInRegion(region)) const regionStreams = streams.filter((stream: Stream) => stream.isBroadcastInRegion(region))
const playlist = new Playlist(regionStreams, { public: true }) const playlist = new Playlist(regionStreams, { public: true })
@@ -39,25 +37,5 @@ export class RegionsGenerator implements Generator {
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
) )
}) })
const internationalStreams = streams.filter((stream: Stream) => stream.isInternational())
const internationalPlaylist = new Playlist(internationalStreams, { public: true })
const internationalFilepath = 'regions/int.m3u'
await this.storage.save(internationalFilepath, internationalPlaylist.toString())
this.logFile.append(
JSON.stringify({
type: 'region',
filepath: internationalFilepath,
count: internationalPlaylist.streams.count()
}) + EOL
)
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
const playlist = new Playlist(undefinedStreams, { public: true })
const filepath = 'regions/undefined.m3u'
await this.storage.save(filepath, playlist.toString())
this.logFile.append(
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
)
} }
} }

View File

@@ -1,11 +1,101 @@
type BroadcastAreaProps = { import { Collection, Dictionary } from '@freearhey/core'
code: string import { City, Subdivision, Region, Country } from './'
}
export class BroadcastArea { export class BroadcastArea {
code: string codes: Collection
citiesIncluded: Collection
subdivisionsIncluded: Collection
countriesIncluded: Collection
regionsIncluded: Collection
constructor(data: BroadcastAreaProps) { constructor(codes: Collection) {
this.code = data.code this.codes = codes
}
withLocations(
citiesKeyByCode: Dictionary,
subdivisionsKeyByCode: Dictionary,
countriesKeyByCode: Dictionary,
regionsKeyByCode: Dictionary
): this {
let citiesIncluded = new Collection()
let subdivisionsIncluded = new Collection()
let countriesIncluded = new Collection()
let regionsIncluded = new Collection()
this.codes.forEach((value: string) => {
const [type, code] = value.split('/')
switch (type) {
case 'ct': {
const city: City = citiesKeyByCode.get(code)
if (!city) return
citiesIncluded.add(city)
}
case 's': {
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
if (!subdivision) return
citiesIncluded = citiesIncluded.concat(subdivision.getCities())
subdivisionsIncluded.add(subdivision)
}
case 'c': {
const country: Country = countriesKeyByCode.get(code)
if (!country) return
citiesIncluded = citiesIncluded.concat(country.getCities())
subdivisionsIncluded = subdivisionsIncluded.concat(country.getSubdivisions())
countriesIncluded.add(country)
regionsIncluded = regionsIncluded.concat(country.getRegions())
}
case 'r': {
const region: Region = regionsKeyByCode.get(code)
if (!region) return
countriesIncluded = countriesIncluded.concat(region.getCountries())
regionsIncluded = regionsIncluded.concat(region.getRegions())
}
}
})
this.citiesIncluded = citiesIncluded.uniqBy((city: City) => city.code)
this.subdivisionsIncluded = subdivisionsIncluded.uniqBy(
(subdivision: Subdivision) => subdivision.code
)
this.countriesIncluded = countriesIncluded.uniqBy((country: Country) => country.code)
this.regionsIncluded = regionsIncluded.uniqBy((region: Region) => region.code)
return this
}
getCountries(): Collection {
return this.countriesIncluded || new Collection()
}
getSubdivisions(): Collection {
return this.subdivisionsIncluded || new Collection()
}
getCities(): Collection {
return this.citiesIncluded || new Collection()
}
getRegions(): Collection {
return this.regionsIncluded || new Collection()
}
includesCountry(country: Country): boolean {
return this.getCountries().includes((_country: Country) => _country.code === country.code)
}
includesSubdivision(subdivision: Subdivision): boolean {
return this.getSubdivisions().includes(
(_subdivision: Subdivision) => _subdivision.code === subdivision.code
)
}
includesRegion(region: Region): boolean {
return this.getRegions().includes((_region: Region) => _region.code === region.code)
}
includesCity(city: City): boolean {
return this.getCities().includes((_city: City) => _city.code === city.code)
} }
} }

78
scripts/models/city.ts Normal file
View File

@@ -0,0 +1,78 @@
import { Collection, Dictionary } from '@freearhey/core'
import { Country, Region, Subdivision } from '.'
import type { CityData, CitySerializedData } from '../types/city'
export class City {
code: string
name: string
countryCode: string
country?: Country
subdivisionCode?: string
subdivision?: Subdivision
wikidataId: string
regions?: Collection
constructor(data?: CityData) {
if (!data) return
this.code = data.code
this.name = data.name
this.countryCode = data.country
this.subdivisionCode = data.subdivision || undefined
this.wikidataId = data.wikidata_id
}
withCountry(countriesKeyByCode: Dictionary): this {
this.country = countriesKeyByCode.get(this.countryCode)
return this
}
withSubdivision(subdivisionsKeyByCode: Dictionary): this {
if (!this.subdivisionCode) return this
this.subdivision = subdivisionsKeyByCode.get(this.subdivisionCode)
return this
}
withRegions(regions: Collection): this {
this.regions = regions.filter((region: Region) =>
region.countryCodes.includes(this.countryCode)
)
return this
}
getRegions(): Collection {
if (!this.regions) return new Collection()
return this.regions
}
serialize(): CitySerializedData {
return {
code: this.code,
name: this.name,
countryCode: this.countryCode,
country: this.country ? this.country.serialize() : undefined,
subdivisionCode: this.subdivisionCode || null,
subdivision: this.subdivision ? this.subdivision.serialize() : undefined,
wikidataId: this.wikidataId
}
}
deserialize(data: CitySerializedData): this {
this.code = data.code
this.name = data.name
this.countryCode = data.countryCode
this.country = data.country ? new Country().deserialize(data.country) : undefined
this.subdivisionCode = data.subdivisionCode || undefined
this.subdivision = data.subdivision
? new Subdivision().deserialize(data.subdivision)
: undefined
this.wikidataId = data.wikidataId
return this
}
}

View File

@@ -12,6 +12,7 @@ export class Country {
language?: Language language?: Language
subdivisions?: Collection subdivisions?: Collection
regions?: Collection regions?: Collection
cities?: Collection
constructor(data?: CountryData) { constructor(data?: CountryData) {
if (!data) return if (!data) return
@@ -23,15 +24,19 @@ export class Country {
} }
withSubdivisions(subdivisionsGroupedByCountryCode: Dictionary): this { withSubdivisions(subdivisionsGroupedByCountryCode: Dictionary): this {
this.subdivisions = subdivisionsGroupedByCountryCode.get(this.code) || new Collection() this.subdivisions = new Collection(subdivisionsGroupedByCountryCode.get(this.code))
return this return this
} }
withRegions(regions: Collection): this { withRegions(regions: Collection): this {
this.regions = regions.filter( this.regions = regions.filter((region: Region) => region.includesCountryCode(this.code))
(region: Region) => region.code !== 'INT' && region.includesCountryCode(this.code)
) return this
}
withCities(citiesGroupedByCountryCode: Dictionary): this {
this.cities = new Collection(citiesGroupedByCountryCode.get(this.code))
return this return this
} }
@@ -54,6 +59,10 @@ export class Country {
return this.subdivisions || new Collection() return this.subdivisions || new Collection()
} }
getCities(): Collection {
return this.cities || new Collection()
}
serialize(): CountrySerializedData { serialize(): CountrySerializedData {
return { return {
code: this.code, code: this.code,

View File

@@ -1,4 +1,4 @@
import { Country, Language, Region, Channel, Subdivision } from './index' import { Country, Language, Region, Channel, Subdivision, BroadcastArea } from './index'
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
import type { FeedData } from '../types/feed' import type { FeedData } from '../types/feed'
@@ -9,12 +9,7 @@ export class Feed {
name: string name: string
isMain: boolean isMain: boolean
broadcastAreaCodes: Collection broadcastAreaCodes: Collection
broadcastCountryCodes: Collection broadcastArea?: BroadcastArea
broadcastCountries?: Collection
broadcastRegionCodes: Collection
broadcastRegions?: Collection
broadcastSubdivisionCodes: Collection
broadcastSubdivisions?: Collection
languageCodes: Collection languageCodes: Collection
languages?: Collection languages?: Collection
timezoneIds: Collection timezoneIds: Collection
@@ -32,25 +27,6 @@ export class Feed {
this.languageCodes = new Collection(data.languages) this.languageCodes = new Collection(data.languages)
this.timezoneIds = new Collection(data.timezones) this.timezoneIds = new Collection(data.timezones)
this.videoFormat = data.video_format this.videoFormat = data.video_format
this.broadcastCountryCodes = new Collection()
this.broadcastRegionCodes = new Collection()
this.broadcastSubdivisionCodes = new Collection()
this.broadcastAreaCodes.forEach((areaCode: string) => {
const [type, code] = areaCode.split('/')
switch (type) {
case 'c':
this.broadcastCountryCodes.add(code)
break
case 'r':
this.broadcastRegionCodes.add(code)
break
case 's':
this.broadcastSubdivisionCodes.add(code)
break
}
})
} }
withChannel(channelsKeyById: Dictionary): this { withChannel(channelsKeyById: Dictionary): this {
@@ -93,76 +69,36 @@ export class Feed {
return this return this
} }
withBroadcastSubdivisions(subdivisionsKeyByCode: Dictionary): this { withBroadcastArea(
this.broadcastSubdivisions = this.broadcastSubdivisionCodes.map((code: string) => citiesKeyByCode: Dictionary,
subdivisionsKeyByCode.get(code) subdivisionsKeyByCode: Dictionary,
)
return this
}
withBroadcastCountries(
countriesKeyByCode: Dictionary, countriesKeyByCode: Dictionary,
regionsKeyByCode: Dictionary, regionsKeyByCode: Dictionary
subdivisionsKeyByCode: Dictionary
): this { ): this {
const broadcastCountries = new Collection() this.broadcastArea = new BroadcastArea(this.broadcastAreaCodes).withLocations(
citiesKeyByCode,
if (this.isInternational()) { subdivisionsKeyByCode,
this.broadcastCountries = broadcastCountries countriesKeyByCode,
return this regionsKeyByCode
} )
this.broadcastCountryCodes.forEach((code: string) => {
broadcastCountries.add(countriesKeyByCode.get(code))
})
this.broadcastRegionCodes.forEach((code: string) => {
const region: Region = regionsKeyByCode.get(code)
if (region) {
region.countryCodes.forEach((countryCode: string) => {
broadcastCountries.add(countriesKeyByCode.get(countryCode))
})
}
})
this.broadcastSubdivisionCodes.forEach((code: string) => {
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
if (subdivision) {
broadcastCountries.add(countriesKeyByCode.get(subdivision.countryCode))
}
})
this.broadcastCountries = broadcastCountries.uniq().filter(Boolean)
return this
}
withBroadcastRegions(regions: Collection): this {
if (!this.broadcastCountries) return this
const countriesCodes = this.broadcastCountries.map((country: Country) => country.code)
this.broadcastRegions = regions.filter((region: Region) => {
if (region.code === 'INT') return false
const intersected = region.countryCodes.intersects(countriesCodes)
return intersected.notEmpty()
})
return this return this
} }
hasBroadcastArea(): boolean { hasBroadcastArea(): boolean {
return ( return !!this.broadcastArea
this.isInternational() || (!!this.broadcastCountries && this.broadcastCountries.notEmpty())
)
} }
getBroadcastCountries(): Collection { getBroadcastCountries(): Collection {
return this.broadcastCountries || new Collection() if (!this.broadcastArea) return new Collection()
return this.broadcastArea.getCountries()
} }
getBroadcastRegions(): Collection { getBroadcastRegions(): Collection {
return this.broadcastRegions || new Collection() if (!this.broadcastArea) return new Collection()
return this.broadcastArea.getRegions()
} }
getTimezones(): Collection { getTimezones(): Collection {
@@ -184,35 +120,22 @@ export class Feed {
) )
} }
isInternational(): boolean {
return this.broadcastAreaCodes.includes('r/INT')
}
isBroadcastInSubdivision(subdivision: Subdivision): boolean { isBroadcastInSubdivision(subdivision: Subdivision): boolean {
if (this.isInternational()) return false if (!this.broadcastArea) return false
if (this.broadcastSubdivisionCodes.includes(subdivision.code)) return true
if (
this.broadcastSubdivisionCodes.isEmpty() &&
subdivision.country &&
this.isBroadcastInCountry(subdivision.country)
)
return true
return false return this.broadcastArea.includesSubdivision(subdivision)
} }
isBroadcastInCountry(country: Country): boolean { isBroadcastInCountry(country: Country): boolean {
if (this.isInternational()) return false if (!this.broadcastArea) return false
return this.getBroadcastCountries().includes( return this.broadcastArea?.includesCountry(country)
(_country: Country) => _country.code === country.code
)
} }
isBroadcastInRegion(region: Region): boolean { isBroadcastInRegion(region: Region): boolean {
if (this.isInternational()) return false if (!this.broadcastArea) return false
return this.getBroadcastRegions().includes((_region: Region) => _region.code === region.code) return this.broadcastArea?.includesRegion(region)
} }
getGuides(): Collection { getGuides(): Collection {

View File

@@ -2,6 +2,7 @@ export * from './blocklistRecord'
export * from './broadcastArea' export * from './broadcastArea'
export * from './category' export * from './category'
export * from './channel' export * from './channel'
export * from './city'
export * from './country' export * from './country'
export * from './feed' export * from './feed'
export * from './guide' export * from './guide'

View File

@@ -1,15 +1,18 @@
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
import { Country, Subdivision } from '.' import { City, Country, Subdivision } from '.'
import type { RegionData, RegionSerializedData } from '../types/region' import type { RegionData, RegionSerializedData } from '../types/region'
import { CountrySerializedData } from '../types/country' import { CountrySerializedData } from '../types/country'
import { SubdivisionSerializedData } from '../types/subdivision' import { SubdivisionSerializedData } from '../types/subdivision'
import { CitySerializedData } from '../types/city'
export class Region { export class Region {
code: string code: string
name: string name: string
countryCodes: Collection countryCodes: Collection
countries: Collection = new Collection() countries?: Collection
subdivisions: Collection = new Collection() subdivisions?: Collection
cities?: Collection
regions?: Collection
constructor(data?: RegionData) { constructor(data?: RegionData) {
if (!data) return if (!data) return
@@ -33,30 +36,61 @@ export class Region {
return this return this
} }
withCities(cities: Collection): this {
this.cities = cities.filter((city: City) => this.countryCodes.indexOf(city.countryCode) > -1)
return this
}
withRegions(regions: Collection): this {
this.regions = regions.filter(
(region: Region) => !region.countryCodes.intersects(this.countryCodes).isEmpty()
)
return this
}
getSubdivisions(): Collection { getSubdivisions(): Collection {
if (!this.subdivisions) return new Collection()
return this.subdivisions return this.subdivisions
} }
getCountries(): Collection { getCountries(): Collection {
if (!this.countries) return new Collection()
return this.countries return this.countries
} }
getCities(): Collection {
if (!this.cities) return new Collection()
return this.cities
}
getRegions(): Collection {
if (!this.regions) return new Collection()
return this.regions
}
includesCountryCode(code: string): boolean { includesCountryCode(code: string): boolean {
return this.countryCodes.includes((countryCode: string) => countryCode === code) return this.countryCodes.includes((countryCode: string) => countryCode === code)
} }
isWorldwide(): boolean {
return this.code === 'INT'
}
serialize(): RegionSerializedData { serialize(): RegionSerializedData {
return { return {
code: this.code, code: this.code,
name: this.name, name: this.name,
countryCodes: this.countryCodes.all(), countryCodes: this.countryCodes.all(),
countries: this.countries.map((country: Country) => country.serialize()).all(), countries: this.getCountries()
subdivisions: this.subdivisions .map((country: Country) => country.serialize())
.all(),
subdivisions: this.getSubdivisions()
.map((subdivision: Subdivision) => subdivision.serialize()) .map((subdivision: Subdivision) => subdivision.serialize())
.all(),
cities: this.getCities()
.map((city: City) => city.serialize())
.all() .all()
} }
} }
@@ -71,6 +105,9 @@ export class Region {
this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) => this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) =>
new Subdivision().deserialize(data) new Subdivision().deserialize(data)
) )
this.cities = new Collection(data.cities).map((data: CitySerializedData) =>
new City().deserialize(data)
)
return this return this
} }

View File

@@ -342,10 +342,6 @@ export class Stream {
return this.feed ? this.feed.isBroadcastInRegion(region) : false return this.feed ? this.feed.isBroadcastInRegion(region) : false
} }
isInternational(): boolean {
return this.feed ? this.feed.isInternational() : false
}
getLogos(): Collection { getLogos(): Collection {
function format(logo: Logo): number { function format(logo: Logo): number {
const levelByFormat = { SVG: 0, PNG: 3, APNG: 1, WebP: 1, AVIF: 1, JPEG: 2, GIF: 1 } const levelByFormat = { SVG: 0, PNG: 3, APNG: 1, WebP: 1, AVIF: 1, JPEG: 2, GIF: 1 }

View File

@@ -1,12 +1,15 @@
import { SubdivisionData, SubdivisionSerializedData } from '../types/subdivision' import { SubdivisionData, SubdivisionSerializedData } from '../types/subdivision'
import { Dictionary } from '@freearhey/core' import { Dictionary, Collection } from '@freearhey/core'
import { Country } from '.' import { Country, Region } from '.'
export class Subdivision { export class Subdivision {
code: string code: string
name: string name: string
countryCode: string countryCode: string
country?: Country country?: Country
parentCode?: string
regions?: Collection
cities?: Collection
constructor(data?: SubdivisionData) { constructor(data?: SubdivisionData) {
if (!data) return if (!data) return
@@ -14,6 +17,7 @@ export class Subdivision {
this.code = data.code this.code = data.code
this.name = data.name this.name = data.name
this.countryCode = data.country this.countryCode = data.country
this.parentCode = data.parent || undefined
} }
withCountry(countriesKeyByCode: Dictionary): this { withCountry(countriesKeyByCode: Dictionary): this {
@@ -22,12 +26,39 @@ export class Subdivision {
return this return this
} }
withRegions(regions: Collection): this {
this.regions = regions.filter((region: Region) =>
region.countryCodes.includes(this.countryCode)
)
return this
}
withCities(citiesGroupedBySubdivisionCode: Dictionary): this {
this.cities = new Collection(citiesGroupedBySubdivisionCode.get(this.code))
return this
}
getRegions(): Collection {
if (!this.regions) return new Collection()
return this.regions
}
getCities(): Collection {
if (!this.cities) return new Collection()
return this.cities
}
serialize(): SubdivisionSerializedData { serialize(): SubdivisionSerializedData {
return { return {
code: this.code, code: this.code,
name: this.name, name: this.name,
countryCode: this.code, countryCode: this.countryCode,
country: this.country ? this.country.serialize() : undefined country: this.country ? this.country.serialize() : undefined,
parentCode: this.parentCode || null
} }
} }
@@ -36,6 +67,7 @@ export class Subdivision {
this.name = data.name this.name = data.name
this.countryCode = data.countryCode this.countryCode = data.countryCode
this.country = data.country ? new Country().deserialize(data.country) : undefined this.country = data.country ? new Country().deserialize(data.country) : undefined
this.parentCode = data.parentCode || undefined
return this return this
} }

20
scripts/types/city.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
import { CountrySerializedData } from './country'
import { SubdivisionSerializedData } from './subdivision'
export type CitySerializedData = {
code: string
name: string
countryCode: string
country?: CountrySerializedData
subdivisionCode: string | null
subdivision?: SubdivisionSerializedData
wikidataId: string
}
export type CityData = {
code: string
name: string
country: string
subdivision: string | null
wikidata_id: string
}

View File

@@ -17,4 +17,5 @@ export type DataLoaderData = {
timezones: object | object[] timezones: object | object[]
guides: object | object[] guides: object | object[]
streams: object | object[] streams: object | object[]
cities: object | object[]
} }

View File

@@ -15,6 +15,7 @@ export type DataProcessorData = {
regionsKeyByCode: Dictionary regionsKeyByCode: Dictionary
blocklistRecords: Collection blocklistRecords: Collection
channelsKeyById: Dictionary channelsKeyById: Dictionary
citiesKeyByCode: Dictionary
subdivisions: Collection subdivisions: Collection
categories: Collection categories: Collection
countries: Collection countries: Collection
@@ -23,6 +24,8 @@ export type DataProcessorData = {
channels: Collection channels: Collection
regions: Collection regions: Collection
streams: Collection streams: Collection
cities: Collection
guides: Collection guides: Collection
feeds: Collection feeds: Collection
logos: Collection
} }

View File

@@ -1,12 +1,10 @@
import { Collection } from '@freearhey/core'
export type FeedData = { export type FeedData = {
channel: string channel: string
id: string id: string
name: string name: string
is_main: boolean is_main: boolean
broadcast_area: Collection broadcast_area: string[]
languages: Collection languages: string[]
timezones: Collection timezones: string[]
video_format: string video_format: string
} }

View File

@@ -1,9 +1,14 @@
import { CitySerializedData } from './city'
import { CountrySerializedData } from './country'
import { SubdivisionSerializedData } from './subdivision'
export type RegionSerializedData = { export type RegionSerializedData = {
code: string code: string
name: string name: string
countryCodes: string[] countryCodes: string[]
countries?: CountrySerializedData[] countries?: CountrySerializedData[]
subdivisions?: SubdivisionSerializedData[] subdivisions?: SubdivisionSerializedData[]
cities?: CitySerializedData[]
} }
export type RegionData = { export type RegionData = {

View File

@@ -1,12 +1,16 @@
import { CountrySerializedData } from './country'
export type SubdivisionSerializedData = { export type SubdivisionSerializedData = {
code: string code: string
name: string name: string
countryCode: string countryCode: string
country?: CountrySerializedData country?: CountrySerializedData
parentCode: string | null
} }
export type SubdivisionData = { export type SubdivisionData = {
code: string code: string
name: string name: string
country: string country: string
parent: string | null
} }