fix(matching): Match SAB to Sonarr by downloadId first
Build and Push Docker Image / build (push) Successful in 41s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 56s
CI / Security audit (push) Successful in 1m6s
CI / Tests & coverage (push) Successful in 1m27s

Sonarr tracks the exact SAB download ID (nzo_id). Now tries to match
by downloadId first, then falls back to title matching. Also adds
debug to show if matches are via downloadId vs title, and logs
downloadIds in history to verify the link exists.
This commit is contained in:
2026-05-19 22:13:43 +01:00
parent 77beef787f
commit 5dfe0b1216
+45 -18
View File
@@ -1016,21 +1016,38 @@ router.get('/stream', requireAuth, async (req, res) => {
// Normalize SAB name (dots to spaces) for better matching // Normalize SAB name (dots to spaces) for better matching
const nzbNameNormalized = nzbNameLower.replace(/\./g, ' '); const nzbNameNormalized = nzbNameLower.replace(/\./g, ' ');
// Check Sonarr/Radarr QUEUE (active downloads) // Try to match by downloadId first (most reliable)
let sonarrMatch = sonarrQueue.data.records.find(r => { const sabDownloadId = slot.nzo_id || slot.id;
const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); let sonarrMatch = sabDownloadId ? sonarrQueue.data.records.find(r => r.downloadId === sabDownloadId) : null;
return rTitle && ( let radarrMatch = sabDownloadId ? radarrQueue.data.records.find(r => r.downloadId === sabDownloadId) : null;
rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle) ||
rTitle.includes(nzbNameNormalized) || nzbNameNormalized.includes(rTitle) // Also check HISTORY by downloadId
); if (!sonarrMatch && sabDownloadId) {
}); sonarrMatch = sonarrHistory.data.records.find(r => r.downloadId === sabDownloadId);
let radarrMatch = radarrQueue.data.records.find(r => { }
const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); if (!radarrMatch && sabDownloadId) {
return rTitle && ( radarrMatch = radarrHistory.data.records.find(r => r.downloadId === sabDownloadId);
rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle) || }
rTitle.includes(nzbNameNormalized) || nzbNameNormalized.includes(rTitle)
); // Fallback: Check by title matching
}); if (!sonarrMatch) {
sonarrMatch = sonarrQueue.data.records.find(r => {
const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
return rTitle && (
rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle) ||
rTitle.includes(nzbNameNormalized) || nzbNameNormalized.includes(rTitle)
);
});
}
if (!radarrMatch) {
radarrMatch = radarrQueue.data.records.find(r => {
const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
return rTitle && (
rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle) ||
rTitle.includes(nzbNameNormalized) || nzbNameNormalized.includes(rTitle)
);
});
}
// Also check HISTORY (completed downloads) if no queue match // Also check HISTORY (completed downloads) if no queue match
if (!sonarrMatch) { if (!sonarrMatch) {
@@ -1055,19 +1072,29 @@ router.get('/stream', requireAuth, async (req, res) => {
if (sabSlotsChecked <= 5) { if (sabSlotsChecked <= 5) {
if (sonarrMatch) { if (sonarrMatch) {
const source = sonarrQueue.data.records.includes(sonarrMatch) ? 'queue' : 'history'; const source = sonarrQueue.data.records.includes(sonarrMatch) ? 'queue' : 'history';
console.log(`[SSE] ✓ Sonarr ${source} match: SAB:"${nzbNameLower.substring(0, 50)}" → Sonarr:"${(sonarrMatch.title || sonarrMatch.sourceTitle || '').substring(0, 50)}"`); const matchType = (sonarrMatch.downloadId === sabDownloadId) ? 'downloadId' : 'title';
console.log(`[SSE] ✓ Sonarr ${source} ${matchType} match: SAB:"${nzbNameLower.substring(0, 40)}" → Sonarr:"${(sonarrMatch.title || sonarrMatch.sourceTitle || '').substring(0, 40)}"`);
} else if (radarrMatch) { } else if (radarrMatch) {
const source = radarrQueue.data.records.includes(radarrMatch) ? 'queue' : 'history'; const source = radarrQueue.data.records.includes(radarrMatch) ? 'queue' : 'history';
console.log(`[SSE] ✓ Radarr ${source} match: SAB:"${nzbNameLower.substring(0, 50)}" → Radarr:"${(radarrMatch.title || radarrMatch.sourceTitle || '').substring(0, 50)}"`); const matchType = (radarrMatch.downloadId === sabDownloadId) ? 'downloadId' : 'title';
console.log(`[SSE] ✓ Radarr ${source} ${matchType} match: SAB:"${nzbNameLower.substring(0, 40)}" → Radarr:"${(radarrMatch.title || radarrMatch.sourceTitle || '').substring(0, 40)}"`);
} else { } else {
console.log(`[SSE] ✗ No match for SAB: "${nzbNameLower.substring(0, 60)}"`); console.log(`[SSE] ✗ No match for SAB: "${nzbNameLower.substring(0, 60)}"`);
// Show counts // Show counts
console.log(`[SSE] Queue: ${sonarrQueue.data.records.length}, History: ${sonarrHistory.data.records.length}`); console.log(`[SSE] Queue: ${sonarrQueue.data.records.length}, History: ${sonarrHistory.data.records.length}`);
// Show history titles if there are any // Show history titles if there are any
if (sonarrHistory.data.records.length > 0) { if (sonarrHistory.data.records.length > 0) {
const histTitles = sonarrHistory.data.records.slice(0, 3).map(r => (r.title || r.sourceTitle || 'NO_TITLE').substring(0, 40)); const histTitles = sonarrHistory.data.records.slice(0, 3).map(r => {
const title = (r.title || r.sourceTitle || 'NO_TITLE').substring(0, 35);
const dlId = r.downloadId ? r.downloadId.substring(0, 15) : 'no-dl-id';
return `${title}[${dlId}]`;
});
console.log(`[SSE] History titles: ${histTitles.join(' | ')}`); console.log(`[SSE] History titles: ${histTitles.join(' | ')}`);
} }
// Also check if SAB slots have nzo_id we could use
if (slot.nzo_id) {
console.log(`[SSE] SAB nzo_id: ${slot.nzo_id.substring(0, 20)}...`);
}
} }
} }
if (sonarrMatch && sonarrMatch.seriesId) { if (sonarrMatch && sonarrMatch.seriesId) {