mirror of
https://github.com/iptv-org/iptv
synced 2026-09-09 13:27:58 -04:00
Update scripts
This commit is contained in:
@@ -0,0 +1,144 @@
|
|||||||
|
import { LOGS_DIR, STREAMS_DIR } from '../../constants'
|
||||||
|
import { loadData, data as apiData } from '../../api'
|
||||||
|
import { Collection, Logger } from '@freearhey/core'
|
||||||
|
import { hasValidDomain, isURI, parseIssueBody } from '../../utils'
|
||||||
|
import { Storage } from '@freearhey/storage-js'
|
||||||
|
import { PlaylistParser } from '../../core'
|
||||||
|
import { Stream } from '../../models'
|
||||||
|
import * as sdk from '@iptv-org/sdk'
|
||||||
|
import { program } from 'commander'
|
||||||
|
|
||||||
|
program
|
||||||
|
.requiredOption('--body <body>', 'The full markdown body text of the issue')
|
||||||
|
.option('--labels <labels>', 'Comma-separated string of label names', '')
|
||||||
|
.parse(process.argv)
|
||||||
|
|
||||||
|
const { body, labels } = program.opts()
|
||||||
|
|
||||||
|
const logsStorage = new Storage(LOGS_DIR)
|
||||||
|
let streams = new Collection<Stream>()
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const logger = new Logger()
|
||||||
|
|
||||||
|
logger.info('loading data from api...')
|
||||||
|
await loadData()
|
||||||
|
|
||||||
|
logger.info('loading streams...')
|
||||||
|
await loadStreams()
|
||||||
|
|
||||||
|
const data = parseIssueBody(body)
|
||||||
|
if (labels.includes('streams:add')) {
|
||||||
|
if (data.missing('stream_id')) {
|
||||||
|
errors.push('The request is missing the "Stream ID"')
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamUrl = data.getString('stream_url')
|
||||||
|
if (!streamUrl) {
|
||||||
|
errors.push('The request is missing the "Stream URL"')
|
||||||
|
done()
|
||||||
|
} else if (!isURI(streamUrl) || !hasValidDomain(streamUrl)) {
|
||||||
|
errors.push(`The stream URL "${streamUrl}" is invalid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streams.includes((_stream: Stream) => _stream.url === streamUrl)) {
|
||||||
|
errors.push(`The stream with the URL "${streamUrl}" is already included in the playlists`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamId = data.getString('stream_id') || ''
|
||||||
|
const [channelId, feedId] = streamId.split('@')
|
||||||
|
|
||||||
|
const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId)
|
||||||
|
if (!channel) {
|
||||||
|
errors.push(`There is no channel with the ID "${channelId}" in the database`)
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocklistRecords: sdk.Models.BlocklistRecord[] | undefined =
|
||||||
|
apiData.blocklistRecordsGroupedByChannel.get(channelId)
|
||||||
|
if (blocklistRecords) {
|
||||||
|
blocklistRecords.forEach((record: sdk.Models.BlocklistRecord) => {
|
||||||
|
if (record.reason === 'dmca') {
|
||||||
|
errors.push(
|
||||||
|
`The channel "${channelId}" has been added to our blocklist due to the claims of the copyright holder: ${record.ref}`
|
||||||
|
)
|
||||||
|
} else if (record.reason === 'nsfw') {
|
||||||
|
errors.push(
|
||||||
|
`The channel "${channelId}" has been added to our blocklist due to NSFW content: ${record.ref}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const feed: sdk.Models.Feed | undefined = apiData.feedsKeyByStreamId.get(streamId)
|
||||||
|
if (!feed) {
|
||||||
|
errors.push(
|
||||||
|
`There is no feed with the ID "${feedId}" for the "${channelId}" channel in the database`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (labels.includes('streams:remove')) {
|
||||||
|
const streamUrls = data.getString('stream_url') || ''
|
||||||
|
if (!streamUrls) {
|
||||||
|
errors.push('The request is missing the "Stream URL"')
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
streamUrls
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach((link: string) => {
|
||||||
|
if (!isURI(link) || !hasValidDomain(link)) {
|
||||||
|
errors.push(`The stream URL "${link}" is invalid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const found: Stream = streams.first((_stream: Stream) => _stream.url === link.trim())
|
||||||
|
if (!found) {
|
||||||
|
errors.push(`The stream with the URL "${link}" is missing from the playlists`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (labels.includes('streams:edit')) {
|
||||||
|
const streamUrl = data.getString('stream_url')
|
||||||
|
|
||||||
|
if (!streamUrl) {
|
||||||
|
errors.push('The request is missing the "Stream URL"')
|
||||||
|
done()
|
||||||
|
} else if (!isURI(streamUrl) || !hasValidDomain(streamUrl)) {
|
||||||
|
errors.push(`The stream URL "${streamUrl}" is invalid`)
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream: Stream = streams.first((_stream: Stream) => _stream.url === streamUrl)
|
||||||
|
if (!stream) {
|
||||||
|
errors.push(`The stream with the URL "${streamUrl}" is missing from the playlists`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
if (errors.length) {
|
||||||
|
let message = 'The request contains error(s):'
|
||||||
|
errors.forEach(error => {
|
||||||
|
message += `\r\n- ${error}`
|
||||||
|
})
|
||||||
|
logsStorage.saveSync('errors.txt', message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -109,7 +109,7 @@ async function removeStream(issue: Issue) {
|
|||||||
const log = createThread(issue, 'streams/remove')
|
const log = createThread(issue, 'streams/remove')
|
||||||
log.start()
|
log.start()
|
||||||
|
|
||||||
const data = issue.data
|
const data = issue.dataSet
|
||||||
if (data.missing('stream_url')) {
|
if (data.missing('stream_url')) {
|
||||||
log.error('The request is missing the "Stream URL"')
|
log.error('The request is missing the "Stream URL"')
|
||||||
skippedIssues.add(issue)
|
skippedIssues.add(issue)
|
||||||
@@ -145,7 +145,7 @@ async function editStream(issue: Issue) {
|
|||||||
const log = createThread(issue, 'streams/edit')
|
const log = createThread(issue, 'streams/edit')
|
||||||
log.start()
|
log.start()
|
||||||
|
|
||||||
const data = issue.data
|
const data = issue.dataSet
|
||||||
|
|
||||||
const streamUrl = data.getString('stream_url')
|
const streamUrl = data.getString('stream_url')
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ async function editStream(issue: Issue) {
|
|||||||
|
|
||||||
const stream: Stream = streams.first((_stream: Stream) => _stream.url === streamUrl)
|
const stream: Stream = streams.first((_stream: Stream) => _stream.url === streamUrl)
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
log.error(`The stream with the URL "${streamUrl}" is already in the playlists`)
|
log.error(`The stream with the URL "${streamUrl}" is missing from the playlists`)
|
||||||
skippedIssues.add(issue)
|
skippedIssues.add(issue)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ async function addStream(issue: Issue) {
|
|||||||
const log = createThread(issue, 'streams/add')
|
const log = createThread(issue, 'streams/add')
|
||||||
log.start()
|
log.start()
|
||||||
|
|
||||||
const data = issue.data
|
const data = issue.dataSet
|
||||||
if (data.missing('stream_id')) {
|
if (data.missing('stream_id')) {
|
||||||
log.error('The request is missing the "Stream ID"')
|
log.error('The request is missing the "Stream ID"')
|
||||||
skippedIssues.add(issue)
|
skippedIssues.add(issue)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async function main() {
|
|||||||
issue.labels.find((label: string) => label === 'streams:remove')
|
issue.labels.find((label: string) => label === 'streams:remove')
|
||||||
)
|
)
|
||||||
removeRequests.forEach((issue: Issue) => {
|
removeRequests.forEach((issue: Issue) => {
|
||||||
const streamUrls = issue.data.getArray('stream_url') || []
|
const streamUrls = issue.dataSet.getArray('stream_url') || []
|
||||||
|
|
||||||
if (!streamUrls.length) {
|
if (!streamUrls.length) {
|
||||||
const result = {
|
const result = {
|
||||||
@@ -84,8 +84,8 @@ async function main() {
|
|||||||
const addRequests = issues.filter(issue => issue.labels.includes('streams:add'))
|
const addRequests = issues.filter(issue => issue.labels.includes('streams:add'))
|
||||||
const addRequestsBuffer = new Dictionary()
|
const addRequestsBuffer = new Dictionary()
|
||||||
addRequests.forEach((issue: Issue) => {
|
addRequests.forEach((issue: Issue) => {
|
||||||
const streamId = issue.data.getString('stream_id') || ''
|
const streamId = issue.dataSet.getString('stream_id') || ''
|
||||||
const streamUrl = issue.data.getString('stream_url') || ''
|
const streamUrl = issue.dataSet.getString('stream_url') || ''
|
||||||
const [channelId] = streamId.split('@')
|
const [channelId] = streamId.split('@')
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
@@ -116,8 +116,8 @@ async function main() {
|
|||||||
issue.labels.find((label: string) => label === 'streams:edit')
|
issue.labels.find((label: string) => label === 'streams:edit')
|
||||||
)
|
)
|
||||||
editRequests.forEach((issue: Issue) => {
|
editRequests.forEach((issue: Issue) => {
|
||||||
const streamId = issue.data.getString('stream_id') || ''
|
const streamId = issue.dataSet.getString('stream_id') || ''
|
||||||
const streamUrl = issue.data.getString('stream_url') || ''
|
const streamUrl = issue.dataSet.getString('stream_url') || ''
|
||||||
const [channelId] = streamId.split('@')
|
const [channelId] = streamId.split('@')
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { DataSet } from '../core'
|
|||||||
type IssueProps = {
|
type IssueProps = {
|
||||||
number: number
|
number: number
|
||||||
labels: string[]
|
labels: string[]
|
||||||
data: DataSet
|
dataSet: DataSet
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Issue {
|
export class Issue {
|
||||||
number: number
|
number: number
|
||||||
labels: string[]
|
labels: string[]
|
||||||
data: DataSet
|
dataSet: DataSet
|
||||||
|
|
||||||
constructor({ number, labels, data }: IssueProps) {
|
constructor({ number, labels, dataSet }: IssueProps) {
|
||||||
this.number = number
|
this.number = number
|
||||||
this.labels = labels
|
this.labels = labels
|
||||||
this.data = data
|
this.dataSet = dataSet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-6
@@ -15,6 +15,20 @@ import { orderBy } from 'es-toolkit'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
export function hasValidDomain(string: string): boolean {
|
||||||
|
const FORBIDDEN_DOMAINS = ['iptv-org.github.io']
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(string)
|
||||||
|
const hostname = parsedUrl.hostname
|
||||||
|
const isForbidden = FORBIDDEN_DOMAINS.some(
|
||||||
|
domain => hostname === domain || hostname.endsWith(`.${domain}`)
|
||||||
|
)
|
||||||
|
return !isForbidden
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isURI(string: string): boolean {
|
export function isURI(string: string): boolean {
|
||||||
try {
|
try {
|
||||||
const url = new URL(string)
|
const url = new URL(string)
|
||||||
@@ -174,10 +188,13 @@ export async function loadIssues(props?: { labels: string | string[] }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Collection(issues).map(parseIssue)
|
return new Collection(issues).map(({ number, body, labels }) => {
|
||||||
|
const dataSet = parseIssueBody(body)
|
||||||
|
return new Issue({ number, labels: labels.map(l => l.name), dataSet })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseIssue(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
export function parseIssueBody(body: string): DataSet {
|
||||||
const FIELDS = new Dictionary({
|
const FIELDS = new Dictionary({
|
||||||
'Stream ID': 'stream_id',
|
'Stream ID': 'stream_id',
|
||||||
'Channel ID': 'channel_id',
|
'Channel ID': 'channel_id',
|
||||||
@@ -193,7 +210,7 @@ function parseIssue(issue: { number: number; body: string; labels: { name: strin
|
|||||||
Notes: 'notes'
|
Notes: 'notes'
|
||||||
})
|
})
|
||||||
|
|
||||||
const fields = typeof issue.body === 'string' ? issue.body.split('###') : []
|
const fields = typeof body === 'string' ? body.split('###') : []
|
||||||
|
|
||||||
const data = new Dictionary<string>()
|
const data = new Dictionary<string>()
|
||||||
fields.forEach((field: string) => {
|
fields.forEach((field: string) => {
|
||||||
@@ -213,9 +230,7 @@ function parseIssue(issue: { number: number; body: string; labels: { name: strin
|
|||||||
data.set(id, value)
|
data.set(id, value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const labels = issue.labels.map(label => label.name)
|
return new DataSet(data)
|
||||||
|
|
||||||
return new Issue({ number: issue.number, labels, data: new DataSet(data) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadDiscussions() {
|
export async function loadDiscussions() {
|
||||||
|
|||||||
Reference in New Issue
Block a user