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
- 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
127 lines
4.4 KiB
JavaScript
127 lines
4.4 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
|
|
// Helper function to extract poster/cover art URL from a movie or series object
|
|
function getCoverArt(item) {
|
|
if (!item || !item.images) return null;
|
|
const poster = item.images.find(img => img.coverType === 'poster');
|
|
if (poster) return poster.remoteUrl || poster.url || null;
|
|
// Fallback to fanart if no poster
|
|
const fanart = item.images.find(img => img.coverType === 'fanart');
|
|
return fanart ? (fanart.remoteUrl || fanart.url || null) : null;
|
|
}
|
|
|
|
// Extract import issues from a Sonarr/Radarr queue record
|
|
function getImportIssues(queueRecord) {
|
|
if (!queueRecord) return null;
|
|
const state = queueRecord.trackedDownloadState;
|
|
const status = queueRecord.trackedDownloadStatus;
|
|
if (state !== 'importPending' && status !== 'warning' && status !== 'error') return null;
|
|
const messages = [];
|
|
if (queueRecord.statusMessages && queueRecord.statusMessages.length > 0) {
|
|
for (const sm of queueRecord.statusMessages) {
|
|
if (sm.messages && sm.messages.length > 0) {
|
|
messages.push(...sm.messages);
|
|
} else if (sm.title) {
|
|
messages.push(sm.title);
|
|
}
|
|
}
|
|
}
|
|
if (queueRecord.errorMessage) {
|
|
messages.push(queueRecord.errorMessage);
|
|
}
|
|
if (messages.length === 0) return null;
|
|
return messages;
|
|
}
|
|
|
|
// Helper to build Sonarr web UI link for a series
|
|
function getSonarrLink(series) {
|
|
if (!series || !series._instanceUrl || !series.titleSlug) return null;
|
|
return `${series._instanceUrl}/series/${series.titleSlug}`;
|
|
}
|
|
|
|
// Helper to build Radarr web UI link for a movie
|
|
function getRadarrLink(movie) {
|
|
if (!movie || !movie._instanceUrl || !movie.titleSlug) return null;
|
|
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%)
|
|
function canBlocklist(download, isAdmin) {
|
|
if (isAdmin) return true;
|
|
if (download.importIssues && download.importIssues.length > 0) return true;
|
|
if (download.qbittorrent && download.addedOn && download.availability) {
|
|
const oneHourAgo = Date.now() - 3600000; // 1 hour in ms
|
|
const addedOn = new Date(download.addedOn).getTime();
|
|
const isOldEnough = addedOn < oneHourAgo;
|
|
const availability = parseFloat(download.availability);
|
|
const isLowAvailability = availability < 100;
|
|
return isOldEnough && isLowAvailability;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Extract episode info from a Sonarr queue/history record.
|
|
// Returns { season, episode, title } or null if data is missing.
|
|
function extractEpisode(record) {
|
|
if (!record) return null;
|
|
const ep = record.episode || {};
|
|
const s = ep.seasonNumber != null ? ep.seasonNumber : record.seasonNumber;
|
|
const e = ep.episodeNumber != null ? ep.episodeNumber : record.episodeNumber;
|
|
if (s == null || e == null) return null;
|
|
const title = ep.title || null;
|
|
return { season: s, episode: e, title };
|
|
}
|
|
|
|
// Find all episodes associated with a download by matching all queue/history records
|
|
// that share the same title string. Returns sorted array of { season, episode, title }.
|
|
function gatherEpisodes(titleLower, sonarrRecords) {
|
|
const episodes = [];
|
|
const seen = new Set();
|
|
for (const r of sonarrRecords) {
|
|
const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
|
|
if (rTitle && (rTitle.includes(titleLower) || titleLower.includes(rTitle))) {
|
|
const ep = extractEpisode(r);
|
|
if (ep) {
|
|
const key = `${ep.season}x${ep.episode}`;
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
episodes.push(ep);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
episodes.sort((a, b) => a.season - b.season || a.episode - b.episode);
|
|
return episodes;
|
|
}
|
|
|
|
module.exports = {
|
|
getCoverArt,
|
|
getImportIssues,
|
|
getSonarrLink,
|
|
getRadarrLink,
|
|
getOmbiLink,
|
|
getOmbiSearchLink,
|
|
canBlocklist,
|
|
extractEpisode,
|
|
gatherEpisodes
|
|
};
|