feat: memory cache

This commit is contained in:
Viren070
2025-01-26 23:22:11 +00:00
parent 5294fc967e
commit cb13a175c7
9 changed files with 129 additions and 19 deletions
+8
View File
@@ -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 });
+11
View File
@@ -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 });
+1
View File
@@ -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[];
+58
View File
@@ -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);
}
}
}
+12 -1
View File
@@ -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');
}
+1
View File
@@ -2,3 +2,4 @@ export * from './crypto';
export * from './details';
export * from './settings';
export * from './mediaflow';
export * from './cache';
+18 -1
View File
@@ -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(
+3
View File
@@ -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;
+17 -17
View File
@@ -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 {