import type { MediaItem } from './ipc' /** * Retention / quality-upgrade selection (PINCHFLAT stretch). Pure so the choice * of which items to prune or re-download is unit-tested with an injected `now` * and no filesystem — the impure half (deleting files, re-enqueuing) lives in * the caller. */ export interface PrunePolicy { /** keep only items downloaded within the last N days; older ones are pruned */ maxAgeDays?: number /** keep only the N most-recently-downloaded items; older ones are pruned */ maxCount?: number } /** * Which downloaded items a prune policy would remove. An item is a candidate * when it's downloaded, has a real file on disk to delete, AND falls outside * EITHER limit (age or count) — the two combine as "keep if it passes both". * Ordered newest-first internally so `maxCount` keeps the freshest. * * Returns the items to prune (delete file + clear downloaded state). Never * touches un-downloaded items or ones with no filePath. Both limits omitted = * nothing pruned (retention off). */ export function selectPruneCandidates( items: MediaItem[], policy: PrunePolicy, now: number ): MediaItem[] { const { maxAgeDays, maxCount } = policy if (maxAgeDays == null && maxCount == null) return [] // Only downloaded items with a file are prunable; newest-first for the count cut. const prunable = items .filter((i) => i.downloaded && i.filePath && typeof i.downloadedAt === 'number') .sort((a, b) => (b.downloadedAt ?? 0) - (a.downloadedAt ?? 0)) const ageCutoff = maxAgeDays != null ? now - maxAgeDays * 86_400_000 : null return prunable.filter((item, index) => { const tooOld = ageCutoff != null && (item.downloadedAt ?? 0) < ageCutoff const overCount = maxCount != null && index >= maxCount return tooOld || overCount }) } /** * Which downloaded items were fetched at a quality BELOW the source's current * target, so they're worth re-downloading to upgrade. `qualityRank` maps a * quality label to a comparable number (higher = better); an item ranks below * target when its recorded `downloadedQuality` ranks lower than `targetQuality`. * * An item with no recorded `downloadedQuality` (downloaded before this was * tracked) is NOT selected — we can't prove it's below target, and re-downloading * on a guess would churn. Only downloaded items are considered. */ export function selectUpgradeCandidates( items: MediaItem[], targetQuality: string, qualityRank: (label: string) => number ): MediaItem[] { const target = qualityRank(targetQuality) return items.filter( (i) => i.downloaded && typeof i.downloadedQuality === 'string' && qualityRank(i.downloadedQuality) < target ) }