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 { 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 = 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 { const key = getCacheKey(url); const ttl = env.REDIS_TTL_SECONDS; const entry: CacheEntry = { 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 { 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 { 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 { 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 { 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;