From 57cd8a3d379c001e88f2ddbca9fa1f5c4bb2ab81 Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:15:32 +0100 Subject: [PATCH] Create TurboVidPlay.ts This extractor in a lot of cases uses fake PNG bytes in every ts segment header but also it uses normal links that are directly playable and no need for mediflow. The code supports both cases where it check the link if contains /data/ it playes directly if not it send it to mediaflow for more decoding. I already pr the extractor code support on mediaflow and the author merge it. --- src/extractor/TurboVidPlay.ts | 121 ++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/extractor/TurboVidPlay.ts diff --git a/src/extractor/TurboVidPlay.ts b/src/extractor/TurboVidPlay.ts new file mode 100644 index 0000000..9344057 --- /dev/null +++ b/src/extractor/TurboVidPlay.ts @@ -0,0 +1,121 @@ +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'; + +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 { + + // ---- Correct CustomRequestInit ---- + const headers: Record = { Referer: url.origin }; + + // ---- 1. Fetch embed HTML ---- + const html = await this.fetcher.text(ctx, url, headers); + if (!html || html.includes('File Not Found') || html.includes('Pending in queue')) { + throw new NotFoundError(); + } + + // ---- 2. Extract media URL ---- + const match = html.match(/(?:urlPlay|data-hash)\s*=\s*['"](?[^"']+)/); + const mediaUrl = match?.groups?.['url']; + if (!mediaUrl) throw new NotFoundError('Video link not found'); + + const masterUrl = + mediaUrl.startsWith('//') ? 'https:' + mediaUrl : + mediaUrl.startsWith('/') ? url.origin + mediaUrl : + mediaUrl; + + // ---- 3. Look for /data/ variants ---- + let directDataStream: string | null = null; + + try { + const playlistText = await this.fetcher.text(ctx, new URL(masterUrl), headers); + + const variantMatches = [...playlistText.matchAll( + /#EXT-X-STREAM-INF[^\n]*\n([^\n]+)/g + )]; + + for (const m of variantMatches) { + const variantUrl = m[1]; + if (!variantUrl) continue; + + if (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; + + break; + } + } + } catch { + } + + // ---- 4. Title ---- + const $ = cheerio.load(html); + const title = $('title').text().trim() || this.label; + + // ---- 5. DIRECT if /data/ found ---- + 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, title }, + }]; + } + + // ---- 6. OTHERWISE → pass EMBED URL to MediaFlow ---- + 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, title }, + }]; + } +}