keep only essentials, add height and title parsing and tests

This commit is contained in:
WebStreamr 2026-01-09 19:48:31 +00:00 committed by webstreamr
parent 8b3e2765c6
commit 63b0f34525
7 changed files with 124 additions and 82 deletions

View file

@ -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();
});
});

View file

@ -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<UrlResult[]> {
const referer = `${url.origin}/`;
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
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,
},
},
];

View file

@ -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

View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script id="aclib" type="text/javascript" src="//acscdn.com/script/aclib.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Wake.Up.Dead.Man.A.Knives.Out.Mystery.2025</title>
<script src='https://ssl.p.jwpcdn.com/player/v/8.36.2/jwplayer.js'></script>
<meta name="robots" content="noindex">
<link rel="stylesheet" type="text/css" href="panel/assets/css/jwplayer.css?v=5" />
<link rel="stylesheet" type="text/css" href="panel/assets/css/netflix.css?v=5" />
<script src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/2.1.0/sweetalert.min.js"></script>
<style type="text/css">#video-container,body,html{width:100%;height:100%;overflow:hidden}body,html{font-family:Tahoma;padding:0;margin:0;background-color:#000}#video-container{position:fixed;top:0;left:0;z-index:10}#jwplayer{width:100%!important;height:100%!important}.play-button-outer{position:absolute;z-index:999997;width:6em;height:6em;background-color:gray;cursor:pointer}.play-button{margin:0 auto;top:25%;position:relative;width:0;height:0;border-style:solid;border-width:1.5em 0 1.5em 3em;border-color:transparent transparent transparent #000;opacity:.75}.play-button-outer:hover{background-color:#a9a9a9}.play-button-outer:hover .play-button{opacity:1}.swal-text,.swal-title{font-family:Arial,sans-serif}</style>
</head>
<body>
<div id="video-container">
<div id="jwplayer"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
jwplayer.key="ITWMv7t88JGzI0xPwW8I0+LveiXX9SWbfdmt0ArUSyc=";let player=jwplayer("jwplayer"),globalFilecode=null,positionApplied=!1,totalWatchTime=0;function getFilecodeFromURL(){let e=window.location.pathname.split("/");return e.pop()||e.pop()}function getQueryParam(e){return new URL(window.location.href).searchParams.get(e)}let filecode=getFilecodeFromURL();if(filecode){globalFilecode=filecode;let e=`/ajax/stream?filecode=${encodeURIComponent(filecode)}`,t=new URLSearchParams(window.location.search);t.delete("filecode");let _=t.toString();_&&(e+="&"+_),fetch(e).then(e=>e.json()).then(e=>{window.videoSubtitles=e.subtitles||[],window.defaultSubtitleLang=(e.default_sub_lang||"").toLowerCase(),setupSubtitlesOnly(e)}).catch(e=>console.error("Failed to load stream data:",e))}function setupSubtitlesOnly(e){let t=window.videoSubtitles||[],_=(window.defaultSubtitleLang||"").toLowerCase(),$=t.filter(e=>0===e.type).map(e=>{let t=e.language||"Subtitle",$=t.toLowerCase(),l=_&&($===_||$.includes(_));return{file:e.file_path,label:t,kind:"captions",default:l}});if($.filter(e=>e.default).length>1){let l=$.findIndex(e=>e.default);$=$.map((e,t)=>({...e,default:t===l}))}setupPlayer(e,$)}function setupPlayer(e,t){player.setup({playlist:[{sources:[{file:e.streaming_url}],title:e.title,image:e.thumbnail,tracks:t}],autostart:!1,cast:{},controls:!0,displaytitle:!0,displaydescription:!0,abouttext:"StreamUP",aboutlink:"https://streamup.cc/",skin:{name:"netflix",buttons:"over"},advertising:{client:"googima",schedule:{adBreak1:{offset:"pre",tag:e.vast_ads}}},playbackRateControls:!0,playbackRates:[.5,1,1.5,2]}),player.on("ready",function(){jwplayer().addButton('<svg xmlns="http://www.w3.org/2000/svg" class="jw-svg-icon jw-svg-icon-rewind2" viewBox="0 0 240 240" focusable="false"><path d="m 25.993957,57.778 v 125.3 c 0.03604,2.63589 2.164107,4.76396 4.8,4.8 h 62.7 v -19.3 h -48.2 v -96.4 H 160.99396 v 19.3 c 0,5.3 3.6,7.2 8,4.3 l 41.8,-27.9 c 2.93574,-1.480087 4.13843,-5.04363 2.7,-8 -0.57502,-1.174985 -1.52502,-2.124979 -2.7,-2.7 l -41.8,-27.9 c -4.4,-2.9 -8,-1 -8,4.3 v 19.3 H 30.893957 c -2.689569,0.03972 -4.860275,2.210431 -4.9,4.9 z m 163.422413,73.04577 c -3.72072,-6.30626 -10.38421,-10.29683 -17.7,-10.6 -7.31579,0.30317 -13.97928,4.29374 -17.7,10.6 -8.60009,14.23525 -8.60009,32.06475 0,46.3 3.72072,6.30626 10.38421,10.29683 17.7,10.6 7.31579,-0.30317 13.97928,-4.29374 17.7,-10.6 8.60009,-14.23525 8.60009,-32.06475 0,-46.3 z m -17.7,47.2 c -7.8,0 -14.4,-11 -14.4,-24.1 0,-13.1 6.6,-24.1 14.4,-24.1 7.8,0 14.4,11 14.4,24.1 0,13.1 -6.5,24.1 -14.4,24.1 z m -47.77056,9.72863 v -51 l -4.8,4.8 -6.8,-6.8 13,-12.99999 c 3.02543,-3.03598 8.21053,-0.88605 8.2,3.4 v 62.69999 z"></path></svg>',"Forward 10 sec",function(){jwplayer().seek(jwplayer().getPosition()+10)},"ff11"),jwplayer().addButton('<svg xmlns="http://www.w3.org/2000/svg" class="jw-svg-icon jw-svg-icon-rewind" viewBox="0 0 240 240" focusable="false"><path d="M113.2,131.078a21.589,21.589,0,0,0-17.7-10.6,21.589,21.589,0,0,0-17.7,10.6,44.769,44.769,0,0,0,0,46.3,21.589,21.589,0,0,0,17.7,10.6,21.589,21.589,0,0,0,17.7-10.6,44.769,44.769,0,0,0,0-46.3Zm-17.7,47.2c-7.8,0-14.4-11-14.4-24.1s6.6-24.1,14.4-24.1,14.4,11,14.4,24.1S103.4,178.278,95.5,178.278Zm-43.4,9.7v-51l-4.8,4.8-6.8-6.8,13-13a4.8,4.8,0,0,1,8.2,3.4v62.7l-9.6-.1Zm162-130.2v125.3a4.867,4.867,0,0,1-4.8,4.8H146.6v-19.3h48.2v-96.4H79.1v19.3c0,5.3-3.6,7.2-8,4.3l-41.8-27.9a6.013,6.013,0,0,1-2.7-8,5.887,5.887,0,0,1,2.7-2.7l41.8-27.9c4.4-2.9,8-1,8,4.3v19.3H209.2A4.974,4.974,0,0,1,214.1,57.778Z"></path></svg>',"Rewind 10 sec",function(){var e=jwplayer().getPosition()-10;e<0&&(e=0),jwplayer().seek(e)},"ff00")}),handlePlayerEvents(e.filecode)}function handlePlayerEvents(e){if(!e)return;let t=null;player.on("play",function(){if(!positionApplied){let _=localStorage.getItem(`videoPlaybackPosition_${e}`);_&&(player.seek(parseFloat(_)/1e3),positionApplied=!0)}t||(t=setInterval(()=>{totalWatchTime++},1e3))}),player.on("pause",()=>{clearInterval(t),t=null}),player.on("complete",()=>{clearInterval(t),t=null}),player.on("time",function(t){let _=t.position;localStorage.setItem(`videoPlaybackPosition_${e}`,1e3*_)})}
</script>
<script>
const file_id="2229055",visitorData={file_id:"2229055",gpu:null,cpu:null,screen_res:`${screen.width}x${screen.height}`,iframe_size:`${window.innerWidth}x${window.innerHeight}`,mouse_position:"",click_position:"",page_duration:0,referer:document.referrer||window.location.href,watch_time:0,tab_visibility_changes:0};let startTime=Date.now(),lastActivityTime=Date.now(),inactivityStart=null;function updateActivity(){lastActivityTime=Date.now()}document.addEventListener("mousemove",t=>{visitorData.mouse_position=`${t.pageX}x${t.pageY}`,updateActivity()}),document.addEventListener("click",t=>{visitorData.click_position=`${t.pageX}x${t.pageY}`,updateActivity()}),document.addEventListener("visibilitychange",()=>{document.hidden&&visitorData.tab_visibility_changes++}),setInterval(()=>{let t=Date.now();t-lastActivityTime>=1e4&&!inactivityStart?inactivityStart=lastActivityTime:t-lastActivityTime<1e4&&inactivityStart&&(inactivityStart=null)},1e3);try{let t=document.createElement("canvas"),i=t.getContext("webgl")||t.getContext("experimental-webgl"),a=i.getExtension("WEBGL_debug_renderer_info");visitorData.gpu=a?i.getParameter(a.UNMASKED_RENDERER_WEBGL):"Unknown"}catch{visitorData.gpu="Unavailable"}visitorData.cpu=navigator.hardwareConcurrency?`${navigator.hardwareConcurrency} cores`:"Unknown";let hasSentData=!1;function sendVisitorData(){hasSentData||(hasSentData=!0,visitorData.page_duration=Math.round((Date.now()-startTime)/1e3),visitorData.watch_time=Math.round(totalWatchTime),fetch("/ajax/data",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(visitorData)}).catch(()=>{}))}setInterval(()=>{"undefined"!=typeof totalWatchTime&&!hasSentData&&Math.floor(totalWatchTime)>=87&&sendVisitorData()},500);
</script>
<script src="assets/js/new100.js?v=1"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-MJZHCJR7Q8"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-MJZHCJR7Q8');
</script>
<script>
const l = [{"url":"https://zoovanuzauphoth.net/4/9188322","delay":3000,"lastOpened":0}];
let i = 0;
document.addEventListener("click", function () {
const t = Date.now(), e = l[i % l.length];
if (t - e.lastOpened >= e.delay) {
window.open(e.url, "popupWindow", "width=800,height=600,scrollbars=yes,resizable=yes");
e.lastOpened = t;
}
i++;
});
</script>
<script>
const l=[{"url":"https://obqj2.com/4/9637106","delay":240000,"lastOpened":0},{"url":"https://obqj2.com/4/9637106","delay":240000,"lastOpened":0}];let i=0;document.addEventListener("click",function(){const t=Date.now(),e=l[i%l.length];t-e.lastOpened>=e.delay&&(window.open(e.url,"popupWindow","width=800,height=600,scrollbars=yes,resizable=yes"),e.lastOpened=t),i++});
</script><script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9bb6680ecad95d57',t:'MTc2Nzk4NzYyNw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>

View file

@ -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"}

View file

@ -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",
},
]
`;

View file

@ -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),