This commit is contained in:
freearhey
2026-05-02 13:29:34 +03:00
parent 6826742c6b
commit 8f7940c8ca
8 changed files with 160 additions and 78 deletions

47
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"@freearhey/core": "^0.14.3", "@freearhey/core": "^0.14.3",
"@freearhey/storage-js": "^0.1.0", "@freearhey/storage-js": "^0.1.0",
"@inquirer/prompts": "^7.8.0", "@inquirer/prompts": "^7.8.0",
"@iptv-org/sdk": "^1.1.4", "@iptv-org/sdk": "^1.4.0",
"@octokit/core": "^7.0.3", "@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-graphql": "^6.0.0", "@octokit/plugin-paginate-graphql": "^6.0.0",
"@octokit/plugin-paginate-rest": "^13.1.1", "@octokit/plugin-paginate-rest": "^13.1.1",
@@ -726,15 +726,12 @@
} }
}, },
"node_modules/@freearhey/search-js": { "node_modules/@freearhey/search-js": {
"version": "0.2.1", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/@freearhey/search-js/-/search-js-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@freearhey/search-js/-/search-js-0.3.0.tgz",
"integrity": "sha512-RXVJ2AaXjnrLPpLHCOWrdgtYc4SZplYl905INFmhL6V8jcyIrX+qrjkAjwAHqWDTnJSYfSG9D9Xr+EyKx/eXng==", "integrity": "sha512-wsUBM1vA+fRqGZ+G5EnR7v52KxomoCsVOnlv43kHWt2G/LNs5pGM+aLAyx0MO/sDyBo3ADgw1h30Ydi6+1CMeQ==",
"license": "MIT", "license": "MIT",
"dependencies": {
"lodash": "^4.17.21"
},
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.6.0"
} }
}, },
"node_modules/@freearhey/storage-js": { "node_modules/@freearhey/storage-js": {
@@ -1126,15 +1123,15 @@
} }
}, },
"node_modules/@iptv-org/sdk": { "node_modules/@iptv-org/sdk": {
"version": "1.1.4", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@iptv-org/sdk/-/sdk-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@iptv-org/sdk/-/sdk-1.4.0.tgz",
"integrity": "sha512-NFviCin8V9rKPP+GG7xZ0mVi+WCwxqTFzjRH8or9KVLemFXrGI+ibGQ1PgiGUadRFSxTejXo2Dvwjwdwr2NTiQ==", "integrity": "sha512-elnrVBBaZAGKlcb/2+5M1BuHSjD10z5yUsPn3sRY/9a9kwgbcsZ2h6ylh0QON0FY54xy8Ee1Y5ORuc45o3VLVw==",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@freearhey/core": "^0.15.1", "@freearhey/core": "^0.15.1",
"@freearhey/search-js": "^0.2.1", "@freearhey/search-js": "^0.3.0",
"@ntlab/sfetch": "^1.2.0", "@ntlab/sfetch": "^1.2.0",
"axios": "^1.11.0", "axios": "^1.15.2",
"dayjs": "^1.11.18" "dayjs": "^1.11.18"
} }
}, },
@@ -3169,9 +3166,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3185,9 +3179,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3201,9 +3192,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3217,9 +3205,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3233,9 +3218,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3249,9 +3231,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3265,9 +3244,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3281,9 +3257,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [

View File

@@ -38,7 +38,7 @@
"@freearhey/core": "^0.14.3", "@freearhey/core": "^0.14.3",
"@freearhey/storage-js": "^0.1.0", "@freearhey/storage-js": "^0.1.0",
"@inquirer/prompts": "^7.8.0", "@inquirer/prompts": "^7.8.0",
"@iptv-org/sdk": "^1.1.4", "@iptv-org/sdk": "^1.4.0",
"@octokit/core": "^7.0.3", "@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-graphql": "^6.0.0", "@octokit/plugin-paginate-graphql": "^6.0.0",
"@octokit/plugin-paginate-rest": "^13.1.1", "@octokit/plugin-paginate-rest": "^13.1.1",

View File

@@ -1,4 +1,4 @@
import { isURI, getStreamInfo, loadIssues, createThread } from '../../utils' import { getStreamInfo, loadIssues, createThread } from '../../utils'
import { STREAMS_DIR, LOGS_DIR } from '../../constants' import { STREAMS_DIR, LOGS_DIR } from '../../constants'
import { Playlist, Issue, Stream } from '../../models' import { Playlist, Issue, Stream } from '../../models'
import { loadData, data as apiData } from '../../api' import { loadData, data as apiData } from '../../api'
@@ -12,6 +12,15 @@ const skippedIssues = new Collection<Issue>()
const logger = new Logger({ level: 5 }) const logger = new Logger({ level: 5 })
let streams = new Collection<Stream>() let streams = new Collection<Stream>()
let cache = new Collection<Stream>()
function cacheData() {
cache = streams.clone()
}
function resetData() {
streams = cache
}
async function main() { async function main() {
logger.info('loading data from api...') logger.info('loading data from api...')
@@ -97,7 +106,11 @@ async function removeStream(issue: Issue) {
log.start() log.start()
const data = issue.data const data = issue.data
if (data.missing('stream_url')) return if (data.missing('stream_url')) {
log.error('The request is missing the "Stream URL"')
skippedIssues.add(issue)
return
}
const streamUrls = data.getString('stream_url') || '' const streamUrls = data.getString('stream_url') || ''
@@ -105,55 +118,119 @@ async function removeStream(issue: Issue) {
streamUrls streamUrls
.split(/\r?\n/) .split(/\r?\n/)
.filter(Boolean) .filter(Boolean)
.forEach(link => { .forEach((link: string) => {
const found: Stream = streams.first((_stream: Stream) => _stream.url === link.trim()) const found: Stream = streams.first((_stream: Stream) => _stream.url === link.trim())
if (found) { if (found) {
found.removed = true found.removed = true
changed = 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) 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) { async function editStream(issue: Issue) {
const log = createThread(issue, 'streams/edit')
log.start()
const data = issue.data const data = issue.data
if (data.missing('stream_url')) return const streamUrl = data.getString('stream_url')
const stream: Stream = streams.first( if (!streamUrl) {
(_stream: Stream) => _stream.url === data.getString('stream_url') log.error('The request is missing the "Stream URL"')
) skippedIssues.add(issue)
if (!stream) return return
const streamId = data.getString('stream_id') || ''
const [channelId, feedId] = streamId.split('@')
if (channelId) {
stream.channel = channelId
stream.feed = feedId
stream.updateTvgId().updateTitle().updateFilepath()
} }
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) 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) processedIssues.add(issue)
} }
async function addStream(issue: Issue) { async function addStream(issue: Issue) {
const log = createThread(issue, 'streams/add')
log.start()
const data = issue.data const data = issue.data
if (data.missing('stream_id') || data.missing('stream_url')) return if (data.missing('stream_id')) {
if (streams.includes((_stream: Stream) => _stream.url === data.getString('stream_url'))) return log.error('The request is missing the "Stream ID"')
const streamUrl = data.getString('stream_url') || '' skippedIssues.add(issue)
if (!isURI(streamUrl)) return 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 streamId = data.getString('stream_id') || ''
const [channelId, feedId] = streamId.split('@') const [channelId, feedId] = streamId.split('@')
const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId) const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId)
if (!channel) return 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 label = data.getString('label') || ''
const httpUserAgent = data.getString('http_user_agent') || null const httpUserAgent = data.getString('http_user_agent') || null
const httpReferrer = data.getString('http_referrer') || null const httpReferrer = data.getString('http_referrer') || null
@@ -177,12 +254,27 @@ async function addStream(issue: Issue) {
url: streamUrl, url: streamUrl,
user_agent: httpUserAgent, user_agent: httpUserAgent,
referrer: httpReferrer, referrer: httpReferrer,
quality quality,
label: data.getString('label') || ''
}) })
stream.label = label
stream.updateTitle().updateFilepath() stream.updateTitle().updateFilepath()
streams.add(stream) 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) processedIssues.add(issue)
} }

View File

@@ -1,15 +1,15 @@
import { IssueData } from '../core' import { DataSet } from '../core'
type IssueProps = { type IssueProps = {
number: number number: number
labels: string[] labels: string[]
data: IssueData data: DataSet
} }
export class Issue { export class Issue {
number: number number: number
labels: string[] labels: string[]
data: IssueData data: DataSet
constructor({ number, labels, data }: IssueProps) { constructor({ number, labels, data }: IssueProps) {
this.number = number this.number = number

View File

@@ -1,8 +1,8 @@
import { normalizeURL, isURI } from '../utils'
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
import parser from 'iptv-playlist-parser' import parser from 'iptv-playlist-parser'
import { normalizeURL } from '../utils'
import * as sdk from '@iptv-org/sdk' import * as sdk from '@iptv-org/sdk'
import { IssueData } from '../core' import { DataSet } from '../core'
import { data } from '../api' import { data } from '../api'
import path from 'node:path' import path from 'node:path'
@@ -12,15 +12,32 @@ export class Stream extends sdk.Models.Stream {
groupTitle: string = 'Undefined' groupTitle: string = 'Undefined'
removed: boolean = false removed: boolean = false
tvgId?: string tvgId?: string
label: string | null
statusCode?: string statusCode?: string
updateWithIssue(issueData: IssueData): this { validate(): Collection<Error> {
const errors = new Collection<Error>()
if (!isURI(this.url)) {
errors.add(new Error(`The stream URL "${this.url}" is invalid`))
}
return errors
}
updateWithIssue(dataSet: DataSet): this {
const streamId = dataSet.getString('stream_id') || ''
const [channelId, feedId] = streamId.split('@')
if (channelId) {
this.channel = channelId
this.feed = feedId
this.updateTvgId().updateTitle().updateFilepath()
}
const data = { const data = {
label: issueData.getString('label'), label: dataSet.getString('label'),
quality: issueData.getString('quality'), quality: dataSet.getString('quality'),
httpUserAgent: issueData.getString('http_user_agent'), httpUserAgent: dataSet.getString('http_user_agent'),
httpReferrer: issueData.getString('http_referrer') httpReferrer: dataSet.getString('http_referrer')
} }
if (data.label !== undefined) this.label = data.label if (data.label !== undefined) this.label = data.label

View File

@@ -18,7 +18,7 @@ import fs from 'node:fs'
export function isURI(string: string): boolean { export function isURI(string: string): boolean {
try { try {
const url = new URL(string) const url = new URL(string)
return /^(http:|https:|mmsh:|rtsp:|rtmp:)/.test(url.protocol) return /^(http:|https:|mmsh:|rtsp:|rtmp:|srt:|rtp:|udp:)/.test(url.protocol)
} catch { } catch {
return false return false
} }

View File

@@ -1,5 +1,5 @@
#EXTM3U #EXTM3U
#EXTINF:-1 tvg-id="TFX.fr@SD",TFX (720p) #EXTINF:-1 tvg-id="TFX.fr@SD",TFX
#EXTVLCOPT:http-referrer=https://pkpakiplay.xyz/ #EXTVLCOPT:http-referrer=https://pkpakiplay.xyz/
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 #EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1
https://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.mpd?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY srt://stream.alabbassia.com:8890?mode=caller&latency=200&streamid=read:live/alabbassia

View File

@@ -538,7 +538,7 @@ module.exports = [
closed_at: null, closed_at: null,
author_association: 'COLLABORATOR', author_association: 'COLLABORATOR',
active_lock_reason: null, active_lock_reason: null,
body: '### Stream ID\n\nTFX.fr\n\n### Stream URL\n\nhttps://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.mpd?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY\n\n### Label\n\nNone\n\n### HTTP User Agent\n\nMozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1\n\n### HTTP Referrer\n\nhttps://pkpakiplay.xyz/\n\n### Notes (optional)\n\nSource: https://github.com/iptv-org/iptv-org.github.io/issues/1381\n\n### Contributing Guide\n\n- [X] I have read [Contributing Guide](https://github.com/iptv-org/iptv/blob/master/CONTRIBUTING.md)', body: '### Stream ID\n\nTFX.fr\n\n### Stream URL\n\nsrt://stream.alabbassia.com:8890?mode=caller&latency=200&streamid=read:live/alabbassia\n\n### Label\n\nNone\n\n### HTTP User Agent\n\nMozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1\n\n### HTTP Referrer\n\nhttps://pkpakiplay.xyz/\n\n### Notes (optional)\n\nSource: https://github.com/iptv-org/iptv-org.github.io/issues/1381\n\n### Contributing Guide\n\n- [X] I have read [Contributing Guide](https://github.com/iptv-org/iptv/blob/master/CONTRIBUTING.md)',
reactions: { reactions: {
url: 'https://api.github.com/repos/iptv-org/iptv/issues/14175/reactions', url: 'https://api.github.com/repos/iptv-org/iptv/issues/14175/reactions',
total_count: 0, total_count: 0,