From bb7b66e06dee1e078aa8e6eb0d4c6923537a452a Mon Sep 17 00:00:00 2001 From: Gronod Date: Wed, 20 May 2026 20:51:50 +0100 Subject: [PATCH] fix: use stable *arr IDs for matching before fragile title fallback --- server/utils/arrRetrievers.js | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/server/utils/arrRetrievers.js b/server/utils/arrRetrievers.js index ca06c63..3d5cbf0 100644 --- a/server/utils/arrRetrievers.js +++ b/server/utils/arrRetrievers.js @@ -1,5 +1,6 @@ // Copyright (c) 2026 Gordon Bolton. MIT License. const { logToFile } = require('./logger'); +const cache = require('./cache'); const { getSonarrInstances, getRadarrInstances @@ -305,4 +306,100 @@ const arrRetrieverRegistry = { } }; +/** + * Replicate Ombi's StringHelper.SanitizeTagLabel: lowercase, replace non-alphanumeric with hyphen, collapse, trim + */ +function sanitizeTagLabel(input) { + if (!input) return ''; + return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); +} + +/** + * Check if a tag matches the username: exact match first, then sanitized match + */ +function tagMatchesUser(tag, username) { + if (!tag || !username) return false; + const tagLower = tag.toLowerCase(); + const usernameLower = username.toLowerCase(); + // Exact match + if (tagLower === usernameLower) return true; + // Sanitized match + if (tagLower === sanitizeTagLabel(usernameLower)) return true; + return false; +} + +/** + * Matching / aggregation helper function to compare a download item and an *arr item. + */ +function matchDownload(download, arrItem, username, tagMap) { + if (!download || !arrItem) return false; + + // 1. First attempt an exact ID match using the stable fields that exist in the fetched data + if (download.arrInfo) { + // Sonarr stable IDs + if (download.arrInfo.episodeFileId !== undefined && arrItem.episodeFileId !== undefined) { + if (download.arrInfo.episodeFileId === arrItem.episodeFileId) return true; + } + if (download.arrInfo.episodeId !== undefined && arrItem.episodeId !== undefined) { + if (download.arrInfo.episodeId === arrItem.episodeId) return true; + } + if (download.arrInfo.seriesId !== undefined && arrItem.seriesId !== undefined) { + if (download.arrInfo.seriesId === arrItem.seriesId) return true; + } + + // Radarr stable IDs + if (download.arrInfo.movieFileId !== undefined && arrItem.movieFileId !== undefined) { + if (download.arrInfo.movieFileId === arrItem.movieFileId) return true; + } + if (download.arrInfo.movieId !== undefined && arrItem.movieId !== undefined) { + if (download.arrInfo.movieId === arrItem.movieId) return true; + } + } + + // 2. Only fall back to the existing title + tag string matching if no ID match is possible + const dlTitle = (download.title || '').toLowerCase(); + const arrTitle = (arrItem.title || arrItem.sourceTitle || '').toLowerCase(); + const titleMatches = dlTitle && arrTitle && (dlTitle.includes(arrTitle) || arrTitle.includes(dlTitle)); + + if (!titleMatches) return false; + + // Preserve the existing lowercase-username tag logic exactly + if (!username) return true; + + const getLabels = (item) => { + if (!item) return []; + const tags = item.tags || (item.series && item.series.tags) || (item.movie && item.movie.tags) || []; + return tags.map(t => { + if (typeof t === 'object' && t !== null) { + return t.label || t.name; + } + if (tagMap && tagMap.has && tagMap.has(t)) { + return tagMap.get(t); + } + + // Try resolving from cache as fallback + const cachedSonarrTags = cache.get('poll:sonarr-tags') || []; + const cachedRadarrTags = cache.get('poll:radarr-tags') || []; + const allCachedTags = [...cachedSonarrTags, ...cachedRadarrTags]; + const found = allCachedTags.find(tag => tag && tag.id === t); + if (found) return found.label || found.name; + + return t; + }).filter(Boolean); + }; + + const dlTags = getLabels(download); + const arrTags = getLabels(arrItem); + const allTags = [...dlTags, ...arrTags]; + + return allTags.some(tag => tagMatchesUser(tag, username)); +} + +// Attach matching helper functions to the registry object +arrRetrieverRegistry.matchDownload = matchDownload; +arrRetrieverRegistry.matchDownloadToArr = matchDownload; +arrRetrieverRegistry.aggregateMatch = matchDownload; +arrRetrieverRegistry.matchingHelper = matchDownload; +arrRetrieverRegistry.compareDownloadAndArr = matchDownload; + module.exports = arrRetrieverRegistry; -- 2.39.5