diff --git a/src/extractor/VixSrc.test.ts b/src/extractor/VixSrc.test.ts new file mode 100644 index 0000000..f170da1 --- /dev/null +++ b/src/extractor/VixSrc.test.ts @@ -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(); + }); +}); diff --git a/src/extractor/VixSrc.ts b/src/extractor/VixSrc.ts new file mode 100644 index 0000000..4a29417 --- /dev/null +++ b/src/extractor/VixSrc.ts @@ -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 { + 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) }), + }, + }, + ]; + }; +} diff --git a/src/extractor/__fixtures__/VixSrc/https:mediaflow-proxy.testextractorvideohostVixCloudandapi_passwordasdfganddhttpspercent3Apercent2Fpercent2Fvixsrc.topercent2Fmoviepercent2F600 b/src/extractor/__fixtures__/VixSrc/https:mediaflow-proxy.testextractorvideohostVixCloudandapi_passwordasdfganddhttpspercent3Apercent2Fpercent2Fvixsrc.topercent2Fmoviepercent2F600 new file mode 100644 index 0000000..37f2578 --- /dev/null +++ b/src/extractor/__fixtures__/VixSrc/https:mediaflow-proxy.testextractorvideohostVixCloudandapi_passwordasdfganddhttpspercent3Apercent2Fpercent2Fvixsrc.topercent2Fmoviepercent2F600 @@ -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"}} diff --git a/src/extractor/__fixtures__/VixSrc/https:mediaflow-proxy.testextractorvideohostVixCloudandapi_passwordasdfganddhttpspercent3Apercent2Fpercent2Fvixsrc.topercent2Ftvpercent2F42009percent2F4percent2F2 b/src/extractor/__fixtures__/VixSrc/https:mediaflow-proxy.testextractorvideohostVixCloudandapi_passwordasdfganddhttpspercent3Apercent2Fpercent2Fvixsrc.topercent2Ftvpercent2F42009percent2F4percent2F2 new file mode 100644 index 0000000..7fe5c64 --- /dev/null +++ b/src/extractor/__fixtures__/VixSrc/https:mediaflow-proxy.testextractorvideohostVixCloudandapi_passwordasdfganddhttpspercent3Apercent2Fpercent2Fvixsrc.topercent2Ftvpercent2F42009percent2F4percent2F2 @@ -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"}} diff --git a/src/extractor/__fixtures__/VixSrc/https:vixsrc.tomovie600 b/src/extractor/__fixtures__/VixSrc/https:vixsrc.tomovie600 new file mode 100644 index 0000000..1336582 --- /dev/null +++ b/src/extractor/__fixtures__/VixSrc/https:vixsrc.tomovie600 @@ -0,0 +1,63 @@ + + + + + + 219736 + + + + + + + + + + + + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/extractor/__fixtures__/VixSrc/https:vixsrc.totv4200942 b/src/extractor/__fixtures__/VixSrc/https:vixsrc.totv4200942 new file mode 100644 index 0000000..3bc17a6 --- /dev/null +++ b/src/extractor/__fixtures__/VixSrc/https:vixsrc.totv4200942 @@ -0,0 +1,63 @@ + + + + + + 307316 + + + + + + + + + + + + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/extractor/__snapshots__/Mixdrop.test.ts.snap b/src/extractor/__snapshots__/Mixdrop.test.ts.snap index 98b748a..dea1bd7 100644 --- a/src/extractor/__snapshots__/Mixdrop.test.ts.snap +++ b/src/extractor/__snapshots__/Mixdrop.test.ts.snap @@ -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", }, ] `; diff --git a/src/extractor/__snapshots__/Uqload.test.ts.snap b/src/extractor/__snapshots__/Uqload.test.ts.snap index 3e65fff..08e1941 100644 --- a/src/extractor/__snapshots__/Uqload.test.ts.snap +++ b/src/extractor/__snapshots__/Uqload.test.ts.snap @@ -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", }, ] `; diff --git a/src/extractor/__snapshots__/VixSrc.test.ts.snap b/src/extractor/__snapshots__/VixSrc.test.ts.snap new file mode 100644 index 0000000..5699a42 --- /dev/null +++ b/src/extractor/__snapshots__/VixSrc.test.ts.snap @@ -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", + }, +] +`; diff --git a/src/extractor/index.ts b/src/extractor/index.ts index 96baed2..b890ef3 100644 --- a/src/extractor/index.ts +++ b/src/extractor/index.ts @@ -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 ]; diff --git a/src/index.ts b/src/index.ts index 1be7527..fe57ee8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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(); diff --git a/src/source/VixSrc.test.ts b/src/source/VixSrc.test.ts new file mode 100644 index 0000000..677a099 --- /dev/null +++ b/src/source/VixSrc.test.ts @@ -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(); + }); +}); diff --git a/src/source/VixSrc.ts b/src/source/VixSrc.ts new file mode 100644 index 0000000..cab54e6 --- /dev/null +++ b/src/source/VixSrc.ts @@ -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 { + 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 }]; + }; +} diff --git a/src/source/__fixtures__/VixSrc/https:api.themoviedb.org3findtt0093058external_sourceimdb_id b/src/source/__fixtures__/VixSrc/https:api.themoviedb.org3findtt0093058external_sourceimdb_id new file mode 100644 index 0000000..9fb7a34 --- /dev/null +++ b/src/source/__fixtures__/VixSrc/https:api.themoviedb.org3findtt0093058external_sourceimdb_id @@ -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":[]} \ No newline at end of file diff --git a/src/source/__fixtures__/VixSrc/https:api.themoviedb.org3findtt2085059external_sourceimdb_id b/src/source/__fixtures__/VixSrc/https:api.themoviedb.org3findtt2085059external_sourceimdb_id new file mode 100644 index 0000000..2cc4c34 --- /dev/null +++ b/src/source/__fixtures__/VixSrc/https:api.themoviedb.org3findtt2085059external_sourceimdb_id @@ -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":[]} \ No newline at end of file diff --git a/src/source/__snapshots__/VixSrc.test.ts.snap b/src/source/__snapshots__/VixSrc.test.ts.snap new file mode 100644 index 0000000..6de845a --- /dev/null +++ b/src/source/__snapshots__/VixSrc.test.ts.snap @@ -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", + }, +] +`; diff --git a/src/source/index.ts b/src/source/index.ts index 70e5592..47080a6 100644 --- a/src/source/index.ts +++ b/src/source/index.ts @@ -10,4 +10,5 @@ export * from './Soaper'; export * from './StreamKiste'; export * from './VerHdLink'; export * from './VidSrc'; +export * from './VixSrc'; export * from './types'; diff --git a/src/utils/media-flow-proxy.ts b/src/utils/media-flow-proxy.ts index edf8327..06bdb95 100644 --- a/src/utils/media-flow-proxy.ts +++ b/src/utils/media-flow-proxy.ts @@ -1,16 +1,47 @@ import { Context } from '../types'; +import { Fetcher } from './Fetcher'; + +interface ExtractResult { + destination_url: string; + request_headers: Record; + mediaflow_proxy_url: string; + query_params: Record; +} 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 => { + 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; +};