fix(photos): cap search to 5000 photos + abort pending requests

Large Immich libraries (7k+ photos) caused timeouts and pending
requests when using "All Photos". Cap pagination at 5 pages (5000
photos) and abort in-flight requests when switching tabs.
This commit is contained in:
Maurice
2026-04-13 21:31:03 +02:00
parent 3a52b80e3a
commit e395935f6a
2 changed files with 21 additions and 10 deletions
+17 -6
View File
@@ -1374,6 +1374,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
const [customTo, setCustomTo] = useState('') const [customTo, setCustomTo] = useState('')
const [targetEntryId, setTargetEntryId] = useState<number | null>(null) const [targetEntryId, setTargetEntryId] = useState<number | null>(null)
const [addToOpen, setAddToOpen] = useState(false) const [addToOpen, setAddToOpen] = useState(false)
const abortRef = useRef<AbortController | null>(null)
// compute trip range // compute trip range
const tripRange = useMemo(() => { const tripRange = useMemo(() => {
@@ -1385,26 +1386,36 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
return { from, to } return { from, to }
}, [trips]) }, [trips])
const cancelPending = () => {
if (abortRef.current) abortRef.current.abort()
abortRef.current = new AbortController()
return abortRef.current.signal
}
const searchPhotos = async (from: string, to: string) => { const searchPhotos = async (from: string, to: string) => {
const signal = cancelPending()
setLoading(true) setLoading(true)
setPhotos([])
try { try {
const res = await fetch(`/api/integrations/memories/${provider}/search`, { const res = await fetch(`/api/integrations/memories/${provider}/search`, {
method: 'POST', credentials: 'include', method: 'POST', credentials: 'include', signal,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ from, to }), body: JSON.stringify({ from, to }),
}) })
if (res.ok) setPhotos((await res.json()).assets || []) if (res.ok) setPhotos((await res.json()).assets || [])
} catch {} } catch (e: any) { if (e.name !== 'AbortError') {} }
setLoading(false) if (!signal.aborted) setLoading(false)
} }
const loadAlbumPhotos = async (albumId: string) => { const loadAlbumPhotos = async (albumId: string) => {
const signal = cancelPending()
setLoading(true) setLoading(true)
setPhotos([])
try { try {
const res = await fetch(`/api/integrations/memories/${provider}/albums/${albumId}/photos`, { credentials: 'include' }) const res = await fetch(`/api/integrations/memories/${provider}/albums/${albumId}/photos`, { credentials: 'include', signal })
if (res.ok) setPhotos((await res.json()).assets || []) if (res.ok) setPhotos((await res.json()).assets || [])
} catch {} } catch (e: any) { if (e.name !== 'AbortError') {} }
setLoading(false) if (!signal.aborted) setLoading(false)
} }
const loadAlbums = async () => { const loadAlbums = async () => {
@@ -155,10 +155,10 @@ export async function searchPhotos(
if (!creds) return { error: 'Immich not configured', status: 400 }; if (!creds) return { error: 'Immich not configured', status: 400 };
try { try {
// Paginate through all results (Immich limits per-page to 1000)
const allAssets: any[] = []; const allAssets: any[] = [];
let page = 1; let page = 1;
const pageSize = 1000; const pageSize = 1000;
const maxPages = 5; // Cap at 5000 photos to avoid timeouts on large libraries
while (true) { while (true) {
const resp = await safeFetch(`${creds.immich_url}/api/search/metadata`, { const resp = await safeFetch(`${creds.immich_url}/api/search/metadata`, {
method: 'POST', method: 'POST',
@@ -176,9 +176,9 @@ export async function searchPhotos(
const data = await resp.json() as { assets?: { items?: any[] } }; const data = await resp.json() as { assets?: { items?: any[] } };
const items = data.assets?.items || []; const items = data.assets?.items || [];
allAssets.push(...items); allAssets.push(...items);
if (items.length < pageSize) break; // Last page if (items.length < pageSize) break;
page++; page++;
if (page > 20) break; // Safety limit (20k photos max) if (page > maxPages) break;
} }
const assets = allAssets.map((a: any) => ({ const assets = allAssets.map((a: any) => ({
id: a.id, id: a.id,
@@ -186,7 +186,7 @@ export async function searchPhotos(
city: a.exifInfo?.city || null, city: a.exifInfo?.city || null,
country: a.exifInfo?.country || null, country: a.exifInfo?.country || null,
})); }));
return { assets }; return { assets, hasMore: page > maxPages };
} catch { } catch {
return { error: 'Could not reach Immich', status: 502 }; return { error: 'Could not reach Immich', status: 502 };
} }