// Copyright (c) 2026 Gordon Bolton. MIT License. const ArrRetriever = require('./ArrRetriever'); const OmbiClient = require('./OmbiClient'); const { logToFile } = require('../utils/logger'); /** * Ombi data retriever with caching support. * Extends ArrRetriever for PALDRA compliance. * Manages Ombi request data and provides lookup maps for efficient matching. */ class OmbiRetriever extends ArrRetriever { constructor(instanceConfig) { super(instanceConfig); this.client = new OmbiClient(this.url, this.apiKey); this.baseUrl = this.url; this.cache = { movieRequests: [], tvRequests: [], movieMap: new Map(), // tmdbId -> request tvMap: new Map(), // tvdbId -> request lastFetch: 0, ttl: 5 * 60 * 1000 // 5 minutes TTL }; } /** * Get retriever type * @returns {string} The retriever type */ getRetrieverType() { return 'ombi'; } /** * Get the unique instance ID * @returns {string} The instance ID */ getInstanceId() { return this.id; } /** * Get tags from Ombi (not applicable, returns empty array) * @returns {Promise} Empty array (Ombi doesn't have tags) */ async getTags() { return []; } /** * Get queue from Ombi (active requests) * @returns {Promise} Queue object with records array */ async getQueue() { await this.refreshCache(); return { records: [...this.cache.movieRequests, ...this.cache.tvRequests] }; } /** * Get history from Ombi (not applicable, returns empty records) * @param {Object} options - Optional parameters (ignored for Ombi) * @returns {Promise} History object with empty records array */ async getHistory(options = {}) { return { records: [] }; } /** * Test connection to Ombi * @returns {Promise} True if connection is successful */ async testConnection() { return await this.client.testConnection(); } /** * Check if cache is expired * @returns {boolean} True if cache needs refresh */ isCacheExpired() { return Date.now() - this.cache.lastFetch > this.cache.ttl; } /** * Refresh cached data from Ombi API * @param {boolean} force - Whether to force a refresh regardless of TTL * @returns {Promise} */ async refreshCache(force = false) { if (!force && !this.isCacheExpired()) { return; } try { logToFile('[OmbiRetriever] Refreshing cache'); // Fetch requests in parallel const [movieRequests, tvRequests] = await Promise.all([ this.client.getMovieRequests(), this.client.getTvRequests() ]); // Update cache this.cache.movieRequests = movieRequests; this.cache.tvRequests = tvRequests; this.cache.lastFetch = Date.now(); // Build lookup maps this.cache.movieMap.clear(); this.cache.tvMap.clear(); // Build movie map (tmdbId -> request) movieRequests.forEach(request => { if (request.theMovieDbId) { this.cache.movieMap.set(request.theMovieDbId, request); } if (request.imdbId) { this.cache.movieMap.set(request.imdbId, request); } }); // Build TV map (tvdbId -> request, fallback to tmdbId) tvRequests.forEach(request => { if (request.theTvDbId) { this.cache.tvMap.set(request.theTvDbId, request); } if (request.theMovieDbId) { this.cache.tvMap.set(request.theMovieDbId, request); } }); logToFile(`[OmbiRetriever] Cache refreshed: ${movieRequests.length} movies, ${tvRequests.length} TV shows`); } catch (error) { logToFile(`[OmbiRetriever] Cache refresh failed: ${error.message}`); // Don't throw error, continue with stale cache if available } } /** * Get all movie requests * @param {boolean} force - Whether to force refresh from API * @returns {Promise} Array of movie request objects */ async getMovieRequests(force = false) { await this.refreshCache(force); return this.cache.movieRequests; } /** * Get all TV requests * @param {boolean} force - Whether to force refresh from API * @returns {Promise} Array of TV request objects */ async getTvRequests(force = false) { await this.refreshCache(force); return this.cache.tvRequests; } /** * Find movie request by external ID * @param {string} tmdbId - TheMovieDB ID * @param {string} imdbId - IMDB ID (optional fallback) * @returns {Promise} Request object or null if not found */ async findMovieRequest(tmdbId, imdbId = null) { await this.refreshCache(); // Try TMDB ID first if (tmdbId && this.cache.movieMap.has(tmdbId)) { return this.cache.movieMap.get(tmdbId); } // Try IMDB ID as fallback if (imdbId && this.cache.movieMap.has(imdbId)) { return this.cache.movieMap.get(imdbId); } return null; } /** * Find TV request by external ID * @param {string} tvdbId - TheTVDB ID * @param {string} tmdbId - TheMovieDB ID (optional fallback) * @returns {Promise} Request object or null if not found */ async findTvRequest(tvdbId, tmdbId = null) { await this.refreshCache(); // Try TVDB ID first if (tvdbId && this.cache.tvMap.has(tvdbId)) { return this.cache.tvMap.get(tvdbId); } // Try TMDB ID as fallback if (tmdbId && this.cache.tvMap.has(tmdbId)) { return this.cache.tvMap.get(tmdbId); } return null; } /** * Search for movie by external ID (for fallback when no request found) * @param {string} tmdbId - TheMovieDB ID * @param {string} imdbId - IMDB ID (optional fallback) * @returns {Promise} Search result object or null if not found */ async searchMovie(tmdbId, imdbId = null) { if (tmdbId) { const result = await this.client.searchMovieByTmdbId(tmdbId); if (result) return result; } if (imdbId) { const result = await this.client.searchMovieByImdbId(imdbId); if (result) return result; } return null; } /** * Search for TV show by external ID (for fallback when no request found) * @param {string} tvdbId - TheTVDB ID * @param {string} tmdbId - TheMovieDB ID (optional fallback) * @returns {Promise} Search result object or null if not found */ async searchTv(tvdbId, tmdbId = null) { if (tvdbId) { const result = await this.client.searchTvByTvdbId(tvdbId); if (result) return result; } if (tmdbId) { const result = await this.client.searchTvByTmdbId(tmdbId); if (result) return result; } return null; } /** * Get cache statistics * @returns {Object} Cache statistics */ getCacheStats() { return { movieRequests: this.cache.movieRequests.length, tvRequests: this.cache.tvRequests.length, movieMapSize: this.cache.movieMap.size, tvMapSize: this.cache.tvMap.size, lastFetch: this.cache.lastFetch, age: Date.now() - this.cache.lastFetch }; } } module.exports = OmbiRetriever;