This commit is contained in:
GSTAR 2025-12-09 15:25:55 +00:00 committed by GitHub
commit 08b7d6337c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,159 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, Meta, UrlResult } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
supportsMediaFlowProxy,
} from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/tuboviplay.py */
export class TurboVidPlay extends Extractor {
public readonly id = 'TurboVidPlay';
public readonly label = 'TurboVidPlay';
public override readonly ttl = 10800000;
public override viaMediaFlowProxy = true;
private domains = [
'turboviplay.com',
'emturbovid.com',
'tuborstb.co',
'javggvideo.xyz',
'stbturbo.xyz',
'turbovidhls.com',
];
public supports(ctx: Context, url: URL): boolean {
return (
this.domains.some(d => url.host.includes(d))
&& supportsMediaFlowProxy(ctx)
);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace(/\/(e|d|embed-)\//, '/t/'));
}
protected async extractInternal(
ctx: Context,
url: URL,
meta: Meta,
): Promise<UrlResult[]> {
const headers: Record<string, string> = {
Referer: url.origin,
};
const html = await this.fetcher.text(ctx, url, headers);
if (
!html
|| html.includes('File Not Found')
|| html.includes('Pending in queue')
) {
throw new NotFoundError();
}
const match = html.match(
/(?:urlPlay|data-hash)\s*=\s*['"](?<url>[^"']+)/,
);
const groups = match?.groups as Record<string, string> | undefined;
const mediaUrl = groups?.['url'];
if (!mediaUrl) {
throw new NotFoundError('Video link not found');
}
const masterUrl
= mediaUrl.startsWith('//')
? `https:${mediaUrl}`
: mediaUrl.startsWith('/')
? url.origin + mediaUrl
: mediaUrl;
let directDataStream: string | null = null;
let height: number | undefined;
try {
const playlistText = await this.fetcher.text(
ctx,
new URL(masterUrl),
headers,
);
const variants = playlistText.matchAll(
/#EXT-X-STREAM-INF:([^\n]+)\n([^\n]+)/g,
);
for (const v of variants) {
const attrs = v[1];
const variantUrl = v[2];
if (!attrs || !variantUrl) continue;
const resMatch = attrs.match(/RESOLUTION=\d+x(\d+)/);
if (resMatch) {
const h = Number(resMatch[1]);
if (!height || h > height) {
height = h;
}
}
if (!directDataStream && variantUrl.includes('/data/')) {
const base = new URL(masterUrl);
directDataStream
= variantUrl.startsWith('http')
? variantUrl
: variantUrl.startsWith('/')
? new URL(variantUrl, base.origin).href
: new URL(variantUrl, base.href).href;
}
}
} catch {
// ignore → fallback to MediaFlow
}
const $ = cheerio.load(html);
const title = $('title').text().trim() || this.label;
if (directDataStream) {
return [
{
url: new URL(directDataStream),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${meta.countryCodes?.join('_') ?? 'all'}`,
ttl: this.ttl,
requestHeaders: headers,
meta: {
...meta,
height,
title,
},
},
];
}
const proxiedUrl = await buildMediaFlowProxyExtractorStreamUrl(
ctx,
this.fetcher,
this.id,
url,
headers,
);
return [
{
url: proxiedUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${meta.countryCodes?.join('_') ?? 'all'}`,
ttl: this.ttl,
requestHeaders: headers,
meta: {
...meta,
height,
title,
},
},
];
}
}