mirror of
https://github.com/iptv-org/iptv
synced 2026-05-12 04:25:17 -04:00
281 lines
7.7 KiB
TypeScript
281 lines
7.7 KiB
TypeScript
import { getStreamInfo, loadIssues, createThread } from '../../utils'
|
|
import { STREAMS_DIR, LOGS_DIR } from '../../constants'
|
|
import { Playlist, Issue, Stream } from '../../models'
|
|
import { loadData, data as apiData } from '../../api'
|
|
import { Logger, Collection } from '@freearhey/core'
|
|
import { Storage } from '@freearhey/storage-js'
|
|
import { PlaylistParser } from '../../core'
|
|
import * as sdk from '@iptv-org/sdk'
|
|
|
|
const processedIssues = new Collection<Issue>()
|
|
const skippedIssues = new Collection<Issue>()
|
|
const logger = new Logger({ level: 5 })
|
|
|
|
let streams = new Collection<Stream>()
|
|
let cache = new Collection<Stream>()
|
|
|
|
function cacheData() {
|
|
cache = streams.clone()
|
|
}
|
|
|
|
function resetData() {
|
|
streams = cache
|
|
}
|
|
|
|
async function main() {
|
|
logger.info('loading data from api...')
|
|
await loadData()
|
|
|
|
logger.info('loading issues...')
|
|
const issues = await loadIssues()
|
|
|
|
logger.info('loading streams...')
|
|
await loadStreams()
|
|
|
|
logger.info('processing issues...')
|
|
await processIssues(issues)
|
|
|
|
logger.info('saving streams...')
|
|
await saveStreams()
|
|
|
|
logger.info('saving logs...')
|
|
await saveLogs()
|
|
|
|
logger.info(
|
|
`skipped ${skippedIssues.count()} issue(s): ${skippedIssues
|
|
.map((issue: Issue) => `#${issue.number}`)
|
|
.join(', ')}`
|
|
)
|
|
logger.info(
|
|
`processed ${processedIssues.count()} issue(s): ${processedIssues
|
|
.map((issue: Issue) => `#${issue.number}`)
|
|
.join(', ')}`
|
|
)
|
|
}
|
|
|
|
main()
|
|
|
|
async function saveLogs() {
|
|
const logStorage = new Storage(LOGS_DIR)
|
|
const output = processedIssues.map((issue: Issue) => `closes #${issue.number}`).join(', ')
|
|
await logStorage.save('playlist_update.log', output)
|
|
}
|
|
|
|
async function saveStreams() {
|
|
const streamsStorage = new Storage(STREAMS_DIR)
|
|
const groupedStreams = streams.groupBy((stream: Stream) => stream.getFilepath())
|
|
for (const filepath of groupedStreams.keys()) {
|
|
let filteredStreams = new Collection<Stream>(groupedStreams.get(filepath))
|
|
filteredStreams = filteredStreams.filter((stream: Stream) => stream.removed === false)
|
|
|
|
const playlist = new Playlist(filteredStreams, { public: false })
|
|
await streamsStorage.save(filepath, playlist.toString())
|
|
}
|
|
}
|
|
|
|
async function loadStreams() {
|
|
const streamsStorage = new Storage(STREAMS_DIR)
|
|
const parser = new PlaylistParser({
|
|
storage: streamsStorage
|
|
})
|
|
const files = await streamsStorage.list('**/*.m3u')
|
|
|
|
streams = await parser.parse(files)
|
|
}
|
|
|
|
async function processIssues(issues: Collection<Issue>) {
|
|
const requests = issues.filter((issue: Issue) => issue.labels.includes('approved')).all()
|
|
|
|
for (const issue of requests) {
|
|
switch (true) {
|
|
case issue.labels.includes('streams:remove'):
|
|
await removeStream(issue)
|
|
break
|
|
case issue.labels.includes('streams:edit'):
|
|
await editStream(issue)
|
|
break
|
|
case issue.labels.includes('streams:add'):
|
|
await addStream(issue)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
async function removeStream(issue: Issue) {
|
|
const log = createThread(issue, 'streams/remove')
|
|
log.start()
|
|
|
|
const data = issue.data
|
|
if (data.missing('stream_url')) {
|
|
log.error('The request is missing the "Stream URL"')
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
const streamUrls = data.getString('stream_url') || ''
|
|
|
|
let changed = false
|
|
streamUrls
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.forEach((link: string) => {
|
|
const found: Stream = streams.first((_stream: Stream) => _stream.url === link.trim())
|
|
if (found) {
|
|
found.removed = true
|
|
changed = true
|
|
log.info(`The stream with the URL "${link}" has been removed from the playlists`)
|
|
} else {
|
|
log.error(`The stream with the URL "${link}" is missing from the playlists`)
|
|
}
|
|
})
|
|
|
|
if (changed) {
|
|
processedIssues.add(issue)
|
|
} else {
|
|
log.error(`None of the URLs specified in the request were found in the playlists`)
|
|
skippedIssues.add(issue)
|
|
}
|
|
}
|
|
|
|
async function editStream(issue: Issue) {
|
|
const log = createThread(issue, 'streams/edit')
|
|
log.start()
|
|
|
|
const data = issue.data
|
|
|
|
const streamUrl = data.getString('stream_url')
|
|
|
|
if (!streamUrl) {
|
|
log.error('The request is missing the "Stream URL"')
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
const stream: Stream = streams.first((_stream: Stream) => _stream.url === streamUrl)
|
|
if (!stream) {
|
|
log.error(`The stream with the URL "${streamUrl}" is already in the playlists`)
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
cacheData()
|
|
|
|
stream.updateWithIssue(data)
|
|
|
|
const errors = new Collection<Error>()
|
|
errors.concat(stream.validate())
|
|
if (errors.isNotEmpty()) {
|
|
errors.forEach((err: Error) => {
|
|
log.error(err.message)
|
|
})
|
|
skippedIssues.add(issue)
|
|
resetData()
|
|
log.info('All changes have been reverted')
|
|
return
|
|
}
|
|
|
|
log.info('The stream description has been updated')
|
|
|
|
processedIssues.add(issue)
|
|
}
|
|
|
|
async function addStream(issue: Issue) {
|
|
const log = createThread(issue, 'streams/add')
|
|
log.start()
|
|
|
|
const data = issue.data
|
|
if (data.missing('stream_id')) {
|
|
log.error('The request is missing the "Stream ID"')
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
const streamUrl = data.getString('stream_url')
|
|
if (!streamUrl) {
|
|
log.error('The request is missing the "Stream URL"')
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
if (streams.includes((_stream: Stream) => _stream.url === streamUrl)) {
|
|
log.error(`The stream with the URL "${streamUrl}" is already included in the playlists`)
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
const streamId = data.getString('stream_id') || ''
|
|
const [channelId, feedId] = streamId.split('@')
|
|
|
|
const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId)
|
|
if (!channel) {
|
|
log.error(`There is no channel with the ID "${channelId}" in the database`)
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
const blocklistRecords: sdk.Models.BlocklistRecord[] | undefined =
|
|
apiData.blocklistRecordsGroupedByChannel.get(channelId)
|
|
if (blocklistRecords) {
|
|
blocklistRecords.forEach((record: sdk.Models.BlocklistRecord) => {
|
|
if (record.reason === 'dmca') {
|
|
log.error(
|
|
`The channel has been added to our blocklist due to the claims of the copyright holder: ${record.ref}`
|
|
)
|
|
} else if (record.reason === 'nsfw') {
|
|
log.error(`The channel has been added to our blocklist due to NSFW content: ${record.ref}`)
|
|
}
|
|
})
|
|
skippedIssues.add(issue)
|
|
return
|
|
}
|
|
|
|
cacheData()
|
|
|
|
const httpUserAgent = data.getString('http_user_agent') || null
|
|
const httpReferrer = data.getString('http_referrer') || null
|
|
|
|
let quality = data.getString('quality') || null
|
|
if (!quality) {
|
|
const streamInfo = await getStreamInfo(streamUrl, { httpUserAgent, httpReferrer })
|
|
|
|
if (streamInfo) {
|
|
const height = streamInfo?.resolution?.height
|
|
|
|
if (height) {
|
|
quality = `${height}p`
|
|
}
|
|
}
|
|
}
|
|
|
|
const stream = new Stream({
|
|
channel: channelId,
|
|
feed: feedId,
|
|
title: channel.name,
|
|
url: streamUrl,
|
|
user_agent: httpUserAgent,
|
|
referrer: httpReferrer,
|
|
quality,
|
|
label: data.getString('label') || ''
|
|
})
|
|
|
|
stream.updateTitle().updateFilepath()
|
|
|
|
streams.add(stream)
|
|
|
|
const errors = new Collection<Error>()
|
|
errors.concat(stream.validate())
|
|
if (errors.isNotEmpty()) {
|
|
errors.forEach((err: Error) => {
|
|
log.error(err.message)
|
|
})
|
|
skippedIssues.add(issue)
|
|
resetData()
|
|
log.info('All changes have been reverted')
|
|
return
|
|
}
|
|
|
|
log.info('The stream has been added to playlists')
|
|
|
|
processedIssues.add(issue)
|
|
}
|