refactor: simplify passing around meta

This commit is contained in:
WebStreamr 2025-09-03 19:55:58 +00:00
parent 4002301fa6
commit cfcf81b665
No known key found for this signature in database
97 changed files with 1417 additions and 790 deletions

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { DoodStream } from './DoodStream';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -12,22 +11,22 @@ const ctx = createTestContext();
describe('DoodStream', () => {
test('dood.to', async () => {
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'))).toMatchSnapshot();
});
test('doodster', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dooodster.com/e/1cfcevn6dg8shrfvht22odxw2lty18hr'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dooodster.com/e/1cfcevn6dg8shrfvht22odxw2lty18hr'))).toMatchSnapshot();
});
test('missing pass_md5 -> not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dood.to/e/gy8l8mb2i311'), CountryCode.mx)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dood.to/e/gy8l8mb2i311'))).toMatchSnapshot();
});
test('can guess height from title', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'), CountryCode.fr)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'))).toMatchSnapshot();
});
test('cloudflare storage', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://doodstream.com/e/wfpwtsgyr1xi'), CountryCode.mx)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://doodstream.com/e/wfpwtsgyr1xi'))).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import * as cheerio from 'cheerio';
import randomstring from 'randomstring';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { guessSizeFromMp4 } from '../utils/size';
import { Extractor } from './Extractor';
@ -23,7 +23,7 @@ export class DoodStream extends Extractor {
return new URL(`http://dood.to/e/${videoId}`);
};
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const passMd5Match = html.match(/\/pass_md5\/[\w-]+\/([\w-]+)/);
@ -52,10 +52,10 @@ export class DoodStream extends Extractor {
url: mp4Url,
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
...(bytes && { bytes }),
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { Dropload } from './Dropload';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -12,18 +11,18 @@ const ctx = createTestContext();
describe('Dropload', () => {
test('dropload.io', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'))).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'))).toMatchSnapshot();
});
test('processing / internal problem', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/7xtxuac84xyh.html'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/7xtxuac84xyh.html'))).toMatchSnapshot();
});
test('download URL', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/d/xhcmgcc2txnv'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/d/xhcmgcc2txnv'))).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { extractUrlFromPacked } from '../utils';
import { Extractor } from './Extractor';
@ -18,7 +18,7 @@ export class Dropload extends Extractor {
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/d/', '/').replace('/e/', '/').replace('/embed-', '/'));
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (html.includes('File Not Found') || html.includes('Pending in queue')) {
@ -37,11 +37,11 @@ export class Dropload extends Extractor {
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
...meta,
bytes: bytes.parse(sizeMatch[1] as string) as number,
countryCodes: [countryCode],
height: parseInt(heightMatch[1] as string),
title,
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExternalUrl } from './ExternalUrl';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -12,14 +11,10 @@ const ctx = createTestContext({ includeExternalUrls: 'on' });
describe('ExternalUrl', () => {
test('404 - not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/gjA1OQ4klyHxgJ'), CountryCode.fr)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/gjA1OQ4klyHxgJ'))).toMatchSnapshot();
});
test('netu.fanstream.us', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://netu.fanstream.us/e/0DFgfkcXOsDP'), CountryCode.fr)).toMatchSnapshot();
});
test('johnalwayssame.com with title', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://johnalwayssame.com/e/cqy9oue7sv0g'), CountryCode.fr, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://netu.fanstream.us/e/0DFgfkcXOsDP'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import { BlockedError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { showExternalUrls } from '../utils';
import { Extractor } from './Extractor';
@ -14,7 +14,7 @@ export class ExternalUrl extends Extractor {
return showExternalUrls(ctx.config) && null !== url.host.match(/.*/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
try {
// Make sure the URL is accessible, but avoid causing noise and delays doing this
await this.fetcher.head(ctx, url, { noFlareSolverr: true, timeout: 1000, headers: { Referer: url.origin } });
@ -31,12 +31,9 @@ export class ExternalUrl extends Extractor {
format: Format.unknown,
isExternal: true,
label: `${url.host}`,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
title,
},
meta,
},
];
};

View file

@ -1,5 +1,5 @@
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { Fetcher } from '../utils';
export abstract class Extractor {
@ -23,11 +23,11 @@ export abstract class Extractor {
return url;
};
protected abstract extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]>;
protected abstract extractInternal(ctx: Context, url: URL, meta?: Meta): Promise<UrlResult[]>;
public async extract(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
public async extract(ctx: Context, url: URL, meta?: Meta): Promise<UrlResult[]> {
try {
return await this.extractInternal(ctx, url, countryCode, title);
return await this.extractInternal(ctx, url, meta);
} catch (error) {
if (error instanceof NotFoundError) {
return [];
@ -41,10 +41,7 @@ export abstract class Extractor {
error,
label: this.label,
sourceId: `${this.id}`,
meta: {
countryCodes: [countryCode],
...(title && { title }),
},
...(meta && meta),
},
];
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { createExtractors } from './index';
@ -12,46 +11,41 @@ describe('ExtractorRegistry', () => {
const ctx = createTestContext();
test('returns error result from extractor', async () => {
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'), CountryCode.de);
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'));
expect(urlResult).toMatchSnapshot();
});
test('returns external URLs if enabled by config', async () => {
const urlResult = await extractorRegistry.handle({ ...ctx, config: { ...ctx.config, includeExternalUrls: 'on' } }, new URL('https://mixdrop.ag/e/3nzwveprim63or6'), CountryCode.de);
const urlResult = await extractorRegistry.handle({ ...ctx, config: { ...ctx.config, includeExternalUrls: 'on' } }, new URL('https://mixdrop.ag/e/3nzwveprim63or6'));
expect(urlResult).toMatchSnapshot();
});
test('does not return external URLs by default', async () => {
const urlResult = await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/l7v73zqrfdj19z'), CountryCode.de);
const urlResult = await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/l7v73zqrfdj19z'));
expect(urlResult).toStrictEqual([]);
});
test('returns from memory cache if possible', async () => {
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), CountryCode.de);
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), CountryCode.de);
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'));
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'));
expect(urlResults1).not.toStrictEqual([]);
expect(urlResults2).not.toStrictEqual([]);
});
test('ignores not found errors but caches them', async () => {
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de);
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de);
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'));
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'));
expect(urlResults1).toStrictEqual([]);
expect(urlResults2).toStrictEqual([]);
});
test('returns external url for error', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked.html'), CountryCode.de);
expect(urlResults).toMatchSnapshot();
});
test('returns external url for error with title', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked-2.html'), CountryCode.de, 'a title!');
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked.html'));
expect(urlResults).toMatchSnapshot();
});

View file

@ -2,7 +2,7 @@
import KeyvSqlite from '@keyv/sqlite';
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
import winston from 'winston';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, Meta, UrlResult } from '../types';
import { getCacheDir, isExtractorDisabled } from '../utils';
import { Extractor } from './Extractor';
@ -28,7 +28,7 @@ export class ExtractorRegistry {
};
};
public async handle(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
public async handle(ctx: Context, url: URL, meta?: Meta): Promise<UrlResult[]> {
const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url));
if (!extractor) {
return [];
@ -54,7 +54,7 @@ export class ExtractorRegistry {
this.logger.info(`Extract stream URL using ${extractor.id} extractor from ${url}`, ctx);
const urlResults = await extractor.extract(ctx, normalizedUrl, countryCode, title);
const urlResults = await extractor.extract(ctx, normalizedUrl, meta || { countryCodes: [] });
if (!urlResults.some(urlResult => urlResult.error) && extractor.ttl) {
await this.urlResultCache.set<UrlResult[]>(cacheKey, urlResults, extractor.ttl);

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fastream } from './Fastream';
@ -12,6 +11,6 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test
describe('Fastream', () => {
test('fastream.to embed', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'), CountryCode.es)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import bytes from 'bytes';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
@ -18,7 +18,7 @@ export class Fastream extends Extractor {
return new URL(url.href.replace('/e/', '/embed-').replace('/d/', '/embed-'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url);
const downloadUrl = new URL(url.href.replace('/embed-', '/d/'));
@ -32,10 +32,10 @@ export class Fastream extends Extractor {
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
height: parseInt(heightAndSizeMatch[1] as string),
title: titleMatch[1],

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fsst } from './Fsst';
@ -12,14 +11,14 @@ const ctx = createTestContext();
describe('Fsst', () => {
test('Blood & Sinners', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/900576/'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/900576/'))).toMatchSnapshot();
});
test('Dead City', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/901994/'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/901994/'))).toMatchSnapshot();
});
test('How to Train Your Dragon', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/902668/'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/902668/'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import * as cheerio from 'cheerio';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { Extractor } from './Extractor';
export class Fsst extends Extractor {
@ -11,7 +11,7 @@ export class Fsst extends Extractor {
return null !== url.host.match(/fsst/);
};
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const $ = cheerio.load(html);
@ -19,7 +19,7 @@ export class Fsst extends Extractor {
const filesMatch = html.match(/file:"(.*)"/) as string[];
return (filesMatch[1] as string).split(',').map((fileString, index) => {
return (filesMatch[1] as string).split(',').map((fileString) => {
const heightAndUrlMatch = fileString.match(/\[?([\d]*)p?]?(.*)/) as string[];
const fileHref = heightAndUrlMatch[2] as string;
const heightFromFileHrefMatch = fileHref.match(/([\d]+)p/) as string[];
@ -28,9 +28,9 @@ export class Fsst extends Extractor {
url: new URL(fileHref),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}_${index}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
meta: {
countryCodes: [countryCode],
...meta,
height: parseInt(heightAndUrlMatch[1] || heightFromFileHrefMatch[1] as string),
title,
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { KinoGer } from './KinoGer';
@ -12,10 +11,10 @@ const ctx = createTestContext();
describe('KinoGer', () => {
test('Blood & Sinners', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#ge5fhb'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#ge5fhb'))).toMatchSnapshot();
});
test('Dead City', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import crypto from 'crypto';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
@ -35,7 +35,7 @@ export class KinoGer extends Extractor {
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const hexData = await this.fetcher.text(ctx, url, { headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' } });
const encrypted = Buffer.from(hexData, 'hex');
@ -53,10 +53,10 @@ export class KinoGer extends Extractor {
url: new URL(cf),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: url.origin } }),
title,
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Mixdrop } from './Mixdrop';
@ -12,10 +11,10 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test
describe('Mixdrop', () => {
test('mixdrop.my /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'))).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'))).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
@ -18,7 +18,7 @@ export class Mixdrop extends Extractor {
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/f/', '/e/'));
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const fileUrl = new URL(url.href.replace('/e/', '/f/'));
const html = await this.fetcher.text(ctx, fileUrl);
@ -36,10 +36,10 @@ export class Mixdrop extends Extractor {
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
title,
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SaveFiles } from './SaveFiles';
@ -12,22 +11,22 @@ const ctx = createTestContext();
describe('SafeFiles', () => {
test('savefiles', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'))).toMatchSnapshot();
});
test('savefiles /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'))).toMatchSnapshot();
});
test('savefiles /d/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'))).toMatchSnapshot();
});
test('savefiles locked file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/omqq55i59nvv'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/omqq55i59nvv'))).toMatchSnapshot();
});
test('streamhls /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'))).toMatchSnapshot();
});
});

View file

@ -1,6 +1,6 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { Extractor } from './Extractor';
export class SaveFiles extends Extractor {
@ -18,7 +18,7 @@ export class SaveFiles extends Extractor {
return new URL(url.href.replace('/e/', '/').replace('/d/', '/'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (/File was locked by administrator/.test(html)) {
@ -36,10 +36,10 @@ export class SaveFiles extends Extractor {
url: new URL(fileMatch[1] as string),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
height: parseInt(sizeMatch[1] as string),
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Soaper } from './Soaper';
@ -12,14 +11,14 @@ const ctx = createTestContext();
describe('Soaper', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/movie_d8kdeypDY9.html'), CountryCode.en, 'Full Metal Jacket (1987)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/movie_d8kdeypDY9.html'))).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_5KDq78eGp1.html'), CountryCode.en, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_5KDq78eGp1.html'))).toMatchSnapshot();
});
test('last of us s2e3', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_rYg3vMEDL1.html'), CountryCode.de, 'The Last of Us 2x3')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_rYg3vMEDL1.html'))).toMatchSnapshot();
});
});

View file

@ -1,4 +1,4 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
@ -18,7 +18,7 @@ export class Soaper extends Extractor {
return null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const movieOrEpisodeId = (url.pathname.match(/\/\w+_(\w+)/) as string[])[1] as string;
const form = new URLSearchParams();
@ -48,12 +48,11 @@ export class Soaper extends Extractor {
url: m3u8Url,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
title: `${title}`,
},
},
];

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { StreamEmbed } from './StreamEmbed';
@ -12,6 +11,6 @@ const ctx = createTestContext();
describe('StreamEmbed', () => {
test('watch.gxplayer.xyz', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'))).toMatchSnapshot();
});
});

View file

@ -1,4 +1,4 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { Extractor } from './Extractor';
export class StreamEmbed extends Extractor {
@ -10,7 +10,7 @@ export class StreamEmbed extends Extractor {
return null !== url.host.match(/bullstream|mp4player|watch\.gxplayer/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const video = JSON.parse((html.match(/video ?= ?(.*);/) as string[])[1] as string);
@ -20,10 +20,10 @@ export class StreamEmbed extends Extractor {
url: new URL(`/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}`, url.origin),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
height: parseInt(JSON.parse(video.quality)[0]),
title: decodeURIComponent(video.title),
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Streamtape } from './Streamtape';
@ -12,6 +11,6 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org',
describe('Streamtape', () => {
test('streamtape.com /e', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/84Kb3mkoYAiokmW'), CountryCode.es)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/84Kb3mkoYAiokmW'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import * as cheerio from 'cheerio';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import {
buildMediaFlowProxyExtractorRedirectUrl,
supportsMediaFlowProxy,
@ -16,7 +16,7 @@ export class Streamtape extends Extractor {
return null !== url.host.match(/streamtape/) && supportsMediaFlowProxy(ctx);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const $ = cheerio.load(html);
@ -29,10 +29,10 @@ export class Streamtape extends Extractor {
url: mp4Url,
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
bytes: await guessSizeFromMp4(ctx, this.fetcher, mp4Url),
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SuperVideo } from './SuperVideo';
@ -12,22 +11,22 @@ const ctx = createTestContext();
describe('SuperVideo', () => {
test('supervideo.cc /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/q7i0sw1oytw3'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/q7i0sw1oytw3'))).toMatchSnapshot();
});
test('supervideo.tv /embed-/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.tv/embed-1p0m1fi9mok8.html'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.tv/embed-1p0m1fi9mok8.html'))).toMatchSnapshot();
});
test('embed only', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/bj6szat1pval'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/bj6szat1pval'))).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/ndf5shmy9lpt'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/ndf5shmy9lpt'))).toMatchSnapshot();
});
test('processing video', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/3h1qqoqtldo8'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/3h1qqoqtldo8'))).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
@ -20,11 +20,11 @@ export class SuperVideo extends Extractor {
return new URL(url.href.replace('/e/', '/').replace('/embed-', '/'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (html.includes('This video can be watched as embed only')) {
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), countryCode);
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
}
if (/'The file was deleted|The file expired|Video is processing/.test(html)) {
@ -47,10 +47,10 @@ export class SuperVideo extends Extractor {
url: m3u8Url,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
...(size && { bytes: size }),
...(height && { height }),

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Uqload } from './Uqload';
@ -12,6 +11,6 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org',
describe('Uqload', () => {
test('uqload.net /embed-', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'), CountryCode.fr)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import * as cheerio from 'cheerio';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { guessSizeFromMp4 } from '../utils/size';
import { Extractor } from './Extractor';
@ -19,7 +19,7 @@ export class Uqload extends Extractor {
return new URL(url.href.replace('/embed-', '/'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const heightMatch = html.match(/\d{3,}x(\d{3,})/);
@ -34,10 +34,10 @@ export class Uqload extends Extractor {
url: mp4Url,
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
bytes: await guessSizeFromMp4(ctx, this.fetcher, mp4Url),
...(heightMatch && {

View file

@ -1,7 +1,6 @@
import { MockAgent, setGlobalDispatcher } from 'undici';
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { Fetcher, FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VidSrc } from './VidSrc';
@ -13,15 +12,15 @@ const ctx = createTestContext();
describe('VidSrc', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt0093058'), CountryCode.en, 'Full Metal Jacket (1987)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt0093058'))).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/tv/tt2085059/4/2'), CountryCode.en, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/tv/tt2085059/4/2'))).toMatchSnapshot();
});
test('not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt35628853'), CountryCode.en, 'Titan: The OceanGate Disaster')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt35628853'))).toMatchSnapshot();
});
test('rate limit issues are retried and fail if no tlds are left', async () => {
@ -37,6 +36,6 @@ describe('VidSrc', () => {
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
const vidSrc = new VidSrc(fetcher, ['net', 'xyz']);
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/1'), CountryCode.en, 'Dexter: Resurrection 1x1')).toMatchSnapshot();
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/1'))).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import * as cheerio from 'cheerio';
import slugify from 'slugify';
import { NotFoundError, TooManyRequestsError } from '../error';
import { Context, CountryCode, Format, NonEmptyArray, UrlResult } from '../types';
import { Context, Format, Meta, NonEmptyArray, UrlResult } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
@ -24,11 +24,11 @@ export class VidSrc extends Extractor {
return null !== url.host.match(/vidsrc/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
return this.extractUsingRandomTld(ctx, url, countryCode, [...this.tlds]);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
return this.extractUsingRandomTld(ctx, url, meta, [...this.tlds]);
};
private async extractUsingRandomTld(ctx: Context, url: URL, countryCode: CountryCode, tlds: string[]): Promise<UrlResult[]> {
private async extractUsingRandomTld(ctx: Context, url: URL, meta: Meta, tlds: string[]): Promise<UrlResult[]> {
const tldIndex = Math.floor(Math.random() * tlds.length);
const [tld] = tlds.splice(tldIndex, 1) as [string];
@ -42,7 +42,7 @@ export class VidSrc extends Extractor {
html = await this.fetcher.text(ctx, newUrl);
} catch (error) {
if (error instanceof TooManyRequestsError && tlds.length) {
return this.extractUsingRandomTld(ctx, url, countryCode, tlds);
return this.extractUsingRandomTld(ctx, url, meta, tlds);
}
throw error;
@ -74,10 +74,10 @@ export class VidSrc extends Extractor {
url: m3u8Url,
format: Format.hls,
label: `${this.label} (${serverName})`,
sourceId: `${this.id}_${slugify(serverName)}_${countryCode}`,
sourceId: `${this.id}_${slugify(serverName)}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
title,
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VixSrc } from './VixSrc';
@ -12,16 +11,16 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test
describe('VixSrc', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/600'), CountryCode.it, 'Full Metal Jacket (1987)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/600'))).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'), CountryCode.it, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
});
test('Black Mirror is excluded if no matching language was found', async () => {
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy2.test', mediaFlowProxyPassword: 'asdfg', de: 'on' });
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'), CountryCode.it, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import bytes from 'bytes';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, CountryCode, Format, Meta, UrlResult } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
hasMultiEnabled,
@ -19,7 +19,7 @@ export class VixSrc extends Extractor {
return null !== url.host.match(/vixsrc/) && supportsMediaFlowProxy(ctx);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const filenameMatch = html.match(/"filename":"(.*?)"/);
@ -38,7 +38,7 @@ export class VixSrc extends Extractor {
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes,

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { XPrime } from './XPrime';
@ -12,18 +11,18 @@ const ctx = createTestContext();
describe('XPrime', () => {
test('unknown backend is ignored', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/unknown?name=Superman&year=2025'), CountryCode.en, 'Superman (2025)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/unknown?name=Superman&year=2025'))).toMatchSnapshot();
});
test('primebox Superman', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Superman&year=2025'), CountryCode.en, 'Superman (2025)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Superman&year=2025'))).toMatchSnapshot();
});
test('primebox Alien: Earth', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Alien%3A%20Earth&year=2025&season=1&episode=1'), CountryCode.en, 'Alien: Earth 1x1')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Alien%3A%20Earth&year=2025&season=1&episode=1'))).toMatchSnapshot();
});
test('primebox not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Jurassic%20World%20Rebirth&year=2025'), CountryCode.en, 'Jurassic World Rebirth (2025)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Jurassic%20World%20Rebirth&year=2025'))).toMatchSnapshot();
});
});

View file

@ -1,4 +1,4 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { guessSizeFromMp4 } from '../utils/size';
import { Extractor } from './Extractor';
@ -24,7 +24,7 @@ export class XPrime extends Extractor {
return null !== url.host.match(/xprime/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const urlResults: UrlResult[] = [];
const referer = url.protocol + '//' + url.hostname.split('.').slice(-2).join('.'); // Strip subdomains
@ -42,10 +42,10 @@ export class XPrime extends Extractor {
url,
format: Format.mp4,
label: `${this.label} Primebox`,
sourceId: `${this.id}_${countryCode}_primebox`,
sourceId: `${this.id}_primebox_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
bytes: await guessSizeFromMp4(ctx, this.fetcher, url, { headers: { Referer: referer }, minCacheTtl: this.ttl }),
height: parseInt(resolution),
title,

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { YouTube } from './YouTube';
@ -12,10 +11,10 @@ const ctx = createTestContext();
describe('YouTube', () => {
test('Solaris', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'))).toMatchSnapshot();
});
test('unsupported format', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'))).toMatchSnapshot();
});
});

View file

@ -1,4 +1,4 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, Meta, UrlResult } from '../types';
import { Extractor } from './Extractor';
export class YouTube extends Extractor {
@ -12,7 +12,7 @@ export class YouTube extends Extractor {
return null !== url.host.match(/youtube/) && url.searchParams.has('v');
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const titleMatch = html.match(/"title":{"runs":\[{"text":"(.*?)"/) as string[];
@ -23,10 +23,10 @@ export class YouTube extends Extractor {
format: Format.unknown,
ytId: url.searchParams.get('v') as string,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title: titleMatch[1] as string,
},
},

View file

@ -6,15 +6,13 @@ exports[`DoodStream can guess height from title 1`] = `
"format": "mp4",
"label": "DoodStream",
"meta": {
"countryCodes": [
"fr",
],
"countryCodes": [],
"title": "Black Mirror S04E02 FRENCH 720p WEB x264-RiPiT",
},
"requestHeaders": {
"Referer": "http://dood.to",
},
"sourceId": "doodstream_fr",
"sourceId": "doodstream_",
"ttl": 21600000,
"url": "https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbhjliz6xwv2xr53uq/lco4d3heif~mocked-random-string?token=fh3rdlhjvao7chgxndsi3ra7&expiry=639837296000",
},
@ -27,15 +25,13 @@ exports[`DoodStream cloudflare storage 1`] = `
"format": "mp4",
"label": "DoodStream",
"meta": {
"countryCodes": [
"mx",
],
"countryCodes": [],
"title": "241653--5bf448d3-184a-4b3f-a766-fa98d93c1300--bllz--2100527-doodstream",
},
"requestHeaders": {
"Referer": "http://dood.to",
},
"sourceId": "doodstream_mx",
"sourceId": "doodstream_",
"ttl": 21600000,
"url": "https://wec6bnh7rx7rw11bv9e9vslw4rud8nam.5f1ebd98099ce35faeeddb30c1752191.r2.cloudflarestorage.com/icxxqqdsfdc4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ab085b515b950aae0a86cae59456cacd%2F20250714%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250714T154400Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=845ebfa5feedd840027e431f31e013513dd4c4f74364b4e75a2c8ddb213bd97a",
},
@ -49,15 +45,13 @@ exports[`DoodStream dood.to 1`] = `
"label": "DoodStream",
"meta": {
"bytes": 791702409,
"countryCodes": [
"de",
],
"countryCodes": [],
"title": "des-teufels-bad-2024",
},
"requestHeaders": {
"Referer": "http://dood.to",
},
"sourceId": "doodstream_de",
"sourceId": "doodstream_",
"ttl": 21600000,
"url": "https://aa360cc.cloudatacdn.com/u5kjv4jxytd3sdgge5uogji5dg4huat2pxrm2qibdbm5rlyusvl46iz3u3ta/1f4isxg0tw~mocked-random-string?token=7uebebipnnhusa4xnyea1er4&expiry=639837296000",
},
@ -70,15 +64,13 @@ exports[`DoodStream doodster 1`] = `
"format": "mp4",
"label": "DoodStream",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"title": "Brokeback Mountain 2005",
},
"requestHeaders": {
"Referer": "http://dood.to",
},
"sourceId": "doodstream_de",
"sourceId": "doodstream_",
"ttl": 21600000,
"url": "https://ijs155l.cloudatacdn.com/u5kj2rz5qtdlsdgge4hrgj2flz5cjodea6auatveaphxogr2vvukxfzmwzva/egprvdjvpr~mocked-random-string?token=fbsg3301phkef2s4swo8bssg&expiry=639837296000",
},

View file

@ -7,13 +7,11 @@ exports[`Dropload download URL 1`] = `
"label": "Dropload",
"meta": {
"bytes": 333027737,
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 720,
"title": "[~Squid~Game~EnG~]~S03E01~720p~WebRip ~❤GLD❤~",
},
"sourceId": "dropload_en",
"sourceId": "dropload_",
"ttl": 10800000,
"url": "https://srv29.dropload.io/hls2/01/00305/xhcmgcc2txnv_h/master.m3u8?t=MBKiIoVlA42YcZEixT7ZR1xJA5qKhcKgBi84GPmyT7U&s=1752613129&e=14400&f=1526758&i=0.0&sp=0&ii=130.61.236.204",
},
@ -27,13 +25,11 @@ exports[`Dropload dropload.io 1`] = `
"label": "Dropload",
"meta": {
"bytes": 1395864371,
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 1080,
"title": "des-teufels-bad-2024",
},
"sourceId": "dropload_de",
"sourceId": "dropload_",
"ttl": 10800000,
"url": "https://srv34.dropload.io/hls2/01/00197/lyo2h1snpe5c_h/master.m3u8?t=ESjD9Le53ttKN2JPyxrhLUqTebDZvx3vcQUlNgP6Vko&s=1749845553&e=14400&f=987607&srv=srv27&i=0.0&sp=0&ii=50.99.15.93&p1=srv27&p2=srv27",
},

View file

@ -2,25 +2,6 @@
exports[`ExternalUrl 404 - not found 1`] = `[]`;
exports[`ExternalUrl johnalwayssame.com with title 1`] = `
[
{
"format": "unknown",
"isExternal": true,
"label": "johnalwayssame.com",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"sourceId": "external_fr",
"ttl": 21600000,
"url": "https://johnalwayssame.com/e/cqy9oue7sv0g",
},
]
`;
exports[`ExternalUrl netu.fanstream.us 1`] = `
[
{
@ -28,12 +9,9 @@ exports[`ExternalUrl netu.fanstream.us 1`] = `
"isExternal": true,
"label": "netu.fanstream.us",
"meta": {
"countryCodes": [
"fr",
],
"title": undefined,
"countryCodes": [],
},
"sourceId": "external_fr",
"sourceId": "external_",
"ttl": 21600000,
"url": "https://netu.fanstream.us/e/0DFgfkcXOsDP",
},

View file

@ -9,12 +9,9 @@ exports[`ExtractorRegistry returns external URLs if enabled by config 1`] = `
"isExternal": true,
"label": "mixdrop.ag",
"meta": {
"countryCodes": [
"de",
],
"title": undefined,
"countryCodes": [],
},
"sourceId": "external_de",
"sourceId": "external_",
"ttl": 21600000,
"url": "https://mixdrop.ag/e/3nzwveprim63or6",
},
@ -24,38 +21,14 @@ exports[`ExtractorRegistry returns external URLs if enabled by config 1`] = `
exports[`ExtractorRegistry returns external url for error 1`] = `
[
{
"countryCodes": [],
"error": [Error: Fetcher error: 403: Forbidden
],
"format": "unknown",
"isExternal": true,
"label": "Dropload",
"meta": {
"countryCodes": [
"de",
],
},
"sourceId": "dropload",
"url": "https://dropload.io/mocked-blocked.html",
},
]
`;
exports[`ExtractorRegistry returns external url for error with title 1`] = `
[
{
"error": [Error: Fetcher error: 403: Forbidden
],
"format": "unknown",
"isExternal": true,
"label": "Dropload",
"meta": {
"countryCodes": [
"de",
],
"title": "a title!",
},
"sourceId": "dropload",
"url": "https://dropload.io/mocked-blocked-2.html",
},
]
`;

View file

@ -7,13 +7,11 @@ exports[`Fastream fastream.to embed 1`] = `
"label": "Fastream (via MediaFlow Proxy)",
"meta": {
"bytes": 1073741824,
"countryCodes": [
"es",
],
"countryCodes": [],
"height": 1080,
"title": "black.mirror.s01e01.lat.720p.mp4",
},
"sourceId": "fastream_es",
"sourceId": "fastream_",
"ttl": 900000,
"url": "https://mediaflow-proxy.test/proxy/hls/manifest.m3u8?api_password=asdfg&h_user-agent=Mozilla%2F5.0+%28X11%3B+Linux+x86_64%3B+rv%3A138.0%29+Gecko%2F20100101+Firefox%2F138.0&h_Accept=*%2F*&h_Connection=keep-alive&h_Accept-Language=en-US%2Cen%3Bq%3D0.5&h_referer=https%3A%2F%2Ffastream.to%2F&h_origin=https%3A%2F%2Ffastream.to&d=https%3A%2F%2Fs28.fastream.to%2Fhls2%2F09%2F00024%2F3aooif4ozt10_%2Cl%2Cn%2Ch%2Cx%2C.urlset%2Fmaster.m3u8%3Ft%3Dnw6_aeN6pDROuXsJQwyrJVm3qHKFedVfbTNj5LgXH-4%26s%3D1752863379%26e%3D43200%26v%3D1058373%26i%3D0.3%26sp%3D0",
},

View file

@ -6,39 +6,33 @@ exports[`Fsst Blood & Sinners 1`] = `
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 360,
"title": "Blood.and.Sinners.2025.mkv",
},
"sourceId": "fsst_de_0",
"sourceId": "fsst_",
"url": "https://www.secvideo1.online/get_file/16/a135ab46b1243e41c0282c16d7cdcbcdf0faf7ee79/900000/900576/900576_360p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 720,
"title": "Blood.and.Sinners.2025.mkv",
},
"sourceId": "fsst_de_1",
"sourceId": "fsst_",
"url": "https://www.secvideo1.online/get_file/16/801a42b7ac11f7e08101fe8c3cb537045ddf5c9b19/900000/900576/900576_720p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 1080,
"title": "Blood.and.Sinners.2025.mkv",
},
"sourceId": "fsst_de_2",
"sourceId": "fsst_",
"url": "https://www.secvideo1.online/get_file/16/fae180e05d8d7af50fff44c7a24232b5c58023d7f5/900000/900576/900576.mp4/",
},
]
@ -50,39 +44,33 @@ exports[`Fsst Dead City 1`] = `
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 360,
"title": "the.walking.dead.dead.city.s02e06.german.dl.1080p.web.x264-wvf.mkv",
},
"sourceId": "fsst_de_0",
"sourceId": "fsst_",
"url": "https://www.secvideo1.online/get_file/9/99510c9abde68b5e82b08cd2391724050c8e47cc86/901000/901994/901994_360p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 720,
"title": "the.walking.dead.dead.city.s02e06.german.dl.1080p.web.x264-wvf.mkv",
},
"sourceId": "fsst_de_1",
"sourceId": "fsst_",
"url": "https://www.secvideo1.online/get_file/9/868ae0229e543e7a9a6f7cf4b3df88f45a49aa11fa/901000/901994/901994_720p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 1080,
"title": "the.walking.dead.dead.city.s02e06.german.dl.1080p.web.x264-wvf.mkv",
},
"sourceId": "fsst_de_2",
"sourceId": "fsst_",
"url": "https://www.secvideo1.online/get_file/9/44ff1bf6024db32840054f00029afb1164408b3c1f/901000/901994/901994.mp4/",
},
]
@ -94,13 +82,11 @@ exports[`Fsst How to Train Your Dragon 1`] = `
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 360,
"title": "How To Train Your Dragon 2025.mp4",
},
"sourceId": "fsst_de_0",
"sourceId": "fsst_",
"url": "https://www.secvideo1.online/get_file/10/648fc9b266670e9ad6756a066a68a8db0b36868457/902000/902668/902668_360p.mp4/",
},
]

View file

@ -6,16 +6,14 @@ exports[`KinoGer Blood & Sinners 1`] = `
"format": "hls",
"label": "KinoGer",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 1080,
"title": "Blood.and.Sinners.2025.mkv",
},
"requestHeaders": {
"Referer": "https://kinoger.re",
},
"sourceId": "kinoger_de",
"sourceId": "kinoger_",
"ttl": 21600000,
"url": "https://scvo.processorprediction.shop/v4/bgm/ge5fhb/cf-master.txt?v=1748766126",
},
@ -28,16 +26,14 @@ exports[`KinoGer Dead City 1`] = `
"format": "hls",
"label": "KinoGer",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 1080,
"title": "the.walking.dead.dead.city.s02e05.german.dl.1080p.web.x264-wvf.mkv",
},
"requestHeaders": {
"Referer": "https://kinoger.re",
},
"sourceId": "kinoger_de",
"sourceId": "kinoger_",
"ttl": 21600000,
"url": "https://sipt.responseautomation.cyou/v4/vz1/x6tsx9/cf-master.txt?v=1748957184",
},

View file

@ -9,12 +9,10 @@ exports[`Mixdrop mixdrop.my /e/ 1`] = `
"label": "Mixdrop (via MediaFlow Proxy)",
"meta": {
"bytes": 1059806248,
"countryCodes": [
"de",
],
"countryCodes": [],
"title": "28_giorni_dopo_2002_HD_-_Altadefinizione01.mp4",
},
"sourceId": "mixdrop_de",
"sourceId": "mixdrop_",
"ttl": 900000,
"url": "https://mediaflow-proxy.test/extractor/video?host=Mixdrop&api_password=asdfg&d=https%3A%2F%2Fmixdrop.my%2Fe%2Fknq0kj8waq44l8&redirect_stream=true",
},

View file

@ -6,13 +6,11 @@ exports[`SafeFiles savefiles /d/ 1`] = `
"format": "hls",
"label": "SaveFiles",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 532,
"title": "dexter resurrection s01e01 1080p web h264-successfulcrab",
},
"sourceId": "savefiles_en",
"sourceId": "savefiles_",
"ttl": 21600000,
"url": "https://s3.savefiles.com/hls2/01/00133/,c51qm3p0rhtm_n,.urlset/master.m3u8?t=ptFA7PhoGXdNqH2jfu3EVKT4Fn5laaqfHBW1MMOa74o&s=1755775635&e=43200&v=199331&srv=s1&i=0.0&sp=0",
},
@ -25,13 +23,11 @@ exports[`SafeFiles savefiles /e/ 1`] = `
"format": "hls",
"label": "SaveFiles",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 720,
"title": "dexter resurrection s01e01 1080p web h264-successfulcrab",
},
"sourceId": "savefiles_en",
"sourceId": "savefiles_",
"ttl": 21600000,
"url": "https://s3.savefiles.com/hls2/01/00133/,j4orffalv89e_n,.urlset/master.m3u8?t=uwZgBnJBJ-wfgWvDwmLamHkR-vuqzrBFuLH7QoAH-QE&s=1755775635&e=43200&v=199330&srv=s1&i=0.0&sp=0",
},
@ -44,13 +40,11 @@ exports[`SafeFiles savefiles 1`] = `
"format": "hls",
"label": "SaveFiles",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 720,
"title": "King Of The Hill S14E01 1080p HEVC x265-MeGusta",
},
"sourceId": "savefiles_en",
"sourceId": "savefiles_",
"ttl": 21600000,
"url": "https://s3.savefiles.com/hls2/01/00153/,ip0k0dj2g0i3_n,lang/eng/ip0k0dj2g0i3_eng,.urlset/master.m3u8?t=Emm2ipTW9Db4fBNcQqc8qNju3Yysg0GjGoYiC0FB-G4&s=1755078897&e=43200&v=406428&srv=s1&i=0.0&sp=0&fr=ip0k0dj2g0i3",
},
@ -65,13 +59,11 @@ exports[`SafeFiles streamhls /e/ 1`] = `
"format": "hls",
"label": "SaveFiles",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 720,
"title": "King Of The Hill S14E01 1080p HEVC x265-MeGusta",
},
"sourceId": "savefiles_en",
"sourceId": "savefiles_",
"ttl": 21600000,
"url": "https://s3.savefiles.com/hls2/01/00153/,ip0k0dj2g0i3_n,lang/eng/ip0k0dj2g0i3_eng,.urlset/master.m3u8?t=Emm2ipTW9Db4fBNcQqc8qNju3Yysg0GjGoYiC0FB-G4&s=1755078897&e=43200&v=406428&srv=s1&i=0.0&sp=0&fr=ip0k0dj2g0i3",
},

View file

@ -6,13 +6,10 @@ exports[`Soaper Black Mirror 1`] = `
"format": "hls",
"label": "Soaper",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 720,
"title": "Black Mirror 4x2",
},
"sourceId": "soaper_en",
"sourceId": "soaper_",
"ttl": 43200000,
"url": "https://soaper.live/home/index/TVM3U8?key=RnX8mnL4l5hEz38dxBAbFjV27jbe3Yh13LqxV48ViM7j60OLjmh3w5PJ8nQPh2brz0eEAAt50Zvv7d89hpM3OLWQ6nsjb4n0l3ApSna13LB3EbUxbbyJ70KwuVjaooMP5mIE4MbjyvqJHjRq3amYA5SPV8NRl8wmivl8YyrlMmFx6VomOAp2C5WR46w10v.m3u8",
},
@ -25,13 +22,10 @@ exports[`Soaper Full Metal Jacket 1`] = `
"format": "hls",
"label": "Soaper",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 1080,
"title": "Full Metal Jacket (1987)",
},
"sourceId": "soaper_en",
"sourceId": "soaper_",
"ttl": 43200000,
"url": "https://soaper.live/home/index/M3U8?key=RnX8BOr9NRiEz3W5ownqUjVQ3LQew9S13L8VbnEQCM72wz1AEyIrld1z4NXwF2ByZneoX7i5Ez9BlPpxUpMeqJ2qdOhjb4PlArnASnlEpp2b8Whx1lwpx4oRIVzEKX0Z6rcEQWArMlxvTjRq3aMEnQUP3mvva8RZfvldjM9mz5FxZZv6QKdzcVxqX8q.m3u8",
},
@ -44,13 +38,10 @@ exports[`Soaper last of us s2e3 1`] = `
"format": "hls",
"label": "Soaper",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": undefined,
"title": "The Last of Us 2x3",
},
"sourceId": "soaper_de",
"sourceId": "soaper_",
"ttl": 43200000,
"url": "https://soaper.live/home/index/TVM3U8?key=LrezaYpLOxT4O61m8e3mSLdBzLNyRaf8zPmERxvwh1Mrj6zbVEUvAzRv2KZLu5PVvRKqwbcz6qmb9vbOi3wQZ4ae8RsZ9N3Jm9Yatw4yE55bBp.m3u8",
},

View file

@ -6,13 +6,11 @@ exports[`StreamEmbed watch.gxplayer.xyz 1`] = `
"format": "hls",
"label": "StreamEmbed",
"meta": {
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 720,
"title": "Baymax - Riesiges Robowabohu.mp4",
},
"sourceId": "streamembed_de",
"sourceId": "streamembed_",
"ttl": 900000,
"url": "https://watch.gxplayer.xyz/m3u8/3/30b4a92517ace5825f5944c8a794ad3e/master.txt?s=1&id=11782&cache=1",
},

View file

@ -7,12 +7,10 @@ exports[`Streamtape streamtape.com /e 1`] = `
"label": "Streamtape (via MediaFlow Proxy)",
"meta": {
"bytes": 1187340184,
"countryCodes": [
"es",
],
"countryCodes": [],
"title": "mickey-17-2025-[latino].mp4",
},
"sourceId": "streamtape_es",
"sourceId": "streamtape_",
"ttl": 900000,
"url": "https://mediaflow.test.org/extractor/video?host=Streamtape&api_password=test&d=https%3A%2F%2Fstreamtape.com%2Fe%2F84Kb3mkoYAiokmW&redirect_stream=true",
},

View file

@ -8,13 +8,11 @@ exports[`SuperVideo embed only 1`] = `
"format": "hls",
"label": "SuperVideo",
"meta": {
"countryCodes": [
"it",
],
"countryCodes": [],
"height": 1080,
"title": "",
},
"sourceId": "supervideo_it",
"sourceId": "supervideo_",
"ttl": 10800000,
"url": "https://hfs308.serversicuro.cc/hls/,dnzpe7rf5xg4a3gyva3h5orlyw23xosfzwunl5xbmjpz2l6t3doh5zbwynna,.urlset/master.m3u8",
},
@ -30,13 +28,11 @@ exports[`SuperVideo supervideo.cc /e/ 1`] = `
"label": "SuperVideo",
"meta": {
"bytes": 1073741824,
"countryCodes": [
"de",
],
"countryCodes": [],
"height": 720,
"title": "des-teufels-bad-2024",
},
"sourceId": "supervideo_de",
"sourceId": "supervideo_",
"ttl": 10800000,
"url": "https://hfs309.serversicuro.cc/hls/,dnzpfi3d27g4a3gyvbmh5klwtl65qh654hyjtgupd6p577fze64cp3rsmdyq,.urlset/master.m3u8",
},
@ -50,13 +46,11 @@ exports[`SuperVideo supervideo.tv /embed-/ 1`] = `
"label": "SuperVideo",
"meta": {
"bytes": 219571814,
"countryCodes": [
"it",
],
"countryCodes": [],
"height": 344,
"title": "Babylon 5 2x3",
},
"sourceId": "supervideo_it",
"sourceId": "supervideo_",
"ttl": 10800000,
"url": "https://hfs307.serversicuro.cc/hls/,dnzpaalv5xg4a3gyvavh52jryoft7o5cd5nm7cwtnu74kmc4ohiklh5qwq4q,.urlset/master.m3u8",
},

View file

@ -7,13 +7,11 @@ exports[`Uqload uqload.net /embed- 1`] = `
"label": "Uqload (via MediaFlow Proxy)",
"meta": {
"bytes": 431690639,
"countryCodes": [
"fr",
],
"countryCodes": [],
"height": 360,
"title": "Black Mirror S07E06 MULTI VFF 1080p WEBrip EAC3 5 1 x265-TyHD",
},
"sourceId": "uqload_fr",
"sourceId": "uqload_",
"ttl": 900000,
"url": "https://mediaflow.test.org/extractor/video?host=Uqload&api_password=test&d=https%3A%2F%2Fuqload.net%2Fz0xbr87oz637.html&redirect_stream=true",
},

View file

@ -6,13 +6,11 @@ exports[`VidSrc Black Mirror 1`] = `
"format": "hls",
"label": "VidSrc (CloudStream Pro)",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 1040,
"title": "Black Mirror (2011) S04E02",
},
"sourceId": "vidsrc_CloudStream-Pro_en",
"sourceId": "vidsrc_CloudStream-Pro_",
"ttl": 10800000,
"url": "https://tmstr5.shadowlandschronicles.com/pl/H4sIAAAAAAAAAxXK226DIAAA0F8CrGvd27oCq1UcyP1NQdtUaozptsyvX3aez9DBQxgjgBmAPeqK_Yh2eTeG4tAPMCDwysHNaeDbevKVMM.XgMWps2XtTWGkLR8M.feQ2KroeeXG_db4vCq82.T8lkerZ0OTlLLsWuA2lnwT1FK7e6r0B5nEHL_4pNAgme2sJj28_kT0f0kuya0Sj6W6wONRnkrpAHP9vZwEzRE3T8JOohmwtwMlTimNeiw2BQWRsM4CKZ3PFuUyT1usLy6LWb3F1s3p0hPRSpqqxibcPpaG3.Mq5qS4vn4bqm.NTp8KFzOfmBykWP4AirgCPSEBAAA-/master.m3u8",
},
@ -25,13 +23,11 @@ exports[`VidSrc Full Metal Jacket 1`] = `
"format": "hls",
"label": "VidSrc (CloudStream Pro)",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 714,
"title": "Full Metal Jacket (1987)",
},
"sourceId": "vidsrc_CloudStream-Pro_en",
"sourceId": "vidsrc_CloudStream-Pro_",
"ttl": 10800000,
"url": "https://tmstr4.shadowlandschronicles.com/pl/H4sIAAAAAAAAAwXB7XqCIBgA0FtCsa_97IMcFSa.gPBPwc1H0Fm5Wl79zllVS4eRdZv10uJkuarrJF7Yr8at1lWyRvZDC7cFP3IT2YU4PHAmiGKB59C1Z41bbUQ7czDQ.AgpMv6csHtqNTFGzMMGessV_bOSvRkadab4riBhWx0nxuPxDtFnXMSjqAOdM5AtK3lyjjxiyD.LYAaRbqvC8_MF_L0Bsq8GeuGHxUlLU9rSZapvkSTyUfsNkcEJi8eb9jRpRPJkMTO6H4mV33cnaCqP_LeIpw46iVlHrxDp2c6G5ILtbeoC9I42itxy_3plqev50N5FLDHfTS94T1AQKXUZrmwOA1dtVIFFOaDNP7JZ1PhBAQAA/master.m3u8",
},
@ -47,12 +43,6 @@ exports[`VidSrc rate limit issues are retried and fail if no tlds are left 1`] =
"format": "unknown",
"isExternal": true,
"label": "VidSrc",
"meta": {
"countryCodes": [
"en",
],
"title": "Dexter: Resurrection 1x1",
},
"sourceId": "vidsrc",
"url": "https://vidsrc.xyz/embed/movie/tt33043892/1/1",
},

View file

@ -14,7 +14,7 @@ exports[`VixSrc Black Mirror 1`] = `
"height": 1080,
"title": "Black.Mirror.S04E02.Arkangel.1080p.NF.WEB-DL.ITA.ENG.DDP5.1.H.264.mkv",
},
"sourceId": "vixsrc_it",
"sourceId": "vixsrc_",
"ttl": 900000,
"url": "https://mediaflow-proxy.test/proxy/hls/manifest.m3u8?api_password=asdfg&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fvixsrc.to%2Ftv%2F42009%2F4%2F2&d=https%3A%2F%2Fvixsrc.to%2Fplaylist%2F307316%3Ftoken%3D2f5004a3b15f4ccdb3446f2b132a0957%26expires%3D1755949828%26h%3D1",
},
@ -37,7 +37,7 @@ exports[`VixSrc Full Metal Jacket 1`] = `
"height": 1080,
"title": "Full.metal.jacket.1987.1080p.x264.AC3.ITA.AAC.ENG.Subs.mkv",
},
"sourceId": "vixsrc_it",
"sourceId": "vixsrc_",
"ttl": 900000,
"url": "https://mediaflow-proxy.test/proxy/hls/manifest.m3u8?api_password=asdfg&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fvixsrc.to%2Fmovie%2F600&d=https%3A%2F%2Fvixsrc.to%2Fplaylist%2F219736%3Ftoken%3D689cf5587c2fec67fd83503a2a7dfd64%26expires%3D1755949827%26h%3D1",
},

View file

@ -7,16 +7,14 @@ exports[`XPrime primebox Alien: Earth 1`] = `
"label": "XPrime Primebox",
"meta": {
"bytes": 871281904,
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 1080,
"title": "Alien: Earth 1x1 (2025)",
},
"requestHeaders": {
"Referer": "https://xprime.tv",
},
"sourceId": "xprime_en_primebox",
"sourceId": "xprime_primebox_",
"ttl": 21600000,
"url": "https://oca.flutch09.workers.dev/?v=cenBDs9Hbnv6PuXYzNUtVR2ZZ2YtmVtQIt6JqHB1dsbz2Y%2BXC9UwPurnE%2BQNk27Yr1WeYeLOOazAnSL8cU0DvVfg4pwKsjHKWIJICiVlvv9ndXII0bR7eaIi%2B11NbAj72pFFryHZjO6MkMKEJJnF4G%2BH1BXIO1ALds7J1A7gj2vc7DbUWL8kPxwH1rMw7NAys20M5s3PsbhgSrMpguZSBm8X&headers=%7B%22origin%22%3A%22https%3A%2F%2Fmoviebox.ng%22%2C%22referer%22%3A%22https%3A%2F%2Fmoviebox.ng%22%7D&safe",
},
@ -25,16 +23,14 @@ exports[`XPrime primebox Alien: Earth 1`] = `
"label": "XPrime Primebox",
"meta": {
"bytes": 420170402,
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 480,
"title": "Alien: Earth 1x1 (2025)",
},
"requestHeaders": {
"Referer": "https://xprime.tv",
},
"sourceId": "xprime_en_primebox",
"sourceId": "xprime_primebox_",
"ttl": 21600000,
"url": "https://oca.flutch09.workers.dev/?v=oS2n5QbXtpa%2BWXMpIIi0SqeqadEyYEAg6AmHvpTa3jzooh6zxINciUJrR%2B6b1xLZtxbnbAM3mrGGXOa5yckYkNSC%2BKGEFSdofXw298vq3ssHpI9LBN6XLvhtxnDfmub0QoMG%2Fkvgjhb8T%2FwiCHo9NeSX%2FetW6ANppW9jhZn0Hv3hsWdmES1jqj6nfIjQ1ur53htjr2vF7mZbZSsJ&headers=%7B%22origin%22%3A%22https%3A%2F%2Fmoviebox.ng%22%2C%22referer%22%3A%22https%3A%2F%2Fmoviebox.ng%22%7D&safe",
},
@ -48,16 +44,14 @@ exports[`XPrime primebox Superman 1`] = `
"label": "XPrime Primebox",
"meta": {
"bytes": 401394599,
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 480,
"title": "Superman",
},
"requestHeaders": {
"Referer": "https://xprime.tv",
},
"sourceId": "xprime_en_primebox",
"sourceId": "xprime_primebox_",
"ttl": 21600000,
"url": "https://oca.flutch09.workers.dev/?v=3RVZDNxcKNs6%2BqcdL1bDCgq9Yp1iPYqb6a9yToLq66vlzu08R1Hz7jiKx4OBH6YzJBcK7skuiDGDwIpro78ReVjMLtMHCfa0fBo0aJ9RYULOUMkLrZxjzSTXvo5Jss1Jiss8e9Z8pFdlC1VqwAUm6G9mx0u41dFLJ0GTJrwCesPe4zflbL8I76WkWYKBMT2MwHvDTIO4r3V0nf8BsRswsNJZ&headers=%7B%22origin%22%3A%22https%3A%2F%2Fmoviebox.ng%22%2C%22referer%22%3A%22https%3A%2F%2Fmoviebox.ng%22%7D&safe",
},
@ -66,16 +60,14 @@ exports[`XPrime primebox Superman 1`] = `
"label": "XPrime Primebox",
"meta": {
"bytes": 896638128,
"countryCodes": [
"en",
],
"countryCodes": [],
"height": 720,
"title": "Superman",
},
"requestHeaders": {
"Referer": "https://xprime.tv",
},
"sourceId": "xprime_en_primebox",
"sourceId": "xprime_primebox_",
"ttl": 21600000,
"url": "https://oca.flutch09.workers.dev/?v=gUbhHOwpFIqajH6NnoH%2BYE9AWORUUogamkdYQedBfKi5eQtyniWtxmI1%2B5Swyl1ThC6vM0qHl53s0Q7j%2BDaWFhVRJQL%2BJRxbuOL1iCz1w5xVPLDU%2FTJ3zhf9EbULPGbbee14kdNYNc5%2FlF2sNe6RiUupsvhlj%2BjcgsT8uypX%2BudVyw1jVEUVogufPnc0lSrfoE98IZlOgdjBZKE3%2Fcw3erD2&headers=%7B%22origin%22%3A%22https%3A%2F%2Fmoviebox.ng%22%2C%22referer%22%3A%22https%3A%2F%2Fmoviebox.ng%22%7D&safe",
},

View file

@ -6,12 +6,10 @@ exports[`YouTube Solaris 1`] = `
"format": "unknown",
"label": "YouTube",
"meta": {
"countryCodes": [
"en",
],
"countryCodes": [],
"title": "Solaris | SCIENCE FICTION | FULL MOVIE | directed by Tarkovsky",
},
"sourceId": "youtube_en",
"sourceId": "youtube_",
"ttl": 21600000,
"url": "https://www.youtube.com/watch?v=Z8ZhQPaw4rE",
"ytId": "Z8ZhQPaw4rE",

View file

@ -35,9 +35,9 @@ export class CineHDPlus extends Source {
const $ = cheerio.load(html);
const countryCode: CountryCode = ($('.details__langs').html() as string).includes('Latino') ? CountryCode.mx : CountryCode.es;
const countryCodes = [($('.details__langs').html() as string).includes('Latino') ? CountryCode.mx : CountryCode.es];
const title = $('meta[property="og:title"]').attr('content') as string;
const title = `${($('meta[property="og:title"]').attr('content') as string).trim()} ${imdbId.season}x${imdbId.episode}`;
return Promise.all(
$(`[data-num="${imdbId.season}x${imdbId.episode}"]`)
@ -46,7 +46,7 @@ export class CineHDPlus extends Source {
.map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')))
.toArray()
.filter(url => !url.host.match(/cinehdplus/))
.map(url => ({ countryCode, referer: seriesPageUrl, title: `${title.trim()} ${imdbId.season}x${imdbId.episode}`, url })),
.map(url => ({ url, meta: { countryCodes, title } })),
);
};

View file

@ -60,27 +60,27 @@ export class Cuevana extends Source {
if (elText.includes('Latino')) {
return $('[data-tr], [data-video]', el)
.map((_i, el) => ({ countryCode: CountryCode.mx, title, url: new URL($(el).attr('data-tr') ?? $(el).attr('data-video') as string) }))
.map((_i, el) => ({ url: new URL($(el).attr('data-tr') ?? $(el).attr('data-video') as string), meta: { countryCodes: [CountryCode.mx], title } }))
.toArray();
}
return $('[data-tr], [data-video]', el)
.map((_i, el) => ({ countryCode: CountryCode.es, title, url: new URL($(el).attr('data-tr') ?? $(el).attr('data-video') as string) }))
.map((_i, el) => ({ url: new URL($(el).attr('data-tr') ?? $(el).attr('data-video') as string), meta: { countryCodes: [CountryCode.es], title } }))
.toArray();
})
.toArray();
return Promise.all(
urlResults.map(async (urlResult) => {
if (!urlResult.url.host.includes('cuevana3')) {
return urlResult;
urlResults.map(async ({ url, meta }) => {
if (!url.host.includes('cuevana3')) {
return { url, meta };
}
const html = await this.fetcher.text(ctx, urlResult.url, { headers: { Referer: pageUrl.origin } });
const html = await this.fetcher.text(ctx, url, { headers: { Referer: pageUrl.origin } });
const urlMatcher = html.match(/url ?= ?'(.*)'/) as string[];
return { ...urlResult, url: new URL(urlMatcher[1] as string) };
return { url: new URL(urlMatcher[1] as string), meta };
}),
);
};

View file

@ -30,8 +30,8 @@ export class Einschalten extends Source {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const { releaseName, streamUrl } = JSON.parse(await this.fetcher.text(ctx, new URL(`/api/movies/${tmdbId.id}/watch`, this.baseUrl))) as EinschaltenResponse;
const { releaseName: title, streamUrl } = JSON.parse(await this.fetcher.text(ctx, new URL(`/api/movies/${tmdbId.id}/watch`, this.baseUrl))) as EinschaltenResponse;
return [{ countryCode: CountryCode.de, title: releaseName, url: new URL(streamUrl) }];
return [{ url: new URL(streamUrl), meta: { countryCodes: [CountryCode.de], title } }];
};
}

View file

@ -45,7 +45,7 @@ export class Eurostreaming extends Source {
.map((_i, el) => new URL($(el).attr('data-link') as string))
.toArray()
.filter(url => !url.host.match(/eurostreaming/))
.map(url => ({ countryCode: CountryCode.it, title, url })),
.map(url => ({ url, meta: { countryCodes: [CountryCode.it], title } })),
);
};

View file

@ -46,6 +46,6 @@ export class Frembed extends Source {
? `${json['title']} ${tmdbId.season}x${tmdbId.episode}`
: json['title'];
return urls.map(url => ({ countryCode: CountryCode.fr, title, url }));
return urls.map(url => ({ url, meta: { countryCodes: [CountryCode.fr], title } }));
};
}

View file

@ -36,7 +36,7 @@ export class FrenchCloud extends Source {
.map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')))
.toArray()
.filter(url => !url.host.match(/frenchcloud/))
.map(url => ({ countryCode: CountryCode.fr, referer: pageUrl, url })),
.map(url => ({ url, meta: { countryCodes: [CountryCode.fr] } })),
);
};
}

View file

@ -51,16 +51,16 @@ export class HomeCine extends Source {
return $('.les-content a')
.map((_i, el) => {
let countryCode: CountryCode;
let countryCodes: CountryCode[];
if ($(el).text().toLowerCase().includes('latino')) {
countryCode = CountryCode.mx;
countryCodes = [CountryCode.mx];
} else if ($(el).text().toLowerCase().includes('castellano')) {
countryCode = CountryCode.es;
countryCodes = [CountryCode.es];
} else {
return [];
}
return { countryCode, title, url: new URL($('iframe', $(el).attr('href')).attr('src') as string) };
return { url: new URL($('iframe', $(el).attr('href')).attr('src') as string), meta: { countryCodes, title } };
}).toArray();
};

View file

@ -42,7 +42,7 @@ export class KinoGer extends Source {
return Array.from(html.matchAll(/\.show\(.*/g))
.map(showJsMatch => this.findEpisodeUrlInShowJs(showJsMatch[0], seasonIndex, episodeIndex))
.filter((url): url is URL => url !== undefined && !['kinoger.be', 'kinoger.ru'].includes(url.host))
.map(url => ({ countryCode: CountryCode.de, title, url }));
.map(url => ({ url, meta: { countryCodes: [CountryCode.de], title } }));
};
private readonly findEpisodeUrlInShowJs = (showJs: string, seasonIndex: number, episodeIndex: number): URL | undefined => {

View file

@ -41,7 +41,7 @@ export class MegaKino extends Source {
$(`.video-inside iframe`)
.map((_i, el) => new URL(($(el).attr('data-src') ?? $(el).attr('src')) as string))
.toArray()
.map(url => ({ countryCode: CountryCode.de, referer: pageUrl, title, url })),
.map(url => ({ url, meta: { countryCodes: [CountryCode.de], title } })),
);
};

View file

@ -36,7 +36,7 @@ export class MeineCloud extends Source {
.map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')))
.toArray()
.filter(url => !url.host.match(/meinecloud/))
.map(url => ({ countryCode: CountryCode.de, referer: pageUrl, url })),
.map(url => ({ url, meta: { countryCodes: [CountryCode.de] } })),
);
};
}

View file

@ -36,7 +36,7 @@ export class MostraGuarda extends Source {
.map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')))
.toArray()
.filter(url => !url.host.match(/mostraguarda/))
.map(url => ({ countryCode: CountryCode.it, referer: pageUrl, url })),
.map(url => ({ url, meta: { countryCodes: [CountryCode.it] } })),
);
};
}

View file

@ -46,6 +46,6 @@ export class Movix extends Source {
? `${json['tmdb_details']['title']} ${tmdbId.season}x${tmdbId.episode}`
: json['tmdb_details']['title'];
return urls.map(url => ({ countryCode: CountryCode.fr, title, url }));
return urls.map(url => ({ url, meta: { countryCodes: [CountryCode.fr], title } }));
};
}

View file

@ -85,7 +85,7 @@ export class PrimeWire extends Source {
await this.redirectUrlCache.set<string>(redirectUrl.href, targetUrlHref);
}
return { countryCode: CountryCode.en, url: new URL(targetUrlHref) };
return { url: new URL(targetUrlHref), meta: { countryCodes: [CountryCode.en] } };
}),
);
};

View file

@ -47,12 +47,12 @@ export class Soaper extends Source {
const episodeUrl = new URL(episodePageHref, this.baseUrl);
const title = `${name} ${tmdbId.season}x${tmdbId.episode}`;
return [{ countryCode: CountryCode.en, title, url: episodeUrl }];
return [{ url: episodeUrl, meta: { countryCodes: [CountryCode.en], title } }];
}
const title = `${name} (${year})`;
return [{ countryCode: CountryCode.en, title, url: pageUrl }];
return [{ url: pageUrl, meta: { countryCodes: [CountryCode.en], title } }];
};
private readonly fetchPageUrl = async (ctx: Context, keyword: string, year: number, hrefPrefix: string): Promise<URL | undefined> => {

View file

@ -3,13 +3,12 @@ import KeyvSqlite from '@keyv/sqlite';
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
import { ContentType } from 'stremio-addon-sdk';
import { NotFoundError } from '../error';
import { Context, CountryCode } from '../types';
import { Context, CountryCode, Meta } from '../types';
import { getCacheDir, Id } from '../utils';
export interface SourceResult {
countryCode: CountryCode;
title?: string;
url: URL;
meta: Meta;
}
export abstract class Source {
@ -27,7 +26,7 @@ export abstract class Source {
private static readonly sourceResultCache = new Cacheable({
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
secondary: new Keyv(new KeyvSqlite(`sqlite://${getCacheDir()}/webstreamr-source-cache.sqlite`)),
secondary: new Keyv(new KeyvSqlite(`sqlite://${getCacheDir()}/webstreamr-source-cache-v2.sqlite`)),
stats: true,
});
@ -59,6 +58,6 @@ export abstract class Source {
await Source.sourceResultCache.set<SourceResult[]>(cacheKey, sourceResults, this.ttl);
}
return sourceResults.filter(sourceResult => sourceResult.countryCode in ctx.config);
return sourceResults.filter(sourceResult => sourceResult.meta.countryCodes?.some(countryCode => countryCode in ctx.config));
}
}

View file

@ -35,7 +35,7 @@ export class StreamKiste extends Source {
const $ = cheerio.load(html);
const title = $('meta[property="og:title"]').attr('content') as string;
const title = `${($('meta[property="og:title"]').attr('content') as string).trim()} ${imdbId.season}x${imdbId.episode}`;
return Promise.all(
$(`[data-num="${imdbId.season}x${imdbId.episode}"]`)
@ -44,7 +44,7 @@ export class StreamKiste extends Source {
.map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')))
.toArray()
.filter(url => !url.host.match(/streamkiste/))
.map(url => ({ countryCode: CountryCode.de, referer: seriesPageUrl, title: `${title.trim()} ${imdbId.season}x${imdbId.episode}`, url })),
.map(url => ({ url, meta: { countryCodes: [CountryCode.de], title } })),
);
};

View file

@ -33,11 +33,11 @@ export class VerHdLink extends Source {
return $('._player-mirrors')
.map((_i, el) => {
let countryCode: CountryCode;
let countryCodes: CountryCode[];
if ($(el).hasClass('latino')) {
countryCode = CountryCode.mx;
countryCodes = [CountryCode.mx];
} else if ($(el).hasClass('castellano')) {
countryCode = CountryCode.es;
countryCodes = [CountryCode.es];
} else {
return [];
}
@ -46,7 +46,7 @@ export class VerHdLink extends Source {
.map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')))
.toArray()
.filter(url => !url.host.match(/verhdlink/))
.map(url => ({ countryCode, referer: pageUrl, url }));
.map(url => ({ url, meta: { countryCodes } }));
}).toArray();
};
}

View file

@ -29,6 +29,6 @@ export class VidSrc extends Source {
? new URL(`/embed/tv/${imdbId.id}/${imdbId.season}/${imdbId.episode}`, this.baseUrl)
: new URL(`/embed/movie/${imdbId.id}`, this.baseUrl);
return [{ countryCode: CountryCode.en, url }];
return [{ url, meta: { countryCodes: [CountryCode.en] } }];
};
}

View file

@ -29,6 +29,6 @@ export class VixSrc extends Source {
? new URL(`/tv/${tmdbId.id}/${tmdbId.season}/${tmdbId.episode}`, this.baseUrl)
: new URL(`/movie/${tmdbId.id}`, this.baseUrl);
return [{ countryCode: CountryCode.multi, url }];
return [{ url, meta: { countryCodes: [CountryCode.multi] } }];
};
}

View file

@ -32,13 +32,13 @@ export class XPrime extends Source {
const urlPrimebox = new URL(`/primebox?name=${encodeURIComponent(name)}&year=${year}&season=${tmdbId.season}&episode=${tmdbId.episode}`, this.baseUrl);
return [{ countryCode: CountryCode.en, title, url: urlPrimebox }];
return [{ url: urlPrimebox, meta: { countryCodes: [CountryCode.en], title } }];
}
const title = `${name} (${year})`;
const urlPrimebox = new URL(`/primebox?name=${encodeURIComponent(name)}&year=${year}`, this.baseUrl);
return [{ countryCode: CountryCode.en, title, url: urlPrimebox }];
return [{ url: urlPrimebox, meta: { countryCodes: [CountryCode.en], title } }];
};
}

View file

@ -3,15 +3,21 @@
exports[`CineHDPlus handle imdb babylon 5 s2e3 (es) 1`] = `
[
{
"countryCode": "es",
"referer": "https://cinehdplus.cam/peliculas/19678-babylon-5-ver-online-hd.html",
"title": "Babylon 5 2x3",
"meta": {
"countryCodes": [
"es",
],
"title": "Babylon 5 2x3",
},
"url": "https://supervideo.tv/embed-1p0m1fi9mok8.html",
},
{
"countryCode": "es",
"referer": "https://cinehdplus.cam/peliculas/19678-babylon-5-ver-online-hd.html",
"title": "Babylon 5 2x3",
"meta": {
"countryCodes": [
"es",
],
"title": "Babylon 5 2x3",
},
"url": "https://dropload.io/embed-957hny8mzrvh.html",
},
]
@ -20,15 +26,21 @@ exports[`CineHDPlus handle imdb babylon 5 s2e3 (es) 1`] = `
exports[`CineHDPlus handle imdb black mirror s2e3 (mx) 1`] = `
[
{
"countryCode": "mx",
"referer": "https://cinehdplus.cam/peliculas/9582-black-mirror-ver-online-hd.html",
"title": "Black Mirror 2x3",
"meta": {
"countryCodes": [
"mx",
],
"title": "Black Mirror 2x3",
},
"url": "https://supervideo.tv/embed-avmgppo37sx1.html",
},
{
"countryCode": "mx",
"referer": "https://cinehdplus.cam/peliculas/9582-black-mirror-ver-online-hd.html",
"title": "Black Mirror 2x3",
"meta": {
"countryCodes": [
"mx",
],
"title": "Black Mirror 2x3",
},
"url": "https://dropload.io/embed-t058ulwwkwn6.html",
},
]

View file

@ -3,43 +3,75 @@
exports[`Cuevana does not return es content for mx 1`] = `
[
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://streamwish.to/e/9icce2ca0yir",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://filemoon.sx/e/fgxxkidqq4jp",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://voe.sx/e/h87m8wyj5x8j",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://doodstream.com/e/ju53ufkqy9hc",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://streamtape.com/e/WgxgBAwOyGfbZxY",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://waaw.to/f/xtG7M9GL2MmV",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://filelions.to/v/tnehp8g7zght",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://plustream.com/embedb2/gHHRwqKdHgF5MYniuxGWnG8j_vIW77uC4a4vrO5iwLktMwS8ryr2i.bhH3SiGHw6PKNBoqxCHLDyc1Pw6EPhwg--",
},
]
@ -48,38 +80,66 @@ exports[`Cuevana does not return es content for mx 1`] = `
exports[`Cuevana does not return mx content for es 1`] = `
[
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://streamwish.to/e/mtb09rms1njz",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://filemoon.sx/e/r77fu9ez9nsv",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://voe.sx/e/cepsnqy0qgyo",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://doodstream.com/e/ny5qlz61x9xt",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://waaw.to/f/hlHhgFkmWHom",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://plustream.com/embedb2/DpAUEfJrAHkBAdb.6erU25OAypGXqMugDfQuWU2sTDBKHNQmIxzyq1b2Ma06kb7EgL6d4hdzw8quWDimAGnIvQ--",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://vidhidepro.com/v/lhaueh479t2v",
},
]
@ -88,78 +148,138 @@ exports[`Cuevana does not return mx content for es 1`] = `
exports[`Cuevana handle el camino 1`] = `
[
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://streamwish.to/e/9icce2ca0yir",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://filemoon.sx/e/fgxxkidqq4jp",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://voe.sx/e/h87m8wyj5x8j",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://doodstream.com/e/ju53ufkqy9hc",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://streamtape.com/e/WgxgBAwOyGfbZxY",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://waaw.to/f/xtG7M9GL2MmV",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://filelions.to/v/tnehp8g7zght",
},
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://plustream.com/embedb2/gHHRwqKdHgF5MYniuxGWnG8j_vIW77uC4a4vrO5iwLktMwS8ryr2i.bhH3SiGHw6PKNBoqxCHLDyc1Pw6EPhwg--",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://streamwish.to/e/mtb09rms1njz",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://filemoon.sx/e/r77fu9ez9nsv",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://voe.sx/e/cepsnqy0qgyo",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://doodstream.com/e/ny5qlz61x9xt",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://waaw.to/f/hlHhgFkmWHom",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://plustream.com/embedb2/DpAUEfJrAHkBAdb.6erU25OAypGXqMugDfQuWU2sTDBKHNQmIxzyq1b2Ma06kb7EgL6d4hdzw8quWDimAGnIvQ--",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad",
},
"url": "https://vidhidepro.com/v/lhaueh479t2v",
},
]
@ -168,33 +288,57 @@ exports[`Cuevana handle el camino 1`] = `
exports[`Cuevana handle la frontera s1e1 1`] = `
[
{
"countryCode": "es",
"title": "La frontera 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "La frontera 1x1",
},
"url": "https://streamwish.to/e/tpzbrzz7p41k",
},
{
"countryCode": "es",
"title": "La frontera 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "La frontera 1x1",
},
"url": "https://filemoon.sx/e/nhbmj6o6zl0l",
},
{
"countryCode": "es",
"title": "La frontera 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "La frontera 1x1",
},
"url": "https://vidhidepro.com/v/x5w6b0wys1fs",
},
{
"countryCode": "es",
"title": "La frontera 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "La frontera 1x1",
},
"url": "https://voe.sx/e/nfal9zqu9yvy",
},
{
"countryCode": "es",
"title": "La frontera 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "La frontera 1x1",
},
"url": "https://doodstream.com/e/dyvg7ofnnqgi",
},
{
"countryCode": "es",
"title": "La frontera 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "La frontera 1x1",
},
"url": "https://waaw.to/f/goypdL0qGqks",
},
]
@ -203,33 +347,57 @@ exports[`Cuevana handle la frontera s1e1 1`] = `
exports[`Cuevana handle walking dead s1e1 1`] = `
[
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"mx",
],
"title": "The Walking Dead 1x1",
},
"url": "https://streamwish.to/e/gudk6hvc4qwl",
},
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"mx",
],
"title": "The Walking Dead 1x1",
},
"url": "https://filemoon.sx/e/kp39yor1z0js",
},
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"mx",
],
"title": "The Walking Dead 1x1",
},
"url": "https://voe.sx/e/4ahrkbnofnty",
},
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"mx",
],
"title": "The Walking Dead 1x1",
},
"url": "https://doodstream.com/e/wvk23p39ikyd",
},
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"mx",
],
"title": "The Walking Dead 1x1",
},
"url": "https://streamtape.com/e/qMjrKleBMpCLO0",
},
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"mx",
],
"title": "The Walking Dead 1x1",
},
"url": "https://filelions.to/v/rvyogjvsejlh",
},
]

View file

@ -3,8 +3,12 @@
exports[`Einschalten handle superman 1`] = `
[
{
"countryCode": "de",
"title": "Superman.2025.German.DL.720p.WEB.H264-ZeroTwo",
"meta": {
"countryCodes": [
"de",
],
"title": "Superman.2025.German.DL.720p.WEB.H264-ZeroTwo",
},
"url": "https://vide0.net/e/qtvqjblhu74i",
},
]

View file

@ -3,13 +3,21 @@
exports[`Eurostreaming game of thrones s1e1 1`] = `
[
{
"countryCode": "it",
"title": "Il Trono di Spade 1x1",
"meta": {
"countryCodes": [
"it",
],
"title": "Il Trono di Spade 1x1",
},
"url": "https://supervideo.cc/e/05jfivnr5hiw",
},
{
"countryCode": "it",
"title": "Il Trono di Spade 1x1",
"meta": {
"countryCodes": [
"it",
],
"title": "Il Trono di Spade 1x1",
},
"url": "https://dropload.io/embed-dub8oh3k3yk5.html",
},
]
@ -18,13 +26,21 @@ exports[`Eurostreaming game of thrones s1e1 1`] = `
exports[`Eurostreaming handle imdb black mirror s2e4 1`] = `
[
{
"countryCode": "it",
"title": "Black Mirror 2x4",
"meta": {
"countryCodes": [
"it",
],
"title": "Black Mirror 2x4",
},
"url": "https://supervideo.cc/e/anf0clp2vb6y",
},
{
"countryCode": "it",
"title": "Black Mirror 2x4",
"meta": {
"countryCodes": [
"it",
],
"title": "Black Mirror 2x4",
},
"url": "https://dropload.io/embed-978xyyxi3ldq.html",
},
]
@ -33,13 +49,21 @@ exports[`Eurostreaming handle imdb black mirror s2e4 1`] = `
exports[`Eurostreaming handle lost s1e1 1`] = `
[
{
"countryCode": "it",
"title": "Lost 1x1",
"meta": {
"countryCodes": [
"it",
],
"title": "Lost 1x1",
},
"url": "https://supervideo.cc/e/lgslt2lst4jv",
},
{
"countryCode": "it",
"title": "Lost 1x1",
"meta": {
"countryCodes": [
"it",
],
"title": "Lost 1x1",
},
"url": "https://dropload.io/embed-vabynwo8glxz.html",
},
]
@ -48,13 +72,21 @@ exports[`Eurostreaming handle lost s1e1 1`] = `
exports[`Eurostreaming last of us s1e1 1`] = `
[
{
"countryCode": "it",
"title": "The Last of Us 1x1",
"meta": {
"countryCodes": [
"it",
],
"title": "The Last of Us 1x1",
},
"url": "https://supervideo.cc/e/ic1mmvrh5xv6",
},
{
"countryCode": "it",
"title": "The Last of Us 1x1",
"meta": {
"countryCodes": [
"it",
],
"title": "The Last of Us 1x1",
},
"url": "https://dropload.io/embed-uyzfrh62n31l.html",
},
]

View file

@ -3,28 +3,48 @@
exports[`Frembed handle battle royal 1`] = `
[
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://d-s.io/e/gtenfs0rffzq",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://netu.fanstream.live/e/p0QUHavRbQD0",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://jilliandescribecompany.com/e/lbeqm6ofmauq",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://video.streamtales.cc/player/frvod.php?url=https://streamtales.cc:8443/videos/3176.mp4",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://uqload.cx/embed-pnxmokgrigu9.html",
},
]
@ -33,23 +53,39 @@ exports[`Frembed handle battle royal 1`] = `
exports[`Frembed handle imdb black mirror s4e2 1`] = `
[
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://d-s.io/e/dfx8me4un4ul",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://netu.fanstream.live/e/0DFgfkcXOsDP",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://jilliandescribecompany.com/e/cqy9oue7sv0g",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://ds2play.com/e/fzfvfq3ngig0",
},
]
@ -58,23 +94,39 @@ exports[`Frembed handle imdb black mirror s4e2 1`] = `
exports[`Frembed handle tmdb black mirror s4e2 1`] = `
[
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://d-s.io/e/dfx8me4un4ul",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://netu.fanstream.live/e/0DFgfkcXOsDP",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://jilliandescribecompany.com/e/cqy9oue7sv0g",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://ds2play.com/e/fzfvfq3ngig0",
},
]

View file

@ -3,28 +3,43 @@
exports[`FrenchCloud handle imdb the devil's bath 1`] = `
[
{
"countryCode": "fr",
"referer": "https://frenchcloud.cam/movie/tt29141112",
"meta": {
"countryCodes": [
"fr",
],
},
"url": "https://supervideo.cc/e/ttnqt211ebtg",
},
{
"countryCode": "fr",
"referer": "https://frenchcloud.cam/movie/tt29141112",
"meta": {
"countryCodes": [
"fr",
],
},
"url": "https://dropload.io/embed-w4yit5wiia9y.html",
},
{
"countryCode": "fr",
"referer": "https://frenchcloud.cam/movie/tt29141112",
"meta": {
"countryCodes": [
"fr",
],
},
"url": "https://mixdrop.ag/e/8l3634dmt68rnp",
},
{
"countryCode": "fr",
"referer": "https://frenchcloud.cam/movie/tt29141112",
"meta": {
"countryCodes": [
"fr",
],
},
"url": "https://streamtape.com/e/gjA1OQ4klyHxgJ",
},
{
"countryCode": "fr",
"referer": "https://frenchcloud.cam/movie/tt29141112",
"meta": {
"countryCodes": [
"fr",
],
},
"url": "https://dood.to/e/osbcylatzy6m",
},
]

View file

@ -3,8 +3,12 @@
exports[`HomeCine does not return es content for mx 1`] = `
[
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad (2019)",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad (2019)",
},
"url": "https://fastream.to/embed-kc5dvcy34x8p.html",
},
]
@ -13,8 +17,12 @@ exports[`HomeCine does not return es content for mx 1`] = `
exports[`HomeCine does not return mx content for es 1`] = `
[
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad (2019)",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad (2019)",
},
"url": "https://fastream.to/embed-c9tmfzsbfj0z.html",
},
]
@ -23,13 +31,21 @@ exports[`HomeCine does not return mx content for es 1`] = `
exports[`HomeCine handle el camino 1`] = `
[
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad (2019)",
"meta": {
"countryCodes": [
"mx",
],
"title": "El Camino: Una película de Breaking Bad (2019)",
},
"url": "https://fastream.to/embed-kc5dvcy34x8p.html",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad (2019)",
"meta": {
"countryCodes": [
"es",
],
"title": "El Camino: Una película de Breaking Bad (2019)",
},
"url": "https://fastream.to/embed-c9tmfzsbfj0z.html",
},
]
@ -38,8 +54,12 @@ exports[`HomeCine handle el camino 1`] = `
exports[`HomeCine handle marvel - the punisher 1`] = `
[
{
"countryCode": "es",
"title": "Marvel - The Punisher 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "Marvel - The Punisher 1x1",
},
"url": "https://fastream.to/embed-ujq9uojoftog.html",
},
]
@ -48,13 +68,21 @@ exports[`HomeCine handle marvel - the punisher 1`] = `
exports[`HomeCine handle walking dead s1e1 1`] = `
[
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"mx",
],
"title": "The Walking Dead 1x1",
},
"url": "https://fastream.to/embed-nkztf1memuca.html",
},
{
"countryCode": "es",
"title": "The Walking Dead 1x1",
"meta": {
"countryCodes": [
"es",
],
"title": "The Walking Dead 1x1",
},
"url": "https://fastream.to/embed-5bv96pdifz0b.html",
},
]

View file

@ -3,18 +3,30 @@
exports[`KinoGer handle imdb blood and sinners 1`] = `
[
{
"countryCode": "de",
"title": "Blood & Sinners (2025)",
"meta": {
"countryCodes": [
"de",
],
"title": "Blood & Sinners (2025)",
},
"url": "https://fsst.online/embed/900576/",
},
{
"countryCode": "de",
"title": "Blood & Sinners (2025)",
"meta": {
"countryCodes": [
"de",
],
"title": "Blood & Sinners (2025)",
},
"url": "https://kinoger.pw/e/6zeREaMlrqREZPa",
},
{
"countryCode": "de",
"title": "Blood & Sinners (2025)",
"meta": {
"countryCodes": [
"de",
],
"title": "Blood & Sinners (2025)",
},
"url": "https://kinoger.re/#ge5fhb",
},
]
@ -23,13 +35,21 @@ exports[`KinoGer handle imdb blood and sinners 1`] = `
exports[`KinoGer handle imdb dead city s2e5 1`] = `
[
{
"countryCode": "de",
"title": "The Walking Dead: Dead City 2x5",
"meta": {
"countryCodes": [
"de",
],
"title": "The Walking Dead: Dead City 2x5",
},
"url": "https://supervideo.cc/e/bj6szat1pval",
},
{
"countryCode": "de",
"title": "The Walking Dead: Dead City 2x5",
"meta": {
"countryCodes": [
"de",
],
"title": "The Walking Dead: Dead City 2x5",
},
"url": "https://kinoger.re/#x6tsx9",
},
]
@ -38,13 +58,21 @@ exports[`KinoGer handle imdb dead city s2e5 1`] = `
exports[`KinoGer handle imdb dead city s2e6 1`] = `
[
{
"countryCode": "de",
"title": "The Walking Dead: Dead City 2x6",
"meta": {
"countryCodes": [
"de",
],
"title": "The Walking Dead: Dead City 2x6",
},
"url": "https://fsst.online/embed/901994/",
},
{
"countryCode": "de",
"title": "The Walking Dead: Dead City 2x6",
"meta": {
"countryCodes": [
"de",
],
"title": "The Walking Dead: Dead City 2x6",
},
"url": "https://kinoger.re/#ep1tcf",
},
]
@ -55,8 +83,12 @@ exports[`KinoGer handle missing episode imdb black mirror s3e4 1`] = `[]`;
exports[`KinoGer handle no fsst via brokeback mountain 1`] = `
[
{
"countryCode": "de",
"title": "Brokeback Mountain (2005)",
"meta": {
"countryCodes": [
"de",
],
"title": "Brokeback Mountain (2005)",
},
"url": "https://dooodster.com/e/1cfcevn6dg8shrfvht22odxw2lty18hr",
},
]

View file

@ -3,15 +3,21 @@
exports[`MegaKino handle imdb baymax 1`] = `
[
{
"countryCode": "de",
"referer": "https://megakino.si/films/693-baymax-riesiges-robowabohu.html",
"title": "Baymax - Riesiges Robowabohu",
"meta": {
"countryCodes": [
"de",
],
"title": "Baymax - Riesiges Robowabohu",
},
"url": "https://voe.sx/e/8doq7gwiwtig",
},
{
"countryCode": "de",
"referer": "https://megakino.si/films/693-baymax-riesiges-robowabohu.html",
"title": "Baymax - Riesiges Robowabohu",
"meta": {
"countryCodes": [
"de",
],
"title": "Baymax - Riesiges Robowabohu",
},
"url": "https://watch.gxplayer.xyz/watch?v=MEKI92PU",
},
]

View file

@ -3,23 +3,35 @@
exports[`MeineCloud handle imdb the devil's bath 1`] = `
[
{
"countryCode": "de",
"referer": "https://meinecloud.click/movie/tt29141112",
"meta": {
"countryCodes": [
"de",
],
},
"url": "https://supervideo.cc/e/q7i0sw1oytw3",
},
{
"countryCode": "de",
"referer": "https://meinecloud.click/movie/tt29141112",
"meta": {
"countryCodes": [
"de",
],
},
"url": "https://dropload.io/embed-lyo2h1snpe5c.html",
},
{
"countryCode": "de",
"referer": "https://meinecloud.click/movie/tt29141112",
"meta": {
"countryCodes": [
"de",
],
},
"url": "https://mixdrop.ag/e/3nzwveprim63or6",
},
{
"countryCode": "de",
"referer": "https://meinecloud.click/movie/tt29141112",
"meta": {
"countryCodes": [
"de",
],
},
"url": "https://dood.to/e/sk1m9eumzyjj",
},
]

View file

@ -3,23 +3,35 @@
exports[`MostraGuarda handle imdb the devil's bath 1`] = `
[
{
"countryCode": "it",
"referer": "https://mostraguarda.stream/movie/tt29141112",
"meta": {
"countryCodes": [
"it",
],
},
"url": "https://supervideo.cc/e/6vri8jyhla3n",
},
{
"countryCode": "it",
"referer": "https://mostraguarda.stream/movie/tt29141112",
"meta": {
"countryCodes": [
"it",
],
},
"url": "https://dropload.io/embed-xsr90y2ltdyx.html",
},
{
"countryCode": "it",
"referer": "https://mostraguarda.stream/movie/tt29141112",
"meta": {
"countryCodes": [
"it",
],
},
"url": "https://mixdrop.ag/e/vk196d6xfzwwo1",
},
{
"countryCode": "it",
"referer": "https://mostraguarda.stream/movie/tt29141112",
"meta": {
"countryCodes": [
"it",
],
},
"url": "https://dood.to/e/37o04m33q3g5",
},
]

View file

@ -3,63 +3,111 @@
exports[`Movix handle battle royal 1`] = `
[
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://lecteur6.com/video/18085327b86002fc604c323b9a07f997",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://vidoza.net/embed-55hvynbdyqvc.html",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://wishonly.site/e/kykhuv6tscs8",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://darkibox.com/embed-5ghz7myucfjq.html",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://supervideo.cc/e/uieh17bid0ce",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://dood.li/e/gha75crrm8nf",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://filemoon.sx/e/c5lhlypfasmm",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://voe.sx/e/aghpmosgkhsu",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "http://vidmoly.me/embed-79w00r2qv88n.html",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://waaw.to/f/JWzt5eJSbpu8",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://veev.to/e/lu4c24e3aetf",
},
{
"countryCode": "fr",
"title": "Battle Royale",
"meta": {
"countryCodes": [
"fr",
],
"title": "Battle Royale",
},
"url": "https://listeamed.net/e/eL2157Kzw8m5rw4",
},
]
@ -68,68 +116,120 @@ exports[`Movix handle battle royal 1`] = `
exports[`Movix handle tmdb black mirror s4e2 1`] = `
[
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://uqload.net/embed-xjmhb7rbzdjh.html",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://lulustream.com/e/c3defa2cbtmm",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://wishonly.site/e/bzfdmjlrsqxk",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://vidoza.net/embed-i2e8gn4m64e1.html",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://filemoon.sx/e/0iuzfpt5cbx6",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://voe.sx/e/mustkdlxo67t",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "http://vidmoly.me/embed-2rls953tvhxc.html",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://waaw.to/f/GdQ6mHpxPdd8",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://veev.to/e/nifwerilwo0i",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://listeamed.net/e/8ozgENePwdLxmjA",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://coflix.upn.one/#x63hig",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://do7go.com/e/2zn4y7ueq1ta",
},
{
"countryCode": "fr",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"fr",
],
"title": "Black Mirror 4x2",
},
"url": "https://movearnpre.com/embed/eiel2p4z74kp",
},
]

View file

@ -3,59 +3,115 @@
exports[`PrimeWire handle el camino 1`] = `
[
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/bv4b7tl9u1ij",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/wayeo7l1cw7d",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/xwhvakkexuob",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/paqi7qqku3zi",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/ysimllytd3zo",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamtape.com/v/6QLoLq0O8DtObX",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamtape.com/v/qMVMPa49M1hz8BR/El.Camino.A.Breaking.Bad.Movie.2019.1080p.WEBRip.x264_YTS.LT.mp4",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/9lw62yyosxc0",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/05fm25orfeux",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/j2pa25q11pdu",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamwish.to/09q73jf6t6jw",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/2bnmi",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/4njk6691b1k60d",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/ql0qrxnjb89nx8",
},
]
@ -64,59 +120,115 @@ exports[`PrimeWire handle el camino 1`] = `
exports[`PrimeWire handle el camino 2`] = `
[
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/bv4b7tl9u1ij",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/wayeo7l1cw7d",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/xwhvakkexuob",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/paqi7qqku3zi",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/ysimllytd3zo",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamtape.com/v/6QLoLq0O8DtObX",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamtape.com/v/qMVMPa49M1hz8BR/El.Camino.A.Breaking.Bad.Movie.2019.1080p.WEBRip.x264_YTS.LT.mp4",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/9lw62yyosxc0",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/05fm25orfeux",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/j2pa25q11pdu",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamwish.to/09q73jf6t6jw",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/2bnmi",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/4njk6691b1k60d",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/ql0qrxnjb89nx8",
},
]
@ -125,139 +237,275 @@ exports[`PrimeWire handle el camino 2`] = `
exports[`PrimeWire handle imdb dead city s2e5 1`] = `
[
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://bigwarp.cc/u66q6mybilfa",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://bigwarp.cc/aazhpuex9ppj",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://bigwarp.cc/4tb37obzt6re",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/odssr9dyjx5n",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/0i0j0qz5o478",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/eqmzrkv5kf5s",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filelions.to/f/fpru8wthtsf4",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/y8lrmf2t7jhp",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/te40pmzra8du",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://voe.sx/grsvpdbxvw3e",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamtape.com/v/D2e3jR83rKTkPJw/",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamtape.com/v/Ap0GJoXglaFbqK/The.Walking.Dead.Dead.City.S02E05.The.Bird.Always.Knows.720p.AMZN.WEB-DL.DDP5.1.H.264-NTb.mkv.mp4",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://savefiles.com/i0bj9gazequy",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/1qfuy5yiq4r6",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/sn87i3ru7plx",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/8odyvc9bre16",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://dood.watch/d/reslxv9siz41",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamwish.to/jult1lcvqjlt",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamwish.to/s986lkdy8mb5",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamwish.to/h9p9qqgpq2lw",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/nlpj3q3ran17rv",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/q1pz4k94uxxkgvo",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://mixdrop.ag/f/9wvz9gp3b3q1llk",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filemoon.sx/d/ftowmfa7f0ge/The.Walking.Dead.Dead.City.S02E05.The.Bird.Always.Knows.720p.AMZN.WEB-DL.DDP5.1.H.264-NTb.mkv",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filemoon.sx/d/8wllnog0cyl7",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://streamplay.to/yrkzvpayg27o",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "http://vidmoly.me/w/okcwblnaqw3t",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://vidmoly.me/w/g1ob8a8h5pc8",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "http://vidmoly.me/w/46bi0sec0966",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://vidmoly.me/w/awjd3z0t0i9l",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://luluvdoo.com/d/au00sj11vqku",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filegram.to/erapbfgcdguq",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filegram.to/6q8gctbf79gc",
},
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://filegram.to/6yuyz0dgqu05",
},
]

View file

@ -3,8 +3,12 @@
exports[`Soaper handle imdb black mirror s4e2 1`] = `
[
{
"countryCode": "en",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"en",
],
"title": "Black Mirror 4x2",
},
"url": "https://soaper.live/episode_5KDq78eGp1.html",
},
]
@ -13,8 +17,12 @@ exports[`Soaper handle imdb black mirror s4e2 1`] = `
exports[`Soaper handle imdb full metal jacket 1`] = `
[
{
"countryCode": "en",
"title": "Full Metal Jacket (1987)",
"meta": {
"countryCodes": [
"en",
],
"title": "Full Metal Jacket (1987)",
},
"url": "https://soaper.live/movie_d8kdeypDY9.html",
},
]
@ -23,8 +31,12 @@ exports[`Soaper handle imdb full metal jacket 1`] = `
exports[`Soaper handle lego heroes flash 1`] = `
[
{
"countryCode": "en",
"title": "LEGO DC Comics Super Heroes: The Flash (2018)",
"meta": {
"countryCodes": [
"en",
],
"title": "LEGO DC Comics Super Heroes: The Flash (2018)",
},
"url": "https://soaper.live/movie_lOGwQVEDV0.html",
},
]
@ -33,8 +45,12 @@ exports[`Soaper handle lego heroes flash 1`] = `
exports[`Soaper handle tmdb black mirror s4e2 1`] = `
[
{
"countryCode": "en",
"title": "Black Mirror 4x2",
"meta": {
"countryCodes": [
"en",
],
"title": "Black Mirror 4x2",
},
"url": "https://soaper.live/episode_5KDq78eGp1.html",
},
]

View file

@ -3,15 +3,21 @@
exports[`StreamKiste handle imdb black mirror s2e4 1`] = `
[
{
"countryCode": "de",
"referer": "https://streamkiste.taxi/movie/29992-black-mirror-stream-kostenlos.html",
"title": "Black Mirror 2x4",
"meta": {
"countryCodes": [
"de",
],
"title": "Black Mirror 2x4",
},
"url": "https://supervideo.cc/embed-22go50mon0ke.html",
},
{
"countryCode": "de",
"referer": "https://streamkiste.taxi/movie/29992-black-mirror-stream-kostenlos.html",
"title": "Black Mirror 2x4",
"meta": {
"countryCodes": [
"de",
],
"title": "Black Mirror 2x4",
},
"url": "https://dropload.io/embed-jvjwrkpijr0f.html",
},
]

View file

@ -3,48 +3,75 @@
exports[`VerHdLink handle titanic 1`] = `
[
{
"countryCode": "mx",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"mx",
],
},
"url": "https://supervideo.cc/e/g6okbr0kd790",
},
{
"countryCode": "mx",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"mx",
],
},
"url": "https://dropload.io/embed-jjjx9j5joiz8.html",
},
{
"countryCode": "mx",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"mx",
],
},
"url": "https://mixdrop.ag/e/vn0wx308fq984q",
},
{
"countryCode": "mx",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"mx",
],
},
"url": "https://streamtape.com/e/Bjp2vjrdBxsK82",
},
{
"countryCode": "mx",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"mx",
],
},
"url": "https://dood.to/e/rw7rxdfrbg09",
},
{
"countryCode": "es",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"es",
],
},
"url": "https://supervideo.cc/e/qe1fviow8uwy",
},
{
"countryCode": "es",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"es",
],
},
"url": "https://dropload.io/embed-ick4ti66vt6s.html",
},
{
"countryCode": "es",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"es",
],
},
"url": "https://mixdrop.ag/e/xokzmv61hrwe8o",
},
{
"countryCode": "es",
"referer": "https://verhdlink.cam/movie/tt0120338",
"meta": {
"countryCodes": [
"es",
],
},
"url": "https://dood.to/e/gy8l8mb2i311",
},
]

View file

@ -3,7 +3,11 @@
exports[`VidSrc handle imdb black mirror s4e2 1`] = `
[
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://vidsrc.xyz/embed/tv/tt2085059/4/2",
},
]
@ -12,7 +16,11 @@ exports[`VidSrc handle imdb black mirror s4e2 1`] = `
exports[`VidSrc handle imdb full metal jacket 1`] = `
[
{
"countryCode": "en",
"meta": {
"countryCodes": [
"en",
],
},
"url": "https://vidsrc.xyz/embed/movie/tt0093058",
},
]

View file

@ -3,7 +3,11 @@
exports[`VixSrc handle imdb black mirror s4e2 1`] = `
[
{
"countryCode": "multi",
"meta": {
"countryCodes": [
"multi",
],
},
"url": "https://vixsrc.to/tv/42009/4/2",
},
]
@ -12,7 +16,11 @@ exports[`VixSrc handle imdb black mirror s4e2 1`] = `
exports[`VixSrc handle imdb full metal jacket 1`] = `
[
{
"countryCode": "multi",
"meta": {
"countryCodes": [
"multi",
],
},
"url": "https://vixsrc.to/movie/600",
},
]

View file

@ -3,8 +3,12 @@
exports[`XPrime handle alien: earth 1x1 1`] = `
[
{
"countryCode": "en",
"title": "Alien: Earth 1x1",
"meta": {
"countryCodes": [
"en",
],
"title": "Alien: Earth 1x1",
},
"url": "https://backend.xprime.tv/primebox?name=Alien%3A%20Earth&year=2025&season=1&episode=1",
},
]
@ -13,8 +17,12 @@ exports[`XPrime handle alien: earth 1x1 1`] = `
exports[`XPrime handle superman 1`] = `
[
{
"countryCode": "en",
"title": "Superman (2025)",
"meta": {
"countryCodes": [
"en",
],
"title": "Superman (2025)",
},
"url": "https://backend.xprime.tv/primebox?name=Superman&year=2025",
},
]

View file

@ -41,7 +41,7 @@ export enum BlockedReason {
export interface Meta {
bytes?: number | undefined;
countryCodes: CountryCode[];
countryCodes?: CountryCode[];
height?: number | undefined;
title?: string | undefined;
}
@ -61,7 +61,7 @@ export interface UrlResult {
label: string;
sourceId: string;
ttl?: number;
meta: Meta;
meta?: Meta;
notWebReady?: boolean;
requestHeaders?: Record<string, string>;
}

View file

@ -91,7 +91,7 @@ describe('resolve', () => {
public readonly baseUrl = 'https://example.com';
public readonly handleInternal = async (): Promise<SourceResult[]> => {
return [{ countryCode: CountryCode.de, url: new URL('https://example.com') }];
return [{ url: new URL('https://example.com'), meta: { countryCodes: [CountryCode.de] } }];
};
}

View file

@ -51,7 +51,7 @@ export class StreamResolver {
this.logger.info(`${source.id} returned ${sourceResults.length} urls`, ctx);
const sourceUrlResults = await Promise.all(
sourceResults.map(({ countryCode, title, url }) => this.extractorRegistry.handle(ctx, url, countryCode, title)),
sourceResults.map(({ url, meta }) => this.extractorRegistry.handle(ctx, url, meta)),
);
urlResults.push(...sourceUrlResults.flat());
@ -70,12 +70,12 @@ export class StreamResolver {
await Promise.all(sourcePromises);
urlResults.sort((a, b) => {
const heightComparison = (b.meta.height ?? 0) - (a.meta.height ?? 0);
const heightComparison = (b.meta?.height ?? 0) - (a.meta?.height ?? 0);
if (heightComparison !== 0) {
return heightComparison;
}
const bytesComparison = (b.meta.bytes ?? 0) - (a.meta.bytes ?? 0);
const bytesComparison = (b.meta?.bytes ?? 0) - (a.meta?.bytes ?? 0);
if (bytesComparison !== 0) {
return bytesComparison;
}
@ -102,7 +102,7 @@ export class StreamResolver {
notWebReady: true,
proxyHeaders: { request: urlResult.requestHeaders },
}),
...(urlResult.meta.bytes && { videoSize: urlResult.meta.bytes }),
...(urlResult.meta?.bytes && { videoSize: urlResult.meta.bytes }),
},
})),
);
@ -142,11 +142,11 @@ export class StreamResolver {
private buildName(ctx: Context, urlResult: UrlResult): string {
let name = envGetAppName();
urlResult.meta.countryCodes.forEach((countryCode) => {
urlResult.meta?.countryCodes?.forEach((countryCode) => {
name += ` ${flagFromCountryCode(countryCode)}`;
});
if (urlResult.meta.height) {
if (urlResult.meta?.height) {
name += ` ${urlResult.meta.height}p`;
}
@ -160,12 +160,12 @@ export class StreamResolver {
private buildTitle(ctx: Context, urlResult: UrlResult): string {
const titleLines = [];
if (urlResult.meta.title) {
if (urlResult.meta?.title) {
titleLines.push(urlResult.meta.title);
}
const titleDetailsLine = [];
if (urlResult.meta.bytes) {
if (urlResult.meta?.bytes) {
titleDetailsLine.push(`💾 ${bytes.format(urlResult.meta.bytes, { unitSeparator: ' ' })}`);
}
titleDetailsLine.push(`🔗 ${urlResult.label}`);