feat: deduplicate history — suppress failed records superseded by successful import, flag failed+hasFile as availableForUpgrade
Some checks failed
Build and Push Docker Image / build (push) Successful in 58s
CI / Security audit (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled
Docs Check / Markdown lint (push) Successful in 1m14s
Licence Check / Dependency licence compatibility (push) Successful in 1m36s
Docs Check / Mermaid diagram parse check (push) Successful in 2m16s
Some checks failed
Build and Push Docker Image / build (push) Successful in 58s
CI / Security audit (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled
Docs Check / Markdown lint (push) Successful in 1m14s
Licence Check / Dependency licence compatibility (push) Successful in 1m36s
Docs Check / Mermaid diagram parse check (push) Successful in 2m16s
This commit is contained in:
@@ -114,6 +114,75 @@ function gatherEpisodes(titleLower, records) {
|
||||
return episodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate history items so that for each unique content item (episode or
|
||||
* movie) only the most-recent record is shown, with the following rules:
|
||||
*
|
||||
* - If the most recent event is 'imported' → show it; suppress older failures.
|
||||
* - If the most recent event is 'failed' and the item currently has a file
|
||||
* (hasFile = true) → show the failure but flag it as availableForUpgrade:true
|
||||
* so the UI can indicate the item is available but an upgrade is in progress.
|
||||
* - If the most recent event is 'failed' and hasFile is false → show normally.
|
||||
*
|
||||
* Items are keyed by: type + instanceName + contentId (episodeId or movieId).
|
||||
* Records without a contentId fall through unchanged (no deduplication possible).
|
||||
*
|
||||
* @param {Array} items - Already-built history items (unsorted)
|
||||
* @param {Array} sonarrRaw - Raw Sonarr records (for hasFile lookup)
|
||||
* @param {Array} radarrRaw - Raw Radarr records (for hasFile lookup)
|
||||
* @returns {Array}
|
||||
*/
|
||||
function deduplicateHistoryItems(items, sonarrRaw, radarrRaw) {
|
||||
// Build hasFile lookup: contentId → boolean
|
||||
const sonarrHasFile = new Map();
|
||||
for (const r of sonarrRaw) {
|
||||
const id = r.episodeId;
|
||||
if (id != null) {
|
||||
const hf = r.episode && r.episode.hasFile != null ? r.episode.hasFile : undefined;
|
||||
if (hf !== undefined && !sonarrHasFile.has(id)) sonarrHasFile.set(id, hf);
|
||||
}
|
||||
}
|
||||
const radarrHasFile = new Map();
|
||||
for (const r of radarrRaw) {
|
||||
const id = r.movieId;
|
||||
if (id != null) {
|
||||
const hf = r.movie && r.movie.hasFile != null ? r.movie.hasFile : undefined;
|
||||
if (hf !== undefined && !radarrHasFile.has(id)) radarrHasFile.set(id, hf);
|
||||
}
|
||||
}
|
||||
|
||||
// Group items by dedup key; preserve insertion order (newest first from caller)
|
||||
const groups = new Map();
|
||||
const noKey = [];
|
||||
for (const item of items) {
|
||||
const cid = item._contentId;
|
||||
if (cid == null) { noKey.push(item); continue; }
|
||||
const key = `${item.type}|${item.instanceName}|${cid}`;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(item);
|
||||
}
|
||||
|
||||
const result = [...noKey];
|
||||
for (const [, group] of groups) {
|
||||
// group[0] is the most recent (items are pushed in date-descending order)
|
||||
const best = group[0];
|
||||
if (best.outcome === 'imported') {
|
||||
result.push(best);
|
||||
continue;
|
||||
}
|
||||
if (best.outcome === 'failed') {
|
||||
const hasFile = best.type === 'series'
|
||||
? sonarrHasFile.get(best._contentId)
|
||||
: radarrHasFile.get(best._contentId);
|
||||
if (hasFile) best.availableForUpgrade = true;
|
||||
result.push(best);
|
||||
continue;
|
||||
}
|
||||
result.push(best);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSonarrLink(series) {
|
||||
if (!series || !series._instanceUrl || !series.titleSlug) return null;
|
||||
return `${series._instanceUrl}/series/${series.titleSlug}`;
|
||||
@@ -223,7 +292,8 @@ router.get('/recent', requireAuth, async (req, res) => {
|
||||
arrLink: getSonarrLink(series),
|
||||
allTags,
|
||||
matchedUserTag: matchedUserTag || null,
|
||||
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
||||
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined,
|
||||
_contentId: record.episodeId != null ? record.episodeId : null
|
||||
};
|
||||
|
||||
if (isAdmin) {
|
||||
@@ -270,7 +340,8 @@ router.get('/recent', requireAuth, async (req, res) => {
|
||||
arrLink: getRadarrLink(movie),
|
||||
allTags,
|
||||
matchedUserTag: matchedUserTag || null,
|
||||
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
||||
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined,
|
||||
_contentId: record.movieId != null ? record.movieId : null
|
||||
};
|
||||
|
||||
if (isAdmin) {
|
||||
@@ -286,16 +357,24 @@ router.get('/recent', requireAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort newest first
|
||||
historyItems.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
|
||||
// Deduplicate: for each content item keep only the most-recent record,
|
||||
// suppressing failures that were superseded by a successful import.
|
||||
// Must run before sort so insertion order (newest-first from arr API) is preserved.
|
||||
const dedupedItems = deduplicateHistoryItems(historyItems, sonarrHistory, radarrHistory);
|
||||
|
||||
console.log(`[History] Returning ${historyItems.length} items for user ${user.name} (days=${days}, showAll=${showAll})`);
|
||||
// Strip internal dedup key before sending to client
|
||||
for (const item of dedupedItems) delete item._contentId;
|
||||
|
||||
// Sort newest first
|
||||
dedupedItems.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
|
||||
|
||||
console.log(`[History] Returning ${dedupedItems.length} items for user ${user.name} (days=${days}, showAll=${showAll})`);
|
||||
|
||||
res.json({
|
||||
user: user.name,
|
||||
isAdmin,
|
||||
days,
|
||||
history: historyItems
|
||||
history: dedupedItems
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[History] Error:', err.message);
|
||||
|
||||
Reference in New Issue
Block a user