feat(handler): limited Frembed support
This commit is contained in:
parent
b331dc66ac
commit
8b82215c32
11 changed files with 129 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
44
src/handler/Frembed.test.ts
Normal file
44
src/handler/Frembed.test.ts
Normal 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
65
src/handler/Frembed.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
export * from './CineHDPlus';
|
||||
export * from './Eurostreaming';
|
||||
export * from './Frembed';
|
||||
export * from './FrenchCloud';
|
||||
export * from './KinoKiste';
|
||||
export * from './MeineCloud';
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
1
src/utils/__fixtures__/Fetcher/http:dood.toedfx8me4un4ul
generated
Normal file
1
src/utils/__fixtures__/Fetcher/http:dood.toedfx8me4un4ul
generated
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbh7fdq4j4mwzraf7q/1y4sn4ndoi~
|
||||
1
src/utils/__fixtures__/Fetcher/https:frembed.clubapiseriesidtt2085059andsa4andepi2andidTypeimdb
generated
Normal file
1
src/utils/__fixtures__/Fetcher/https:frembed.clubapiseriesidtt2085059andsa4andepi2andidTypeimdb
generated
Normal 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"}}
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue