Update src/

This commit is contained in:
freearhey 2025-04-14 21:53:33 +03:00
parent 09b07e9b24
commit 86743c74f5
132 changed files with 4418 additions and 1907 deletions

View file

@ -0,0 +1,50 @@
import type { BlocklistRecordData, BlocklistRecordSerializedData } from '~/types/blocklistRecord'
export class BlocklistRecord {
channelId: string
reason: string
refUrl: string
constructor(data?: BlocklistRecordData) {
if (!data) return
this.channelId = data.channel
this.reason = data.reason
this.refUrl = data.ref
}
getRefLabel(): string {
let refLabel = ''
const isIssue = /issues|pull/.test(this.refUrl)
const isAttachment = /github\.zendesk\.com\/attachments\/token/.test(this.refUrl)
if (isIssue) {
const parts = this.refUrl.split('/')
const issueId = parts.pop()
refLabel = `#${issueId}`
} else if (isAttachment) {
const [, filename] = this.refUrl.match(/\?name=(.*)/) || [null, undefined]
refLabel = filename
} else {
refLabel = this.refUrl.split('/').pop()
}
return refLabel
}
serialize(): BlocklistRecordSerializedData {
return {
channelId: this.channelId,
reason: this.reason,
refUrl: this.refUrl
}
}
deserialize(data: BlocklistRecordSerializedData): this {
this.channelId = data.channelId
this.reason = data.reason
this.refUrl = data.refUrl
return this
}
}

195
src/models/broadcastArea.ts Normal file
View file

@ -0,0 +1,195 @@
import type { BroadcastAreaData, BroadcastAreaSerializedData } from '~/types/broadcastArea'
import type { SubdivisionSerializedData } from '~/types/subdivision'
import type { CountrySerializedData } from '~/types/country'
import type { RegionSerializedData } from '~/types/region'
import { type Dictionary, Collection } from '@freearhey/core/browser'
import { Region, Country, Subdivision } from './'
export class BroadcastArea {
code: string
name?: string
countries?: Collection
subdivisions?: Collection
regions?: Collection
constructor(data?: BroadcastAreaData) {
if (!data) return
this.code = data.code
}
withName(
countriesKeyByCode: Dictionary,
subdivisionsKeyByCode: Dictionary,
regionsKeyByCode: Dictionary
): this {
const [type, code] = this.code.split('/')
switch (type) {
case 's': {
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
if (subdivision) this.name = subdivision.name
break
}
case 'c': {
const country: Country = countriesKeyByCode.get(code)
if (country) this.name = country.name
break
}
case 'r': {
const region: Region = regionsKeyByCode.get(code)
if (region) this.name = region.name
break
}
}
return this
}
withLocations(
countriesKeyByCode: Dictionary,
subdivisionsKeyByCode: Dictionary,
regionsKeyByCode: Dictionary,
regions: Collection
): this {
const [type, code] = this.code.split('/')
let _countries = new Collection()
let _regions = new Collection()
let _subdivisions = new Collection()
regions = regions.filter((region: Region) => region.code !== 'INT')
switch (type) {
case 's': {
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
if (!subdivision) break
_subdivisions.add(subdivision)
const country: Country = countriesKeyByCode.get(subdivision.countryCode)
if (!country) break
_countries.add(country)
const countryRegions = regions.filter((region: Region) =>
region.countryCodes.includes(country.code)
)
countryRegions.forEach((region: Region) => {
_regions.add(region)
})
break
}
case 'c': {
const country = countriesKeyByCode.get(code)
if (!country) break
_countries.add(country)
const countryRegions = regions.filter((region: Region) =>
region.countryCodes.includes(country.code)
)
countryRegions.forEach((region: Region) => {
_regions.add(region)
})
break
}
case 'r': {
const region: Region = regionsKeyByCode.get(code)
if (!region) break
_regions.add(region)
break
}
}
this.countries = _countries.uniqBy((country: Country) => country.code)
this.regions = _regions.uniqBy((region: Region) => region.code)
this.subdivisions = _subdivisions.uniqBy((subdivision: Subdivision) => subdivision.code)
return this
}
getName(): string {
return this.name || ''
}
getCountries(): Collection {
if (!this.countries) return new Collection()
return this.countries
}
getRegions(): Collection {
if (!this.regions) return new Collection()
return this.regions
}
getSubdivisions(): Collection {
if (!this.subdivisions) return new Collection()
return this.subdivisions
}
getLocationCodes(): Collection {
let locationCodes = new Collection()
this.getCountries().forEach((country: Country) => {
locationCodes.add(country.code)
})
this.getRegions().forEach((region: Region) => {
locationCodes.add(region.code)
})
this.getSubdivisions().forEach((subdivision: Subdivision) => {
locationCodes.add(subdivision.code)
})
return locationCodes
}
getLocationNames(): Collection {
let locationNames = new Collection()
this.getCountries().forEach((country: Country) => {
locationNames.add(country.name)
})
this.getRegions().forEach((region: Region) => {
locationNames.add(region.name)
})
this.getSubdivisions().forEach((subdivision: Subdivision) => {
locationNames.add(subdivision.name)
})
return locationNames
}
serialize(): BroadcastAreaSerializedData {
return {
code: this.code,
name: this.getName(),
countries: this.getCountries()
.map((country: Country) => country.serialize())
.all(),
subdivisions: this.getSubdivisions()
.map((subdivision: Subdivision) => subdivision.serialize())
.all(),
regions: this.getRegions()
.map((region: Region) => region.serialize())
.all()
}
}
deserialize(data: BroadcastAreaSerializedData): this {
this.code = data.code
this.name = data.name
this.countries = new Collection(data.countries).map((data: CountrySerializedData) =>
new Country().deserialize(data)
)
this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) =>
new Subdivision().deserialize(data)
)
this.regions = new Collection(data.regions).map((data: RegionSerializedData) =>
new Region().deserialize(data)
)
return this
}
}

27
src/models/category.ts Normal file
View file

@ -0,0 +1,27 @@
import type { CategoryData, CategorySerializedData } from '~/types/category'
export class Category {
id: string
name: string
constructor(data?: CategoryData) {
if (!data) return
this.id = data.id
this.name = data.name
}
serialize(): CategorySerializedData {
return {
id: this.id,
name: this.name
}
}
deserialize(data: CategorySerializedData): this {
this.id = data.id
this.name = data.name
return this
}
}

View file

@ -1,56 +0,0 @@
export class Channel {
constructor(data) {
const _streams = Array.isArray(data.streams) ? data.streams : []
const _guides = Array.isArray(data.guides) ? data.guides : []
const _blocklistRecords = Array.isArray(data.blocklistRecords) ? data.blocklistRecords : []
this.id = data.id
this.name = data.name
this.alt_names = this.alt_name = data.altNames
this.network = data.network
this.owners = this.owner = data.owners
this.city = data.city
this.country = [data.country?.code, data.country?.name].filter(Boolean)
this.subdivision = data.subdivision?.code || null
this.languages = this.language = [
...data.languages.map(language => language.code),
...data.languages.map(language => language.name)
]
this.categories = this.category = data.categories.map(category => category.name)
this.broadcast_area = [
...data.broadcastArea.map(area => `${area.type}/${area.code}`).filter(Boolean),
...data.broadcastArea.map(area => area.name).filter(Boolean),
...data.regionCountries.map(country => country.code).filter(Boolean),
...data.regionCountries.map(country => country.name).filter(Boolean)
]
this.is_nsfw = data.isNSFW
this.launched = data.launched
this.closed = data.closed
this.is_closed = !!data.closed || !!data.replacedBy
this.replaced_by = data.replacedBy
this.website = data.website
this.logo = data.logo
this.streams = _streams.length
this.guides = _guides.length
this.is_blocked = _blocklistRecords.length > 0
this._hasUniqueName = data.hasUniqueName
this._displayName = data.hasUniqueName ? data.name : `${data.name} (${data.country?.name})`
this._country = data.country
this._subdivision = data.subdivision || null
this._languages = data.languages
this._categories = data.categories
this._broadcastArea = data.broadcastArea
this._streams = _streams
this._guides = _guides
this._blocklistRecords = _blocklistRecords
this._guideNames = _guides.map(guide => guide.site_name).filter(Boolean)
this._streamUrls = _streams.map(stream => stream.url).filter(Boolean)
}
toObject() {
const { ...object } = this
return object
}
}

537
src/models/channel.ts Normal file
View file

@ -0,0 +1,537 @@
import type { ChannelSearchable, ChannelSerializedData, ChannelData } from '../types/channel'
import type { BlocklistRecordSerializedData } from '~/types/blocklistRecord'
import type { HTMLPreviewField } from '../types/htmlPreviewField'
import type { CategorySerializedData } from '~/types/category'
import type { FeedSerializedData } from '~/types/feed'
import { Collection, type Dictionary } from '@freearhey/core/browser'
import dayjs, { type Dayjs } from 'dayjs'
import {
BlocklistRecord,
BroadcastArea,
Subdivision,
Category,
Language,
Country,
Stream,
Guide,
Feed
} from '.'
export class Channel {
id: string
name: string
altNames: Collection = new Collection()
networkName?: string
ownerNames: Collection = new Collection()
countryCode: string
country?: Country
subdivisionCode?: string
subdivision?: Subdivision
cityName?: string
categoryIds: Collection = new Collection()
categories: Collection = new Collection()
isNSFW: boolean
launchedDateString?: string
launchedDate?: Dayjs
closedDateString?: string
closedDate?: Dayjs
replacedByStreamId?: string
replacedByChannelId?: string
websiteUrl?: string
logoUrl: string
blocklistRecords: Collection = new Collection()
feeds: Collection = new Collection()
hasUniqueName: boolean = true
constructor(data?: ChannelData) {
if (!data) return
this.id = data.id
this.name = data.name
this.altNames = new Collection(data.alt_names)
this.networkName = data.network
this.ownerNames = new Collection(data.owners)
this.countryCode = data.country
this.subdivisionCode = data.subdivision
this.cityName = data.city
this.categoryIds = new Collection(data.categories)
this.isNSFW = data.is_nsfw
this.launchedDateString = data.launched
this.launchedDate = data.launched ? dayjs(data.launched) : undefined
this.closedDateString = data.closed
this.closedDate = data.closed ? dayjs(data.closed) : undefined
this.replacedByStreamId = data.replaced_by || undefined
const [replacedByChannelId] = data.replaced_by ? data.replaced_by.split('@') : [undefined]
this.replacedByChannelId = replacedByChannelId
this.websiteUrl = data.website
this.logoUrl = data.logo
}
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
}
withCategories(categoriesKeyById: Dictionary): this {
this.categories = this.categoryIds.map((id: string) => categoriesKeyById.get(id))
return this
}
withBlocklistRecords(blocklistGroupedByChannelId: Dictionary): this {
this.blocklistRecords = new Collection(blocklistGroupedByChannelId.get(this.id))
return this
}
withFeeds(feedsGroupedByChannelId: Dictionary): this {
this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
return this
}
setHasUniqueName(channelsGroupedByName: Dictionary): this {
this.hasUniqueName = new Collection(channelsGroupedByName.get(this.name)).count() === 1
return this
}
getUniqueName(): string {
if (this.hasUniqueName) return this.name
if (!this.country) return this.name
return `${this.name} (${this.country.name})`
}
getCategories(): Collection {
if (!this.categories) return new Collection()
return this.categories
}
getStreams(): Collection {
let streams = new Collection()
this.getFeeds().forEach((feed: Feed) => {
streams = streams.concat(feed.getStreams())
})
return streams
}
getGuides(): Collection {
let guides = new Collection()
this.getFeeds().forEach((feed: Feed) => {
guides = guides.concat(feed.getGuides())
})
return guides
}
getLanguages(): Collection {
let languages = new Collection()
this.getFeeds().forEach((feed: Feed) => {
languages = languages.concat(feed.getLanguages())
})
return languages.uniqBy((language: Language) => language.code)
}
getLanguageCodes(): Collection {
return this.getLanguages().map((language: Language) => language.code)
}
getLanguageNames(): Collection {
return this.getLanguages().map((language: Language) => language.name)
}
getBroadcastAreaCodes(): Collection {
let broadcastAreaCodes = new Collection()
this.getFeeds().forEach((feed: Feed) => {
broadcastAreaCodes = broadcastAreaCodes.concat(feed.broadcastAreaCodes)
})
return broadcastAreaCodes.uniq()
}
hasGuides(): boolean {
return this.getGuides().notEmpty()
}
hasFeeds(): boolean {
return this.getFeeds().notEmpty()
}
hasStreams(): boolean {
return this.getStreams().notEmpty()
}
getDisplayName(): string {
return this.name
}
getPagePath(): string {
const [channelSlug, countryCode] = this.id.split('.')
if (!channelSlug || !countryCode) return ''
return `/channels/${countryCode}/${channelSlug}`
}
getPageUrl(): string {
const [channelSlug, countryCode] = this.id.split('.') || [null, null]
if (!channelSlug || !countryCode || typeof window === 'undefined') return ''
return `${window.location.protocol}//${window.location.host}/channels/${countryCode}/${channelSlug}`
}
isClosed(): boolean {
return !!this.closedDateString || !!this.replacedByStreamId
}
isBlocked(): boolean {
return this.blocklistRecords ? this.blocklistRecords.notEmpty() : false
}
getCountryName(): string {
return this.country ? this.country.name : ''
}
getGuideSiteNames(): Collection {
return this.getGuides().map((guide: Guide) => guide.siteName)
}
getStreamUrls(): Collection {
return this.getStreams().map((stream: Stream) => stream.url)
}
getFeeds(): Collection {
if (!this.feeds) return new Collection()
return this.feeds
}
getBroadcastLocationCodes(): Collection {
let broadcastLocationCodes = new Collection()
this.getFeeds().forEach((feed: Feed) => {
broadcastLocationCodes = broadcastLocationCodes.concat(feed.getBroadcastLocationCodes())
})
return broadcastLocationCodes.uniq()
}
getBroadcastLocationNames(): Collection {
let broadcastLocationNames = new Collection()
this.getFeeds().forEach((feed: Feed) => {
broadcastLocationNames = broadcastLocationNames.concat(feed.getBroadcastLocationNames())
})
return broadcastLocationNames.uniq()
}
getVideoFormats(): Collection {
let videoFormats = new Collection()
this.getFeeds().forEach((feed: Feed) => {
videoFormats.add(feed.videoFormat)
})
return videoFormats.uniq()
}
getTimezoneIds(): Collection {
let timezoneIds = new Collection()
this.getFeeds().forEach((feed: Feed) => {
timezoneIds = timezoneIds.concat(feed.timezoneIds)
})
return timezoneIds.uniq()
}
getBroadcastArea(): Collection {
let broadcastArea = new Collection()
this.getFeeds().forEach((feed: Feed) => {
broadcastArea = broadcastArea.concat(feed.getBroadcastArea())
})
return broadcastArea.uniqBy((broadcastArea: BroadcastArea) => broadcastArea.code)
}
getSearchable(): ChannelSearchable {
return {
id: this.id,
name: this.name,
alt_names: this.altNames.all(),
alt_name: this.altNames.all(),
network: this.networkName,
owner: this.ownerNames.all(),
owners: this.ownerNames.all(),
country: this.countryCode,
subdivision: this.subdivisionCode,
city: this.cityName,
category: this.categoryIds.all(),
categories: this.categoryIds.all(),
launched: this.launchedDateString,
closed: this.closedDateString,
replaced_by: this.replacedByStreamId,
website: this.websiteUrl,
is_nsfw: this.isNSFW,
is_closed: this.isClosed(),
is_blocked: this.isBlocked(),
feeds: this.getFeeds().count(),
streams: this.getStreams().count(),
guides: this.getGuides().count(),
language: this.getLanguageCodes().all(),
languages: this.getLanguageCodes().all(),
broadcast_area: this.getBroadcastAreaCodes().all(),
video_format: this.getVideoFormats().all(),
video_formats: this.getVideoFormats().all(),
timezone: this.getTimezoneIds().all(),
timezones: this.getTimezoneIds().all(),
_languageNames: this.getLanguageNames().all(),
_broadcastLocationCodes: this.getBroadcastLocationCodes().all(),
_broadcastLocationNames: this.getBroadcastLocationNames().all(),
_countryName: this.getCountryName(),
_guideSiteNames: this.getGuideSiteNames().all(),
_streamUrls: this.getStreamUrls().all()
}
}
serialize(props = { withFeeds: true }): ChannelSerializedData {
return {
id: this.id,
name: this.name,
altNames: this.altNames.all(),
networkName: this.networkName,
ownerNames: this.ownerNames.all(),
countryCode: this.countryCode,
country: this.country ? this.country.serialize() : null,
subdivisionCode: this.subdivisionCode,
subdivision: this.subdivision ? this.subdivision.serialize() : null,
cityName: this.cityName,
categoryIds: this.categoryIds.all(),
categories: this.categories.map((category: Category) => category.serialize()).all(),
isNSFW: this.isNSFW,
launchedDateString: this.launchedDateString,
launchedDate: this.launchedDate ? this.launchedDate.toJSON() : null,
closedDateString: this.closedDateString,
closedDate: this.closedDate ? this.closedDate.toJSON() : null,
replacedByStreamId: this.replacedByStreamId,
replacedByChannelId: this.replacedByChannelId,
websiteUrl: this.websiteUrl,
logoUrl: this.logoUrl,
blocklistRecords: this.blocklistRecords
.map((blocklistRecord: BlocklistRecord) => blocklistRecord.serialize())
.all(),
feeds: props.withFeeds
? this.getFeeds()
.map((feed: Feed) => feed.serialize())
.all()
: [],
hasUniqueName: this.hasUniqueName
}
}
deserialize(data: ChannelSerializedData): this {
this.id = data.id || ''
this.name = data.name
this.altNames = new Collection(data.altNames)
this.networkName = data.networkName
this.ownerNames = new Collection(data.ownerNames)
this.countryCode = data.countryCode
this.country = new Country().deserialize(data.country)
this.subdivisionCode = data.subdivisionCode
this.cityName = data.cityName
this.categoryIds = new Collection(data.categoryIds)
this.categories = new Collection(data.categories).map((data: CategorySerializedData) =>
new Category().deserialize(data)
)
this.isNSFW = data.isNSFW
this.launchedDateString = data.launchedDateString
this.launchedDate = data.launchedDate ? dayjs(data.launchedDate) : undefined
this.closedDateString = data.closedDateString
this.closedDate = data.closedDate ? dayjs(data.closedDate) : undefined
this.replacedByStreamId = data.replacedByStreamId
this.replacedByChannelId = data.replacedByChannelId
this.websiteUrl = data.websiteUrl
this.logoUrl = data.logoUrl
this.blocklistRecords = new Collection(data.blocklistRecords).map(
(data: BlocklistRecordSerializedData) => new BlocklistRecord().deserialize(data)
)
this.feeds = new Collection(data.feeds).map((data: FeedSerializedData) =>
new Feed().deserialize(data)
)
this.hasUniqueName = data.hasUniqueName
return this
}
getFieldset(): HTMLPreviewField[] {
return [
{
name: 'logo',
type: 'image',
value: { src: this.logoUrl, alt: `${this.name} logo`, title: this.logoUrl }
},
{ name: 'id', type: 'string', value: this.id, title: this.id },
{ name: 'name', type: 'string', value: this.name, title: this.name },
{ name: 'alt_names', type: 'string[]', value: this.altNames.all() },
{
name: 'network',
type: 'link',
value: this.networkName
? { label: this.networkName, query: `network:${normalize(this.networkName)}` }
: null
},
{
name: 'owners',
type: 'link[]',
value: this.ownerNames
.map((name: string) => ({
label: name,
query: `owner:${normalize(name)}`
}))
.all()
},
{
name: 'country',
type: 'link',
value: this.country
? { label: this.country.name, query: `country:${this.country.code}` }
: null
},
{
name: 'subdivision',
type: 'link',
value: this.subdivision
? { label: this.subdivision.name, query: `subdivision:${this.subdivision.code}` }
: null
},
{
name: 'city',
type: 'link',
value: this.cityName ? { label: this.cityName, query: `city:${this.cityName}` } : null
},
{
name: 'broadcast_area',
type: 'link[]',
value: this.getBroadcastArea()
.map((broadcastArea: BroadcastArea) => ({
label: broadcastArea.getName(),
query: `broadcast_area:${broadcastArea.code}`
}))
.all()
},
{
name: 'timezones',
type: 'link[]',
value: this.getTimezoneIds()
.map((id: string) => ({
label: id,
query: `timezone:${id}`
}))
.all()
},
{
name: 'languages',
type: 'link[]',
value: this.getLanguages()
.map((language: Language) => ({
label: language.name,
query: `language:${language.code}`
}))
.all()
},
{
name: 'categories',
type: 'link[]',
value: this.categories
.map((category: Category) => ({
label: category.name,
query: `category:${category.id}`
}))
.all()
},
{
name: 'is_nsfw',
type: 'link',
value: { label: this.isNSFW.toString(), query: `is_nsfw:${this.isNSFW.toString()}` }
},
{
name: 'video_formats',
type: 'link[]',
value: this.getVideoFormats()
.map((format: string) => ({
label: format,
query: `video_format:${format}`
}))
.all()
},
{
name: 'launched',
type: 'string',
value: this.launchedDate ? this.launchedDate.format('D MMMM YYYY') : null,
title: this.launchedDateString
},
{
name: 'closed',
type: 'string',
value: this.closedDate ? this.closedDate.format('D MMMM YYYY') : null,
title: this.closedDateString
},
{
name: 'replaced_by',
type: 'link',
value: this.replacedByStreamId
? {
label: this.replacedByStreamId,
query: `id:${this.replacedByChannelId.replace('.', '\\.')}`
}
: null
},
{
name: 'website',
type: 'external_link',
value: this.websiteUrl
? { href: this.websiteUrl, title: this.websiteUrl, label: this.websiteUrl }
: null
}
].filter((field: HTMLPreviewField) =>
Array.isArray(field.value) ? field.value.length : field.value
)
}
getStructuredData() {
return {
'@context': 'https://schema.org/',
'@type': 'TelevisionChannel',
image: this.logoUrl,
identifier: this.id,
name: this.name,
alternateName: this.altNames.map((name: string) => ({ '@value': name })),
genre: this.categories.map((category: Category) => ({ '@value': category.name })),
sameAs: this.websiteUrl
}
}
}
function normalize(value: string) {
value = value.includes(' ') ? `"${value}"` : value
return encodeURIComponent(value)
}

54
src/models/country.ts Normal file
View file

@ -0,0 +1,54 @@
import type { CountryData, CountrySerializedData } from '~/types/country'
import { Collection, type Dictionary } from '@freearhey/core/browser'
import { Channel } from './channel'
export class Country {
code: string
name: string
flagEmoji: string
languageCode: string
channels: Collection = new Collection()
constructor(data?: CountryData) {
if (!data) return
this.code = data.code
this.name = data.name
this.flagEmoji = data.flag
this.languageCode = data.lang
}
withChannels(channelsGroupedByCountryCode: Dictionary): this {
this.channels = new Collection(channelsGroupedByCountryCode.get(this.code))
return this
}
getChannels(): Collection {
if (!this.channels) return new Collection()
return this.channels
}
getChannelsWithStreams(): Collection {
return this.getChannels().filter((channel: Channel) => channel.hasStreams())
}
serialize(): CountrySerializedData {
return {
code: this.code,
name: this.name,
flagEmoji: this.flagEmoji,
languageCode: this.languageCode
}
}
deserialize(data: CountrySerializedData): this {
this.code = data.code
this.name = data.name
this.flagEmoji = data.flagEmoji
this.languageCode = data.languageCode
return this
}
}

259
src/models/feed.ts Normal file
View file

@ -0,0 +1,259 @@
import { Collection, type Dictionary } from '@freearhey/core/browser'
import type { FeedData, FeedSerializedData } from '~/types/feed'
import { Guide, Stream, Language, BroadcastArea, Channel } from './'
import type { HTMLPreviewField } from '~/types/htmlPreviewField'
export class Feed {
channelId: string
channel: Channel
id: string
name: string
isMain: boolean
broadcastAreaCodes: Collection
broadcastArea?: Collection
timezoneIds: Collection
languageCodes: Collection
languages?: Collection
videoFormat: string
streams?: Collection
guides?: Collection
constructor(data?: FeedData) {
if (!data) return
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.timezoneIds = new Collection(data.timezones)
this.languageCodes = new Collection(data.languages)
this.videoFormat = data.video_format
}
withChannel(channelsKeyById: Dictionary): this {
if (!this.channelId) return this
this.channel = channelsKeyById.get(this.channelId)
return this
}
withStreams(streamsGroupedByStreamId: Dictionary): this {
this.streams = new Collection(streamsGroupedByStreamId.get(`${this.channelId}@${this.id}`))
if (this.isMain) {
this.streams = this.streams.concat(
new Collection(streamsGroupedByStreamId.get(this.channelId))
)
}
return this
}
withGuides(guidesGroupedByStreamId: Dictionary): this {
this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`))
if (this.isMain) {
this.guides = this.guides.concat(new Collection(guidesGroupedByStreamId.get(this.channelId)))
}
return this
}
withLanguages(languagesKeyByCode: Dictionary): this {
this.languages = this.languageCodes
.map((code: string) => languagesKeyByCode.get(code))
.filter(Boolean)
return this
}
withBroadcastArea(
countriesKeyByCode: Dictionary,
subdivisionsKeyByCode: Dictionary,
regionsKeyByCode: Dictionary,
regions: Collection
): this {
this.broadcastArea = this.broadcastAreaCodes
.map((code: string) =>
new BroadcastArea({ code })
.withName(countriesKeyByCode, subdivisionsKeyByCode, regionsKeyByCode)
.withLocations(countriesKeyByCode, subdivisionsKeyByCode, regionsKeyByCode, regions)
)
.filter(Boolean)
return this
}
getUUID(): string {
return this.channelId + this.id
}
getBroadcastArea(): Collection {
if (!this.broadcastArea) return new Collection()
return this.broadcastArea
}
getBroadcastLocationCodes(): Collection {
let broadcastLocationCodes = new Collection()
this.getBroadcastArea().forEach((broadcastArea: BroadcastArea) => {
broadcastLocationCodes = broadcastLocationCodes.concat(broadcastArea.getLocationCodes())
})
return broadcastLocationCodes.uniq()
}
getBroadcastLocationNames(): Collection {
let broadcastLocationNames = new Collection()
this.getBroadcastArea().forEach((broadcastArea: BroadcastArea) => {
broadcastLocationNames = broadcastLocationNames.concat(broadcastArea.getLocationNames())
})
return broadcastLocationNames.uniq()
}
getStreams(): Collection {
if (!this.streams) return new Collection()
return this.streams
}
hasStreams(): boolean {
return this.getStreams().notEmpty()
}
getGuides(): Collection {
if (!this.guides) return new Collection()
return this.guides
}
hasGuides(): boolean {
return this.getGuides().notEmpty()
}
getLanguages(): Collection {
if (!this.languages) return new Collection()
return this.languages
}
getLanguageCodes(): Collection {
return this.getLanguages().map((language: Language) => language.code)
}
getLanguageNames(): Collection {
return this.getLanguages().map((language: Language) => language.name)
}
getDisplayName(): string {
if (!this.channel) return this.name
return [this.channel.name, this.name].join(' ')
}
getPageUrl(): string {
const [channelSlug, countryCode] = this.channelId.split('.') || [null, null]
if (!channelSlug || !countryCode || typeof window === 'undefined') return ''
return `${window.location.protocol}//${window.location.host}/channels/${countryCode}/${channelSlug}#${this.id}`
}
getFieldset(): HTMLPreviewField[] {
return [
{ name: 'id', type: 'string', value: this.id, title: this.id },
{ name: 'name', type: 'string', value: this.name, title: this.name },
{
name: 'is_main',
type: 'string',
value: this.isMain.toString(),
title: this.isMain.toString()
},
{
name: 'broadcast_area',
type: 'link[]',
value: this.getBroadcastArea()
.map((broadcastArea: BroadcastArea) => ({
label: broadcastArea.getName(),
query: `broadcast_area:${broadcastArea.code}`
}))
.all()
},
{
name: 'timezones',
type: 'link[]',
value: this.timezoneIds
.map((id: string) => ({
label: id,
query: `timezone:${id}`
}))
.all()
},
{
name: 'languages',
type: 'link[]',
value: this.getLanguages()
.map((language: Language) => ({
label: language.name,
query: `language:${language.code}`
}))
.all()
},
{
name: 'video_format',
type: 'link',
value: { label: this.videoFormat, query: `video_format:${this.videoFormat}` }
}
]
}
serialize(): FeedSerializedData {
return {
channelId: this.channelId,
channel: this.channel ? this.channel.serialize({ withFeeds: false }) : null,
id: this.id,
name: this.name,
isMain: this.isMain,
broadcastAreaCodes: this.broadcastAreaCodes.all(),
broadcastArea: this.getBroadcastArea()
.map((broadcastArea: BroadcastArea) => broadcastArea.serialize())
.all(),
timezoneIds: this.timezoneIds.all(),
languageCodes: this.languageCodes.all(),
languages: this.getLanguages()
.map((language: Language) => language.serialize())
.all(),
videoFormat: this.videoFormat,
streams: this.getStreams()
.map((stream: Stream) => stream.serialize())
.all(),
guides: this.getGuides()
.map((guide: Guide) => guide.serialize())
.all()
}
}
deserialize(data: FeedSerializedData): this {
this.channelId = data.channelId
this.channel = data.channel ? new Channel().deserialize(data.channel) : undefined
this.id = data.id
this.name = data.name
this.isMain = data.isMain
this.broadcastAreaCodes = new Collection(data.broadcastAreaCodes)
this.broadcastArea = new Collection(data.broadcastArea).map(data =>
new BroadcastArea().deserialize(data)
)
this.timezoneIds = new Collection(data.timezoneIds)
this.languageCodes = new Collection(data.languageCodes)
this.languages = new Collection(data.languages).map(data => new Language().deserialize(data))
this.videoFormat = data.videoFormat
this.streams = new Collection(data.streams).map(data => new Stream().deserialize(data))
this.guides = new Collection(data.guides).map(data => new Guide().deserialize(data))
return this
}
}

58
src/models/guide.ts Normal file
View file

@ -0,0 +1,58 @@
import type { GuideData, GuideSerializedData } from '~/types/guide'
export class Guide {
channelId?: string
feedId?: string
siteDomain: string
siteId: string
siteName: string
languageCode: string
constructor(data?: GuideData) {
if (!data) return
this.channelId = data.channel
this.feedId = data.feed
this.siteDomain = data.site
this.siteId = data.site_id
this.siteName = data.site_name
this.languageCode = data.lang
}
getUUID(): string {
return this.getId() + this.siteId
}
getId(): string | undefined {
if (!this.channelId) return undefined
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
getUrl() {
return `https://${this.siteDomain}`
}
serialize(): GuideSerializedData {
return {
channelId: this.channelId,
feedId: this.feedId,
siteDomain: this.siteDomain,
siteId: this.siteId,
siteName: this.siteName,
languageCode: this.languageCode
}
}
deserialize(data: GuideSerializedData): this {
this.channelId = data.channelId
this.feedId = data.feedId
this.siteDomain = data.siteDomain
this.siteId = data.siteId
this.siteName = data.siteName
this.languageCode = data.languageCode
return this
}
}

View file

@ -1 +0,0 @@
export * from './channel'

11
src/models/index.ts Normal file
View file

@ -0,0 +1,11 @@
export * from './channel'
export * from './country'
export * from './subdivision'
export * from './language'
export * from './category'
export * from './region'
export * from './stream'
export * from './guide'
export * from './blocklistRecord'
export * from './feed'
export * from './broadcastArea'

27
src/models/language.ts Normal file
View file

@ -0,0 +1,27 @@
import type { LanguageData, LanguageSerializedData } from '~/types/language'
export class Language {
code: string
name: string
constructor(data?: LanguageData) {
if (!data) return
this.code = data.code
this.name = data.name
}
serialize(): LanguageSerializedData {
return {
code: this.code,
name: this.name
}
}
deserialize(data: LanguageSerializedData): this {
this.code = data.code
this.name = data.name
return this
}
}

32
src/models/region.ts Normal file
View file

@ -0,0 +1,32 @@
import type { RegionData, RegionSerializedData } from '~/types/region'
import { Collection } from '@freearhey/core/browser'
export class Region {
code: string
name: string
countryCodes: Collection
constructor(data?: RegionData) {
if (!data) return
this.code = data.code
this.name = data.name
this.countryCodes = new Collection(data.countries)
}
serialize(): RegionSerializedData {
return {
code: this.code,
name: this.name,
countryCodes: this.countryCodes.all()
}
}
deserialize(data: RegionSerializedData): this {
this.code = data.code
this.name = data.name
this.countryCodes = new Collection(data.countryCodes)
return this
}
}

149
src/models/stream.ts Normal file
View file

@ -0,0 +1,149 @@
import type { JsonDataViewerField } from '~/types/jsonDataViewerField'
import type { StreamData, StreamSerializedData } from '~/types/stream'
import type { Dictionary } from '@freearhey/core/browser'
import { Link } from 'iptv-playlist-generator'
import type { Channel } from './channel'
import type { Feed } from './feed'
import type { Category } from './category'
export class Stream {
channelId?: string
feedId?: string
url: string
referrer?: string
userAgent?: string
quality?: string
channel?: Channel
feed?: Feed
constructor(data?: StreamData) {
if (!data) return
this.channelId = data.channel
this.feedId = data.feed
this.url = data.url
this.referrer = data.referrer
this.userAgent = data.user_agent
this.quality = data.quality
}
withChannel(channelsKeyById: Dictionary): this {
if (!this.channelId) return this
this.channel = channelsKeyById.get(this.channelId)
return this
}
withFeed(feedsKeyById: Dictionary): this {
if (!this.feedId) return this
this.feed = feedsKeyById.get(this.feedId)
return this
}
getUUID(): string {
return this.url
}
getId(): string | undefined {
if (!this.channelId) return undefined
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
toJSON() {
return {
channel: this.channelId,
feed: this.feedId,
url: this.url,
referrer: this.referrer,
user_agent: this.userAgent,
quality: this.quality
}
}
getFieldset(): JsonDataViewerField[] {
let fieldset = []
const data = this.toJSON()
for (let key in data) {
fieldset.push({
name: key,
value: data[key]
})
}
return fieldset
}
getQuality(): string {
if (!this.quality) return ''
return this.quality
}
getVerticalResolution(): number {
return parseInt(this.getQuality().replace(/p|i/, ''))
}
getTitle(): string {
if (!this.channel) return ''
if (!this.feed) return this.channel.name
return `${this.channel.name} ${this.feed.name}`
}
getPlaylistLink(): Link {
if (!this.channel) return ''
const link = new Link(this.url)
link.title = this.getTitle()
link.attrs = {
'tvg-id': this.getId(),
'tvg-logo': this.channel.logoUrl,
'group-title': this.channel
.getCategories()
.map((category: Category) => category.name)
.sort()
.join(';')
}
if (this.userAgent) {
link.attrs['user-agent'] = this.userAgent
link.vlcOpts['http-user-agent'] = this.userAgent
}
if (this.referrer) {
link.attrs['referrer'] = this.referrer
link.vlcOpts['http-referrer'] = this.referrer
}
return link
}
serialize(): StreamSerializedData {
return {
channelId: this.channelId,
feedId: this.feedId,
url: this.url,
referrer: this.referrer,
userAgent: this.userAgent,
quality: this.quality
}
}
deserialize(data: StreamSerializedData): this {
this.channelId = data.channelId
this.feedId = data.feedId
this.url = data.url
this.referrer = data.referrer
this.userAgent = data.userAgent
this.quality = data.quality
return this
}
}

31
src/models/subdivision.ts Normal file
View file

@ -0,0 +1,31 @@
import type { SubdivisionData, SubdivisionSerializedData } from '~/types/subdivision'
export class Subdivision {
code: string
name: string
countryCode: string
constructor(data?: SubdivisionData) {
if (!data) return
this.code = data.code
this.name = data.name
this.countryCode = data.country
}
serialize(): SubdivisionSerializedData {
return {
code: this.code,
name: this.name,
countryCode: this.countryCode
}
}
deserialize(data: SubdivisionSerializedData): this {
this.code = data.code
this.name = data.name
this.countryCode = data.countryCode
return this
}
}