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
+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;