feat(handler): limited Frembed support

This commit is contained in:
WebStreamr 2025-05-17 20:20:19 +02:00
parent b331dc66ac
commit 8b82215c32
No known key found for this signature in database
11 changed files with 129 additions and 3 deletions

View file

@ -16,7 +16,7 @@ export class DoodStream implements Extractor {
this.fetcher = fetcher;
}
readonly supports = (url: URL): boolean => null !== url.host.match(/dood/);
readonly supports = (url: URL): boolean => null !== url.host.match(/dood|do[0-9]go/);
readonly extract = async (ctx: Context, url: URL, countryCode: string) => {
const videoId = url.pathname.split('/').slice(-1)[0] as string;

View file

@ -0,0 +1,44 @@
import winston from 'winston';
import { Frembed } from './Frembed';
import { ExtractorRegistry } from '../extractor';
import { Fetcher } from '../utils';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
// @ts-expect-error No constructor args needed
const fetcher = new Fetcher();
const handler = new Frembed(fetcher, new ExtractorRegistry(logger, fetcher));
const ctx: Context = { ip: '127.0.0.1' };
describe('Frembed', () => {
test('does not handle non imdb series', async () => {
const streams = await handler.handle(ctx, 'series', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent series gracefully', async () => {
const streams = await handler.handle(ctx, 'series', 'tt12345678:1:1');
expect(streams).toHaveLength(0);
});
test('handle imdb black mirror s4e2', async () => {
const streams = (await handler.handle(ctx, 'series', 'tt2085059:4:2')).filter(stream => stream !== undefined);
expect(streams).toHaveLength(1);
expect(streams[0]).toStrictEqual({
url: expect.any(URL),
label: 'DoodStream',
sourceId: 'doodstream_fr',
height: 0,
bytes: 0,
countryCode: 'fr',
requestHeaders: {
Referer: 'http://dood.to/',
},
});
expect(streams[0]?.url.href).toMatch(/^https:\/\/.*?token.*?expiry/);
});
});

65
src/handler/Frembed.ts Normal file
View file

@ -0,0 +1,65 @@
import axios from 'axios';
import { Handler } from './types';
import { parseImdbId, Fetcher } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context } from '../types';
export class Frembed implements Handler {
readonly id = 'frembed';
readonly label = 'Frembed';
readonly contentTypes = ['series'];
readonly languages = ['fr'];
private readonly fetcher: Fetcher;
private readonly embedExtractors: ExtractorRegistry;
constructor(fetcher: Fetcher, embedExtractors: ExtractorRegistry) {
this.fetcher = fetcher;
this.embedExtractors = embedExtractors;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
const imdbId = parseImdbId(id);
const response = await this.apiCall(ctx, new URL(`https://frembed.club/api/series?id=${imdbId.id}&sa=${imdbId.series}&epi=${imdbId.episode}&idType=imdb`));
if (!response) {
return [];
}
const json = JSON.parse(response);
const urls: URL[] = [];
for (const key in json) {
if (key.startsWith('link')) {
try {
urls.push(new URL(json[key]));
} catch {
// Skip invalid URL
}
}
}
return Promise.all(urls.map(url => this.embedExtractors.handle(ctx, url, 'fr')));
};
private readonly apiCall = async (ctx: Context, url: URL): Promise<string | undefined> => {
try {
return await this.fetcher.text(ctx, url);
} catch (error) {
/* istanbul ignore next */
if (axios.isAxiosError(error) && error.response?.status === 404) {
return undefined;
}
/* istanbul ignore next */
throw error;
}
};
}

View file

@ -1,5 +1,6 @@
export * from './CineHDPlus';
export * from './Eurostreaming';
export * from './Frembed';
export * from './FrenchCloud';
export * from './KinoKiste';
export * from './MeineCloud';

View file

@ -6,6 +6,7 @@ import winston from 'winston';
import {
CineHDPlus,
Eurostreaming,
Frembed,
FrenchCloud,
Handler,
KinoKiste,
@ -48,6 +49,7 @@ const handlers: Handler[] = [
new KinoKiste(fetcher, embedExtractors),
new MeineCloud(fetcher, embedExtractors),
// FR
new Frembed(fetcher, embedExtractors),
new FrenchCloud(fetcher, embedExtractors),
// IT
new Eurostreaming(fetcher, embedExtractors),

View file

@ -32,6 +32,7 @@ describe('fetch', () => {
'X-Forwarded-Proto': 'https',
'X-Real-IP': '127.0.0.1',
},
responseType: 'text',
},
);
});
@ -60,6 +61,7 @@ describe('fetch', () => {
'X-Forwarded-Proto': 'https',
'X-Real-IP': '127.0.0.1',
},
responseType: 'text',
},
);
@ -77,6 +79,7 @@ describe('fetch', () => {
'X-Forwarded-Proto': 'https',
'X-Real-IP': '127.0.0.1',
},
responseType: 'text',
},
);
});

View file

@ -35,6 +35,7 @@ export class Fetcher {
private readonly getConfig = (ctx: Context, url: URL, config?: AxiosRequestConfig): AxiosRequestConfig => {
return {
responseType: 'text',
...config,
headers: {
'User-Agent': this.createUserAgentForIp(ctx.ip),

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbh7fdq4j4mwzraf7q/1y4sn4ndoi~

View file

@ -0,0 +1 @@
{"id":44334,"tmdb":"42009","title":"Black Mirror","sa":4,"epi":2,"link":"https://ahvsh.com/e/83izf7qzwpae,https://netu.frembed.fun/e/0DFgfkcXOsDP,https://likessb.com/e/7yjgl1x56n08.html,https://ds2play.com/e/fzfvfq3ngig0","imdb":"tt2085059","year":null,"version":"VF","link_vostfr":"","link1":"https://do7go.com/e/dfx8me4un4ul\t","link2":"https://netu.frembed.art/e/0DFgfkcXOsDP","link3":"https://johnalwayssame.com/e/cqy9oue7sv0g\t","link4":"https://ds2play.com/e/fzfvfq3ngig0","link5":null,"link6":null,"link7":null,"link1vostfr":"","link2vostfr":null,"link3vostfr":null,"link4vostfr":null,"link5vostfr":null,"link6vostfr":null,"link7vostfr":null,"link1vo":null,"link2vo":null,"link3vo":null,"link4vo":null,"link5vo":null,"link6vo":null,"link7vo":null,"date_creation":"2023-08-17T04:06:31.000Z","update_date":"2025-04-22T23:59:01.000Z","views":1,"tatavid":"https://tatavid.com/embed-d5qb488pe53n.html","backdropImage":"/dg3OindVAGZBjlT3xYKqIAdukPL.jpg","posterImage":"/seN6rRfN0I6n8iDXjlSMk1QjNcq.jpg","prevEpisode":{"sa":4,"epi":1,"url":"/series?id=tt2085059&sa=4&epi=1"},"nextEpisode":{"sa":4,"epi":3,"url":"/series?id=tt2085059&sa=4&epi=3"}}

View file

@ -10,7 +10,7 @@ export class Fetcher {
if (fs.existsSync(path)) {
return fs.readFileSync(path).toString();
} else {
const text = (await Axios.create().get(url.href, config)).data;
const text = (await Axios.create().get(url.href, this.getConfig(config))).data;
fs.writeFileSync(path, text);
@ -24,11 +24,18 @@ export class Fetcher {
if (fs.existsSync(path)) {
return fs.readFileSync(path).toString();
} else {
const text = (await Axios.create().post(url.href, data, config)).data;
const text = (await Axios.create().post(url.href, data, this.getConfig(config))).data;
fs.writeFileSync(path, text);
return text;
}
};
private readonly getConfig = (config?: AxiosRequestConfig): AxiosRequestConfig => {
return {
responseType: 'text',
...config,
};
};
}