From 8b82215c321d7a45575e6efed7adcf74a625d656 Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Sat, 17 May 2025 20:20:19 +0200 Subject: [PATCH] feat(handler): limited Frembed support --- src/extractor/DoodStream.ts | 2 +- src/handler/Frembed.test.ts | 44 +++++++++++++ src/handler/Frembed.ts | 65 +++++++++++++++++++ src/handler/index.ts | 1 + src/index.ts | 2 + src/utils/Fetcher.test.ts | 3 + src/utils/Fetcher.ts | 1 + .../Fetcher/http:dood.toedfx8me4un4ul | 1 + ...7aa6813a60d91f2960fh3rdlhjvao7chgxndsi3ra7 | 1 + ...eriesidtt2085059andsa4andepi2andidTypeimdb | 1 + src/utils/__mocks__/Fetcher.ts | 11 +++- 11 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/handler/Frembed.test.ts create mode 100644 src/handler/Frembed.ts create mode 100644 src/utils/__fixtures__/Fetcher/http:dood.toedfx8me4un4ul create mode 100644 src/utils/__fixtures__/Fetcher/http:dood.topass_md5145783570-107-168-1747504988-3234b77295e6577aa6813a60d91f2960fh3rdlhjvao7chgxndsi3ra7 create mode 100644 src/utils/__fixtures__/Fetcher/https:frembed.clubapiseriesidtt2085059andsa4andepi2andidTypeimdb diff --git a/src/extractor/DoodStream.ts b/src/extractor/DoodStream.ts index 4ed8ff9..2901014 100644 --- a/src/extractor/DoodStream.ts +++ b/src/extractor/DoodStream.ts @@ -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; diff --git a/src/handler/Frembed.test.ts b/src/handler/Frembed.test.ts new file mode 100644 index 0000000..68f1287 --- /dev/null +++ b/src/handler/Frembed.test.ts @@ -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/); + }); +}); diff --git a/src/handler/Frembed.ts b/src/handler/Frembed.ts new file mode 100644 index 0000000..f34f9fb --- /dev/null +++ b/src/handler/Frembed.ts @@ -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 => { + 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; + } + }; +} diff --git a/src/handler/index.ts b/src/handler/index.ts index 9dd3f86..a7c4d92 100644 --- a/src/handler/index.ts +++ b/src/handler/index.ts @@ -1,5 +1,6 @@ export * from './CineHDPlus'; export * from './Eurostreaming'; +export * from './Frembed'; export * from './FrenchCloud'; export * from './KinoKiste'; export * from './MeineCloud'; diff --git a/src/index.ts b/src/index.ts index 9f2398f..ea34048 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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), diff --git a/src/utils/Fetcher.test.ts b/src/utils/Fetcher.test.ts index e98dcee..05235cf 100644 --- a/src/utils/Fetcher.test.ts +++ b/src/utils/Fetcher.test.ts @@ -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', }, ); }); diff --git a/src/utils/Fetcher.ts b/src/utils/Fetcher.ts index 87a3f62..a6ada78 100644 --- a/src/utils/Fetcher.ts +++ b/src/utils/Fetcher.ts @@ -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), diff --git a/src/utils/__fixtures__/Fetcher/http:dood.toedfx8me4un4ul b/src/utils/__fixtures__/Fetcher/http:dood.toedfx8me4un4ul new file mode 100644 index 0000000..c1fe8fb --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/http:dood.toedfx8me4un4ul @@ -0,0 +1 @@ + Black Mirror S04E02 FRENCH 720p WEB x264-RiPiT - DoodStream

Not Found

video you are looking for is not found.

\ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/http:dood.topass_md5145783570-107-168-1747504988-3234b77295e6577aa6813a60d91f2960fh3rdlhjvao7chgxndsi3ra7 b/src/utils/__fixtures__/Fetcher/http:dood.topass_md5145783570-107-168-1747504988-3234b77295e6577aa6813a60d91f2960fh3rdlhjvao7chgxndsi3ra7 new file mode 100644 index 0000000..6e5b435 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/http:dood.topass_md5145783570-107-168-1747504988-3234b77295e6577aa6813a60d91f2960fh3rdlhjvao7chgxndsi3ra7 @@ -0,0 +1 @@ +https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbh7fdq4j4mwzraf7q/1y4sn4ndoi~ \ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/https:frembed.clubapiseriesidtt2085059andsa4andepi2andidTypeimdb b/src/utils/__fixtures__/Fetcher/https:frembed.clubapiseriesidtt2085059andsa4andepi2andidTypeimdb new file mode 100644 index 0000000..9104055 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:frembed.clubapiseriesidtt2085059andsa4andepi2andidTypeimdb @@ -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"}} \ No newline at end of file diff --git a/src/utils/__mocks__/Fetcher.ts b/src/utils/__mocks__/Fetcher.ts index bce0f9f..b6dd500 100644 --- a/src/utils/__mocks__/Fetcher.ts +++ b/src/utils/__mocks__/Fetcher.ts @@ -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, + }; + }; }