mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat: memory cache
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
addonDetails,
|
||||
compressAndEncrypt,
|
||||
parseAndDecryptString,
|
||||
Cache,
|
||||
} from '@aiostreams/utils';
|
||||
|
||||
const app = express();
|
||||
@@ -55,6 +56,8 @@ if (Settings.CUSTOM_CONFIGS) {
|
||||
}
|
||||
}
|
||||
|
||||
const cache = new Cache(Settings.MAX_CACHE_SIZE);
|
||||
|
||||
// Built-in middleware for parsing JSON
|
||||
app.use(express.json());
|
||||
// Built-in middleware for parsing URL-encoded data
|
||||
@@ -73,6 +76,10 @@ app.get('/', (req, res) => {
|
||||
res.redirect('/configure');
|
||||
});
|
||||
|
||||
app.get('/cache-stats', (req, res) => {
|
||||
res.send(cache.stats());
|
||||
});
|
||||
|
||||
app.get(
|
||||
['/_next/*', '/assets/*', '/icon.ico', '/configure.txt'],
|
||||
(req, res) => {
|
||||
@@ -185,6 +192,7 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
configJson.requestingIp = req.get('CF-Connecting-IP') || req.ip;
|
||||
configJson.instanceCache = cache;
|
||||
const aioStreams = new AIOStreams(configJson);
|
||||
aioStreams.getStreams(streamRequest).then((streams) => {
|
||||
res.json({ streams: streams });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AIOStreams, errorResponse, validateConfig } from '@aiostreams/addon';
|
||||
import manifest from '@aiostreams/addon/src/manifest';
|
||||
import { Config, StreamRequest } from '@aiostreams/types';
|
||||
import { Cache } from '@aiostreams/utils';
|
||||
|
||||
const HEADERS = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
@@ -20,6 +21,8 @@ function createResponse(message: string, status: number): Response {
|
||||
});
|
||||
}
|
||||
|
||||
const cache = new Cache(1024);
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx): Promise<Response> {
|
||||
try {
|
||||
@@ -116,6 +119,14 @@ export default {
|
||||
|
||||
let streamRequest: StreamRequest = { id, type };
|
||||
|
||||
decodedConfig.requestingIp =
|
||||
request.headers.get('X-Forwarded-For') ||
|
||||
request.headers.get('CF-Connecting-IP') ||
|
||||
request.headers.get('X-Real-IP') ||
|
||||
request.headers.get('X-Client-IP') ||
|
||||
undefined;
|
||||
decodedConfig.instanceCache = cache;
|
||||
|
||||
const aioStreams = new AIOStreams(decodedConfig);
|
||||
const streams = await aioStreams.getStreams(streamRequest);
|
||||
return createJsonResponse({ streams });
|
||||
|
||||
@@ -96,6 +96,7 @@ export type Encode = { [key: string]: boolean };
|
||||
export type SortBy = { [key: string]: boolean | string | undefined };
|
||||
|
||||
export interface Config {
|
||||
instanceCache?: any;
|
||||
requestingIp?: string;
|
||||
resolutions: Resolution[];
|
||||
qualities: Quality[];
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
class CacheItem<T> {
|
||||
constructor(
|
||||
public value: T,
|
||||
public lastAccessed: number,
|
||||
public ttl: number // Time-To-Live in milliseconds
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Cache<K, V> {
|
||||
private cache: Map<K, CacheItem<V>>;
|
||||
private maxSize: number;
|
||||
|
||||
constructor(maxSize: number) {
|
||||
this.cache = new Map<K, CacheItem<V>>();
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
stats(): string {
|
||||
return `Cache size: ${this.cache.size}`;
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
const item = this.cache.get(key);
|
||||
if (item) {
|
||||
const now = Date.now();
|
||||
if (now - item.lastAccessed > item.ttl) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
item.lastAccessed = now;
|
||||
return item.value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set(key: K, value: V, ttl: number): void {
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
this.evict();
|
||||
}
|
||||
this.cache.set(key, new CacheItem(value, Date.now(), ttl * 1000));
|
||||
}
|
||||
|
||||
private evict(): void {
|
||||
let oldestKey: K | undefined;
|
||||
let oldestTime = Infinity;
|
||||
|
||||
for (const [key, item] of this.cache.entries()) {
|
||||
if (item.lastAccessed < oldestTime) {
|
||||
oldestTime = item.lastAccessed;
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestKey !== undefined) {
|
||||
this.cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { randomBytes, createCipheriv, createDecipheriv } from 'crypto';
|
||||
import {
|
||||
randomBytes,
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
createHash,
|
||||
} from 'crypto';
|
||||
import { deflateSync, inflateSync } from 'zlib';
|
||||
import { Settings } from './settings';
|
||||
|
||||
@@ -78,3 +83,9 @@ export function parseAndDecryptString(data: string): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTextHash(text: string): string {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(text);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './crypto';
|
||||
export * from './details';
|
||||
export * from './settings';
|
||||
export * from './mediaflow';
|
||||
export * from './cache';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Config } from '@aiostreams/types';
|
||||
import path from 'path';
|
||||
import { Settings } from './settings';
|
||||
import { getTextHash } from './crypto';
|
||||
import { Cache } from './cache';
|
||||
|
||||
const PRIVATE_CIDR = /^(10\.|127\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
|
||||
|
||||
@@ -62,7 +64,8 @@ export function createProxiedMediaFlowUrl(
|
||||
}
|
||||
|
||||
export async function getMediaFlowPublicIp(
|
||||
mediaFlowConfig: Config['mediaFlowConfig']
|
||||
mediaFlowConfig: Config['mediaFlowConfig'],
|
||||
cache: Cache<string, string>
|
||||
) {
|
||||
try {
|
||||
if (!mediaFlowConfig) {
|
||||
@@ -92,6 +95,17 @@ export async function getMediaFlowPublicIp(
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = getTextHash(
|
||||
`mediaFlowPublicIp:${mediaFlowConfig.proxyUrl}:${mediaFlowConfig.apiPassword}`
|
||||
);
|
||||
const cachedPublicIp = cache.get(cacheKey);
|
||||
if (cachedPublicIp) {
|
||||
console.debug(
|
||||
`|DBG| mediaflow > getMediaFlowPublicIp > Returning cached public IP`
|
||||
);
|
||||
return cachedPublicIp;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
'|DBG| mediaflow > getMediaFlowPublicIp > GET /proxy/ip?api_password=***'
|
||||
);
|
||||
@@ -117,6 +131,9 @@ export async function getMediaFlowPublicIp(
|
||||
|
||||
const data = await response.json();
|
||||
const publicIp = data.ip;
|
||||
if (publicIp) {
|
||||
cache.set(cacheKey, publicIp, 900);
|
||||
}
|
||||
return publicIp;
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
|
||||
@@ -50,6 +50,9 @@ export class Settings {
|
||||
process.env.DEFAULT_MEDIAFLOW_API_PASSWORD ?? '';
|
||||
public static readonly DEFAULT_MEDIAFLOW_PUBLIC_IP =
|
||||
process.env.DEFAULT_MEDIAFLOW_PUBLIC_IP ?? '';
|
||||
public static readonly MAX_CACHE_SIZE = process.env.MAX_CACHE_SIZE
|
||||
? parseInt(process.env.MAX_CACHE_SIZE)
|
||||
: 1024;
|
||||
public static readonly MAX_ADDONS = process.env.MAX_ADDONS
|
||||
? parseInt(process.env.MAX_ADDONS)
|
||||
: 15;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { parseFilename } from '@aiostreams/parser';
|
||||
import {
|
||||
getMediaFlowConfig,
|
||||
getMediaFlowPublicIp,
|
||||
getTextHash,
|
||||
serviceDetails,
|
||||
Settings,
|
||||
} from '@aiostreams/utils';
|
||||
@@ -67,7 +68,10 @@ export class BaseWrapper {
|
||||
let userIp = this.userConfig.requestingIp;
|
||||
const mediaFlowConfig = getMediaFlowConfig(this.userConfig);
|
||||
if (mediaFlowConfig.mediaFlowEnabled) {
|
||||
const mediaFlowIp = await getMediaFlowPublicIp(mediaFlowConfig);
|
||||
const mediaFlowIp = await getMediaFlowPublicIp(
|
||||
mediaFlowConfig,
|
||||
this.userConfig.instanceCache
|
||||
);
|
||||
if (!mediaFlowIp) {
|
||||
throw new Error('Failed to get public IP from MediaFlow');
|
||||
}
|
||||
@@ -83,6 +87,17 @@ export class BaseWrapper {
|
||||
}, this.indexerTimeout);
|
||||
|
||||
const url = this.getStreamUrl(streamRequest);
|
||||
const cache = this.userConfig.instanceCache;
|
||||
const requestCacheKey = getTextHash(url);
|
||||
const cachedStreams = cache.get(requestCacheKey);
|
||||
const sanitisedUrl =
|
||||
new URL(url).hostname + '/****/' + new URL(url).pathname.split('/').pop();
|
||||
if (cachedStreams) {
|
||||
console.debug(
|
||||
`|DBG| wrappers > base > ${this.addonName}: Returning cached streams for ${sanitisedUrl}`
|
||||
);
|
||||
return cachedStreams;
|
||||
}
|
||||
try {
|
||||
// Add requesting IP to headers
|
||||
const headers = new Headers();
|
||||
@@ -97,7 +112,6 @@ export class BaseWrapper {
|
||||
headers.set('X-Real-IP', userIp);
|
||||
}
|
||||
const urlParts = url.split('/');
|
||||
const sanitisedUrl = `${urlParts[0]}//${urlParts[2]}/*************/${urlParts.slice(-3).join('/')}`;
|
||||
console.log(
|
||||
`|INF| wrappers > base > ${this.addonName}: Fetching with timeout ${this.indexerTimeout}ms from ${sanitisedUrl}`
|
||||
);
|
||||
@@ -143,6 +157,7 @@ export class BaseWrapper {
|
||||
if (!results.streams) {
|
||||
throw new Error('Failed to respond with streams');
|
||||
}
|
||||
cache.set(requestCacheKey, results.streams, 300); // cache for 5 minutes
|
||||
return results.streams;
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeout);
|
||||
@@ -215,9 +230,6 @@ export class BaseWrapper {
|
||||
let description = stream.description || stream.title;
|
||||
|
||||
if (!filename && description) {
|
||||
console.log(
|
||||
`|DBG| wrappers > base > parseStream: No filename found in behaviorHints, attempting to parse from description`
|
||||
);
|
||||
const lines = description.split('\n');
|
||||
filename =
|
||||
lines.find(
|
||||
@@ -226,13 +238,6 @@ export class BaseWrapper {
|
||||
/(?<![^ [_(\-.]])(?:s(?:eason)?[ .\-_]?(\d+)[ .\-_]?(?:e(?:pisode)?[ .\-_]?(\d+))?|(\d+)[xX](\d+))(?![^ \])_.-])/
|
||||
) || line.match(/(?<![^ [_(\-.])(\d{4})(?=[ \])_.-]|$)/i)
|
||||
) || lines[0];
|
||||
console.log(
|
||||
`|DBG| wrappers > base > parseStream: With description: ${description.replace(/\n/g, ' ').trim()}, chose filename as: ${filename.replace(/\n/g, ' ').trim()}`
|
||||
);
|
||||
} else if (!description) {
|
||||
console.log(
|
||||
`|WRN| wrappers > base > parseStream: No description found, filename could not be determined`
|
||||
);
|
||||
}
|
||||
|
||||
let stringToParse: string = filename || description || '';
|
||||
@@ -358,11 +363,6 @@ export class BaseWrapper {
|
||||
};
|
||||
}
|
||||
});
|
||||
if (!provider) {
|
||||
console.log(
|
||||
`|WRN| wrappers > base > parseServiceData: No provider found for ${string}`
|
||||
);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
protected extractSizeInBytes(string: string, k: number): number {
|
||||
|
||||
Reference in New Issue
Block a user