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