diff --git a/packages/core/src/db/schemas.ts b/packages/core/src/db/schemas.ts index 5bc63ff5..ef1070b1 100644 --- a/packages/core/src/db/schemas.ts +++ b/packages/core/src/db/schemas.ts @@ -161,6 +161,7 @@ const DeduplicatorMode = z.enum([ const DeduplicatorOptions = z.object({ enabled: z.boolean().optional(), + excludeAddons: z.array(z.string().min(1)).optional(), multiGroupBehaviour: z .enum(['keep_all', 'aggressive', 'conservative']) .optional(), diff --git a/packages/core/src/streams/deduplicator.ts b/packages/core/src/streams/deduplicator.ts index 67fafb55..5d892081 100644 --- a/packages/core/src/streams/deduplicator.ts +++ b/packages/core/src/streams/deduplicator.ts @@ -5,6 +5,7 @@ import { DSU, getSimpleTextHash, } from '../utils/index.js'; +import StreamUtils from './utils.js'; const logger = createLogger('deduplicator'); @@ -27,6 +28,7 @@ class StreamDeduplicator { deduplicator = { enabled: true, multiGroupBehaviour: deduplicator.multiGroupBehaviour || 'aggressive', + excludeAddons: deduplicator.excludeAddons || [], keys: deduplicationKeys, cached: deduplicator.cached || 'per_addon', uncached: deduplicator.uncached || 'per_addon', @@ -42,6 +44,18 @@ class StreamDeduplicator { const dsu = new DSU(); const keyToStreamIds = new Map(); + const excludedStreamIds = new Set(); + for (const stream of streams) { + const isExcluded = + stream.addon?.instanceId && + deduplicator.excludeAddons?.includes(stream.addon.preset.id); + + if (isExcluded) { + excludedStreamIds.add(stream.id); + } + } + + // Process ALL streams (including excluded ones) for deduplication grouping for (const stream of streams) { // Create a unique key based on the selected deduplication methods dsu.makeSet(stream.id); @@ -113,6 +127,12 @@ class StreamDeduplicator { } const processedStreams = new Set(); + for (const excludedStreamId of excludedStreamIds) { + const stream = idToStreamMap.get(excludedStreamId); + if (stream) { + processedStreams.add(stream); + } + } for (const group of finalDuplicateGroupsMap.values()) { // Group streams by type @@ -349,9 +369,11 @@ class StreamDeduplicator { } } - let deduplicatedStreams = Array.from(processedStreams); + let deduplicatedStreams = StreamUtils.mergeStreams( + Array.from(processedStreams) + ); logger.info( - `Filtered out ${streams.length - deduplicatedStreams.length} duplicate streams to ${deduplicatedStreams.length} streams in ${getTimeTakenSincePoint(start)}` + `Filtered out ${streams.length - deduplicatedStreams.length} duplicate streams to ${deduplicatedStreams.length}${Array.from(excludedStreamIds.values()).length > 0 ? ' (' + Array.from(excludedStreamIds.values()).length + ' streams excluded from deduplication) ' : ''} streams in ${getTimeTakenSincePoint(start)}` ); return deduplicatedStreams; } diff --git a/packages/frontend/src/components/menu/filters.tsx b/packages/frontend/src/components/menu/filters.tsx index f8167a3d..571cf70b 100644 --- a/packages/frontend/src/components/menu/filters.tsx +++ b/packages/frontend/src/components/menu/filters.tsx @@ -2616,6 +2616,7 @@ function Content() { }} /> + {mode === 'pro' && ( <> + { + setUserData((prev) => ({ + ...prev, + deduplicator: { + ...prev.deduplicator, + excludeAddons: value, + }, + })); + }} + options={userData.presets.map((preset) => ({ + label: preset.options.name || preset.type, + value: preset.instanceId, + textValue: preset.options.name, + }))} + emptyMessage="You haven't installed any addons..." + placeholder="Select addons..." + multiple + disabled={userData.deduplicator?.enabled === false} + /> +