From 7c600427e6161253bbb3d3d144bd8fca23b6cd1d Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:30:33 +0000 Subject: [PATCH] feat(extractor): implement Mixdrop via MediaFlow Proxy --- src/extractor/ExtractorRegistry.ts | 2 +- src/extractor/Mixdrop.test.ts | 21 ++ src/extractor/Mixdrop.ts | 56 +++++ .../Mixdrop/https:mixdrop.agf123456789 | 192 ++++++++++++++++ .../Mixdrop/https:mixdrop.myfknq0kj8waq44l8 | 213 ++++++++++++++++++ .../__snapshots__/Mixdrop.test.ts.snap | 19 ++ src/extractor/index.ts | 2 + src/types.ts | 2 +- src/utils/__snapshots__/manifest.test.ts.snap | 48 ++++ src/utils/config.ts | 15 ++ src/utils/manifest.ts | 14 ++ 11 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 src/extractor/Mixdrop.test.ts create mode 100644 src/extractor/Mixdrop.ts create mode 100644 src/extractor/__fixtures__/Mixdrop/https:mixdrop.agf123456789 create mode 100644 src/extractor/__fixtures__/Mixdrop/https:mixdrop.myfknq0kj8waq44l8 create mode 100644 src/extractor/__snapshots__/Mixdrop.test.ts.snap diff --git a/src/extractor/ExtractorRegistry.ts b/src/extractor/ExtractorRegistry.ts index ce9b64b..ada896c 100644 --- a/src/extractor/ExtractorRegistry.ts +++ b/src/extractor/ExtractorRegistry.ts @@ -31,7 +31,7 @@ export class ExtractorRegistry { urlResults = await extractor.extract(ctx, normalizedUrl, countryCode, title); - if (!urlResults.some(urlResult => urlResult.error)) { + if (!urlResults.some(urlResult => urlResult.error) && extractor.ttl) { this.urlResultCache.set(normalizedUrl.href, urlResults, { ttl: extractor.ttl }); } diff --git a/src/extractor/Mixdrop.test.ts b/src/extractor/Mixdrop.test.ts new file mode 100644 index 0000000..4151823 --- /dev/null +++ b/src/extractor/Mixdrop.test.ts @@ -0,0 +1,21 @@ +import winston from 'winston'; +import { FetcherMock } from '../utils'; +import { CountryCode } from '../types'; +import { ExtractorRegistry } from './ExtractorRegistry'; +import { Mixdrop } from './Mixdrop'; +import { createTestContext } from '../test'; + +const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }); +const extractorRegistry = new ExtractorRegistry(logger, [new Mixdrop(new FetcherMock(`${__dirname}/__fixtures__/Mixdrop`))]); + +const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' }); + +describe('Mixdrop', () => { + test('mixdrop.my /e/', async () => { + expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'), CountryCode.de)).toMatchSnapshot(); + }); + + test('deleted or expired file', async () => { + expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'), CountryCode.de)).toMatchSnapshot(); + }); +}); diff --git a/src/extractor/Mixdrop.ts b/src/extractor/Mixdrop.ts new file mode 100644 index 0000000..36bf15e --- /dev/null +++ b/src/extractor/Mixdrop.ts @@ -0,0 +1,56 @@ +import bytes from 'bytes'; +import * as cheerio from 'cheerio'; +import { Extractor } from './Extractor'; +import { buildMediaFlowProxyUrl, Fetcher, hasMediaFlowProxy } from '../utils'; +import { Context, CountryCode, UrlResult } from '../types'; +import { NotFoundError } from '../error'; + +export class Mixdrop extends Extractor { + public readonly id = 'mixdrop'; + + public readonly label = 'Mixdrop'; + + public override readonly ttl = 0; + + private readonly fetcher: Fetcher; + + public constructor(fetcher: Fetcher) { + super(); + + this.fetcher = fetcher; + } + + public supports(ctx: Context, url: URL): boolean { + return null !== url.host.match(/mixdrop/) && hasMediaFlowProxy(ctx.config); + } + + protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise { + const fileUrl = new URL(url.href.replace('/e/', '/f/')); + const html = await this.fetcher.text(ctx, fileUrl); + + if (/can't find the (file|video)/.test(html)) { + throw new NotFoundError(); + } + + const sizeMatch = html.match(/([\d.,]+ ?[GM]B)/); + + const $ = cheerio.load(html); + const title = $('.title b').text().trim(); + + return [ + { + url: buildMediaFlowProxyUrl(ctx.config, 'Mixdrop', url), + label: this.label, + sourceId: `${this.id}_${countryCode}`, + ttl: this.ttl, + meta: { + countryCode, + title, + ...(sizeMatch && { + bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number, + }), + }, + }, + ]; + }; +} diff --git a/src/extractor/__fixtures__/Mixdrop/https:mixdrop.agf123456789 b/src/extractor/__fixtures__/Mixdrop/https:mixdrop.agf123456789 new file mode 100644 index 0000000..440075b --- /dev/null +++ b/src/extractor/__fixtures__/Mixdrop/https:mixdrop.agf123456789 @@ -0,0 +1,192 @@ + + + + + + +MixDrop - File not found + + + + + + + + + + + + + + + + + + +
+
+
+ + + + +
+
+
+
+ +

WE ARE SORRY

+

We can't find the file you are looking for.

+
+ +
+
+
+ + +
+
+
Uploads ( 0 / 0 )
+
+
+
+
+
+
+ + + + + + + + + + +
+ LOGIN + SIGNUP + HOME + USER PANEL + SHARE + EMBED + AFFILIATE + FAQ + NEWS + CONTACT +
+ + + + + + + + + + + + + + + diff --git a/src/extractor/__fixtures__/Mixdrop/https:mixdrop.myfknq0kj8waq44l8 b/src/extractor/__fixtures__/Mixdrop/https:mixdrop.myfknq0kj8waq44l8 new file mode 100644 index 0000000..c11322c --- /dev/null +++ b/src/extractor/__fixtures__/Mixdrop/https:mixdrop.myfknq0kj8waq44l8 @@ -0,0 +1,213 @@ + + + + + + +MixDrop - Watch 28_giorni_dopo_2002_HD_-_Altadefinizione01 + + + + + + + + + + + + + + + + + + +
+
+
+ + + + +
+
+
+
+
+ +
+ +
+
+
+ 28_giorni_dopo_2002_HD_-_Altadefinizione01.mp4 1,010.71 MB
+ +
+
+
+ +
+ DOWNLOAD +
+
+
+
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
Uploads ( 0 / 0 )
+
+
+
+
+
+
+ + + + + + + + + + +
+ LOGIN + SIGNUP + HOME + USER PANEL + SHARE + EMBED + AFFILIATE + FAQ + NEWS + CONTACT +
+ + + + + + + + + + + + + + + diff --git a/src/extractor/__snapshots__/Mixdrop.test.ts.snap b/src/extractor/__snapshots__/Mixdrop.test.ts.snap new file mode 100644 index 0000000..89d6b84 --- /dev/null +++ b/src/extractor/__snapshots__/Mixdrop.test.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Mixdrop deleted or expired file 1`] = `[]`; + +exports[`Mixdrop mixdrop.my /e/ 1`] = ` +[ + { + "label": "Mixdrop", + "meta": { + "bytes": 1059806248, + "countryCode": "de", + "title": "28_giorni_dopo_2002_HD_-_Altadefinizione01.mp4", + }, + "sourceId": "mixdrop_de", + "ttl": 0, + "url": "https://mediaflow-proxy.test/extractor/video?host=Mixdrop&redirect_stream=true&api_password=asdfg&d=https%3A%2F%2Fmixdrop.my%2Fe%2Fknq0kj8waq44l8", + }, +] +`; diff --git a/src/extractor/index.ts b/src/extractor/index.ts index 6f9ba04..48e83e6 100644 --- a/src/extractor/index.ts +++ b/src/extractor/index.ts @@ -8,6 +8,7 @@ import { Soaper } from './Soaper'; import { SuperVideo } from './SuperVideo'; import { VidSrc } from './VidSrc'; import { Fetcher } from '../utils'; +import { Mixdrop } from './Mixdrop'; export * from './Extractor'; export * from './ExtractorRegistry'; @@ -18,6 +19,7 @@ export const createExtractors = (fetcher: Fetcher): Extractor[] => [ new Fsst(fetcher), new SuperVideo(fetcher), new KinoGer(fetcher), + new Mixdrop(fetcher), new Soaper(fetcher), new VidSrc(fetcher), new ExternalUrl(fetcher), // fallback extractor which must come last diff --git a/src/types.ts b/src/types.ts index 2245f89..123e4d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,7 @@ export interface Context { export type ManifestWithConfig = Manifest & { config: ManifestConfig[] }; -export type Config = Partial>; +export type Config = Partial>; export enum CountryCode { de = 'de', diff --git a/src/utils/__snapshots__/manifest.test.ts.snap b/src/utils/__snapshots__/manifest.test.ts.snap index 53ea9c6..5aba05f 100644 --- a/src/utils/__snapshots__/manifest.test.ts.snap +++ b/src/utils/__snapshots__/manifest.test.ts.snap @@ -8,6 +8,18 @@ exports[`buildManifest has checked includeExternalUrls 1`] = ` "title": "Include external URLs from results", "type": "checkbox", }, + { + "default": "", + "key": "mediaFlowProxyUrl", + "title": "MediaFlow Proxy URL", + "type": "text", + }, + { + "default": "", + "key": "mediaFlowProxyPassword", + "title": "MediaFlow Proxy Password", + "type": "password", + }, ] `; @@ -35,6 +47,18 @@ exports[`buildManifest has checked source with appropriate config 1`] = ` "title": "Include external URLs from results", "type": "checkbox", }, + { + "default": "", + "key": "mediaFlowProxyUrl", + "title": "MediaFlow Proxy URL", + "type": "text", + }, + { + "default": "", + "key": "mediaFlowProxyPassword", + "title": "MediaFlow Proxy Password", + "type": "password", + }, ] `; @@ -60,6 +84,18 @@ exports[`buildManifest has unchecked source without a config 1`] = ` "title": "Include external URLs from results", "type": "checkbox", }, + { + "default": "", + "key": "mediaFlowProxyUrl", + "title": "MediaFlow Proxy URL", + "type": "text", + }, + { + "default": "", + "key": "mediaFlowProxyPassword", + "title": "MediaFlow Proxy Password", + "type": "password", + }, ] `; @@ -70,5 +106,17 @@ exports[`buildManifest includeExternalUrls is unchecked by default 1`] = ` "title": "Include external URLs from results", "type": "checkbox", }, + { + "default": "", + "key": "mediaFlowProxyUrl", + "title": "MediaFlow Proxy URL", + "type": "text", + }, + { + "default": "", + "key": "mediaFlowProxyPassword", + "title": "MediaFlow Proxy Password", + "type": "password", + }, ] `; diff --git a/src/utils/config.ts b/src/utils/config.ts index 0181cbe..b0e2411 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -5,3 +5,18 @@ export const getDefaultConfig = (): Config => { }; export const showExternalUrls = (config: Config): boolean => 'includeExternalUrls' in config; + +export const hasMediaFlowProxy = (config: Config): boolean => !!(config['mediaFlowProxyUrl'] && config['mediaFlowProxyPassword']); + +export const buildMediaFlowProxyUrl = (config: Config, host: string, url: URL): URL => { + const mediaFlowProxyUrl = new URL(`${config.mediaFlowProxyUrl}/extractor/video`); + + const params = new URLSearchParams(); + params.append('host', host); + params.append('redirect_stream', 'true'); + params.append('api_password', `${config.mediaFlowProxyPassword}`); + params.append('d', url.href); + mediaFlowProxyUrl.search = params.toString(); + + return mediaFlowProxyUrl; +}; diff --git a/src/utils/manifest.ts b/src/utils/manifest.ts index a8984f5..a1ed323 100644 --- a/src/utils/manifest.ts +++ b/src/utils/manifest.ts @@ -57,5 +57,19 @@ export const buildManifest = (sources: Source[], config: Config): ManifestWithCo ...('includeExternalUrls' in config && { default: 'checked' }), }); + manifest.config.push({ + key: 'mediaFlowProxyUrl', + type: 'text', + title: 'MediaFlow Proxy URL', + default: config['mediaFlowProxyUrl'] ?? '', + }); + + manifest.config.push({ + key: 'mediaFlowProxyPassword', + type: 'password', + title: 'MediaFlow Proxy Password', + default: config['mediaFlowProxyPassword'] ?? '', + }); + return manifest; };