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 1/7] 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 }, + }]; + } +} From 724740383d1ee8ebcb83f6cfbbb4ef849cf8619a Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:24:33 +0100 Subject: [PATCH 2/7] Update TurboVidPlay.ts Lints fix. --- src/extractor/TurboVidPlay.ts | 114 +++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/src/extractor/TurboVidPlay.ts b/src/extractor/TurboVidPlay.ts index 9344057..5ec534b 100644 --- a/src/extractor/TurboVidPlay.ts +++ b/src/extractor/TurboVidPlay.ts @@ -23,44 +23,62 @@ export class TurboVidPlay extends Extractor { ]; public supports(ctx: Context, url: URL): boolean { - return this.domains.some(d => url.host.includes(d)) && - supportsMediaFlowProxy(ctx); + 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 { + protected async extractInternal( + ctx: Context, + url: URL, + meta: Meta, + ): Promise { + const headers: Record = { + Referer: url.origin, + }; - // ---- 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')) { + 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; + 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 playlistText = await this.fetcher.text( + ctx, + new URL(masterUrl), + headers, + ); - const variantMatches = [...playlistText.matchAll( - /#EXT-X-STREAM-INF[^\n]*\n([^\n]+)/g - )]; + const variantMatches = [ + ...playlistText.matchAll(/#EXT-X-STREAM-INF[^\n]*\n([^\n]+)/g), + ]; for (const m of variantMatches) { const variantUrl = m[1]; @@ -69,8 +87,8 @@ export class TurboVidPlay extends Extractor { if (variantUrl.includes('/data/')) { const base = new URL(masterUrl); - directDataStream = - variantUrl.startsWith('http') + directDataStream + = variantUrl.startsWith('http') ? variantUrl : variantUrl.startsWith('/') ? new URL(variantUrl, base.origin).href @@ -80,42 +98,50 @@ export class TurboVidPlay extends Extractor { } } } catch { + // intentionally ignored } - // ---- 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 }, - }]; + 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 + 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 }, - }]; + return [ + { + url: proxiedUrl, + format: Format.hls, + label: this.label, + sourceId: `${this.id}_${meta.countryCodes?.join('_') ?? 'all'}`, + ttl: this.ttl, + requestHeaders: headers, + meta: { + ...meta, + title, + }, + }, + ]; } } From dc91c51e8b6d32167fb953b976beb45c6deaa3fa Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:28:35 +0100 Subject: [PATCH 3/7] Update TurboVidPlay.ts Last fix. --- src/extractor/TurboVidPlay.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/extractor/TurboVidPlay.ts b/src/extractor/TurboVidPlay.ts index 5ec534b..09d0ad4 100644 --- a/src/extractor/TurboVidPlay.ts +++ b/src/extractor/TurboVidPlay.ts @@ -51,10 +51,13 @@ export class TurboVidPlay extends Extractor { throw new NotFoundError(); } - const match = html.match(/(?:urlPlay|data-hash)\s*=\s*['"](?[^"']+)/); + // Extract media URL via named capture group + const match = html.match( + /(?:urlPlay|data-hash)\s*=\s*['"](?[^"']+)/, + ); - const mediaUrl - = match?.groups?.url; + const groups = match?.groups as Record | undefined; + const mediaUrl = groups?.['url']; if (!mediaUrl) { throw new NotFoundError('Video link not found'); @@ -98,7 +101,7 @@ export class TurboVidPlay extends Extractor { } } } catch { - // intentionally ignored + // Ignore playlist parsing errors } const $ = cheerio.load(html); From f65e6dea400646099d1ddb59da0c9e089b82e096 Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Mon, 1 Dec 2025 09:39:50 +0100 Subject: [PATCH 4/7] Update TurboVidPlay.ts Give to Caesar what belongs to Caesar. --- src/extractor/TurboVidPlay.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extractor/TurboVidPlay.ts b/src/extractor/TurboVidPlay.ts index 09d0ad4..c7a5c19 100644 --- a/src/extractor/TurboVidPlay.ts +++ b/src/extractor/TurboVidPlay.ts @@ -7,6 +7,7 @@ import { } 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'; From e6c5201b97b8502539503c02f3f65b1a4cd2e53c Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:19:54 +0100 Subject: [PATCH 5/7] Update TurboVidPlay.ts Add height on both direct and proxied. --- src/extractor/TurboVidPlay.ts | 49 +++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/extractor/TurboVidPlay.ts b/src/extractor/TurboVidPlay.ts index c7a5c19..f52b2ac 100644 --- a/src/extractor/TurboVidPlay.ts +++ b/src/extractor/TurboVidPlay.ts @@ -43,6 +43,7 @@ export class TurboVidPlay extends Extractor { Referer: url.origin, }; + // ---- 1. Fetch embed HTML ---- const html = await this.fetcher.text(ctx, url, headers); if ( !html @@ -52,7 +53,7 @@ export class TurboVidPlay extends Extractor { throw new NotFoundError(); } - // Extract media URL via named capture group + // ---- 2. Extract media URL via named capture group ---- const match = html.match( /(?:urlPlay|data-hash)\s*=\s*['"](?[^"']+)/, ); @@ -64,14 +65,17 @@ export class TurboVidPlay extends Extractor { throw new NotFoundError('Video link not found'); } - const masterUrl - = mediaUrl.startsWith('//') + // ---- 3. Build master playlist URL ---- + const masterUrl = + mediaUrl.startsWith('//') ? `https:${mediaUrl}` : mediaUrl.startsWith('/') ? url.origin + mediaUrl : mediaUrl; + // ---- 4. Parse playlist → height + /data/ direct stream ---- let directDataStream: string | null = null; + let height: number | undefined; try { const playlistText = await this.fetcher.text( @@ -80,34 +84,44 @@ export class TurboVidPlay extends Extractor { headers, ); - const variantMatches = [ - ...playlistText.matchAll(/#EXT-X-STREAM-INF[^\n]*\n([^\n]+)/g), - ]; + const variants = playlistText.matchAll( + /#EXT-X-STREAM-INF:([^\n]+)\n([^\n]+)/g, + ); - for (const m of variantMatches) { - const variantUrl = m[1]; - if (!variantUrl) continue; + for (const v of variants) { + const attrs = v[1]; + const variantUrl = v[2]; + if (!attrs || !variantUrl) continue; - if (variantUrl.includes('/data/')) { + // Always attempt to extract height (max height wins) + const resMatch = attrs.match(/RESOLUTION=\d+x(\d+)/); + if (resMatch) { + const h = Number(resMatch[1]); + if (!height || h > height) { + height = h; + } + } + + // Decide if direct playback possible (first /data/ wins) + if (!directDataStream && variantUrl.includes('/data/')) { const base = new URL(masterUrl); - - directDataStream - = variantUrl.startsWith('http') + directDataStream = + variantUrl.startsWith('http') ? variantUrl : variantUrl.startsWith('/') ? new URL(variantUrl, base.origin).href : new URL(variantUrl, base.href).href; - - break; } } } catch { - // Ignore playlist parsing errors + // Ignore playlist parsing errors → fallback to MediaFlow only } + // ---- 5. Title ---- const $ = cheerio.load(html); const title = $('title').text().trim() || this.label; + // ---- 6. Direct play if /data/ available ---- if (directDataStream) { return [ { @@ -119,12 +133,14 @@ export class TurboVidPlay extends Extractor { requestHeaders: headers, meta: { ...meta, + height, title, }, }, ]; } + // ---- 7. MediaFlow proxy fallback ---- const proxiedUrl = await buildMediaFlowProxyExtractorStreamUrl( ctx, this.fetcher, @@ -143,6 +159,7 @@ export class TurboVidPlay extends Extractor { requestHeaders: headers, meta: { ...meta, + height, title, }, }, From 194079d272f3378766f6477ac399b3e2242e3b7f Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:23:40 +0100 Subject: [PATCH 6/7] Update TurboVidPlay.ts --- src/extractor/TurboVidPlay.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/extractor/TurboVidPlay.ts b/src/extractor/TurboVidPlay.ts index f52b2ac..8d59349 100644 --- a/src/extractor/TurboVidPlay.ts +++ b/src/extractor/TurboVidPlay.ts @@ -43,7 +43,6 @@ export class TurboVidPlay extends Extractor { Referer: url.origin, }; - // ---- 1. Fetch embed HTML ---- const html = await this.fetcher.text(ctx, url, headers); if ( !html @@ -53,27 +52,24 @@ export class TurboVidPlay extends Extractor { throw new NotFoundError(); } - // ---- 2. Extract media URL via named capture group ---- const match = html.match( /(?:urlPlay|data-hash)\s*=\s*['"](?[^"']+)/, ); const groups = match?.groups as Record | undefined; - const mediaUrl = groups?.['url']; + const mediaUrl = groups?.url; if (!mediaUrl) { throw new NotFoundError('Video link not found'); } - // ---- 3. Build master playlist URL ---- - const masterUrl = - mediaUrl.startsWith('//') + const masterUrl + = mediaUrl.startsWith('//') ? `https:${mediaUrl}` : mediaUrl.startsWith('/') ? url.origin + mediaUrl : mediaUrl; - // ---- 4. Parse playlist → height + /data/ direct stream ---- let directDataStream: string | null = null; let height: number | undefined; @@ -93,7 +89,6 @@ export class TurboVidPlay extends Extractor { const variantUrl = v[2]; if (!attrs || !variantUrl) continue; - // Always attempt to extract height (max height wins) const resMatch = attrs.match(/RESOLUTION=\d+x(\d+)/); if (resMatch) { const h = Number(resMatch[1]); @@ -102,11 +97,10 @@ export class TurboVidPlay extends Extractor { } } - // Decide if direct playback possible (first /data/ wins) if (!directDataStream && variantUrl.includes('/data/')) { const base = new URL(masterUrl); - directDataStream = - variantUrl.startsWith('http') + directDataStream + = variantUrl.startsWith('http') ? variantUrl : variantUrl.startsWith('/') ? new URL(variantUrl, base.origin).href @@ -114,14 +108,12 @@ export class TurboVidPlay extends Extractor { } } } catch { - // Ignore playlist parsing errors → fallback to MediaFlow only + // ignore → fallback to MediaFlow } - // ---- 5. Title ---- const $ = cheerio.load(html); const title = $('title').text().trim() || this.label; - // ---- 6. Direct play if /data/ available ---- if (directDataStream) { return [ { @@ -140,7 +132,6 @@ export class TurboVidPlay extends Extractor { ]; } - // ---- 7. MediaFlow proxy fallback ---- const proxiedUrl = await buildMediaFlowProxyExtractorStreamUrl( ctx, this.fetcher, From 904288f9628ace071a13b6e4e3c371c4ebaa4b42 Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:25:52 +0100 Subject: [PATCH 7/7] Update TurboVidPlay.ts --- src/extractor/TurboVidPlay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extractor/TurboVidPlay.ts b/src/extractor/TurboVidPlay.ts index 8d59349..aeceaba 100644 --- a/src/extractor/TurboVidPlay.ts +++ b/src/extractor/TurboVidPlay.ts @@ -57,7 +57,7 @@ export class TurboVidPlay extends Extractor { ); const groups = match?.groups as Record | undefined; - const mediaUrl = groups?.url; + const mediaUrl = groups?.['url']; if (!mediaUrl) { throw new NotFoundError('Video link not found');