From 9ee91dff31636d6bf980614bfd0f375135c735e7 Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:12:25 +0000 Subject: [PATCH] feat(extractor): add back VidSrc This reverts commit f5f47403cc15290c4473e3bdcdc4d131b8c9c4b2. --- src/extractor/VidSrc.test.ts | 20 + src/extractor/VidSrc.ts | 87 ++ ...estra.com-703b00ec9aad1d3b1955000ff0707ce7 | 895 ++++++++++++++++++ ...estra.com-9e55cf8c8fd4f9ec84f3a5c9796f0467 | 145 +++ ...estra.com-c6c000e7831c051c2fa78d6040c2a149 | 145 +++ ...estra.com-e39c615055a3f28e44318426b4d8d304 | 895 ++++++++++++++++++ ...estra.com-593b3064b9b62cf12146532d53b786a4 | 8 + ...estra.com-f0ba2303af3ed374101fa448800e8261 | 6 + .../https:vidsrc-embed.ruembedmoviett0093058 | 274 ++++++ .../https:vidsrc-embed.ruembedtvtt20850594-2 | 274 ++++++ .../__snapshots__/VidSrc.test.ts.snap | 33 + src/extractor/index.ts | 10 + 12 files changed, 2792 insertions(+) create mode 100644 src/extractor/VidSrc.test.ts create mode 100644 src/extractor/VidSrc.ts create mode 100644 src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-703b00ec9aad1d3b1955000ff0707ce7 create mode 100644 src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-9e55cf8c8fd4f9ec84f3a5c9796f0467 create mode 100644 src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-c6c000e7831c051c2fa78d6040c2a149 create mode 100644 src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-e39c615055a3f28e44318426b4d8d304 create mode 100644 src/extractor/__fixtures__/VidSrc/https:tmstr1.cloudnestra.com-593b3064b9b62cf12146532d53b786a4 create mode 100644 src/extractor/__fixtures__/VidSrc/https:tmstr5.cloudnestra.com-f0ba2303af3ed374101fa448800e8261 create mode 100644 src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedmoviett0093058 create mode 100644 src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedtvtt20850594-2 create mode 100644 src/extractor/__snapshots__/VidSrc.test.ts.snap diff --git a/src/extractor/VidSrc.test.ts b/src/extractor/VidSrc.test.ts new file mode 100644 index 0000000..f09bda0 --- /dev/null +++ b/src/extractor/VidSrc.test.ts @@ -0,0 +1,20 @@ +import winston from 'winston'; +import { createTestContext } from '../test'; +import { FetcherMock } from '../utils'; +import { ExtractorRegistry } from './ExtractorRegistry'; +import { VidSrc } from './VidSrc'; + +const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }); +const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['vidsrc-embed.ru'])]); + +const ctx = createTestContext(); + +describe('VidSrc', () => { + test('Full Metal Jacket', async () => { + expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/movie/tt0093058'))).toMatchSnapshot(); + }); + + test('Black Mirror', async () => { + expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/tv/tt2085059/4-2'))).toMatchSnapshot(); + }); +}); diff --git a/src/extractor/VidSrc.ts b/src/extractor/VidSrc.ts new file mode 100644 index 0000000..e0b811d --- /dev/null +++ b/src/extractor/VidSrc.ts @@ -0,0 +1,87 @@ +import * as cheerio from 'cheerio'; +import { BlockedError, TooManyRequestsError } from '../error'; +import { Context, Format, Meta, NonEmptyArray, UrlResult } from '../types'; +import { Fetcher, guessHeightFromPlaylist } from '../utils'; +import { Extractor } from './Extractor'; + +export class VidSrc extends Extractor { + public readonly id = 'vidsrc'; + + public readonly label = 'VidSrc'; + + public override readonly ttl: number = 10800000; // 3h + + private readonly domains: NonEmptyArray; + + public constructor(fetcher: Fetcher, domains: NonEmptyArray) { + super(fetcher); + + this.domains = domains; + } + + public supports(_ctx: Context, url: URL): boolean { + return null !== url.host.match(/vidsrc|vsrc/); + } + + protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise { + // While this is a crappy thing to do, they seem to be blocking overly strict IMO + const randomIp = `${Math.floor(Math.random() * 223) + 1}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`; + const newCtx = { ...ctx, ip: randomIp }; + + return this.extractUsingRandomDomain(newCtx, url, meta, [...this.domains]); + }; + + private async extractUsingRandomDomain(ctx: Context, url: URL, meta: Meta, domains: string[]): Promise { + const domainIndex = Math.floor(Math.random() * domains.length); + const [domain] = domains.splice(domainIndex, 1) as [string]; + + const newUrl = new URL(url); + newUrl.hostname = domain; + + let html: string; + try { + html = await this.fetcher.text(ctx, newUrl, { queueLimit: 1 }); + } catch (error) { + /* istanbul ignore next */ + if (domains.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) { + return this.extractUsingRandomDomain(ctx, url, meta, domains); + } + + /* istanbul ignore next */ + throw error; + } + + const $ = cheerio.load(html.replace('', '')); // server HTML is commented-out + + const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://')); + const title = $('title').text().trim(); + + return Promise.all( + $('.server') + .map((_i, el) => ({ serverName: $(el).text(), dataHash: $(el).data('hash') })) + .toArray() + .filter(({ serverName }) => serverName === 'CloudStream Pro') + .map(async ({ serverName, dataHash }) => { + const rcpUrl = new URL(`/rcp/${dataHash}`, iframeUrl.origin); + const iframeHtml = await this.fetcher.text(ctx, rcpUrl, { headers: { Referer: newUrl.origin } }); + const srcMatch = iframeHtml.match(`src:\\s?'(.*)'`) as string[]; + + const playerHtml = await this.fetcher.text(ctx, new URL(srcMatch[1] as string, iframeUrl.origin), { headers: { Referer: rcpUrl.href } }); + const fileMatch = playerHtml.match(`(https:\\/\\/.*?{v\\d}.*?) or`) as string[]; + const m3u8Url = new URL((fileMatch[1] as string).replace(/{v\d}/, iframeUrl.host)); + + return { + url: m3u8Url, + format: Format.hls, + label: serverName, + ttl: this.ttl, + meta: { + ...meta, + height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: iframeUrl.href } }), + title, + }, + }; + }), + ); + } +} diff --git a/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-703b00ec9aad1d3b1955000ff0707ce7 b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-703b00ec9aad1d3b1955000ff0707ce7 new file mode 100644 index 0000000..928482f --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-703b00ec9aad1d3b1955000ff0707ce7 @@ -0,0 +1,895 @@ + + + +Black Mirror (2011) S04E02 + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-9e55cf8c8fd4f9ec84f3a5c9796f0467 b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-9e55cf8c8fd4f9ec84f3a5c9796f0467 new file mode 100644 index 0000000..5d94b59 --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-9e55cf8c8fd4f9ec84f3a5c9796f0467 @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + +
+
+
+ Full Metal Jacket (1987)
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + diff --git a/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-c6c000e7831c051c2fa78d6040c2a149 b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-c6c000e7831c051c2fa78d6040c2a149 new file mode 100644 index 0000000..836b164 --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-c6c000e7831c051c2fa78d6040c2a149 @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + +
+
+
+ Black Mirror (2011) S04E02
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + diff --git a/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-e39c615055a3f28e44318426b4d8d304 b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-e39c615055a3f28e44318426b4d8d304 new file mode 100644 index 0000000..843346d --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:cloudnestra.com-e39c615055a3f28e44318426b4d8d304 @@ -0,0 +1,895 @@ + + + +Full Metal Jacket (1987) + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extractor/__fixtures__/VidSrc/https:tmstr1.cloudnestra.com-593b3064b9b62cf12146532d53b786a4 b/src/extractor/__fixtures__/VidSrc/https:tmstr1.cloudnestra.com-593b3064b9b62cf12146532d53b786a4 new file mode 100644 index 0000000..a86f8a6 --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:tmstr1.cloudnestra.com-593b3064b9b62cf12146532d53b786a4 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=884887,CODECS="mp4a.40.2,avc1.42c01e",RESOLUTION=640x346,FRAME-RATE=25,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +/pl/H4sIAAAAAAAAAwXB3ZKCIBgA0FcC0X72bmuU1pRW4EPlDsHGkBxr2nXz6fcct.lwl_Sx6XC0M1tntrYnZtejTXzF6Hr9UMjGdb3PWTTcWIqLc8Rb8C0qqf6WkmEZhkI0LVG1OnCKcOlZYikfOc6K1g.RTUdUZrMA0D_KcykgqyT9m.wUDoYOF1vnSquQKOKai9SNgUDNCs8z_nqatfyVaf4QgXGlZtD3MFji0h7CiaFEAsXHi3htoPmMxO3l69TJCu3f_PhigLNayXJhqz4WpH1XUx4LPL_PSC8ahkexukrfs4cjagNeEwGIOO.ybsomATm1mJ3MFGY18gpwWARhQa7D2HmNS1C1HvUCEu3_ASDZlyFBAQAA/e2f2579229bebcfbb0eafde12cbc1bf9/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2911865,CODECS="mp4a.40.2,avc1.42c01f",RESOLUTION=1280x694,FRAME-RATE=25,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +/pl/H4sIAAAAAAAAAwXB3ZKCIBgA0FcC0X72bmuU1pRW4EPlDsHGkBxr2nXz6fcct.lwl_Sx6XC0M1tntrYnZtejTXzF6Hr9UMjGdb3PWTTcWIqLc8Rb8C0qqf6WkmEZhkI0LVG1OnCKcOlZYikfOc6K1g.RTUdUZrMA0D_KcykgqyT9m.wUDoYOF1vnSquQKOKai9SNgUDNCs8z_nqatfyVaf4QgXGlZtD3MFji0h7CiaFEAsXHi3htoPmMxO3l69TJCu3f_PhigLNayXJhqz4WpH1XUx4LPL_PSC8ahkexukrfs4cjagNeEwGIOO.ybsomATm1mJ3MFGY18gpwWARhQa7D2HmNS1C1HvUCEu3_ASDZlyFBAQAA/0e258eeccefba231686dc455bae2c1be/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5301648,CODECS="mp4a.40.2,avc1.42c028",RESOLUTION=1920x1040,FRAME-RATE=25,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +/pl/H4sIAAAAAAAAAwXB3ZKCIBgA0FcC0X72bmuU1pRW4EPlDsHGkBxr2nXz6fcct.lwl_Sx6XC0M1tntrYnZtejTXzF6Hr9UMjGdb3PWTTcWIqLc8Rb8C0qqf6WkmEZhkI0LVG1OnCKcOlZYikfOc6K1g.RTUdUZrMA0D_KcykgqyT9m.wUDoYOF1vnSquQKOKai9SNgUDNCs8z_nqatfyVaf4QgXGlZtD3MFji0h7CiaFEAsXHi3htoPmMxO3l69TJCu3f_PhigLNayXJhqz4WpH1XUx4LPL_PSC8ahkexukrfs4cjagNeEwGIOO.ybsomATm1mJ3MFGY18gpwWARhQa7D2HmNS1C1HvUCEu3_ASDZlyFBAQAA/f4afc016a800ba375016f4bc9771890f/index.m3u8 \ No newline at end of file diff --git a/src/extractor/__fixtures__/VidSrc/https:tmstr5.cloudnestra.com-f0ba2303af3ed374101fa448800e8261 b/src/extractor/__fixtures__/VidSrc/https:tmstr5.cloudnestra.com-f0ba2303af3ed374101fa448800e8261 new file mode 100644 index 0000000..3a1d85a --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:tmstr5.cloudnestra.com-f0ba2303af3ed374101fa448800e8261 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=833322,CODECS="mp4a.40.2,avc1.42c01e",RESOLUTION=640x358,FRAME-RATE=24,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +/pl/H4sIAAAAAAAAAw3O3W6CMBgA0FdqQRR2p2IlXQqjtF9_7qCQYEHGwATH02_nCc7JtTFKgqZBxy4OExyfGhc3iYtdjZyL2g85kA0y.BHX.CAUCaUABBOpK88zINaX4.XLYgtMu6jc26PF53eREt0ALfg0PxRhiwioqqYWq9vwZprhHOWKpVS43S1C92vnqTXBWJVAkPC94Zpt.ZMbuM9G3NfABvOxmXj2ifClS8._.TBeYcA39jyEbZizSlyIkPxmCRFmpKyWUVENs5apWQD3r0JFYTPM4___1flclwqHrZe7nIBytC3u8SIcw1YJ.nCea6f4LghDRuJV.iFw2djXKc1yTCVgWHl6DgHsdylQ8gd2WpeiQQEAAA--/f2a0a2e3ac935bed168e40c6c28105d7/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2728709,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x714,FRAME-RATE=24,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +/pl/H4sIAAAAAAAAAw3O3W6CMBgA0FdqQRR2p2IlXQqjtF9_7qCQYEHGwATH02_nCc7JtTFKgqZBxy4OExyfGhc3iYtdjZyL2g85kA0y.BHX.CAUCaUABBOpK88zINaX4.XLYgtMu6jc26PF53eREt0ALfg0PxRhiwioqqYWq9vwZprhHOWKpVS43S1C92vnqTXBWJVAkPC94Zpt.ZMbuM9G3NfABvOxmXj2ifClS8._.TBeYcA39jyEbZizSlyIkPxmCRFmpKyWUVENs5apWQD3r0JFYTPM4___1flclwqHrZe7nIBytC3u8SIcw1YJ.nCea6f4LghDRuJV.iFw2djXKc1yTCVgWHl6DgHsdylQ8gd2WpeiQQEAAA--/ea18a1816b821e1ba4ee5d4a76b0b7cf/index.m3u8 \ No newline at end of file diff --git a/src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedmoviett0093058 b/src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedmoviett0093058 new file mode 100644 index 0000000..d67fcf8 --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedmoviett0093058 @@ -0,0 +1,274 @@ + + + +Full Metal Jacket (1987) + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedtvtt20850594-2 b/src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedtvtt20850594-2 new file mode 100644 index 0000000..46ca618 --- /dev/null +++ b/src/extractor/__fixtures__/VidSrc/https:vidsrc-embed.ruembedtvtt20850594-2 @@ -0,0 +1,274 @@ + + + +Black Mirror (2011) S04E02 + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/src/extractor/__snapshots__/VidSrc.test.ts.snap b/src/extractor/__snapshots__/VidSrc.test.ts.snap new file mode 100644 index 0000000..4838dad --- /dev/null +++ b/src/extractor/__snapshots__/VidSrc.test.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`VidSrc Black Mirror 1`] = ` +[ + { + "format": "hls", + "label": "CloudStream Pro", + "meta": { + "extractorId": "vidsrc", + "height": 1040, + "title": "Black Mirror (2011) S04E02", + }, + "ttl": 10800000, + "url": "https://tmstr1.cloudnestra.com/pl/H4sIAAAAAAAAAwXB3ZKCIBgA0FcC0X72bmuU1pRW4EPlDsHGkBxr2nXz6fcct.lwl_Sx6XC0M1tntrYnZtejTXzF6Hr9UMjGdb3PWTTcWIqLc8Rb8C0qqf6WkmEZhkI0LVG1OnCKcOlZYikfOc6K1g.RTUdUZrMA0D_KcykgqyT9m.wUDoYOF1vnSquQKOKai9SNgUDNCs8z_nqatfyVaf4QgXGlZtD3MFji0h7CiaFEAsXHi3htoPmMxO3l69TJCu3f_PhigLNayXJhqz4WpH1XUx4LPL_PSC8ahkexukrfs4cjagNeEwGIOO.ybsomATm1mJ3MFGY18gpwWARhQa7D2HmNS1C1HvUCEu3_ASDZlyFBAQAA/master.m3u8", + }, +] +`; + +exports[`VidSrc Full Metal Jacket 1`] = ` +[ + { + "format": "hls", + "label": "CloudStream Pro", + "meta": { + "extractorId": "vidsrc", + "height": 714, + "title": "Full Metal Jacket (1987)", + }, + "ttl": 10800000, + "url": "https://tmstr5.cloudnestra.com/pl/H4sIAAAAAAAAAw3O3W6CMBgA0FdqQRR2p2IlXQqjtF9_7qCQYEHGwATH02_nCc7JtTFKgqZBxy4OExyfGhc3iYtdjZyL2g85kA0y.BHX.CAUCaUABBOpK88zINaX4.XLYgtMu6jc26PF53eREt0ALfg0PxRhiwioqqYWq9vwZprhHOWKpVS43S1C92vnqTXBWJVAkPC94Zpt.ZMbuM9G3NfABvOxmXj2ifClS8._.TBeYcA39jyEbZizSlyIkPxmCRFmpKyWUVENs5apWQD3r0JFYTPM4___1flclwqHrZe7nIBytC3u8SIcw1YJ.nCea6f4LghDRuJV.iFw2djXKc1yTCVgWHl6DgHsdylQ8gd2WpeiQQEAAA--/master.m3u8", + }, +] +`; diff --git a/src/extractor/index.ts b/src/extractor/index.ts index 5dbcf4f..e7b3e6a 100644 --- a/src/extractor/index.ts +++ b/src/extractor/index.ts @@ -19,6 +19,7 @@ import { StreamUp } from './StreamUp'; import { SuperVideo } from './SuperVideo'; import { Uqload } from './Uqload'; import { Vidora } from './Vidora'; +import { VidSrc } from './VidSrc'; import { VixSrc } from './VixSrc'; import { Voe } from './Voe'; import { YouTube } from './YouTube'; @@ -48,6 +49,15 @@ export const createExtractors = (fetcher: Fetcher): Extractor[] => { new SuperVideo(fetcher), new Uqload(fetcher), new Vidora(fetcher), + new VidSrc(fetcher, [ // https://vidsrc.domains/ + 'vidsrcme.ru', + 'vidsrcme.su', + 'vidsrc-me.ru', + 'vidsrc-me.su', + 'vidsrc-embed.ru', + 'vidsrc-embed.su', + 'vsrc.su', + ]), new VixSrc(fetcher), new Voe(fetcher), new YouTube(fetcher),