180 lines
4.3 KiB
TypeScript
180 lines
4.3 KiB
TypeScript
import redis from '../config/redis.js';
|
|
import { env } from '../config/env.js';
|
|
import { emitCacheEvent } from '../config/socket.js';
|
|
import logger from '../utils/logger.js';
|
|
import type { GetInfoResponse, CacheEntry } from '../types/index.js';
|
|
import { parseSupportedContentUrl } from '../utils/contentUrl.js';
|
|
|
|
/**
|
|
* Cache key prefix for scraped content
|
|
*/
|
|
const CACHE_PREFIX = 'content:';
|
|
|
|
/**
|
|
* Generate cache key from URL
|
|
*/
|
|
function getCacheKey(url: string): string {
|
|
const parsed = parseSupportedContentUrl(url);
|
|
|
|
if (parsed) {
|
|
return `${CACHE_PREFIX}${parsed.provider}:${parsed.id}`;
|
|
}
|
|
|
|
return `${CACHE_PREFIX}url:${encodeURIComponent(url)}`;
|
|
}
|
|
|
|
function normalizeCachedResponse(url: string, data: GetInfoResponse): GetInfoResponse {
|
|
if (data.provider === 'netflix' || data.provider === 'primevideo') {
|
|
return data;
|
|
}
|
|
|
|
return {
|
|
...data,
|
|
provider: parseSupportedContentUrl(url)?.provider ?? 'netflix',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Cache Service for Redis operations
|
|
* Handles caching with TTL support
|
|
*/
|
|
export class CacheService {
|
|
/**
|
|
* Get cached content by URL
|
|
*/
|
|
static async get(url: string): Promise<GetInfoResponse | null> {
|
|
const key = getCacheKey(url);
|
|
|
|
try {
|
|
const cached = await redis.get(key);
|
|
|
|
if (!cached) {
|
|
logger.debug('Cache miss', { url });
|
|
return null;
|
|
}
|
|
|
|
logger.debug('Cache hit', { url });
|
|
const entry: CacheEntry<GetInfoResponse> = JSON.parse(cached);
|
|
return normalizeCachedResponse(url, entry.data);
|
|
} catch (error) {
|
|
logger.error('Cache get error', {
|
|
url,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set cache entry with TTL
|
|
*/
|
|
static async set(url: string, data: GetInfoResponse): Promise<void> {
|
|
const key = getCacheKey(url);
|
|
const ttl = env.REDIS_TTL_SECONDS;
|
|
|
|
const entry: CacheEntry<GetInfoResponse> = {
|
|
data: normalizeCachedResponse(url, data),
|
|
cachedAt: Date.now(),
|
|
ttl,
|
|
};
|
|
|
|
try {
|
|
await redis.setex(key, ttl, JSON.stringify(entry));
|
|
emitCacheEvent({
|
|
action: 'written',
|
|
key,
|
|
ttlSeconds: ttl,
|
|
occurredAt: new Date().toISOString(),
|
|
});
|
|
logger.debug('Cache set', { url, ttl });
|
|
} catch (error) {
|
|
logger.error('Cache set error', {
|
|
url,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete cached content
|
|
*/
|
|
static async delete(url: string): Promise<void> {
|
|
const key = getCacheKey(url);
|
|
|
|
try {
|
|
await redis.del(key);
|
|
emitCacheEvent({
|
|
action: 'deleted',
|
|
key,
|
|
occurredAt: new Date().toISOString(),
|
|
});
|
|
logger.debug('Cache deleted', { url });
|
|
} catch (error) {
|
|
logger.error('Cache delete error', {
|
|
url,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if cache exists
|
|
*/
|
|
static async exists(url: string): Promise<boolean> {
|
|
const key = getCacheKey(url);
|
|
|
|
try {
|
|
const result = await redis.exists(key);
|
|
return result === 1;
|
|
} catch (error) {
|
|
logger.error('Cache exists check error', {
|
|
url,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get cache TTL remaining
|
|
*/
|
|
static async getTTL(url: string): Promise<number> {
|
|
const key = getCacheKey(url);
|
|
|
|
try {
|
|
return await redis.ttl(key);
|
|
} catch (error) {
|
|
logger.error('Cache TTL check error', {
|
|
url,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all scraped content cache
|
|
*/
|
|
static async clearAll(): Promise<void> {
|
|
try {
|
|
const keys = await redis.keys(`${CACHE_PREFIX}*`);
|
|
|
|
if (keys.length > 0) {
|
|
await redis.del(...keys);
|
|
emitCacheEvent({
|
|
action: 'cleared',
|
|
count: keys.length,
|
|
occurredAt: new Date().toISOString(),
|
|
});
|
|
logger.info('Cache cleared', { count: keys.length });
|
|
}
|
|
} catch (error) {
|
|
logger.error('Cache clear error', {
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export default CacheService;
|