diff --git a/src/extractor/StreamUp.test.ts b/src/extractor/StreamUp.test.ts new file mode 100644 index 0000000..a4a8b26 --- /dev/null +++ b/src/extractor/StreamUp.test.ts @@ -0,0 +1,16 @@ +import winston from 'winston'; +import { createTestContext } from '../test'; +import { FetcherMock } from '../utils'; +import { ExtractorRegistry } from './ExtractorRegistry'; +import { StreamUp } from './StreamUp'; + +const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }); +const extractorRegistry = new ExtractorRegistry(logger, [new StreamUp(new FetcherMock(`${__dirname}/__fixtures__/StreamUp`))]); + +const ctx = createTestContext(); + +describe('StreamUp', () => { + test('handle one battle after another', async () => { + expect(await extractorRegistry.handle(ctx, new URL('https://strmup.to/6950ae79eaa4c'))).toMatchSnapshot(); + }); +}); diff --git a/src/extractor/StreamUp.ts b/src/extractor/StreamUp.ts index a3a35aa..9b10cba 100644 --- a/src/extractor/StreamUp.ts +++ b/src/extractor/StreamUp.ts @@ -1,111 +1,52 @@ -import crypto from 'crypto'; -import { NotFoundError } from '../error'; +import * as cheerio from 'cheerio'; import { Context, Format, Meta, UrlResult } from '../types'; +import { guessHeightFromPlaylist } from '../utils'; import { Extractor } from './Extractor'; -/** - * Port of: - * https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/streamup.py - */ +interface StreamUpApiData { + streaming_url: string; +} + +/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/streamup.py */ export class StreamUp extends Extractor { public readonly id = 'streamup'; - public readonly label = 'StreamUp'; - public override readonly ttl = 6 * 60 * 60 * 1000; // 6 hours + + public readonly label = 'StreamUP'; + + public override readonly ttl: number = 10800000; // 3h public supports(_ctx: Context, url: URL): boolean { - return [ - 'streamup.ws', - 'streamup.cc', - 'strmup.to', - 'strmup.cc', + return null !== url.host.match(/streamup|strmup/) || [ 'vfaststream.com', ].includes(url.host); } - public override normalize(url: URL): URL { - return new URL(`/${url.pathname.split('/').at(-1)}`, url.origin); - } - - protected async extractInternal( - ctx: Context, - url: URL, - meta: Meta, - ): Promise { - const referer = `${url.origin}/`; - + protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise { const headers = { - 'Referer': referer, + 'Referer': `${url.origin}/`, + 'Origin': url.origin, 'User-Agent': 'Mozilla/5.0', }; const html = await this.fetcher.text(ctx, url, { headers }); - const sessionMatch = html.match(/'([a-f0-9]{32})'/); - const encryptedMatch = html.match(/'([A-Za-z0-9+/=]{200,})'/); + const data = await this.fetcher.json(ctx, new URL(`/ajax/stream?filecode=${url.pathname.split('/').at(-1)}`, url), { headers }) as StreamUpApiData; + const playlistUrl = new URL(data.streaming_url); - let streamUrl: string | undefined; - - if (sessionMatch && encryptedMatch) { - /* encrypted flow */ - const sessionId = sessionMatch[1] as string; - const encryptedB64 = encryptedMatch[1] as string; - - const keyUrl = new URL(`/ajax/stream?session=${sessionId}`, url.origin); - const keyB64 = await this.fetcher.text(ctx, keyUrl, { headers }); - - const key = Buffer.from(keyB64, 'base64'); - const encrypted = Buffer.from(encryptedB64, 'base64'); - - const iv = encrypted.subarray(0, 16); - const ciphertext = encrypted.subarray(16); - - const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); - const decrypted = Buffer.concat([ - decipher.update(ciphertext), - decipher.final(), - ]).toString('utf-8'); - - const data = JSON.parse(decrypted) as { - streaming_url?: string; - }; - - streamUrl = data.streaming_url; - } else { - /* fallback flow */ - const filecode = url.pathname.split('/').at(-1); - const apiUrl = new URL(`/ajax/stream?filecode=${filecode}`, url.origin); - - const jsonText = await this.fetcher.text(ctx, apiUrl, { headers }); - const data = JSON.parse(jsonText) as { - streaming_url?: string; - }; - - streamUrl = data.streaming_url; - } - - if (!streamUrl) { - throw new NotFoundError(); - } - - /* cleanup identical to Python resolver */ - streamUrl = streamUrl - .replace(/\\r\\n/g, '') - .replace(/\r|\n/g, '') - .replace(/\\\//g, '/'); + const $ = cheerio.load(html); + const title = $('title').text().trim(); return [ { - url: new URL(streamUrl), + url: playlistUrl, format: Format.hls, label: this.label, - sourceId: `${this.id}_${meta.countryCodes?.join('_')}`, ttl: this.ttl, - requestHeaders: { - Referer: referer, - Origin: url.origin, - }, + requestHeaders: headers, meta: { ...meta, + height: await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, url, { headers }), + title, }, }, ]; diff --git a/src/extractor/__fixtures__/StreamUp/https:s3-hls2-cdn59.strmupcdn.comhlsSJbD5Zm1pj06jZ2LDbHa13YBmryxasmaster.m3u8token35f702a94c0e324d607d8761795a3788-1768016464-104.28.197.7-5ebad85b212e5e349b388501e051c992a2b68e70fdcaa6cc18270adee1f7c003 b/src/extractor/__fixtures__/StreamUp/https:s3-hls2-cdn59.strmupcdn.comhlsSJbD5Zm1pj06jZ2LDbHa13YBmryxasmaster.m3u8token35f702a94c0e324d607d8761795a3788-1768016464-104.28.197.7-5ebad85b212e5e349b388501e051c992a2b68e70fdcaa6cc18270adee1f7c003 new file mode 100644 index 0000000..508fb09 --- /dev/null +++ b/src/extractor/__fixtures__/StreamUp/https:s3-hls2-cdn59.strmupcdn.comhlsSJbD5Zm1pj06jZ2LDbHa13YBmryxasmaster.m3u8token35f702a94c0e324d607d8761795a3788-1768016464-104.28.197.7-5ebad85b212e5e349b388501e051c992a2b68e70fdcaa6cc18270adee1f7c003 @@ -0,0 +1,4 @@ +#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-STREAM-INF:BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1350000,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=30 +index_1920x1080.m3u8?token=35f702a94c0e324d607d8761795a3788-1768016464-104.28.197.7-5ebad85b212e5e349b388501e051c992a2b68e70fdcaa6cc18270adee1f7c003 diff --git a/src/extractor/__fixtures__/StreamUp/https:strmup.to6950ae79eaa4c b/src/extractor/__fixtures__/StreamUp/https:strmup.to6950ae79eaa4c new file mode 100644 index 0000000..20f2127 --- /dev/null +++ b/src/extractor/__fixtures__/StreamUp/https:strmup.to6950ae79eaa4c @@ -0,0 +1,55 @@ + + + + + + + Wake.Up.Dead.Man.A.Knives.Out.Mystery.2025 + + + + + + + + + +
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/extractor/__fixtures__/StreamUp/https:strmup.toajaxstreamfilecode6950ae79eaa4c b/src/extractor/__fixtures__/StreamUp/https:strmup.toajaxstreamfilecode6950ae79eaa4c new file mode 100644 index 0000000..7e9b3ee --- /dev/null +++ b/src/extractor/__fixtures__/StreamUp/https:strmup.toajaxstreamfilecode6950ae79eaa4c @@ -0,0 +1 @@ +{"title":"","thumbnail":"https:\/\/add4.cdnup.cc\/thumbnail\/xWcziV3Qim\/6950ae79eaa4c.jpg","streaming_url":"https:\/\/s3-hls2-cdn59.strmupcdn.com\/hls\/SJbD5Zm1pj06jZ2LDbHa13YBmryxas\/master.m3u8?token=35f702a94c0e324d607d8761795a3788-1768016464-104.28.197.7-5ebad85b212e5e349b388501e051c992a2b68e70fdcaa6cc18270adee1f7c003","vast_ads":null,"filecode":"6950ae79eaa4c","subtitles":[{"id":521875,"status":1,"file_id":2229055,"type":0,"file_path":"https:\/\/add4.cdnup.cc\/subtitles\/QUKzfdu1n4\/PPTRdlsmJY94PKK_subtitle_0.vtt","language":"German \u2013 German (Forced)"},{"id":521876,"status":1,"file_id":2229055,"type":0,"file_path":"https:\/\/add4.cdnup.cc\/subtitles\/gxReoqgF82\/ABVuNSZeq61xYhM_subtitle_1.vtt","language":"German \u2013 German"},{"id":521877,"status":1,"file_id":2229055,"type":0,"file_path":"https:\/\/add4.cdnup.cc\/subtitles\/YSzO4RQUAu\/DNRW2kgCksC8YYq_subtitle_2.vtt","language":"German \u2013 German"},{"id":521878,"status":1,"file_id":2229055,"type":0,"file_path":"https:\/\/add4.cdnup.cc\/subtitles\/l7MtB81Fsb\/DSFucpIAjME1ZdV_subtitle_3.vtt","language":"German \u2013 German (SDH)"}],"default_sub_lang":"German"} \ No newline at end of file diff --git a/src/extractor/__snapshots__/StreamUp.test.ts.snap b/src/extractor/__snapshots__/StreamUp.test.ts.snap new file mode 100644 index 0000000..01262ec --- /dev/null +++ b/src/extractor/__snapshots__/StreamUp.test.ts.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`StreamUp handle one battle after another 1`] = ` +[ + { + "format": "hls", + "label": "StreamUP", + "meta": { + "countryCodes": [], + "extractorId": "streamup", + "height": 1080, + "title": "Wake.Up.Dead.Man.A.Knives.Out.Mystery.2025", + }, + "requestHeaders": { + "Origin": "https://strmup.to", + "Referer": "https://strmup.to/", + "User-Agent": "Mozilla/5.0", + }, + "ttl": 10800000, + "url": "https://s3-hls2-cdn59.strmupcdn.com/hls/SJbD5Zm1pj06jZ2LDbHa13YBmryxas/master.m3u8?token=35f702a94c0e324d607d8761795a3788-1768016464-104.28.197.7-5ebad85b212e5e349b388501e051c992a2b68e70fdcaa6cc18270adee1f7c003", + }, +] +`; diff --git a/src/extractor/index.ts b/src/extractor/index.ts index 19536ba..5dbcf4f 100644 --- a/src/extractor/index.ts +++ b/src/extractor/index.ts @@ -15,6 +15,7 @@ import { RgShows } from './RgShows'; import { SaveFiles } from './SaveFiles'; import { StreamEmbed } from './StreamEmbed'; import { Streamtape } from './Streamtape'; +import { StreamUp } from './StreamUp'; import { SuperVideo } from './SuperVideo'; import { Uqload } from './Uqload'; import { Vidora } from './Vidora'; @@ -43,6 +44,7 @@ export const createExtractors = (fetcher: Fetcher): Extractor[] => { new SaveFiles(fetcher), new StreamEmbed(fetcher), new Streamtape(fetcher), + new StreamUp(fetcher), new SuperVideo(fetcher), new Uqload(fetcher), new Vidora(fetcher),