feat(pinchflat): Retention & quality-upgrade per Library source

On-demand maintenance panel (SourceRetention) on the source detail —
not a persisted schedule (roadmap's niche-for-desktop scope).

Prune: keep the newest N downloaded files, delete the rest's files
(download archive prevents silent re-download); dry-run count + two-step
confirm before any deletion. Upgrade: re-enqueue items whose recorded
downloadedQuality ranks below the source's current target (profile or
global). Selection is pure + injected-now in @shared/retention.ts
(selectPruneCandidates/selectUpgradeCandidates + qualityRank), unit-
tested; pruneMediaItems (main) deletes best-effort and clears state.
MediaItem.downloadedQuality now recorded through the completion path.
Live-verified the panel renders. 366 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:14:23 -04:00
parent 15cc96dd10
commit 0956bd536a
13 changed files with 487 additions and 17 deletions
+9 -2
View File
@@ -216,8 +216,8 @@ const api = {
/** Persist that a media item has finished downloading (drives incremental sync).
* Named to match the main handler `setMediaItemDownloaded` (L92). */
setMediaItemDownloaded: (id: string, filePath?: string): Promise<MediaItem[]> =>
ipcRenderer.invoke(IpcChannels.sourceItemDownloaded, id, filePath),
setMediaItemDownloaded: (id: string, filePath?: string, quality?: string): Promise<MediaItem[]> =>
ipcRenderer.invoke(IpcChannels.sourceItemDownloaded, id, filePath, quality),
/** Toggle whether a source is watched for new uploads. */
setSourceWatched: (id: string, watched: boolean): Promise<Source[]> =>
@@ -227,6 +227,13 @@ const api = {
setSourceProfile: (id: string, profileId: string | null): Promise<Source[]> =>
ipcRenderer.invoke(IpcChannels.sourceSetProfile, id, profileId),
/** Prune (delete files of) the given media items; returns the source's updated list + count. */
pruneMediaItems: (
sourceId: string,
itemIds: string[]
): Promise<{ items: MediaItem[]; deleted: number }> =>
ipcRenderer.invoke(IpcChannels.sourcePruneItems, sourceId, itemIds),
/** Re-index all watched sources; resolves with the videos found new. Named to
* match the main function `syncWatchedSources` (L92). */
syncWatchedSources: (): Promise<SyncResult> => ipcRenderer.invoke(IpcChannels.sourcesSync),