diff --git a/package-lock.json b/package-lock.json index 1606c334e6..cc90c2a09b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,15 +36,18 @@ "commander": "^14.0.0", "console-table-printer": "^2.14.6", "cross-env": "^10.0.0", + "es-toolkit": "^1.45.1", "eslint": "^9.32.0", "glob": "^11.0.3", "globals": "^16.3.0", + "hls-parser": "^0.16.0", "iptv-playlist-parser": "^0.15.1", "jest": "^30.0.5", "jest-expect-message": "^1.1.3", "lodash.uniqueid": "^4.0.1", "m3u-linter": "^0.4.2", "mediainfo.js": "^0.3.6", + "mpd-parser": "^1.3.1", "node-cleanup": "^2.1.2", "normalize-url": "^8.1.0", "socks-proxy-agent": "^8.0.5", @@ -497,6 +500,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -3453,6 +3465,30 @@ "win32" ] }, + "node_modules/@videojs/vhs-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", + "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4107,6 +4143,11 @@ "node": ">=8" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4204,6 +4245,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", @@ -4635,9 +4686,10 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -4897,6 +4949,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", @@ -4979,6 +5041,12 @@ "node": ">= 0.4" } }, + "node_modules/hls-parser": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/hls-parser/-/hls-parser-0.16.0.tgz", + "integrity": "sha512-jb8tX36Nn49MtvSBTWcQjD4ktwJtzrfOlnI8zpo4zQjTq0ZGmG0RsoCQXwvKQGvt4CBFrs8Q3znq0HoNCpt5BA==", + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6617,6 +6685,15 @@ "node": ">=6" } }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "license": "MIT", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -6637,6 +6714,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mpd-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz", + "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7024,6 +7116,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7719,6 +7820,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==", + "license": "Apache-2.0" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index 7b642c0309..918989f2fa 100644 --- a/package.json +++ b/package.json @@ -60,15 +60,18 @@ "commander": "^14.0.0", "console-table-printer": "^2.14.6", "cross-env": "^10.0.0", + "es-toolkit": "^1.45.1", "eslint": "^9.32.0", "glob": "^11.0.3", "globals": "^16.3.0", + "hls-parser": "^0.16.0", "iptv-playlist-parser": "^0.15.1", "jest": "^30.0.5", "jest-expect-message": "^1.1.3", "lodash.uniqueid": "^4.0.1", "m3u-linter": "^0.4.2", "mediainfo.js": "^0.3.6", + "mpd-parser": "^1.3.1", "node-cleanup": "^2.1.2", "normalize-url": "^8.1.0", "socks-proxy-agent": "^8.0.5", diff --git a/scripts/commands/playlist/format.ts b/scripts/commands/playlist/format.ts index 15b4bc017e..442b1cdf2f 100644 --- a/scripts/commands/playlist/format.ts +++ b/scripts/commands/playlist/format.ts @@ -1,13 +1,34 @@ import { Collection, Logger } from '@freearhey/core' +import { OptionValues, program } from 'commander' import { Stream, Playlist } from '../../models' import { Storage } from '@freearhey/storage-js' import { STREAMS_DIR } from '../../constants' import { PlaylistParser } from '../../core' +import { getStreamInfo } from '../../utils' +import cliProgress from 'cli-progress' import { loadData } from '../../api' -import { program } from 'commander' +import { eachLimit } from 'async' import path from 'node:path' +import os from 'node:os' -program.argument('[filepath...]', 'Path to file to format').parse(process.argv) +program + .argument('[filepath...]', 'Path to file to format') + .option( + '-p, --parallel ', + 'Batch size of streams to test concurrently', + (value: string) => parseInt(value), + os.cpus().length + ) + .option('-x, --proxy ', 'Use the specified proxy') + .option( + '-t, --timeout ', + 'The number of milliseconds before the request will be aborted', + (value: string) => parseInt(value), + 1000 + ) + .parse(process.argv) + +const options: OptionValues = program.opts() async function main() { const logger = new Logger() @@ -58,6 +79,33 @@ async function main() { return stream }) + logger.info('adding the missing quality...') + const progressBar = new cliProgress.SingleBar({ + clearOnComplete: true, + format: `[{bar}] {percentage}% | {value}/{total}` + }) + progressBar.start(streams.count(), 0) + await eachLimit(streams.all(), options.parallel, async (stream: Stream) => { + progressBar.increment() + if (stream.quality) return + + const streamInfo = await getStreamInfo(stream.url, { + httpUserAgent: stream.user_agent, + httpReferrer: stream.referrer, + timeout: options.timeout, + proxy: options.proxy + }) + + if (streamInfo) { + const height = streamInfo?.resolution?.height + + if (height) { + stream.quality = `${height}p` + } + } + }) + progressBar.stop() + logger.info('sorting links...') streams = streams.sortBy( [ diff --git a/scripts/commands/playlist/update.ts b/scripts/commands/playlist/update.ts index 1c5f17ee33..38dd69b7e0 100644 --- a/scripts/commands/playlist/update.ts +++ b/scripts/commands/playlist/update.ts @@ -2,10 +2,10 @@ import { IssueLoader, PlaylistParser } from '../../core' import { Playlist, Issue, Stream } from '../../models' import { loadData, data as apiData } from '../../api' import { Logger, Collection } from '@freearhey/core' +import { isURI, getStreamInfo } from '../../utils' import { Storage } from '@freearhey/storage-js' import { STREAMS_DIR } from '../../constants' import * as sdk from '@iptv-org/sdk' -import { isURI } from '../../utils' const processedIssues = new Collection() @@ -136,24 +136,38 @@ async function addStreams({ const requests = issues.filter( issue => issue.labels.includes('streams:add') && issue.labels.includes('approved') ) - requests.forEach((issue: Issue) => { + + for (const issue of requests.all()) { const data = issue.data - if (data.missing('stream_id') || data.missing('stream_url')) return - if (streams.includes((_stream: Stream) => _stream.url === data.getString('stream_url'))) return + if (data.missing('stream_id') || data.missing('stream_url')) continue + if (streams.includes((_stream: Stream) => _stream.url === data.getString('stream_url'))) + continue const streamUrl = data.getString('stream_url') || '' - if (!isURI(streamUrl)) return + if (!isURI(streamUrl)) continue const streamId = data.getString('stream_id') || '' const [channelId, feedId] = streamId.split('@') const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId) - if (!channel) return + if (!channel) continue const label = data.getString('label') || '' - const quality = data.getString('quality') || null 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, @@ -169,5 +183,5 @@ async function addStreams({ streams.add(stream) processedIssues.add(issue.number) - }) + } } diff --git a/scripts/utils.ts b/scripts/utils.ts index ecce8da58b..eb3b4e2f0b 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,4 +1,13 @@ +import axios, { AxiosProxyConfig, AxiosRequestConfig } from 'axios' +import { parse as parsePlaylist, setOptions } from 'hls-parser' +import { parse as parseManifest } from 'mpd-parser' +import { SocksProxyAgent } from 'socks-proxy-agent' +import { ProxyParser } from './core/proxyParser.js' +import { TESTING } from './constants.js' import normalizeUrl from 'normalize-url' +import { orderBy } from 'es-toolkit' +import path from 'node:path' +import fs from 'node:fs' export function isURI(string: string): boolean { try { @@ -21,3 +30,111 @@ export function truncate(string: string, limit: number = 100) { return string.slice(0, limit - 3) + '...' } + +type StreamInfo = { + resolution: { width: number; height: number } + bandwidth: number + frameRate: number + codecs: string +} + +export async function getStreamInfo( + url: string, + options: { + httpUserAgent?: string | null + httpReferrer?: string | null + timeout?: number + proxy?: string + } +): Promise { + let data: string | undefined + if (TESTING) { + if (url.includes('.m3u8')) { + data = fs.readFileSync( + path.resolve(__dirname, '../tests/__data__/input/playlist_update/playlist.m3u8'), + 'utf8' + ) + } else if (url.includes('.mpd')) { + data = fs.readFileSync( + path.resolve(__dirname, '../tests/__data__/input/playlist_update/manifest.mpd'), + 'utf8' + ) + } + } else { + try { + const timeout = options.timeout || 1000 + let request: AxiosRequestConfig = { + signal: AbortSignal.timeout(timeout), + responseType: 'text', + headers: { + 'User-Agent': options.httpUserAgent || 'Mozilla/5.0', + Referer: options.httpReferrer + } + } + + if (options.proxy !== undefined) { + const proxyParser = new ProxyParser() + const proxy = proxyParser.parse(options.proxy) as AxiosProxyConfig + if ( + proxy.protocol && + ['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol)) + ) { + const socksProxyAgent = new SocksProxyAgent(options.proxy) + + request = { ...request, ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } } + } else { + request = { ...request, ...{ proxy } } + } + } + + const response = await axios(url, request) + + data = response.data + } catch {} + } + + if (!data) return undefined + + let info: StreamInfo | undefined + + if (url.includes('.m3u8')) { + setOptions({ silent: true }) + + try { + const playlist = parsePlaylist(data) + + if (playlist && playlist.isMasterPlaylist && playlist.variants.length) { + const v = orderBy(playlist.variants, ['bandwidth'], ['desc'])[0] + + if (v && v.resolution && v.frameRate && v.codecs) { + info = { + resolution: { width: v.resolution.width, height: v.resolution.height }, + bandwidth: v.bandwidth, + frameRate: v.frameRate, + codecs: v.codecs + } + } + } + } catch {} + } else if (url.includes('.mpd')) { + const manifest = parseManifest(data, { + manifestUri: url, + eventHandler: ({ type, message }) => console.log(`${type}: ${message}`) + }) + + const playlist = orderBy(manifest.playlists, [p => p.attributes.BANDWIDTH], ['desc'])[0] + + if (playlist) { + const attr = playlist.attributes + + info = { + resolution: { width: attr.RESOLUTION.width, height: attr.RESOLUTION.height }, + bandwidth: attr.BANDWIDTH, + frameRate: attr['FRAME-RATE'], + codecs: attr.CODECS + } + } + } + + return info +} diff --git a/tests/__data__/expected/playlist_format/at.m3u b/tests/__data__/expected/playlist_format/at.m3u new file mode 100644 index 0000000000..171c8c8b13 --- /dev/null +++ b/tests/__data__/expected/playlist_format/at.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="",Hitradio O3 (1080p) +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/master.m3u8 +#EXTINF:-1 tvg-id="",Hitradio O3 (720p) +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/manifest.mpd diff --git a/tests/__data__/expected/playlist_format/in.m3u b/tests/__data__/expected/playlist_format/in.m3u index f733f449a4..324b076d47 100644 --- a/tests/__data__/expected/playlist_format/in.m3u +++ b/tests/__data__/expected/playlist_format/in.m3u @@ -1,5 +1,5 @@ #EXTM3U -#EXTINF:-1 tvg-id="",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] +#EXTINF:-1 tvg-id="",Manorama News -2 (1080p) [U3] (Alt) [Geo-blocked] [Not 24/7] #EXTVLCOPT:http-referrer=http://test.com #EXTVLCOPT:http-user-agent=Mozilla/5.0 https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 diff --git a/tests/__data__/expected/playlist_format/nl.m3u b/tests/__data__/expected/playlist_format/nl.m3u index b9d1f9e4cf..d9aea26043 100644 --- a/tests/__data__/expected/playlist_format/nl.m3u +++ b/tests/__data__/expected/playlist_format/nl.m3u @@ -1,11 +1,11 @@ #EXTM3U #EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked] http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8 +#EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 (1080p) [Geo-blocked] +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 #EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 (342p) http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 #EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 (302p) [Geo-blocked] #EXTVLCOPT:http-referrer=http://imn.iq #EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8?|Referer="https://referer.xyz/"|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"|Origin="https://origin.xyz" -#EXTINF:-1 tvg-id="NPO2.nl@SD",NPO 2 [Geo-blocked] -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 diff --git a/tests/__data__/expected/playlist_update/fr.m3u b/tests/__data__/expected/playlist_update/fr.m3u index 08556c744c..12427bf56b 100644 --- a/tests/__data__/expected/playlist_update/fr.m3u +++ b/tests/__data__/expected/playlist_update/fr.m3u @@ -1,5 +1,5 @@ #EXTM3U -#EXTINF:-1 tvg-id="TFX.fr@SD",TFX +#EXTINF:-1 tvg-id="TFX.fr@SD",TFX (720p) #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 -https://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.m3u8?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 +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 diff --git a/tests/__data__/expected/playlist_update/uk.m3u b/tests/__data__/expected/playlist_update/uk.m3u index 2333877803..9caa57c157 100644 --- a/tests/__data__/expected/playlist_update/uk.m3u +++ b/tests/__data__/expected/playlist_update/uk.m3u @@ -3,5 +3,5 @@ http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 #EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 -#EXTINF:-1 tvg-id="BeanoTV.uk@SD",Beano TV +#EXTINF:-1 tvg-id="BeanoTV.uk@SD",Beano TV (1080p) https://a5b4bacecd47433dad06d3189fc7422e.mediatailor.us-east-1.amazonaws.com/v1/manifest/04fd913bb278d8775298c26fdca9d9841f37601f/RakutenTV-eu_BeanoTV/b1f233d5-847c-437d-aa4f-f73e67a85323/2.m3u8|Referer="https://referer.xyz/"|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"|Origin="https://origin.xyz" diff --git a/tests/__data__/expected/playlist_update/us.m3u b/tests/__data__/expected/playlist_update/us.m3u index d16ed56287..4c76334ffd 100644 --- a/tests/__data__/expected/playlist_update/us.m3u +++ b/tests/__data__/expected/playlist_update/us.m3u @@ -1,3 +1,3 @@ #EXTM3U -#EXTINF:-1 tvg-id="FastTV.us@SD",Fast TV +#EXTINF:-1 tvg-id="FastTV.us@SD",Fast TV (1080p) https://3fa797d5.wurl.com/manifest/f36d25e7e52f1ba8d7e56eb859c636563214f541/T05PX01vdG9yVHJlbmRGYXN0VFZfSExT/b5e5e0e2-12b3-4312-93c9-c0a7c50b41ca/4.m3u8 diff --git a/tests/__data__/input/issues.js b/tests/__data__/input/issues.js index 82d14a73fa..206a5571a7 100644 --- a/tests/__data__/input/issues.js +++ b/tests/__data__/input/issues.js @@ -538,7 +538,7 @@ module.exports = [ closed_at: null, author_association: 'COLLABORATOR', 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.m3u8?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\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)', reactions: { url: 'https://api.github.com/repos/iptv-org/iptv/issues/14175/reactions', total_count: 0, diff --git a/tests/__data__/input/playlist_format/at.m3u b/tests/__data__/input/playlist_format/at.m3u new file mode 100644 index 0000000000..9436c2c875 --- /dev/null +++ b/tests/__data__/input/playlist_format/at.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="HitradioO3.at@SD",Hitradio O3 +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/manifest.mpd +#EXTINF:-1 tvg-id="HitradioO3.at@SD",Hitradio O3 +https://studiocam-oe3.mdn.ors.at/orf/studiocam_oe3/q6a/master.m3u8 \ No newline at end of file diff --git a/tests/__data__/input/playlist_format/in.m3u b/tests/__data__/input/playlist_format/in.m3u index 0a013ba7a1..fda7a41b14 100644 --- a/tests/__data__/input/playlist_format/in.m3u +++ b/tests/__data__/input/playlist_format/in.m3u @@ -1,3 +1,3 @@ #EXTM3U -#EXTINF:-1 tvg-id="mn.in" http-referrer="http://test.com" http-user-agent="Mozilla/5.0",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] +#EXTINF:-1 tvg-id="mn.in" http-referrer="http://test.com" http-user-agent="Mozilla/5.0",Manorama News -2 [U3] (Alt) [Geo-blocked] [Not 24/7] https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 diff --git a/tests/__data__/input/playlist_update/manifest.mpd b/tests/__data__/input/playlist_update/manifest.mpd new file mode 100644 index 0000000000..80aded62a5 --- /dev/null +++ b/tests/__data__/input/playlist_update/manifest.mpd @@ -0,0 +1,45 @@ + + + + + dash/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/__data__/input/playlist_update/playlist.m3u8 b/tests/__data__/input/playlist_update/playlist.m3u8 new file mode 100644 index 0000000000..28ff1f079d --- /dev/null +++ b/tests/__data__/input/playlist_update/playlist.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +## Generated with https://github.com/shaka-project/shaka-packager version v3.4.2-c819dea-release +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="pl",NAME="stream_4",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2" +#EXT-X-STREAM-INF:BANDWIDTH=3688787,AVERAGE-BANDWIDTH=8948756,CODECS="avc1.424028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_0.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1988787,AVERAGE-BANDWIDTH=3688345,CODECS="avc1.42401f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_1.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1188787,AVERAGE-BANDWIDTH=1774276,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=853x480,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_2.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=688787,AVERAGE-BANDWIDTH=766090,CODECS="avc1.42401e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=25.000,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +stream_3.m3u8 diff --git a/tests/__data__/input/playlist_update/br.m3u b/tests/__data__/input/playlist_update/streams/br.m3u similarity index 100% rename from tests/__data__/input/playlist_update/br.m3u rename to tests/__data__/input/playlist_update/streams/br.m3u diff --git a/tests/__data__/input/playlist_update/br_example.m3u b/tests/__data__/input/playlist_update/streams/br_example.m3u similarity index 100% rename from tests/__data__/input/playlist_update/br_example.m3u rename to tests/__data__/input/playlist_update/streams/br_example.m3u diff --git a/tests/__data__/input/playlist_update/bz.m3u b/tests/__data__/input/playlist_update/streams/bz.m3u similarity index 100% rename from tests/__data__/input/playlist_update/bz.m3u rename to tests/__data__/input/playlist_update/streams/bz.m3u diff --git a/tests/__data__/input/playlist_update/cy.m3u b/tests/__data__/input/playlist_update/streams/cy.m3u similarity index 100% rename from tests/__data__/input/playlist_update/cy.m3u rename to tests/__data__/input/playlist_update/streams/cy.m3u diff --git a/tests/__data__/input/playlist_update/uk.m3u b/tests/__data__/input/playlist_update/streams/uk.m3u similarity index 100% rename from tests/__data__/input/playlist_update/uk.m3u rename to tests/__data__/input/playlist_update/streams/uk.m3u diff --git a/tests/commands/playlist/update.test.ts b/tests/commands/playlist/update.test.ts index d8552e3d25..8e4cf99e01 100644 --- a/tests/commands/playlist/update.test.ts +++ b/tests/commands/playlist/update.test.ts @@ -8,7 +8,7 @@ const ENV_VAR = beforeEach(() => { fs.emptyDirSync('tests/__data__/output') - fs.copySync('tests/__data__/input/playlist_update', 'tests/__data__/output/streams') + fs.copySync('tests/__data__/input/playlist_update/streams', 'tests/__data__/output/streams') }) describe('playlist:update', () => {