mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
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:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user