mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat: optimise stream generation and use new mediaflow endpoints with filename support
This commit is contained in:
+110
-136
@@ -28,12 +28,12 @@ import {
|
||||
} from '@aiostreams/formatters';
|
||||
import {
|
||||
addonDetails,
|
||||
createProxiedMediaFlowUrl,
|
||||
getMediaFlowConfig,
|
||||
getMediaFlowPublicIp,
|
||||
getTimeTakenSincePoint,
|
||||
Settings,
|
||||
createLogger,
|
||||
generateMediaFlowStreams,
|
||||
} from '@aiostreams/utils';
|
||||
import { errorStream } from './responses';
|
||||
|
||||
@@ -508,9 +508,7 @@ export class AIOStreams {
|
||||
|
||||
// Create stream objects
|
||||
const streamsStartTime = new Date().getTime();
|
||||
const streamObjects = await Promise.all(
|
||||
filteredResults.map(this.createStreamObject.bind(this))
|
||||
);
|
||||
const streamObjects = await this.createStreamObjects(filteredResults);
|
||||
streams.push(...streamObjects.filter((s) => s !== null));
|
||||
|
||||
// Add error streams to the end
|
||||
@@ -527,56 +525,6 @@ export class AIOStreams {
|
||||
return streams;
|
||||
}
|
||||
|
||||
private async createMediaFlowStream(
|
||||
parsedStream: ParsedStream,
|
||||
name: string,
|
||||
description: string
|
||||
): Promise<Stream> {
|
||||
if (!parsedStream.url) {
|
||||
logger.error(
|
||||
`Stream URL is missing, cannot proxy a stream without a URL`,
|
||||
{ func: 'createMediaFlowStream' }
|
||||
);
|
||||
throw new Error('Stream URL is missing');
|
||||
}
|
||||
|
||||
const mediaFlowConfig = getMediaFlowConfig(this.config);
|
||||
const proxiedUrl = await createProxiedMediaFlowUrl(
|
||||
parsedStream.url,
|
||||
mediaFlowConfig,
|
||||
parsedStream.stream?.behaviorHints?.proxyHeaders
|
||||
);
|
||||
if (!proxiedUrl) {
|
||||
throw new Error('Could not create MediaFlow proxied URL');
|
||||
}
|
||||
const combinedTags = [
|
||||
parsedStream.resolution,
|
||||
parsedStream.quality,
|
||||
parsedStream.encode,
|
||||
...parsedStream.visualTags,
|
||||
...parsedStream.audioTags,
|
||||
...parsedStream.languages,
|
||||
];
|
||||
|
||||
return {
|
||||
url: proxiedUrl,
|
||||
name: this.config.addonNameInDescription
|
||||
? Settings.ADDON_NAME
|
||||
: `🕵️ ${name}`,
|
||||
description: this.config.addonNameInDescription
|
||||
? `🕵️ ${name.split('\n').join(' ')}\n${description}`
|
||||
: description,
|
||||
subtitles: parsedStream.stream?.subtitles,
|
||||
behaviorHints: {
|
||||
notWebReady: parsedStream.stream?.behaviorHints?.notWebReady,
|
||||
filename: parsedStream.filename,
|
||||
videoSize: Math.floor(parsedStream.size || 0) || undefined,
|
||||
videoHash: parsedStream.stream?.behaviorHints?.videoHash,
|
||||
bingeGroup: `mfp.${Settings.ADDON_ID}|${parsedStream.addon.name}|${combinedTags.join('|')}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private shouldProxyStream(stream: ParsedStream): boolean {
|
||||
const mediaFlowConfig = getMediaFlowConfig(this.config);
|
||||
if (!mediaFlowConfig.mediaFlowEnabled) return false;
|
||||
@@ -608,107 +556,133 @@ export class AIOStreams {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async createStreamObject(
|
||||
parsedStream: ParsedStream
|
||||
): Promise<Stream | null> {
|
||||
let name: string = '';
|
||||
let description: string = '';
|
||||
private getFormattedText(parsedStream: ParsedStream): {
|
||||
name: string;
|
||||
description: string;
|
||||
} {
|
||||
switch (this.config.formatter) {
|
||||
case 'gdrive': {
|
||||
const { name: _name, description: _description } =
|
||||
gdriveFormat(parsedStream);
|
||||
name = _name;
|
||||
description = _description;
|
||||
break;
|
||||
return gdriveFormat(parsedStream, false);
|
||||
}
|
||||
case 'minimalistic-gdrive': {
|
||||
const { name: _name, description: _description } = gdriveFormat(
|
||||
parsedStream,
|
||||
true
|
||||
);
|
||||
name = _name;
|
||||
description = _description;
|
||||
break;
|
||||
return gdriveFormat(parsedStream, true);
|
||||
}
|
||||
case 'imposter': {
|
||||
const { name: _name, description: _description } =
|
||||
imposterFormat(parsedStream);
|
||||
name = _name;
|
||||
description = _description;
|
||||
break;
|
||||
return imposterFormat(parsedStream);
|
||||
}
|
||||
case 'torrentio': {
|
||||
const { name: _name, description: _description } =
|
||||
torrentioFormat(parsedStream);
|
||||
name = _name;
|
||||
description = _description;
|
||||
break;
|
||||
return torrentioFormat(parsedStream);
|
||||
}
|
||||
case 'torbox': {
|
||||
const { name: _name, description: _description } =
|
||||
torboxFormat(parsedStream);
|
||||
name = _name;
|
||||
description = _description;
|
||||
break;
|
||||
return torboxFormat(parsedStream);
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unsupported formatter');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const combinedTags = [
|
||||
parsedStream.resolution,
|
||||
parsedStream.quality,
|
||||
parsedStream.encode,
|
||||
...parsedStream.visualTags,
|
||||
...parsedStream.audioTags,
|
||||
...parsedStream.languages,
|
||||
];
|
||||
private async createStreamObjects(
|
||||
parsedStreams: ParsedStream[]
|
||||
): Promise<Stream[]> {
|
||||
// Step 1: Format all stream metadata
|
||||
let streamObjects: Stream[] = await Promise.all(
|
||||
parsedStreams.map(async (parsedStream) => {
|
||||
const { name, description } = this.getFormattedText(parsedStream);
|
||||
|
||||
let stream: Stream;
|
||||
const shouldProxy = this.shouldProxyStream(parsedStream);
|
||||
if (shouldProxy) {
|
||||
try {
|
||||
const mediaFlowStream = await this.createMediaFlowStream(
|
||||
parsedStream,
|
||||
name,
|
||||
description
|
||||
);
|
||||
if (!mediaFlowStream) {
|
||||
throw new Error('Unknown error creating MediaFlow stream');
|
||||
}
|
||||
return mediaFlowStream;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create MediaFlow stream URL: ${error}`);
|
||||
return null;
|
||||
}
|
||||
const combinedTags = [
|
||||
parsedStream.resolution,
|
||||
parsedStream.quality,
|
||||
parsedStream.encode,
|
||||
...parsedStream.visualTags,
|
||||
...parsedStream.audioTags,
|
||||
...parsedStream.languages,
|
||||
];
|
||||
|
||||
return {
|
||||
url: parsedStream.url,
|
||||
externalUrl: parsedStream.externalUrl,
|
||||
infoHash: parsedStream.torrent?.infoHash,
|
||||
fileIdx: parsedStream.torrent?.fileIdx,
|
||||
name: this.config.addonNameInDescription
|
||||
? Settings.ADDON_NAME
|
||||
: Settings.SHOW_DIE
|
||||
? `🎲 ${name}`
|
||||
: name,
|
||||
description: this.config.addonNameInDescription
|
||||
? `🎲 ${name.split('\n').join(' ')}\n${description}`
|
||||
: description,
|
||||
subtitles: parsedStream.stream?.subtitles,
|
||||
sources: parsedStream.torrent?.sources,
|
||||
behaviorHints: {
|
||||
videoSize: parsedStream.size
|
||||
? Math.floor(parsedStream.size)
|
||||
: undefined,
|
||||
filename: parsedStream.filename,
|
||||
bingeGroup: `${Settings.ADDON_ID}|${parsedStream.addon.name}|${combinedTags.join('|')}`,
|
||||
proxyHeaders: parsedStream.stream?.behaviorHints?.proxyHeaders,
|
||||
notWebReady: parsedStream.stream?.behaviorHints?.notWebReady,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Determine which streams need proxying and remember their indexes
|
||||
const streamsToProxy = parsedStreams
|
||||
.map((stream, index) => ({ stream, index }))
|
||||
.filter(({ stream }) => stream.url !== undefined) // cannot proxy a stream without a URL
|
||||
.filter(({ stream }) => this.shouldProxyStream(stream));
|
||||
|
||||
// Generate proxied URLs
|
||||
const proxiedUrls =
|
||||
streamsToProxy.length > 0
|
||||
? await generateMediaFlowStreams(
|
||||
getMediaFlowConfig(this.config),
|
||||
streamsToProxy.map(({ stream }) => ({
|
||||
url: stream.url!,
|
||||
filename: stream.filename,
|
||||
headers: stream.stream?.behaviorHints?.proxyHeaders,
|
||||
}))
|
||||
)
|
||||
: null;
|
||||
|
||||
if (
|
||||
streamsToProxy &&
|
||||
proxiedUrls &&
|
||||
proxiedUrls.length !== streamsToProxy.length
|
||||
) {
|
||||
logger.error(
|
||||
`Proxied URLs length (${proxiedUrls.length}) does not match streamsToProxy length (${streamsToProxy.length})`
|
||||
);
|
||||
return streamObjects;
|
||||
} else if (streamsToProxy.length > 0 && !proxiedUrls) {
|
||||
logger.error(
|
||||
`Proxied URLs is null, but streamsToProxy length is ${streamsToProxy.length}, filtering out streams that needed proxying`
|
||||
);
|
||||
streamObjects = streamObjects.filter(
|
||||
(_, index) => !streamsToProxy.some((s) => s.index === index)
|
||||
);
|
||||
} else if (proxiedUrls) {
|
||||
// inject proxied URLs back into their original positions
|
||||
streamsToProxy.forEach(({ index }, i) => {
|
||||
streamObjects[index].url = proxiedUrls[i] || streamObjects[index].url;
|
||||
|
||||
streamObjects[index].name = this.config.addonNameInDescription
|
||||
? Settings.ADDON_NAME
|
||||
: `🕵️ ${streamObjects[index].name}`;
|
||||
|
||||
streamObjects[index].description = this.config.addonNameInDescription
|
||||
? `🕵️ ${streamObjects[index].name.split('\n').join(' ')}\n${streamObjects[index].description}`
|
||||
: streamObjects[index].description;
|
||||
|
||||
streamObjects[index].behaviorHints = {
|
||||
...streamObjects[index].behaviorHints,
|
||||
bingeGroup: `mfp.${streamObjects[index].behaviorHints?.bingeGroup}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
stream = {
|
||||
url: parsedStream.url,
|
||||
externalUrl: parsedStream.externalUrl,
|
||||
infoHash: parsedStream.torrent?.infoHash,
|
||||
fileIdx: parsedStream.torrent?.fileIdx,
|
||||
name: this.config.addonNameInDescription
|
||||
? Settings.ADDON_NAME
|
||||
: Settings.SHOW_DIE
|
||||
? `🎲 ${name}`
|
||||
: name,
|
||||
description: this.config.addonNameInDescription
|
||||
? `🎲 ${name.split('\n').join(' ')}\n${description}`
|
||||
: description,
|
||||
subtitles: parsedStream.stream?.subtitles,
|
||||
sources: parsedStream.torrent?.sources,
|
||||
behaviorHints: {
|
||||
videoSize: Math.floor(parsedStream.size || 0) || undefined,
|
||||
filename: parsedStream.filename,
|
||||
bingeGroup: `${Settings.ADDON_ID}|${parsedStream.addon.name}|${combinedTags.join('|')}`,
|
||||
proxyHeaders: parsedStream.stream?.behaviorHints?.proxyHeaders,
|
||||
notWebReady: parsedStream.stream?.behaviorHints?.notWebReady,
|
||||
},
|
||||
};
|
||||
|
||||
return stream;
|
||||
return streamObjects;
|
||||
}
|
||||
|
||||
private compareLanguages(a: ParsedStream, b: ParsedStream) {
|
||||
|
||||
@@ -3,115 +3,86 @@ import path from 'path';
|
||||
import { Settings } from './settings';
|
||||
import { getTextHash } from './crypto';
|
||||
import { Cache } from './cache';
|
||||
import { createLogger } from './logger';
|
||||
import { createLogger, maskSensitiveInfo } from './logger';
|
||||
|
||||
const logger = createLogger('mediaflow');
|
||||
|
||||
const PRIVATE_CIDR = /^(10\.|127\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
|
||||
|
||||
export async function createProxiedMediaFlowUrl(
|
||||
url: string,
|
||||
export async function generateMediaFlowStreams(
|
||||
mediaFlowConfig: Config['mediaFlowConfig'],
|
||||
headers?: {
|
||||
request?: Record<string, string>;
|
||||
response?: Record<string, string>;
|
||||
}
|
||||
) {
|
||||
if (!url) {
|
||||
logger.error('streamUrl is missing, could not create proxied URL');
|
||||
throw new Error('Stream URL is missing');
|
||||
}
|
||||
if (!mediaFlowConfig) {
|
||||
logger.error('mediaFlowConfig is missing');
|
||||
throw new Error('MediaFlow configuration is missing');
|
||||
}
|
||||
if (!mediaFlowConfig?.proxyUrl || !mediaFlowConfig?.apiPassword) {
|
||||
logger.error('mediaFlowUrl or API password is missing');
|
||||
throw new Error('MediaFlow URL or API password is missing');
|
||||
}
|
||||
|
||||
const queryParams: Record<string, string> = {
|
||||
api_password: mediaFlowConfig.apiPassword,
|
||||
};
|
||||
queryParams.d = url;
|
||||
|
||||
const responseHeaders = headers?.response || {
|
||||
'Content-Disposition': `attachment; filename=${path.basename(url)}`,
|
||||
};
|
||||
const requestHeaders = headers?.request || {};
|
||||
|
||||
if (Settings.ENCRYPT_MEDIAFLOW_URLS) {
|
||||
const encryptedUrl = await encryptMediaFlowUrl(
|
||||
url,
|
||||
mediaFlowConfig,
|
||||
responseHeaders,
|
||||
requestHeaders
|
||||
);
|
||||
return encryptedUrl;
|
||||
}
|
||||
|
||||
if (requestHeaders) {
|
||||
Object.entries(requestHeaders).forEach(([key, value]) => {
|
||||
queryParams[`h_${key}`] = value;
|
||||
});
|
||||
}
|
||||
|
||||
if (responseHeaders) {
|
||||
Object.entries(responseHeaders).forEach(([key, value]) => {
|
||||
queryParams[`r_${key}`] = value;
|
||||
});
|
||||
}
|
||||
|
||||
const encodedParams = new URLSearchParams(queryParams).toString();
|
||||
const proxiedUrl = new URL(mediaFlowConfig.proxyUrl.replace(/\/$/, ''));
|
||||
const proxyEndpoint = '/proxy/stream';
|
||||
proxiedUrl.pathname = `${proxiedUrl.pathname === '/' ? '' : proxiedUrl.pathname}${proxyEndpoint}`;
|
||||
proxiedUrl.search = encodedParams;
|
||||
return proxiedUrl.toString();
|
||||
}
|
||||
|
||||
async function encryptMediaFlowUrl(
|
||||
url: string,
|
||||
mediaFlowConfig: Config['mediaFlowConfig'],
|
||||
responseHeaders: Record<string, string>,
|
||||
requestHeaders: Record<string, string>
|
||||
) {
|
||||
streams: {
|
||||
url: string;
|
||||
filename?: string;
|
||||
headers?: {
|
||||
request?: Record<string, string>;
|
||||
response?: Record<string, string>;
|
||||
};
|
||||
}[]
|
||||
): Promise<string[] | null> {
|
||||
if (!mediaFlowConfig) {
|
||||
throw new Error('MediaFlow configuration is missing');
|
||||
}
|
||||
const proxyUrl = new URL(mediaFlowConfig.proxyUrl.replace(/\/$/, ''));
|
||||
const generateEncryptedUrlEndpoint = '/generate_encrypted_or_encoded_url';
|
||||
proxyUrl.pathname = `${proxyUrl.pathname === '/' ? '' : proxyUrl.pathname}${generateEncryptedUrlEndpoint}`;
|
||||
const generateUrlsEndpoint = '/generate_urls';
|
||||
proxyUrl.pathname = `${proxyUrl.pathname === '/' ? '' : proxyUrl.pathname}${generateUrlsEndpoint}`;
|
||||
|
||||
const data = {
|
||||
mediaflow_proxy_url: mediaFlowConfig.proxyUrl.replace(/\/$/, ''),
|
||||
endpoint: '/proxy/stream',
|
||||
destination_url: url,
|
||||
request_headers: requestHeaders,
|
||||
response_headers: responseHeaders,
|
||||
expiration: 3600 * 24, // URL will expire in 24 hours
|
||||
api_password: mediaFlowConfig.apiPassword,
|
||||
api_password: Settings.ENCRYPT_MEDIAFLOW_URLS
|
||||
? mediaFlowConfig.apiPassword
|
||||
: undefined,
|
||||
urls: streams.map((stream) => {
|
||||
return {
|
||||
endpoint: '/proxy/stream',
|
||||
filename: stream.filename || path.basename(stream.url),
|
||||
query_params: Settings.ENCRYPT_MEDIAFLOW_URLS
|
||||
? undefined
|
||||
: {
|
||||
api_password: mediaFlowConfig.apiPassword,
|
||||
},
|
||||
destination_url: stream.url,
|
||||
request_headers: stream.headers?.request,
|
||||
response_headers: stream.headers?.response,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch(proxyUrl.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
signal: AbortSignal.timeout(Settings.MEDIAFLOW_IP_TIMEOUT),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}: ${response.statusText}`);
|
||||
}
|
||||
const responseData = await response.json();
|
||||
if (responseData.error) {
|
||||
throw new Error(responseData.error);
|
||||
}
|
||||
if (responseData.encoded_url) {
|
||||
return responseData.encoded_url;
|
||||
} else {
|
||||
throw new Error('No encrypted or encoded URL returned');
|
||||
try {
|
||||
if (Settings.LOG_SENSITIVE_INFO) {
|
||||
logger.debug(`POST ${proxyUrl.toString()}`);
|
||||
} else {
|
||||
logger.debug(
|
||||
`POST ${proxyUrl.protocol}://${maskSensitiveInfo(proxyUrl.hostname)}${proxyUrl.port ? `:${proxyUrl.port}` : ''}/${generateUrlsEndpoint}`
|
||||
);
|
||||
}
|
||||
const response = await fetch(proxyUrl.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
signal: AbortSignal.timeout(Settings.MEDIAFLOW_IP_TIMEOUT),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
if (responseData.error) {
|
||||
throw new Error(responseData.error);
|
||||
}
|
||||
if (responseData.urls) {
|
||||
return responseData.urls;
|
||||
} else {
|
||||
throw new Error('No encrypted or encoded URL returned');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to encrypt MediaFlow URL using request to ${maskSensitiveInfo(proxyUrl.toString())}: ${error}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +155,7 @@ export async function getMediaFlowPublicIp(
|
||||
}
|
||||
return publicIp;
|
||||
} catch (error: any) {
|
||||
logger.error(`${error.message}`);
|
||||
logger.error(`Failed to get MediaFlow public IP: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user