Files
sofarr/server/clients/OmbiRetriever.js
T
gronod ed4237debb
Docs Check / Markdown lint (push) Successful in 1m43s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m1s
CI / Security audit (push) Successful in 2m48s
Docs Check / Mermaid diagram parse check (push) Successful in 3m8s
CI / Tests & coverage (push) Failing after 3m33s
CI / Swagger Validation & Coverage (push) Successful in 3m34s
Build and Push Docker Image / build (push) Successful in 4m36s
feat(ombi): Add Ombi PALDRA integration for request management
- Add OmbiRetriever extending ArrRetriever for PALDRA compliance
- Add OmbiClient for low-level Ombi API communication
- Add getOmbiInstances() to config.js following multi-instance pattern
- Register Ombi in PALDRA registry with Ombi-specific methods
- Add external ID matching (TMDB/TVDB/IMDB) to Ombi requests
- Update DownloadMatcher to be async and enrich downloads with Ombi links
- Add getOmbiLink/getOmbiSearchLink helpers to DownloadAssembler
- Implement new service icon layout (Ombi + Sonarr/Radarr icons)
- Add CSS styling for service icons
- Update dashboard routes to include Ombi configuration
- Extend OpenAPI with Ombi tag and NormalizedDownload properties
- Update documentation (README, ARCHITECTURE, SECURITY, CHANGELOG)
- Add Ombi configuration to .env.sample
2026-05-21 17:00:04 +01:00

261 lines
6.8 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
* @returns {Promise<void>}
*/
async refreshCache() {
if (!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
* @returns {Promise<Array>} Array of movie request objects
*/
async getMovieRequests() {
await this.refreshCache();
return this.cache.movieRequests;
}
/**
* Get all TV requests
* @returns {Promise<Array>} Array of TV request objects
*/
async getTvRequests() {
await this.refreshCache();
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;