feat(extractor): implement VixSrc via MediaFlow Proxy

This commit is contained in:
WebStreamr 2025-06-24 19:07:23 +00:00
parent 7ea57222d9
commit f12e3c5bd8
No known key found for this signature in database
18 changed files with 356 additions and 11 deletions

View file

@ -0,0 +1,21 @@
import winston from 'winston';
import { FetcherMock } from '../utils';
import { CountryCode } from '../types';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VixSrc } from './VixSrc';
import { createTestContext } from '../test';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new VixSrc(new FetcherMock(`${__dirname}/__fixtures__/VixSrc`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
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();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'), CountryCode.it, 'Black Mirror 4x2')).toMatchSnapshot();
});
});

48
src/extractor/VixSrc.ts Normal file
View file

@ -0,0 +1,48 @@
import bytes from 'bytes';
import { Extractor } from './Extractor';
import { Fetcher, buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
import { Context, CountryCode, Format, UrlResult } from '../types';
export class VixSrc extends Extractor {
public readonly id = 'vixsrc';
public readonly label = 'VixSrc (via MediaFlow Proxy)';
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(/vixsrc/) && supportsMediaFlowProxy(ctx);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const filenameMatch = html.match(/"filename":"(.*?)"/);
const sizeMatch = html.match(/"size":(\d+)/);
const qualityMatch = html.match(/"quality":(\d+)/);
return [
{
url: await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'VixCloud', url),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCode,
...(filenameMatch && { title: filenameMatch[1] }),
...(sizeMatch && { bytes: bytes.parse(`${sizeMatch[1]} kb`) as number }),
...(qualityMatch && { height: parseInt(qualityMatch[1] as string) }),
},
},
];
};
}

View file

@ -0,0 +1 @@
{"destination_url":"https://vixsrc.to/playlist/219736?token=689cf5587c2fec67fd83503a2a7dfd64&expires=1755949827&h=1","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://vixsrc.to/movie/600"},"mediaflow_proxy_url":"https://mediaflow-proxy.test/proxy/hls/manifest.m3u8","query_params":{"api_password":"asdfg"}}

View file

@ -0,0 +1 @@
{"destination_url":"https://vixsrc.to/playlist/307316?token=2f5004a3b15f4ccdb3446f2b132a0957&expires=1755949828&h=1","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://vixsrc.to/tv/42009/4/2"},"mediaflow_proxy_url":"https://mediaflow-proxy.test/proxy/hls/manifest.m3u8","query_params":{"api_password":"asdfg"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@ exports[`Mixdrop mixdrop.my /e/ 1`] = `
},
"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",
"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

@ -12,7 +12,7 @@ exports[`Uqload uqload.net / 1`] = `
},
"sourceId": "uqload_fr",
"ttl": 0,
"url": "https://mediaflow-proxy.test/extractor/video?host=Uqload&redirect_stream=true&api_password=asdfg&d=https%3A%2F%2Fuqload.net%2Fz0xbr87oz637.html",
"url": "https://mediaflow-proxy.test/extractor/video?host=Uqload&api_password=asdfg&d=https%3A%2F%2Fuqload.net%2Fz0xbr87oz637.html&redirect_stream=true",
},
]
`;
@ -29,7 +29,7 @@ exports[`Uqload uqload.net /embed- 1`] = `
},
"sourceId": "uqload_fr",
"ttl": 0,
"url": "https://mediaflow-proxy.test/extractor/video?host=Uqload&redirect_stream=true&api_password=asdfg&d=https%3A%2F%2Fuqload.net%2Fz0xbr87oz637.html",
"url": "https://mediaflow-proxy.test/extractor/video?host=Uqload&api_password=asdfg&d=https%3A%2F%2Fuqload.net%2Fz0xbr87oz637.html&redirect_stream=true",
},
]
`;

View file

@ -0,0 +1,37 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`VixSrc Black Mirror 1`] = `
[
{
"format": "hls",
"label": "VixSrc (via MediaFlow Proxy)",
"meta": {
"bytes": 2389338112,
"countryCode": "it",
"height": 1080,
"title": "Black.Mirror.S04E02.Arkangel.1080p.NF.WEB-DL.ITA.ENG.DDP5.1.H.264.mkv",
},
"sourceId": "vixsrc_it",
"ttl": 0,
"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",
},
]
`;
exports[`VixSrc Full Metal Jacket 1`] = `
[
{
"format": "hls",
"label": "VixSrc (via MediaFlow Proxy)",
"meta": {
"bytes": 8544718848,
"countryCode": "it",
"height": 1080,
"title": "Full.metal.jacket.1987.1080p.x264.AC3.ITA.AAC.ENG.Subs.mkv",
},
"sourceId": "vixsrc_it",
"ttl": 0,
"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

@ -10,6 +10,7 @@ import { Soaper } from './Soaper';
import { SuperVideo } from './SuperVideo';
import { Uqload } from './Uqload';
import { VidSrc } from './VidSrc';
import { VixSrc } from './VixSrc';
export * from './Extractor';
export * from './ExtractorRegistry';
@ -24,5 +25,6 @@ export const createExtractors = (fetcher: Fetcher): Extractor[] => [
new Soaper(fetcher),
new Uqload(fetcher),
new VidSrc(fetcher),
new VixSrc(fetcher),
new ExternalUrl(fetcher), // fallback extractor which must come last
];

View file

@ -1,7 +1,7 @@
import express, { NextFunction, Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import winston from 'winston';
import { CineHDPlus, Cuevana, Eurostreaming, Frembed, FrenchCloud, Source, KinoGer, MeineCloud, MostraGuarda, Soaper, StreamKiste, VerHdLink, VidSrc } from './source';
import { CineHDPlus, Cuevana, Eurostreaming, Frembed, FrenchCloud, Source, KinoGer, MeineCloud, MostraGuarda, Soaper, StreamKiste, VerHdLink, VidSrc, VixSrc } from './source';
import { createExtractors, ExtractorRegistry } from './extractor';
import { ConfigureController, ManifestController, StreamController } from './controller';
import { envGet, envIsProd, Fetcher, StreamResolver, tmdbFetch, TmdbId } from './utils';
@ -38,6 +38,7 @@ const sources: Source[] = [
// IT
new Eurostreaming(fetcher),
new MostraGuarda(fetcher),
new VixSrc(fetcher),
];
const addon = express();

23
src/source/VixSrc.test.ts Normal file
View file

@ -0,0 +1,23 @@
import { VixSrc } from './VixSrc';
import { FetcherMock, ImdbId } from '../utils';
import { createTestContext } from '../test';
const ctx = createTestContext();
describe('VixSrc', () => {
let handler: VixSrc;
beforeEach(() => {
handler = new VixSrc(new FetcherMock(`${__dirname}/__fixtures__/VixSrc`));
});
test('handle imdb black mirror s4e2', async () => {
const streams = await handler.handle(ctx, 'series', new ImdbId('tt2085059', 4, 2));
expect(streams).toMatchSnapshot();
});
test('handle imdb full metal jacket', async () => {
const streams = await handler.handle(ctx, 'series', new ImdbId('tt0093058', undefined, undefined));
expect(streams).toMatchSnapshot();
});
});

32
src/source/VixSrc.ts Normal file
View file

@ -0,0 +1,32 @@
import { ContentType } from 'stremio-addon-sdk';
import { Source, SourceResult } from './types';
import { Fetcher, getTmdbId, Id } from '../utils';
import { Context, CountryCode } from '../types';
export class VixSrc implements Source {
public readonly id = 'vixsrc';
public readonly label = 'VixSrc';
public readonly contentTypes: ContentType[] = ['movie', 'series'];
public readonly countryCodes: CountryCode[] = [CountryCode.it];
private readonly baseUrl = 'https://vixsrc.to';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const url = tmdbId.season
? new URL(`/tv/${tmdbId.id}/${tmdbId.season}/${tmdbId.episode}`, this.baseUrl)
: new URL(`/movie/${tmdbId.id}`, this.baseUrl);
return [{ countryCode: CountryCode.it, url }];
};
}

View file

@ -0,0 +1 @@
{"movie_results":[{"adult":false,"backdrop_path":"/3k2TRmqMjgt7tcwkYwZQdctnty3.jpg","id":600,"title":"Full Metal Jacket","original_title":"Full Metal Jacket","overview":"A pragmatic U.S. Marine observes the dehumanizing effects the U.S.-Vietnam War has on his fellow recruits from their brutal boot camp training to the bloody street fighting in Hue.","poster_path":"/kMKyx1k8hWWscYFnPbnxxN4Eqo4.jpg","media_type":"movie","original_language":"en","genre_ids":[18,10752],"popularity":8.9033,"release_date":"1987-06-26","video":false,"vote_average":8.122,"vote_count":10846}],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}

View file

@ -0,0 +1 @@
{"movie_results":[],"person_results":[],"tv_results":[{"adult":false,"backdrop_path":"/dg3OindVAGZBjlT3xYKqIAdukPL.jpg","id":42009,"name":"Black Mirror","original_name":"Black Mirror","overview":"Twisted tales run wild in this mind-bending anthology series that reveals humanity's worst traits, greatest innovations and more.","poster_path":"/seN6rRfN0I6n8iDXjlSMk1QjNcq.jpg","media_type":"tv","original_language":"en","genre_ids":[10765,18,9648],"popularity":107.277,"first_air_date":"2011-12-04","vote_average":8.283,"vote_count":5543,"origin_country":["GB"]}],"tv_episode_results":[],"tv_season_results":[]}

View file

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`VixSrc handle imdb black mirror s4e2 1`] = `
[
{
"countryCode": "it",
"url": "https://vixsrc.to/tv/42009/4/2",
},
]
`;
exports[`VixSrc handle imdb full metal jacket 1`] = `
[
{
"countryCode": "it",
"url": "https://vixsrc.to/movie/600",
},
]
`;

View file

@ -10,4 +10,5 @@ export * from './Soaper';
export * from './StreamKiste';
export * from './VerHdLink';
export * from './VidSrc';
export * from './VixSrc';
export * from './types';

View file

@ -1,16 +1,47 @@
import { Context } from '../types';
import { Fetcher } from './Fetcher';
interface ExtractResult {
destination_url: string;
request_headers: Record<string, string>;
mediaflow_proxy_url: string;
query_params: Record<string, string>;
}
export const supportsMediaFlowProxy = (ctx: Context): boolean => !!(ctx.config['mediaFlowProxyUrl'] && ctx.config['mediaFlowProxyPassword']);
export const buildMediaFlowProxyExtractorRedirectUrl = (ctx: Context, host: string, url: URL): URL => {
const buildMediaFlowProxyExtractorUrl = (ctx: Context, host: string, url: URL): URL => {
const mediaFlowProxyUrl = new URL(`${ctx.config.mediaFlowProxyUrl}/extractor/video`);
const params = new URLSearchParams();
params.append('host', host);
params.append('redirect_stream', 'true');
params.append('api_password', `${ctx.config.mediaFlowProxyPassword}`);
params.append('d', url.href);
mediaFlowProxyUrl.search = params.toString();
mediaFlowProxyUrl.searchParams.append('host', host);
mediaFlowProxyUrl.searchParams.append('api_password', `${ctx.config.mediaFlowProxyPassword}`);
mediaFlowProxyUrl.searchParams.append('d', url.href);
return mediaFlowProxyUrl;
};
export const buildMediaFlowProxyExtractorRedirectUrl = (ctx: Context, host: string, url: URL): URL => {
const mediaFlowProxyUrl = buildMediaFlowProxyExtractorUrl(ctx, host, url);
mediaFlowProxyUrl.searchParams.append('redirect_stream', 'true');
return mediaFlowProxyUrl;
};
export const buildMediaFlowProxyExtractorStreamUrl = async (ctx: Context, fetcher: Fetcher, host: string, url: URL): Promise<URL> => {
const mediaFlowProxyUrl = buildMediaFlowProxyExtractorUrl(ctx, host, url);
const extractResult: ExtractResult = JSON.parse(await fetcher.text(ctx, mediaFlowProxyUrl));
const streamUrl = new URL(extractResult.mediaflow_proxy_url);
for (const queryParamsKey in extractResult.query_params) {
streamUrl.searchParams.append(queryParamsKey, extractResult.query_params[queryParamsKey] as string);
}
for (const requestHeadersKey in extractResult.request_headers) {
streamUrl.searchParams.append(`h_${requestHeadersKey}`, extractResult.request_headers[requestHeadersKey] as string);
}
streamUrl.searchParams.append('d', extractResult.destination_url);
return streamUrl;
};