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
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:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user