fix: resolve SABnzbd history matching asymmetry and unify search helpers (#74)
Build and Push Docker Image / build (push) Successful in 40s
CI / Security audit (push) Successful in 1m0s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m6s
CI / Swagger Validation & Coverage (push) Successful in 1m36s
CI / Tests & coverage (push) Has been cancelled
Build and Push Docker Image / build (push) Successful in 40s
CI / Security audit (push) Successful in 1m0s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m6s
CI / Swagger Validation & Coverage (push) Successful in 1m36s
CI / Tests & coverage (push) Has been cancelled
This commit is contained in:
@@ -53,6 +53,65 @@ function titleMatches(clientName, arrTitle, { isFallback = true, caller = 'Downl
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper: Finds the best matching Sonarr or Radarr record for a SABnzbd slot.
|
||||
* Performs robust case-insensitive downloadId matching (queue → history),
|
||||
* then bidirectional title fallback (queue → history).
|
||||
* This eliminates all duplication and asymmetry between matchSabSlots and matchSabHistory.
|
||||
*
|
||||
* @param {string|null} sabDownloadId
|
||||
* @param {string} nzbName
|
||||
* @param {Object} context
|
||||
* @param {string} caller - e.g. 'matchSabHistory' or 'matchSabSlots'
|
||||
* @returns {{ sonarrMatch: Object|null, radarrMatch: Object|null }}
|
||||
*/
|
||||
function findSabMatch(sabDownloadId, nzbName, context, caller = 'DownloadMatcher') {
|
||||
const {
|
||||
sonarrQueueRecords = [],
|
||||
sonarrHistoryRecords = [],
|
||||
radarrQueueRecords = [],
|
||||
radarrHistoryRecords = []
|
||||
} = context;
|
||||
|
||||
const findBest = (queueRecords, historyRecords) => {
|
||||
// 1. Robust ID match (queue first)
|
||||
let match = sabDownloadId
|
||||
? queueRecords.find(r => {
|
||||
const dl = r && r.downloadId;
|
||||
return dl && String(dl).toLowerCase() === String(sabDownloadId).toLowerCase();
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!match && sabDownloadId) {
|
||||
match = historyRecords.find(r => {
|
||||
const dl = r && r.downloadId;
|
||||
return dl && String(dl).toLowerCase() === String(sabDownloadId).toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Title fallback (queue first, then history)
|
||||
if (!match && nzbName) {
|
||||
match = queueRecords.find(r => {
|
||||
const rTitle = r && (r.title || r.sourceTitle);
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller });
|
||||
});
|
||||
}
|
||||
if (!match && nzbName) {
|
||||
match = historyRecords.find(r => {
|
||||
const rTitle = r && (r.title || r.sourceTitle);
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller });
|
||||
});
|
||||
}
|
||||
|
||||
return match || null;
|
||||
};
|
||||
|
||||
return {
|
||||
sonarrMatch: findBest(sonarrQueueRecords, sonarrHistoryRecords),
|
||||
radarrMatch: findBest(radarrQueueRecords, radarrHistoryRecords)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* All callers (matchers + orphan path) must supply client, instanceId, and instanceName via options.
|
||||
* Defaults exist only as a last-resort safety net.
|
||||
@@ -237,46 +296,8 @@ async function matchSabSlots(slots, context) {
|
||||
const slotState = getSlotStatusAndSpeed(slot, queueStatus, queueSpeed, queueKbpersec);
|
||||
const nzbNameLower = nzbName.toLowerCase();
|
||||
|
||||
// Try to match by downloadId first (most reliable)
|
||||
const sabDownloadId = slot.nzo_id || slot.id;
|
||||
let sonarrMatch = sabDownloadId ? sonarrQueueRecords.find(r => r.downloadId === sabDownloadId) : null;
|
||||
let radarrMatch = sabDownloadId ? radarrQueueRecords.find(r => r.downloadId === sabDownloadId) : null;
|
||||
|
||||
// Also check HISTORY by downloadId
|
||||
if (!sonarrMatch && sabDownloadId) {
|
||||
sonarrMatch = sonarrHistoryRecords.find(r => r.downloadId === sabDownloadId);
|
||||
}
|
||||
if (!radarrMatch && sabDownloadId) {
|
||||
radarrMatch = radarrHistoryRecords.find(r => r.downloadId === sabDownloadId);
|
||||
}
|
||||
|
||||
// Fallback: Check by title matching
|
||||
if (!sonarrMatch) {
|
||||
sonarrMatch = sonarrQueueRecords.find(r => {
|
||||
const rTitle = r.title || r.sourceTitle;
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller: 'matchSabSlots' });
|
||||
});
|
||||
}
|
||||
if (!radarrMatch) {
|
||||
radarrMatch = radarrQueueRecords.find(r => {
|
||||
const rTitle = r.title || r.sourceTitle;
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller: 'matchSabSlots' });
|
||||
});
|
||||
}
|
||||
|
||||
// Also check HISTORY (completed downloads) if no queue match
|
||||
if (!sonarrMatch) {
|
||||
sonarrMatch = sonarrHistoryRecords.find(r => {
|
||||
const rTitle = r.title || r.sourceTitle;
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller: 'matchSabSlots' });
|
||||
});
|
||||
}
|
||||
if (!radarrMatch) {
|
||||
radarrMatch = radarrHistoryRecords.find(r => {
|
||||
const rTitle = r.title || r.sourceTitle;
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller: 'matchSabSlots' });
|
||||
});
|
||||
}
|
||||
const { sonarrMatch, radarrMatch } = findSabMatch(sabDownloadId, nzbName, context, 'matchSabSlots');
|
||||
|
||||
// Progress calculation
|
||||
const mbValue = slot.mb !== undefined && slot.mb !== null ? parseFloat(slot.mb) : 0;
|
||||
@@ -322,63 +343,20 @@ async function matchSabSlots(slots, context) {
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches SABnzbd history slots to Sonarr/Radarr activity using title matching.
|
||||
* @param {Array} slots - SABnzbd history slots
|
||||
* @param {Object} context - Matching context with records, maps, and user info
|
||||
* @returns {Array} Array of matched download objects
|
||||
*/
|
||||
async function matchSabHistory(slots, context) {
|
||||
const {
|
||||
sonarrQueueRecords,
|
||||
sonarrHistoryRecords,
|
||||
radarrQueueRecords,
|
||||
radarrHistoryRecords
|
||||
} = context;
|
||||
|
||||
const matched = [];
|
||||
|
||||
for (const slot of slots) {
|
||||
const nzbName = slot.name || slot.nzb_name || slot.nzbname;
|
||||
if (!nzbName) continue;
|
||||
const nzbNameLower = nzbName.toLowerCase();
|
||||
|
||||
// Try to match by downloadId (nzo_id or slot ID) first (most reliable)
|
||||
const sabDownloadId = slot.nzo_id || slot.id;
|
||||
const matchesSabId = (r) => {
|
||||
const dl = r && r.downloadId;
|
||||
if (!dl || !sabDownloadId) return false;
|
||||
return String(dl).toLowerCase() === String(sabDownloadId).toLowerCase();
|
||||
};
|
||||
|
||||
let sonarrMatch = sabDownloadId ? sonarrHistoryRecords.find(matchesSabId) : null;
|
||||
let radarrMatch = sabDownloadId ? radarrHistoryRecords.find(matchesSabId) : null;
|
||||
|
||||
// Dual-lookup: also try against active queue records (history slot may still be in *arr queue)
|
||||
if (!sonarrMatch && sabDownloadId) {
|
||||
sonarrMatch = sonarrQueueRecords.find(matchesSabId);
|
||||
}
|
||||
if (!radarrMatch && sabDownloadId) {
|
||||
radarrMatch = radarrQueueRecords.find(matchesSabId);
|
||||
}
|
||||
|
||||
// Fallback: Check by title matching
|
||||
if (!sonarrMatch) {
|
||||
sonarrMatch = sonarrHistoryRecords.find(r => {
|
||||
const rTitle = r.title || r.sourceTitle;
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller: 'matchSabHistory' });
|
||||
});
|
||||
}
|
||||
if (!radarrMatch) {
|
||||
radarrMatch = radarrHistoryRecords.find(r => {
|
||||
const rTitle = r.title || r.sourceTitle;
|
||||
return rTitle && titleMatches(nzbName, rTitle, { isFallback: true, caller: 'matchSabHistory' });
|
||||
});
|
||||
}
|
||||
const { sonarrMatch, radarrMatch } = findSabMatch(sabDownloadId, nzbName, context, 'matchSabHistory');
|
||||
|
||||
const commonOptions = {
|
||||
title: nzbName,
|
||||
status: slot.status || 'Completed',
|
||||
progress: 100, // History items are completed
|
||||
progress: 100,
|
||||
mb: slot.mb,
|
||||
size: Math.round((slot.mb || 0) * 1024 * 1024),
|
||||
completedAt: slot.completed_time,
|
||||
@@ -392,7 +370,7 @@ async function matchSabHistory(slots, context) {
|
||||
const dlObj = buildArrDownload(sonarrMatch, context, {
|
||||
...commonOptions,
|
||||
arrType: 'sonarr',
|
||||
episodes: DownloadAssembler.gatherEpisodes(nzbNameLower, sonarrHistoryRecords)
|
||||
episodes: DownloadAssembler.gatherEpisodes(nzbName.toLowerCase(), context.sonarrHistoryRecords || [])
|
||||
});
|
||||
if (dlObj) matched.push(dlObj);
|
||||
}
|
||||
@@ -405,6 +383,7 @@ async function matchSabHistory(slots, context) {
|
||||
if (dlObj) matched.push(dlObj);
|
||||
}
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user