Files
iptv/scripts/models/stream.ts

398 lines
9.6 KiB
TypeScript
Raw Normal View History

2025-03-29 11:39:46 +03:00
import { Feed, Channel, Category, Region, Subdivision, Country, Language } from './index'
2025-04-16 20:54:55 +03:00
import { URL, Collection, Dictionary } from '@freearhey/core'
import type { StreamData } from '../types/stream'
2025-03-29 11:39:46 +03:00
import parser from 'iptv-playlist-parser'
2025-05-01 00:51:41 +03:00
import { IssueData } from '../core'
2023-09-15 18:40:35 +03:00
2025-03-29 11:39:46 +03:00
export class Stream {
2025-04-16 20:54:55 +03:00
name?: string
2023-09-15 18:40:35 +03:00
url: string
2025-03-29 11:39:46 +03:00
id?: string
channelId?: string
channel?: Channel
feedId?: string
feed?: Feed
filepath?: string
2025-04-16 20:54:55 +03:00
line?: number
2023-09-15 18:40:35 +03:00
label?: string
2025-03-30 03:01:05 +03:00
verticalResolution?: number
isInterlaced?: boolean
2025-04-16 20:54:55 +03:00
referrer?: string
userAgent?: string
groupTitle: string = 'Undefined'
2023-09-18 01:51:17 +03:00
removed: boolean = false
2023-09-15 18:40:35 +03:00
2025-04-16 20:54:55 +03:00
constructor(data?: StreamData) {
if (!data) return
const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
this.id = id || undefined
this.channelId = data.channel || undefined
this.feedId = data.feed || undefined
this.name = data.name || undefined
this.url = data.url
this.referrer = data.referrer || undefined
this.userAgent = data.user_agent || undefined
this.verticalResolution = verticalResolution || undefined
this.isInterlaced = isInterlaced || undefined
this.label = data.label || undefined
}
2025-05-01 00:51:41 +03:00
update(issueData: IssueData): this {
const data = {
label: issueData.getString('label'),
quality: issueData.getString('quality'),
httpUserAgent: issueData.getString('httpUserAgent'),
httpReferrer: issueData.getString('httpReferrer'),
newStreamUrl: issueData.getString('newStreamUrl')
}
if (data.label !== undefined) this.label = data.label
if (data.quality !== undefined) this.setQuality(data.quality)
if (data.httpUserAgent !== undefined) this.userAgent = data.httpUserAgent
if (data.httpReferrer !== undefined) this.referrer = data.httpReferrer
if (data.newStreamUrl !== undefined) this.url = data.newStreamUrl
return this
}
2025-04-16 20:54:55 +03:00
fromPlaylistItem(data: parser.PlaylistItem): this {
2025-03-29 11:39:46 +03:00
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)
2025-03-30 03:01:05 +03:00
const { verticalResolution, isInterlaced } = parseQuality(quality)
2025-03-29 11:39:46 +03:00
this.id = data.tvg.id || undefined
this.feedId = feedId || undefined
this.channelId = channelId || undefined
this.line = data.line
this.label = label || undefined
2023-09-15 18:40:35 +03:00
this.name = name
2025-03-30 03:01:05 +03:00
this.verticalResolution = verticalResolution || undefined
this.isInterlaced = isInterlaced || undefined
2025-03-29 11:39:46 +03:00
this.url = data.url
2025-04-16 20:54:55 +03:00
this.referrer = data.http.referrer || undefined
this.userAgent = data.http['user-agent'] || undefined
return this
2023-09-15 18:40:35 +03:00
}
2025-04-16 20:54:55 +03:00
withChannel(channelsKeyById: Dictionary): this {
2025-03-29 11:39:46 +03:00
if (!this.channelId) return this
2025-04-16 20:54:55 +03:00
this.channel = channelsKeyById.get(this.channelId)
2025-03-29 11:39:46 +03:00
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)
2025-03-30 03:01:05 +03:00
if (!this.feedId && !this.feed) this.feed = channelFeeds.find((feed: Feed) => feed.isMain)
2025-03-29 11:39:46 +03:00
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
}
setQuality(quality: string): this {
2025-03-30 03:01:05 +03:00
const { verticalResolution, isInterlaced } = parseQuality(quality)
this.verticalResolution = verticalResolution || undefined
this.isInterlaced = isInterlaced || undefined
2025-03-29 11:39:46 +03:00
return this
}
2025-04-16 20:54:55 +03:00
getLine(): number {
return this.line || -1
}
2025-03-29 11:39:46 +03:00
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
}
2025-03-30 03:01:05 +03:00
getChannelId(): string {
return this.channelId || ''
}
getFeedId(): string {
if (this.feedId) return this.feedId
if (this.feed) return this.feed.id
return ''
}
2025-03-29 11:39:46 +03:00
getFilepath(): string {
return this.filepath || ''
}
2025-04-16 20:54:55 +03:00
getReferrer(): string {
return this.referrer || ''
2025-03-29 11:39:46 +03:00
}
2025-04-16 20:54:55 +03:00
getUserAgent(): string {
return this.userAgent || ''
2025-03-29 11:39:46 +03:00
}
getQuality(): string {
2025-03-30 03:01:05 +03:00
if (!this.verticalResolution) return ''
let quality = this.verticalResolution.toString()
if (this.isInterlaced) quality += 'i'
else quality += 'p'
return quality
}
hasId(): boolean {
return !!this.id
2025-03-29 11:39:46 +03:00
}
hasQuality(): boolean {
2025-03-30 03:01:05 +03:00
return !!this.verticalResolution
2025-03-29 11:39:46 +03:00
}
2025-03-30 03:01:05 +03:00
getVerticalResolution(): number {
2025-03-29 11:39:46 +03:00
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
}
2023-09-15 18:40:35 +03:00
normalizeURL() {
const url = new URL(this.url)
this.url = url.normalize().toString()
}
clone(): Stream {
return Object.assign(Object.create(Object.getPrototypeOf(this)), this)
}
hasChannel() {
return !!this.channel
}
2025-03-29 11:39:46 +03:00
getBroadcastRegions(): Collection {
return this.feed ? this.feed.getBroadcastRegions() : new Collection()
}
getBroadcastCountries(): Collection {
return this.feed ? this.feed.getBroadcastCountries() : new Collection()
}
hasBroadcastArea(): boolean {
return this.feed ? this.feed.hasBroadcastArea() : false
}
isSFW(): boolean {
return this.channel ? this.channel.isSFW() : true
2023-09-15 18:40:35 +03:00
}
2025-03-29 11:39:46 +03:00
hasCategories(): boolean {
return this.channel ? this.channel.hasCategories() : false
2023-09-15 18:40:35 +03:00
}
hasCategory(category: Category): boolean {
2025-03-29 11:39:46 +03:00
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()
2023-09-15 18:40:35 +03:00
}
2025-03-29 11:39:46 +03:00
getLanguages(): Collection {
return this.feed ? this.feed.getLanguages() : new Collection()
2023-09-15 18:40:35 +03:00
}
2025-03-29 11:39:46 +03:00
hasLanguages() {
return this.feed ? this.feed.hasLanguages() : false
2023-09-15 18:40:35 +03:00
}
2025-03-29 11:39:46 +03:00
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
2023-09-15 18:40:35 +03:00
}
isInternational(): boolean {
2025-03-29 11:39:46 +03:00
return this.feed ? this.feed.isInternational() : false
2023-09-15 18:40:35 +03:00
}
2025-03-29 11:39:46 +03:00
getLogo(): string {
return this?.channel?.logo || ''
2023-09-15 18:40:35 +03:00
}
2025-04-16 20:54:55 +03:00
getName(): string {
return this.name || ''
}
2023-09-15 18:40:35 +03:00
getTitle(): string {
2025-04-16 20:54:55 +03:00
let title = `${this.getName()}`
2023-09-15 18:40:35 +03:00
2025-03-30 03:01:05 +03:00
if (this.getQuality()) {
title += ` (${this.getQuality()})`
2023-09-15 18:40:35 +03:00
}
if (this.label) {
title += ` [${this.label}]`
}
return title
}
2025-03-29 11:39:46 +03:00
getLabel(): string {
return this.label || ''
}
getId(): string {
return this.id || ''
}
2023-09-15 18:40:35 +03:00
toJSON() {
return {
2025-03-29 11:39:46 +03:00
channel: this.channelId || null,
feed: this.feedId || null,
2023-09-15 18:40:35 +03:00
url: this.url,
2025-04-16 20:54:55 +03:00
referrer: this.referrer || null,
user_agent: this.userAgent || null,
2025-04-03 09:10:01 -04:00
quality: this.getQuality() || null
2023-09-15 18:40:35 +03:00
}
}
toString(options: { public: boolean }) {
2025-03-29 11:39:46 +03:00
let output = `#EXTINF:-1 tvg-id="${this.getId()}"`
2023-09-15 18:40:35 +03:00
if (options.public) {
2025-03-29 11:39:46 +03:00
output += ` tvg-logo="${this.getLogo()}" group-title="${this.groupTitle}"`
2023-09-15 18:40:35 +03:00
}
2025-04-16 20:54:55 +03:00
if (this.referrer) {
output += ` http-referrer="${this.referrer}"`
2025-03-09 19:53:25 +03:00
}
2025-04-16 20:54:55 +03:00
if (this.userAgent) {
output += ` http-user-agent="${this.userAgent}"`
2023-09-15 18:40:35 +03:00
}
output += `,${this.getTitle()}`
2025-04-16 20:54:55 +03:00
if (this.referrer) {
2025-04-23 05:21:33 +03:00
output += `\r\n#EXTVLCOPT:http-referrer=${this.referrer}`
2023-09-15 18:40:35 +03:00
}
2025-04-16 20:54:55 +03:00
if (this.userAgent) {
2025-04-23 05:21:33 +03:00
output += `\r\n#EXTVLCOPT:http-user-agent=${this.userAgent}`
2023-09-15 18:40:35 +03:00
}
2025-04-23 05:21:33 +03:00
output += `\r\n${this.url}`
2023-09-15 18:40:35 +03:00
return output
}
}
2025-03-29 11:39:46 +03:00
2025-03-30 03:01:05 +03:00
function parseTitle(title: string): {
name: string
label: string
quality: string
} {
2025-03-29 11:39:46 +03:00
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, '\\$&')
}
2025-03-30 03:01:05 +03:00
2025-04-16 20:54:55 +03:00
function parseQuality(quality: string | null): {
verticalResolution: number | null
isInterlaced: boolean | null
} {
if (!quality) return { verticalResolution: null, isInterlaced: null }
2025-05-01 00:51:41 +03:00
const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
2025-03-30 03:01:05 +03:00
const isInterlaced = /i$/i.test(quality)
let verticalResolution = 0
if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString)
return { verticalResolution, isInterlaced }
}