Update scripts

This commit is contained in:
freearhey 2025-03-29 11:39:46 +03:00
parent 74b3cff1d2
commit 02ec7e6f76
42 changed files with 1317 additions and 694 deletions

View file

@ -5,13 +5,13 @@ type BlockedProps = {
}
export class Blocked {
channel: string
channelId: string
reason: string
ref: string
constructor({ ref, reason, channel }: BlockedProps) {
this.channel = channel
this.reason = reason
this.ref = ref
constructor(data: BlockedProps) {
this.channelId = data.channel
this.reason = data.reason
this.ref = data.ref
}
}

View file

@ -0,0 +1,11 @@
type BroadcastAreaProps = {
code: string
}
export class BroadcastArea {
code: string
constructor(data: BroadcastAreaProps) {
this.code = data.code
}
}

View file

@ -1,4 +1,4 @@
type CategoryProps = {
type CategoryData = {
id: string
name: string
}
@ -7,8 +7,8 @@ export class Category {
id: string
name: string
constructor({ id, name }: CategoryProps) {
this.id = id
this.name = name
constructor(data: CategoryData) {
this.id = data.id
this.name = data.name
}
}

View file

@ -1,17 +1,16 @@
import { Collection } from '@freearhey/core'
import { Collection, Dictionary } from '@freearhey/core'
import { Category, Country, Subdivision } from './index'
type ChannelProps = {
type ChannelData = {
id: string
name: string
alt_names: string[]
network: string
owners: string[]
owners: Collection
country: string
subdivision: string
city: string
broadcast_area: string[]
languages: string[]
categories: string[]
categories: Collection
is_nsfw: boolean
launched: string
closed: string
@ -24,56 +23,86 @@ export class Channel {
id: string
name: string
altNames: Collection
network: string
network?: string
owners: Collection
country: string
subdivision: string
city: string
broadcastArea: Collection
languages: Collection
categories: Collection
countryCode: string
country?: Country
subdivisionCode?: string
subdivision?: Subdivision
cityName?: string
categoryIds: Collection
categories?: Collection
isNSFW: boolean
launched: string
closed: string
replacedBy: string
website: string
launched?: string
closed?: string
replacedBy?: string
website?: string
logo: string
constructor({
id,
name,
alt_names,
network,
owners,
country,
subdivision,
city,
broadcast_area,
languages,
categories,
is_nsfw,
launched,
closed,
replaced_by,
website,
logo
}: ChannelProps) {
this.id = id
this.name = name
this.altNames = new Collection(alt_names)
this.network = network
this.owners = new Collection(owners)
this.country = country
this.subdivision = subdivision
this.city = city
this.broadcastArea = new Collection(broadcast_area)
this.languages = new Collection(languages)
this.categories = new Collection(categories)
this.isNSFW = is_nsfw
this.launched = launched
this.closed = closed
this.replacedBy = replaced_by
this.website = website
this.logo = logo
constructor(data: ChannelData) {
this.id = data.id
this.name = data.name
this.altNames = new Collection(data.alt_names)
this.network = data.network || undefined
this.owners = new Collection(data.owners)
this.countryCode = data.country
this.subdivisionCode = data.subdivision || undefined
this.cityName = data.city || undefined
this.categoryIds = new Collection(data.categories)
this.isNSFW = data.is_nsfw
this.launched = data.launched || undefined
this.closed = data.closed || undefined
this.replacedBy = data.replaced_by || undefined
this.website = data.website || undefined
this.logo = data.logo
}
withSubdivision(subdivisionsGroupedByCode: Dictionary): this {
if (!this.subdivisionCode) return this
this.subdivision = subdivisionsGroupedByCode.get(this.subdivisionCode)
return this
}
withCountry(countriesGroupedByCode: Dictionary): this {
this.country = countriesGroupedByCode.get(this.countryCode)
return this
}
withCategories(groupedCategories: Dictionary): this {
this.categories = this.categoryIds
.map((id: string) => groupedCategories.get(id))
.filter(Boolean)
return this
}
getCountry(): Country | undefined {
return this.country
}
getSubdivision(): Subdivision | undefined {
return this.subdivision
}
getCategories(): Collection {
return this.categories || new Collection()
}
hasCategories(): boolean {
return !!this.categories && this.categories.notEmpty()
}
hasCategory(category: Category): boolean {
return (
!!this.categories &&
this.categories.includes((_category: Category) => _category.id === category.id)
)
}
isSFW(): boolean {
return this.isNSFW === false
}
}

View file

@ -1,20 +1,58 @@
type CountryProps = {
import { Collection, Dictionary } from '@freearhey/core'
import { Region, Language } from '.'
type CountryData = {
code: string
name: string
languages: string[]
lang: string
flag: string
}
export class Country {
code: string
name: string
languages: string[]
flag: string
languageCode: string
language?: Language
subdivisions?: Collection
regions?: Collection
constructor({ code, name, languages, flag }: CountryProps) {
this.code = code
this.name = name
this.languages = languages
this.flag = flag
constructor(data: CountryData) {
this.code = data.code
this.name = data.name
this.flag = data.flag
this.languageCode = data.lang
}
withSubdivisions(subdivisionsGroupedByCountryCode: Dictionary): this {
this.subdivisions = subdivisionsGroupedByCountryCode.get(this.code) || new Collection()
return this
}
withRegions(regions: Collection): this {
this.regions = regions.filter(
(region: Region) => region.code !== 'INT' && region.includesCountryCode(this.code)
)
return this
}
withLanguage(languagesGroupedByCode: Dictionary): this {
this.language = languagesGroupedByCode.get(this.languageCode)
return this
}
getLanguage(): Language | undefined {
return this.language
}
getRegions(): Collection {
return this.regions || new Collection()
}
getSubdivisions(): Collection {
return this.subdivisions || new Collection()
}
}

196
scripts/models/feed.ts Normal file
View file

@ -0,0 +1,196 @@
import { Collection, Dictionary } from '@freearhey/core'
import { Country, Language, Region, Channel, Subdivision } from './index'
type FeedData = {
channel: string
id: string
name: string
is_main: boolean
broadcast_area: Collection
languages: Collection
timezones: Collection
video_format: string
}
export class Feed {
channelId: string
channel?: Channel
id: string
name: string
isMain: boolean
broadcastAreaCodes: Collection
broadcastCountryCodes: Collection
broadcastCountries?: Collection
broadcastRegionCodes: Collection
broadcastRegions?: Collection
broadcastSubdivisionCodes: Collection
broadcastSubdivisions?: Collection
languageCodes: Collection
languages?: Collection
timezoneIds: Collection
timezones?: Collection
videoFormat: string
constructor(data: FeedData) {
this.channelId = data.channel
this.id = data.id
this.name = data.name
this.isMain = data.is_main
this.broadcastAreaCodes = new Collection(data.broadcast_area)
this.languageCodes = new Collection(data.languages)
this.timezoneIds = new Collection(data.timezones)
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(channelsGroupedById: Dictionary): this {
this.channel = channelsGroupedById.get(this.channelId)
return this
}
withLanguages(languagesGroupedByCode: Dictionary): this {
this.languages = this.languageCodes
.map((code: string) => languagesGroupedByCode.get(code))
.filter(Boolean)
return this
}
withTimezones(timezonesGroupedById: Dictionary): this {
this.timezones = this.timezoneIds
.map((id: string) => timezonesGroupedById.get(id))
.filter(Boolean)
return this
}
withBroadcastSubdivisions(subdivisionsGroupedByCode: Dictionary): this {
this.broadcastSubdivisions = this.broadcastSubdivisionCodes.map((code: string) =>
subdivisionsGroupedByCode.get(code)
)
return this
}
withBroadcastCountries(
countriesGroupedByCode: Dictionary,
regionsGroupedByCode: Dictionary,
subdivisionsGroupedByCode: Dictionary
): this {
let broadcastCountries = new Collection()
if (this.isInternational()) {
this.broadcastCountries = broadcastCountries
return this
}
this.broadcastCountryCodes.forEach((code: string) => {
broadcastCountries.add(countriesGroupedByCode.get(code))
})
this.broadcastRegionCodes.forEach((code: string) => {
const region: Region = regionsGroupedByCode.get(code)
broadcastCountries = broadcastCountries.concat(region.countryCodes)
})
this.broadcastSubdivisionCodes.forEach((code: string) => {
const subdivision: Subdivision = subdivisionsGroupedByCode.get(code)
broadcastCountries.add(countriesGroupedByCode.get(subdivision.countryCode))
})
this.broadcastCountries = broadcastCountries.uniq().filter(Boolean)
return this
}
withBroadcastRegions(regions: Collection, regionsGroupedByCode: Dictionary): this {
if (!this.broadcastCountries) return this
const countriesCodes = this.broadcastCountries.map((country: Country) => country.code)
const broadcastRegions = regions.filter((region: Region) =>
region.countryCodes.intersects(countriesCodes)
)
if (this.isInternational()) broadcastRegions.add(regionsGroupedByCode.get('INT'))
this.broadcastRegions = broadcastRegions
return this
}
hasBroadcastArea(): boolean {
return (
this.isInternational() || (!!this.broadcastCountries && this.broadcastCountries.notEmpty())
)
}
getBroadcastCountries(): Collection {
return this.broadcastCountries || new Collection()
}
getBroadcastRegions(): Collection {
return this.broadcastRegions || new Collection()
}
getTimezones(): Collection {
return this.timezones || new Collection()
}
getLanguages(): Collection {
return this.languages || new Collection()
}
hasLanguages(): boolean {
return !!this.languages && this.languages.notEmpty()
}
hasLanguage(language: Language): boolean {
return (
!!this.languages &&
this.languages.includes((_language: Language) => _language.code === language.code)
)
}
isInternational(): boolean {
return this.broadcastAreaCodes.includes('r/INT')
}
isBroadcastInSubdivision(subdivision: Subdivision): boolean {
if (this.isInternational()) return false
return this.broadcastSubdivisionCodes.includes(subdivision.code)
}
isBroadcastInCountry(country: Country): boolean {
if (this.isInternational()) return false
return this.getBroadcastCountries().includes(
(_country: Country) => _country.code === country.code
)
}
isBroadcastInRegion(region: Region): boolean {
if (this.isInternational()) return false
return this.getBroadcastRegions().includes((_region: Region) => _region.code === region.code)
}
}

View file

@ -8,3 +8,6 @@ export * from './language'
export * from './country'
export * from './region'
export * from './subdivision'
export * from './feed'
export * from './broadcastArea'
export * from './timezone'

View file

@ -1,4 +1,4 @@
type LanguageProps = {
type LanguageData = {
code: string
name: string
}
@ -7,8 +7,8 @@ export class Language {
code: string
name: string
constructor({ code, name }: LanguageProps) {
this.code = code
this.name = name
constructor(data: LanguageData) {
this.code = data.code
this.name = data.name
}
}

View file

@ -1,6 +1,7 @@
import { Collection } from '@freearhey/core'
import { Collection, Dictionary } from '@freearhey/core'
import { Subdivision } from '.'
type RegionProps = {
type RegionData = {
code: string
name: string
countries: string[]
@ -9,11 +10,43 @@ type RegionProps = {
export class Region {
code: string
name: string
countries: Collection
countryCodes: Collection
countries?: Collection
subdivisions?: Collection
constructor({ code, name, countries }: RegionProps) {
this.code = code
this.name = name
this.countries = new Collection(countries)
constructor(data: RegionData) {
this.code = data.code
this.name = data.name
this.countryCodes = new Collection(data.countries)
}
withCountries(countriesGroupedByCode: Dictionary): this {
this.countries = this.countryCodes.map((code: string) => countriesGroupedByCode.get(code))
return this
}
withSubdivisions(subdivisions: Collection): this {
this.subdivisions = subdivisions.filter(
(subdivision: Subdivision) => this.countryCodes.indexOf(subdivision.countryCode) > -1
)
return this
}
getSubdivisions(): Collection {
return this.subdivisions || new Collection()
}
getCountries(): Collection {
return this.countries || new Collection()
}
includesCountryCode(code: string): boolean {
return this.countryCodes.includes((countryCode: string) => countryCode === code)
}
isWorldwide(): boolean {
return this.code === 'INT'
}
}

View file

@ -1,64 +1,166 @@
import { URL, Collection } from '@freearhey/core'
import { Category, Language } from './index'
type StreamProps = {
name: string
url: string
filepath: string
line: number
channel?: string
httpReferrer?: string
httpUserAgent?: string
label?: string
quality?: string
}
import { URL, Collection, Dictionary } from '@freearhey/core'
import { Feed, Channel, Category, Region, Subdivision, Country, Language } from './index'
import parser from 'iptv-playlist-parser'
export class Stream {
channel: string
filepath: string
line: number
httpReferrer: string
label: string
name: string
quality: string
url: string
httpUserAgent: string
logo: string
broadcastArea: Collection
categories: Collection
languages: Collection
isNSFW: boolean
id?: string
groupTitle: string
channelId?: string
channel?: Channel
feedId?: string
feed?: Feed
filepath?: string
line: number
label?: string
quality?: string
httpReferrer?: string
httpUserAgent?: string
removed: boolean = false
constructor({
channel,
filepath,
line,
httpReferrer,
label,
name,
quality,
url,
httpUserAgent
}: StreamProps) {
this.channel = channel || ''
this.filepath = filepath
this.line = line
this.httpReferrer = httpReferrer || ''
this.label = label || ''
constructor(data: parser.PlaylistItem) {
if (!data.name) throw new Error('"name" property is required')
if (!data.url) throw new Error('"url" property is required')
const [channelId, feedId] = data.tvg.id.split('@')
const { name, label, quality } = parseTitle(data.name)
this.id = data.tvg.id || undefined
this.feedId = feedId || undefined
this.channelId = channelId || undefined
this.line = data.line
this.label = label || undefined
this.name = name
this.quality = quality || ''
this.url = url
this.httpUserAgent = httpUserAgent || ''
this.logo = ''
this.broadcastArea = new Collection()
this.categories = new Collection()
this.languages = new Collection()
this.isNSFW = false
this.quality = quality || undefined
this.url = data.url
this.httpReferrer = data.http.referrer || undefined
this.httpUserAgent = data.http['user-agent'] || undefined
this.groupTitle = 'Undefined'
}
withChannel(channelsGroupedById: Dictionary): this {
if (!this.channelId) return this
this.channel = channelsGroupedById.get(this.channelId)
return this
}
withFeed(feedsGroupedByChannelId: Dictionary): this {
if (!this.channelId) return this
const channelFeeds = feedsGroupedByChannelId.get(this.channelId) || []
if (this.feedId) this.feed = channelFeeds.find((feed: Feed) => feed.id === this.feedId)
if (!this.feed) this.feed = channelFeeds.find((feed: Feed) => feed.isMain)
return this
}
setId(id: string): this {
this.id = id
return this
}
setChannelId(channelId: string): this {
this.channelId = channelId
return this
}
setFeedId(feedId: string | undefined): this {
this.feedId = feedId
return this
}
setLabel(label: string): this {
this.label = label
return this
}
setQuality(quality: string): this {
this.quality = quality
return this
}
setHttpUserAgent(httpUserAgent: string): this {
this.httpUserAgent = httpUserAgent
return this
}
setHttpReferrer(httpReferrer: string): this {
this.httpReferrer = httpReferrer
return this
}
setFilepath(filepath: string): this {
this.filepath = filepath
return this
}
updateFilepath(): this {
if (!this.channel) return this
this.filepath = `${this.channel.countryCode.toLowerCase()}.m3u`
return this
}
getFilepath(): string {
return this.filepath || ''
}
getHttpReferrer(): string {
return this.httpReferrer || ''
}
getHttpUserAgent(): string {
return this.httpUserAgent || ''
}
getQuality(): string {
return this.quality || ''
}
hasQuality(): boolean {
return !!this.quality
}
getHorizontalResolution(): number {
if (!this.hasQuality()) return 0
return parseInt(this.getQuality().replace(/p|i/, ''))
}
updateName(): this {
if (!this.channel) return this
this.name = this.channel.name
if (this.feed && !this.feed.isMain) {
this.name += ` ${this.feed.name}`
}
return this
}
updateId(): this {
if (!this.channel) return this
if (this.feed) {
this.id = `${this.channel.id}@${this.feed.id}`
} else {
this.id = this.channel.id
}
return this
}
normalizeURL() {
const url = new URL(this.url)
@ -81,36 +183,75 @@ export class Stream {
return !!this.channel
}
hasCategories(): boolean {
return this.categories.notEmpty()
getBroadcastRegions(): Collection {
return this.feed ? this.feed.getBroadcastRegions() : new Collection()
}
noCategories(): boolean {
return this.categories.isEmpty()
getBroadcastCountries(): Collection {
return this.feed ? this.feed.getBroadcastCountries() : new Collection()
}
hasCategory(category: Category): boolean {
return this.categories.includes((_category: Category) => _category.id === category.id)
}
noLanguages(): boolean {
return this.languages.isEmpty()
}
hasLanguage(language: Language): boolean {
return this.languages.includes((_language: Language) => _language.code === language.code)
}
noBroadcastArea(): boolean {
return this.broadcastArea.isEmpty()
}
isInternational(): boolean {
return this.broadcastArea.includes('r/INT')
hasBroadcastArea(): boolean {
return this.feed ? this.feed.hasBroadcastArea() : false
}
isSFW(): boolean {
return this.isNSFW === false
return this.channel ? this.channel.isSFW() : true
}
hasCategories(): boolean {
return this.channel ? this.channel.hasCategories() : false
}
hasCategory(category: Category): boolean {
return this.channel ? this.channel.hasCategory(category) : false
}
getCategoryNames(): string[] {
return this.getCategories()
.map((category: Category) => category.name)
.sort()
.all()
}
getCategories(): Collection {
return this.channel ? this.channel.getCategories() : new Collection()
}
getLanguages(): Collection {
return this.feed ? this.feed.getLanguages() : new Collection()
}
hasLanguages() {
return this.feed ? this.feed.hasLanguages() : false
}
hasLanguage(language: Language) {
return this.feed ? this.feed.hasLanguage(language) : false
}
getBroadcastAreaCodes(): Collection {
return this.feed ? this.feed.broadcastAreaCodes : new Collection()
}
isBroadcastInSubdivision(subdivision: Subdivision): boolean {
return this.feed ? this.feed.isBroadcastInSubdivision(subdivision) : false
}
isBroadcastInCountry(country: Country): boolean {
return this.feed ? this.feed.isBroadcastInCountry(country) : false
}
isBroadcastInRegion(region: Region): boolean {
return this.feed ? this.feed.isBroadcastInRegion(region) : false
}
isInternational(): boolean {
return this.feed ? this.feed.isInternational() : false
}
getLogo(): string {
return this?.channel?.logo || ''
}
getTitle(): string {
@ -127,15 +268,25 @@ export class Stream {
return title
}
getLabel(): string {
return this.label || ''
}
getId(): string {
return this.id || ''
}
data() {
return {
id: this.id,
channel: this.channel,
feed: this.feed,
filepath: this.filepath,
httpReferrer: this.httpReferrer,
label: this.label,
name: this.name,
quality: this.quality,
url: this.url,
httpReferrer: this.httpReferrer,
httpUserAgent: this.httpUserAgent,
line: this.line
}
@ -143,7 +294,8 @@ export class Stream {
toJSON() {
return {
channel: this.channel || null,
channel: this.channelId || null,
feed: this.feedId || null,
url: this.url,
referrer: this.httpReferrer || null,
user_agent: this.httpUserAgent || null
@ -151,10 +303,10 @@ export class Stream {
}
toString(options: { public: boolean }) {
let output = `#EXTINF:-1 tvg-id="${this.channel}"`
let output = `#EXTINF:-1 tvg-id="${this.getId()}"`
if (options.public) {
output += ` tvg-logo="${this.logo}" group-title="${this.groupTitle}"`
output += ` tvg-logo="${this.getLogo()}" group-title="${this.groupTitle}"`
}
if (this.httpReferrer) {
@ -180,3 +332,16 @@ export class Stream {
return output
}
}
function parseTitle(title: string): { name: string; label: string; quality: string } {
const [, label] = title.match(/ \[(.*)\]$/) || [null, '']
title = title.replace(new RegExp(` \\[${escapeRegExp(label)}\\]$`), '')
const [, quality] = title.match(/ \(([0-9]+p)\)$/) || [null, '']
title = title.replace(new RegExp(` \\(${quality}\\)$`), '')
return { name: title, label, quality }
}
function escapeRegExp(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}

View file

@ -1,4 +1,7 @@
type SubdivisionProps = {
import { Dictionary } from '@freearhey/core'
import { Country } from '.'
type SubdivisionData = {
code: string
name: string
country: string
@ -7,11 +10,18 @@ type SubdivisionProps = {
export class Subdivision {
code: string
name: string
country: string
countryCode: string
country?: Country
constructor({ code, name, country }: SubdivisionProps) {
this.code = code
this.name = name
this.country = country
constructor(data: SubdivisionData) {
this.code = data.code
this.name = data.name
this.countryCode = data.country
}
withCountry(countriesGroupedByCode: Dictionary): this {
this.country = countriesGroupedByCode.get(this.countryCode)
return this
}
}

View file

@ -0,0 +1,30 @@
import { Collection, Dictionary } from '@freearhey/core'
type TimezoneData = {
id: string
utc_offset: string
countries: string[]
}
export class Timezone {
id: string
utcOffset: string
countryCodes: Collection
countries?: Collection
constructor(data: TimezoneData) {
this.id = data.id
this.utcOffset = data.utc_offset
this.countryCodes = new Collection(data.countries)
}
withCountries(countriesGroupedByCode: Dictionary): this {
this.countries = this.countryCodes.map((code: string) => countriesGroupedByCode.get(code))
return this
}
getCountries(): Collection {
return this.countries || new Collection()
}
}