feat(ombi): Add Ombi PALDRA integration for request management
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

- 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
This commit is contained in:
2026-05-21 17:00:04 +01:00
parent de9a9284dc
commit ed4237debb
20 changed files with 850 additions and 33 deletions
+130
View File
@@ -0,0 +1,130 @@
// Copyright (c) 2026 Gordon Bolton. MIT License.
const axios = require('axios');
const { logToFile } = require('../utils/logger');
/**
* Ombi API client for fetching requests and searching media.
* Provides integration with Ombi request management system.
*/
class OmbiClient {
constructor(url, apiKey) {
this.url = url.replace(/\/$/, ''); // Remove trailing slash
this.apiKey = apiKey;
this.axios = axios.create({
headers: { 'ApiKey': this.apiKey },
timeout: 10000
});
}
/**
* Get all movie requests from Ombi
* @returns {Promise<Array>} Array of movie request objects
*/
async getMovieRequests() {
try {
const response = await this.axios.get(`${this.url}/api/v1/Request/movie`);
return response.data || [];
} catch (error) {
logToFile(`[OmbiClient] Movie requests error: ${error.message}`);
return [];
}
}
/**
* Get all TV requests from Ombi
* @returns {Promise<Array>} Array of TV request objects
*/
async getTvRequests() {
try {
const response = await this.axios.get(`${this.url}/api/v1/Request/tv`);
return response.data || [];
} catch (error) {
logToFile(`[OmbiClient] TV requests error: ${error.message}`);
return [];
}
}
/**
* Search for movies by TMDB ID
* @param {string} tmdbId - TheMovieDB ID
* @returns {Promise<Object|null>} Search result object or null if not found
*/
async searchMovieByTmdbId(tmdbId) {
if (!tmdbId) return null;
try {
const response = await this.axios.get(`${this.url}/api/v1/Search/movie/${tmdbId}`);
return response.data || null;
} catch (error) {
logToFile(`[OmbiClient] Movie search error for TMDB ID ${tmdbId}: ${error.message}`);
return null;
}
}
/**
* Search for movies by IMDB ID
* @param {string} imdbId - IMDB ID
* @returns {Promise<Object|null>} Search result object or null if not found
*/
async searchMovieByImdbId(imdbId) {
if (!imdbId) return null;
try {
const response = await this.axios.get(`${this.url}/api/v1/Search/movie/imdb/${imdbId}`);
return response.data || null;
} catch (error) {
logToFile(`[OmbiClient] Movie search error for IMDB ID ${imdbId}: ${error.message}`);
return null;
}
}
/**
* Search for TV shows by TVDB ID
* @param {string} tvdbId - TheTVDB ID
* @returns {Promise<Object|null>} Search result object or null if not found
*/
async searchTvByTvdbId(tvdbId) {
if (!tvdbId) return null;
try {
const response = await this.axios.get(`${this.url}/api/v1/Search/tv/${tvdbId}`);
return response.data || null;
} catch (error) {
logToFile(`[OmbiClient] TV search error for TVDB ID ${tvdbId}: ${error.message}`);
return null;
}
}
/**
* Search for TV shows by TMDB ID
* @param {string} tmdbId - TheMovieDB ID
* @returns {Promise<Object|null>} Search result object or null if not found
*/
async searchTvByTmdbId(tmdbId) {
if (!tmdbId) return null;
try {
const response = await this.axios.get(`${this.url}/api/v1/Search/tv/tmdb/${tmdbId}`);
return response.data || null;
} catch (error) {
logToFile(`[OmbiClient] TV search error for TMDB ID ${tmdbId}: ${error.message}`);
return null;
}
}
/**
* Test connection to Ombi API
* @returns {Promise<boolean>} True if connection is successful
*/
async testConnection() {
try {
const response = await this.axios.get(`${this.url}/api/v1/Request/movie`);
return response.status === 200;
} catch (error) {
logToFile(`[OmbiClient] Connection test failed: ${error.message}`);
return false;
}
}
}
module.exports = OmbiClient;
+260
View File
@@ -0,0 +1,260 @@
// 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;
+17
View File
@@ -49,6 +49,8 @@ tags:
description: SABnzbd API proxy
- name: Emby
description: Emby/Jellyfin API proxy
- name: Ombi
description: Ombi request management
security:
- CookieAuth: []
@@ -155,6 +157,21 @@ components:
nullable: true
description: Sonarr/Radarr type
example: "series"
ombiLink:
type: string
nullable: true
description: Link to Ombi request or search
example: "https://ombi.example.com/#/request/movie/123"
ombiRequestId:
type: string
nullable: true
description: Ombi request ID (if request exists)
example: "123"
ombiTooltip:
type: string
nullable: true
description: Tooltip text for Ombi icon ("Request" or "Search")
example: "Request"
DashboardPayload:
type: object
+18 -2
View File
@@ -11,6 +11,8 @@ const sanitizeError = require('../utils/sanitizeError');
const TagMatcher = require('../services/TagMatcher');
const { buildUserDownloads } = require('../services/DownloadBuilder');
const { onHistoryUpdate, offHistoryUpdate } = require('../utils/historyFetcher');
const arrRetrieverRegistry = require('../utils/arrRetrievers');
const { getOmbiInstances } = require('../utils/config');
// Track active SSE clients for disconnect cleanup
@@ -167,6 +169,11 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
const { seriesMap, moviesMap, sonarrTagMap, radarrTagMap } = buildMetadataMaps(snapshot);
const embyUserMap = showAll ? await TagMatcher.getEmbyUsers() : new Map();
// Get Ombi configuration
const ombiInstances = getOmbiInstances();
const ombiRetriever = ombiInstances.length > 0 ? arrRetrieverRegistry.getOmbiRetrievers()[0] : null;
const ombiBaseUrl = ombiInstances.length > 0 ? ombiInstances[0].url : null;
const userDownloads = await buildUserDownloads(snapshot, {
username,
usernameSanitized: user.name,
@@ -176,7 +183,9 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
moviesMap,
sonarrTagMap,
radarrTagMap,
embyUserMap
embyUserMap,
ombiRetriever,
ombiBaseUrl
});
res.json({
@@ -455,6 +464,11 @@ router.get('/stream', requireAuth, async (req, res) => {
const { seriesMap, moviesMap, sonarrTagMap, radarrTagMap } = buildMetadataMaps(snapshot);
const embyUserMap = showAll ? await TagMatcher.getEmbyUsers() : new Map();
// Get Ombi configuration
const ombiInstances = getOmbiInstances();
const ombiRetriever = ombiInstances.length > 0 ? arrRetrieverRegistry.getOmbiRetrievers()[0] : null;
const ombiBaseUrl = ombiInstances.length > 0 ? ombiInstances[0].url : null;
const userDownloads = buildUserDownloads(snapshot, {
username,
usernameSanitized: user.name,
@@ -464,7 +478,9 @@ router.get('/stream', requireAuth, async (req, res) => {
moviesMap,
sonarrTagMap,
radarrTagMap,
embyUserMap
embyUserMap,
ombiRetriever,
ombiBaseUrl
});
console.log(`[SSE] Sending ${userDownloads.length} downloads for ${user.name}`);
+19
View File
@@ -45,6 +45,23 @@ function getRadarrLink(movie) {
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
}
// Helper to build Ombi request link
function getOmbiLink(requestId, type, ombiBaseUrl) {
if (!requestId || !type || !ombiBaseUrl) return null;
return `${ombiBaseUrl}/#/request/${type}/${requestId}`;
}
// Helper to build Ombi search link
function getOmbiSearchLink(searchId, type, ombiBaseUrl) {
if (!searchId || !type || !ombiBaseUrl) return null;
if (type === 'series') {
return `${ombiBaseUrl}/#/tv/search/${searchId}`;
} else if (type === 'movie') {
return `${ombiBaseUrl}/#/movie/search/${searchId}`;
}
return null;
}
// Determine if a download can be blocklisted by the current user
// Admins: always true (they have arrQueueId)
// Non-admins: true if importIssues OR (torrent >1h old AND availability<100%)
@@ -101,6 +118,8 @@ module.exports = {
getImportIssues,
getSonarrLink,
getRadarrLink,
getOmbiLink,
getOmbiSearchLink,
canBlocklist,
extractEpisode,
gatherEpisodes
+6 -2
View File
@@ -22,9 +22,11 @@ const DownloadMatcher = require('./DownloadMatcher');
* @param {Map} options.sonarrTagMap - Map of Sonarr tag IDs to labels
* @param {Map} options.radarrTagMap - Map of Radarr tag IDs to labels
* @param {Map} options.embyUserMap - Map of Emby users for admin view
* @param {OmbiRetriever} options.ombiRetriever - Ombi data retriever instance (optional)
* @param {string} options.ombiBaseUrl - Ombi base URL for link generation (optional)
* @returns {Array} Array of download objects for the user
*/
function buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }) {
function buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap, ombiRetriever, ombiBaseUrl }) {
// Input validation
if (!cacheSnapshot || typeof cacheSnapshot !== 'object') {
console.error('[DownloadBuilder] Invalid cacheSnapshot provided');
@@ -62,7 +64,9 @@ function buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmi
embyUserMap: embyUserMap || new Map(),
queueStatus,
queueSpeed,
queueKbpersec
queueKbpersec,
ombiRetriever,
ombiBaseUrl
};
// Match all download sources
+80 -6
View File
@@ -44,6 +44,68 @@ function buildMoviesMapFromRecords(queueRecords, historyRecords) {
return moviesMap;
}
/**
* Matches a download object with Ombi requests and adds Ombi links
* @param {Object} downloadObj - Download object to enhance
* @param {Object} seriesOrMovie - Series or movie object from Sonarr/Radarr
* @param {Object} context - Context containing Ombi retriever and base URL
* @returns {Promise<void>}
*/
async function addOmbiMatching(downloadObj, seriesOrMovie, context) {
const { ombiRetriever, ombiBaseUrl } = context;
if (!ombiRetriever || !ombiBaseUrl || !seriesOrMovie) {
return;
}
try {
let ombiRequest = null;
let searchResult = null;
if (downloadObj.type === 'series') {
// For TV shows, try TVDB ID first, then TMDB ID
const tvdbId = seriesOrMovie.tvdbId;
const tmdbId = seriesOrMovie.tmdbId;
ombiRequest = await ombiRetriever.findTvRequest(tvdbId, tmdbId);
if (!ombiRequest) {
// Fallback to search
searchResult = await ombiRetriever.searchTv(tvdbId, tmdbId);
}
} else if (downloadObj.type === 'movie') {
// For movies, try TMDB ID first, then IMDB ID
const tmdbId = seriesOrMovie.tmdbId;
const imdbId = seriesOrMovie.imdbId;
ombiRequest = await ombiRetriever.findMovieRequest(tmdbId, imdbId);
if (!ombiRequest) {
// Fallback to search
searchResult = await ombiRetriever.searchMovie(tmdbId, imdbId);
}
}
if (ombiRequest) {
// Found existing request
downloadObj.ombiLink = `${ombiBaseUrl}/#/request/${ombiRequest.type}/${ombiRequest.id}`;
downloadObj.ombiRequestId = ombiRequest.id;
downloadObj.ombiTooltip = 'Request';
} else if (searchResult) {
// No request found, but search succeeded
if (downloadObj.type === 'series') {
downloadObj.ombiLink = `${ombiBaseUrl}/#/tv/search/${searchResult.id}`;
} else {
downloadObj.ombiLink = `${ombiBaseUrl}/#/movie/search/${searchResult.id}`;
}
downloadObj.ombiTooltip = 'Search';
}
} catch (error) {
// Silently fail Ombi matching - don't break the download object creation
console.error('[DownloadMatcher] Ombi matching error:', error.message);
}
}
/**
* Determines the status and speed for a SABnzbd slot based on queue state.
* @param {Object} slot - SABnzbd queue slot
@@ -68,7 +130,7 @@ function getSlotStatusAndSpeed(slot, queueStatus, queueSpeed, queueKbpersec) {
* @param {Object} context - Matching context with records, maps, and user info
* @returns {Array} Array of matched download objects
*/
function matchSabSlots(slots, context) {
async function matchSabSlots(slots, context) {
const {
sonarrQueueRecords,
sonarrHistoryRecords,
@@ -84,7 +146,9 @@ function matchSabSlots(slots, context) {
embyUserMap,
queueStatus,
queueSpeed,
queueKbpersec
queueKbpersec,
ombiRetriever,
ombiBaseUrl
} = context;
const matched = [];
@@ -198,6 +262,7 @@ function matchSabSlots(slots, context) {
dlObj.arrContentType = 'episode';
}
dlObj.canBlocklist = DownloadAssembler.canBlocklist(dlObj, isAdmin);
await addOmbiMatching(dlObj, series, context);
matched.push(dlObj);
}
}
@@ -250,6 +315,7 @@ function matchSabSlots(slots, context) {
dlObj.arrContentType = 'movie';
}
dlObj.canBlocklist = DownloadAssembler.canBlocklist(dlObj, isAdmin);
await addOmbiMatching(dlObj, movie, context);
matched.push(dlObj);
}
}
@@ -264,7 +330,7 @@ function matchSabSlots(slots, context) {
* @param {Object} context - Matching context with records, maps, and user info
* @returns {Array} Array of matched download objects
*/
function matchSabHistory(slots, context) {
async function matchSabHistory(slots, context) {
const {
sonarrHistoryRecords,
radarrHistoryRecords,
@@ -275,7 +341,9 @@ function matchSabHistory(slots, context) {
username,
isAdmin,
showAll,
embyUserMap
embyUserMap,
ombiRetriever,
ombiBaseUrl
} = context;
const matched = [];
@@ -317,6 +385,7 @@ function matchSabHistory(slots, context) {
dlObj.targetPath = series.path || null;
dlObj.arrLink = DownloadAssembler.getSonarrLink(series);
}
await addOmbiMatching(dlObj, series, context);
matched.push(dlObj);
}
}
@@ -355,6 +424,7 @@ function matchSabHistory(slots, context) {
dlObj.targetPath = movie.path || null;
dlObj.arrLink = DownloadAssembler.getRadarrLink(movie);
}
await addOmbiMatching(dlObj, movie, context);
matched.push(dlObj);
}
}
@@ -369,7 +439,7 @@ function matchSabHistory(slots, context) {
* @param {Object} context - Matching context with records, maps, and user info
* @returns {Array} Array of matched download objects
*/
function matchTorrents(torrents, context) {
async function matchTorrents(torrents, context) {
const {
sonarrQueueRecords,
sonarrHistoryRecords,
@@ -382,7 +452,9 @@ function matchTorrents(torrents, context) {
username,
isAdmin,
showAll,
embyUserMap
embyUserMap,
ombiRetriever,
ombiBaseUrl
} = context;
const matched = [];
@@ -430,6 +502,7 @@ function matchTorrents(torrents, context) {
download.arrContentType = 'episode';
}
download.canBlocklist = DownloadAssembler.canBlocklist(download, isAdmin);
await addOmbiMatching(download, series, context);
matched.push(download);
matchedAny = true;
continue;
@@ -474,6 +547,7 @@ function matchTorrents(torrents, context) {
download.arrContentType = 'movie';
}
download.canBlocklist = DownloadAssembler.canBlocklist(download, isAdmin);
await addOmbiMatching(download, movie, context);
matched.push(download);
matchedAny = true;
continue;
+74 -3
View File
@@ -3,17 +3,20 @@ const { logToFile } = require('./logger');
const cache = require('./cache');
const {
getSonarrInstances,
getRadarrInstances
getRadarrInstances,
getOmbiInstances
} = require('./config');
// Import retriever classes
const PollingSonarrRetriever = require('../clients/PollingSonarrRetriever');
const PollingRadarrRetriever = require('../clients/PollingRadarrRetriever');
const OmbiRetriever = require('../clients/OmbiRetriever');
// Retriever type mapping
const retrieverClasses = {
sonarr: PollingSonarrRetriever,
radarr: PollingRadarrRetriever
radarr: PollingRadarrRetriever,
ombi: OmbiRetriever
};
/**
@@ -36,11 +39,13 @@ const arrRetrieverRegistry = {
// Get all instance configurations
const sonarrInstances = getSonarrInstances();
const radarrInstances = getRadarrInstances();
const ombiInstances = getOmbiInstances();
// Create retriever instances
const instanceConfigs = [
...sonarrInstances.map(inst => ({ ...inst, type: 'sonarr' })),
...radarrInstances.map(inst => ({ ...inst, type: 'radarr' }))
...radarrInstances.map(inst => ({ ...inst, type: 'radarr' })),
...ombiInstances.map(inst => ({ ...inst, type: 'ombi' }))
];
for (const config of instanceConfigs) {
@@ -303,6 +308,72 @@ const arrRetrieverRegistry = {
.filter(result => result.status === 'fulfilled')
.map(result => result.value)
};
},
/**
* Get Ombi retrievers
* @returns {Array<OmbiRetriever>} Array of Ombi retriever instances
*/
getOmbiRetrievers() {
return this.getRetrieversByType('ombi');
},
/**
* Get all Ombi requests
* @returns {Promise<Object>} Object with movie and TV request arrays
*/
async getOmbiRequests() {
const ombiRetrievers = this.getOmbiRetrievers();
if (ombiRetrievers.length === 0) {
return { movie: [], tv: [] };
}
// Use the first Ombi retriever (single instance expected)
const retriever = ombiRetrievers[0];
try {
const movieRequests = await retriever.getMovieRequests();
const tvRequests = await retriever.getTvRequests();
return { movie: movieRequests, tv: tvRequests };
} catch (error) {
logToFile(`[ArrRetrieverRegistry] Error fetching Ombi requests: ${error.message}`);
return { movie: [], tv: [] };
}
},
/**
* Get Ombi requests grouped by type
* @returns {Promise<Object>} Requests grouped by type (movie, tv)
*/
async getOmbiRequestsByType() {
return await this.getOmbiRequests();
},
/**
* Find Ombi request by external IDs
* @param {string} type - 'movie' or 'tv'
* @param {Object} externalIds - External IDs to search with
* @param {string} externalIds.tmdbId - TheMovieDB ID
* @param {string} externalIds.tvdbId - TheTVDB ID (for TV)
* @param {string} externalIds.imdbId - IMDB ID (for movies)
* @returns {Promise<Object|null>} Ombi request object or null if not found
*/
async findOmbiRequest(type, externalIds) {
const ombiRetrievers = this.getOmbiRetrievers();
if (ombiRetrievers.length === 0) {
return null;
}
const retriever = ombiRetrievers[0];
try {
if (type === 'movie') {
return await retriever.findMovieRequest(externalIds.tmdbId, externalIds.imdbId);
} else if (type === 'tv') {
return await retriever.findTvRequest(externalIds.tvdbId, externalIds.tmdbId);
}
} catch (error) {
logToFile(`[ArrRetrieverRegistry] Error finding Ombi request: ${error.message}`);
}
return null;
}
};
+9
View File
@@ -84,6 +84,14 @@ function getRadarrInstances() {
);
}
function getOmbiInstances() {
return parseInstances(
process.env.OMBI_INSTANCES,
process.env.OMBI_URL,
process.env.OMBI_API_KEY
);
}
function getQbittorrentInstances() {
return parseInstances(
process.env.QBITTORRENT_INSTANCES,
@@ -126,6 +134,7 @@ module.exports = {
getSABnzbdInstances,
getSonarrInstances,
getRadarrInstances,
getOmbiInstances,
getQbittorrentInstances,
getTransmissionInstances,
getRtorrentInstances,