Files
sofarr/server/clients/OmbiRetriever.js
T
gronod 6ac0a8421e
Build and Push Docker Image / build (push) Successful in 1m34s
Docs Check / Markdown lint (push) Successful in 2m14s
CI / Security audit (push) Successful in 2m30s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m40s
CI / Swagger Validation & Coverage (push) Successful in 3m22s
Docs Check / Mermaid diagram parse check (push) Successful in 3m43s
CI / Tests & coverage (push) Successful in 3m59s
fix: resolve rate-limiting and Ombi requests caching bugs (fixes #42, fixes #43)
2026-05-23 18:55:03 +01:00

264 lines
7.1 KiB
JavaScript

// 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<Array>} Empty array (Ombi doesn't have tags)
*/
async getTags() {
return [];
}
/**
* Get queue from Ombi (active requests)
* @returns {Promise<Object>} 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<Object>} History object with empty records array
*/
async getHistory(options = {}) {
return {
records: []
};
}
/**
* Test connection to Ombi
* @returns {Promise<boolean>} 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<void>}
*/
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>} 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>} 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<Object|null>} 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<Object|null>} 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<Object|null>} 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<Object|null>} 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;