/** * Adapter that shapes PhotoPrism's `q=stack:true` photo list into a * `DuplicateGroup[]` the UI consumes. Mirrors mule-image's * `DuplicateGroup` interface so the view code stays declarative. * * PhotoPrism's `/photos?merged=true` inlines `Files[]` on each row * already — no follow-up GET per group needed (confirmed against * photoprism/photoprism:latest, May 2026). */ import { listPhotos } from '$lib/services/photoprism'; import type { PpFile, PpPhoto } from '$lib/types/photoprism'; export interface DuplicateGroup { /** The Photo record that owns this stack. Carries marks, keywords, * taken_at — everything the metadata sidebar reads off of. */ photo: PpPhoto; /** The variants the user picks among. Always 2+ entries; groups with * a single file are filtered out (PhotoPrism shouldn't return them * for `stack:true` anyway, but the guard is cheap). */ files: PpFile[]; /** Pre-selected "keep this" file = the one PhotoPrism marks Primary. * The view starts with this highlighted and updates it on click. */ bestFileUid: string; } export async function listDuplicateGroups(): Promise { const photos = await listPhotos({ q: 'stack:true', count: 200, merged: true, order: 'newest' }); return photos .filter((p) => (p.Files?.length ?? 0) > 1) .map((p) => { const files = p.Files ?? []; const primary = files.find((f) => f.Primary) ?? files[0]; return { photo: p, files, bestFileUid: primary.UID }; }); }