From 504713d920eb5f8b78fab3a4230409b23786e23f Mon Sep 17 00:00:00 2001 From: Marek Maslowski Date: Sat, 4 Apr 2026 13:36:12 +0200 Subject: [PATCH] change in hadnling return values from unified service --- server/src/routes/memories.ts | 96 +++--- server/src/services/immichService.ts | 13 +- server/src/services/memoriesService.ts | 450 +++++++++++++------------ server/src/services/synologyService.ts | 15 +- 4 files changed, 300 insertions(+), 274 deletions(-) diff --git a/server/src/routes/memories.ts b/server/src/routes/memories.ts index 1b1fac3..e60b9e1 100644 --- a/server/src/routes/memories.ts +++ b/server/src/routes/memories.ts @@ -1,6 +1,5 @@ import express, { Request, Response } from 'express'; import { authenticate } from '../middleware/auth'; -import { broadcast } from '../websocket'; import { AuthRequest } from '../types'; import { listTripPhotos, @@ -10,94 +9,85 @@ import { addTripPhotos, removeTripPhoto, setTripPhotoSharing, - notifySharedTripPhotos, - normalizeSelections, } from '../services/memoriesService'; const router = express.Router(); +//------------------------------------------------ +// routes for managing photos linked to trip router.get('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const result = listTripPhotos(tripId, authReq.user.id); - if ('error' in result) return res.status(result.status).json({ error: result.error }); - res.json({ photos: result.photos }); + if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); + res.json({ photos: result.data }); }); -router.get('/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => { - const authReq = req as AuthRequest; - const { tripId } = req.params; - const result = listTripAlbumLinks(tripId, authReq.user.id); - if ('error' in result) return res.status(result.status).json({ error: result.error }); - res.json({ links: result.links }); -}); - -router.delete('/trips/:tripId/album-links/:linkId', authenticate, (req: Request, res: Response) => { - const authReq = req as AuthRequest; - const { tripId, linkId } = req.params; - const result = removeAlbumLink(tripId, linkId, authReq.user.id); - if ('error' in result) return res.status(result.status).json({ error: result.error }); - res.json({ success: true }); - broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string); -}); - -router.post('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => { +router.post('/trips/:tripId/photos', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; + const sid = req.headers['x-socket-id'] as string; - const selections = normalizeSelections(req.body?.selections, req.body?.provider, req.body?.asset_ids); const shared = req.body?.shared === undefined ? true : !!req.body?.shared; - const result = addTripPhotos( + const result = await addTripPhotos( tripId, authReq.user.id, shared, - selections, + req.body?.selections || [], + sid, ); - if ('error' in result) return res.status(result.status).json({ error: result.error }); + if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); - res.json({ success: true, added: result.added }); - broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string); - - if (result.shared && result.added > 0) { - void notifySharedTripPhotos( - tripId, - authReq.user.id, - authReq.user.username || authReq.user.email, - result.added, - ).catch(() => {}); - } + res.json({ success: true, added: result.data.added }); }); -router.delete('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => { +router.put('/trips/:tripId/photos/sharing', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; - const result = removeTripPhoto(tripId, authReq.user.id, req.body?.provider, req.body?.asset_id); - if ('error' in result) return res.status(result.status).json({ error: result.error }); - res.json({ success: true }); - broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string); -}); - -router.put('/trips/:tripId/photos/sharing', authenticate, (req: Request, res: Response) => { - const authReq = req as AuthRequest; - const { tripId } = req.params; - const result = setTripPhotoSharing( + const result = await setTripPhotoSharing( tripId, authReq.user.id, req.body?.provider, req.body?.asset_id, req.body?.shared, ); - if ('error' in result) return res.status(result.status).json({ error: result.error }); + if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); res.json({ success: true }); - broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string); }); -router.post('/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => { +router.delete('/trips/:tripId/photos', authenticate, async (req: Request, res: Response) => { + const authReq = req as AuthRequest; + const { tripId } = req.params; + const result = await removeTripPhoto(tripId, authReq.user.id, req.body?.provider, req.body?.asset_id); + if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); + res.json({ success: true }); +}); + +//------------------------------ +// routes for managing album links + +router.get('/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => { + const authReq = req as AuthRequest; + const { tripId } = req.params; + const result = listTripAlbumLinks(tripId, authReq.user.id); + if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); + res.json({ links: result.data }); +}); + +router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const result = createTripAlbumLink(tripId, authReq.user.id, req.body?.provider, req.body?.album_id, req.body?.album_name); - if ('error' in result) return res.status(result.status).json({ error: result.error }); + if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); + res.json({ success: true }); +}); + +router.delete('/trips/:tripId/album-links/:linkId', authenticate, async (req: Request, res: Response) => { + const authReq = req as AuthRequest; + const { tripId, linkId } = req.params; + const result = removeAlbumLink(tripId, linkId, authReq.user.id); + if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); res.json({ success: true }); }); diff --git a/server/src/services/immichService.ts b/server/src/services/immichService.ts index 4550b2a..d4a353b 100644 --- a/server/src/services/immichService.ts +++ b/server/src/services/immichService.ts @@ -3,6 +3,7 @@ import { maybe_encrypt_api_key, decrypt_api_key } from './apiKeyCrypto'; import { checkSsrf } from '../utils/ssrfGuard'; import { writeAudit } from './auditLog'; import { addTripPhotos, getAlbumIdFromLink, Selection, updateSyncTimeForAlbumLink } from './memoriesService'; +import { error } from 'node:console'; // ── Credentials ──────────────────────────────────────────────────────────── @@ -314,14 +315,14 @@ export async function syncAlbumAssets( linkId: string, userId: number ): Promise<{ success?: boolean; added?: number; total?: number; error?: string; status?: number }> { - const albumId = getAlbumIdFromLink(tripId, linkId, userId); - if (!albumId) return { error: 'Album link not found', status: 404 }; + const response = getAlbumIdFromLink(tripId, linkId, userId); + if (!response.success) return { error: 'Album link not found', status: 404 }; const creds = getImmichCredentials(userId); if (!creds) return { error: 'Immich not configured', status: 400 }; try { - const resp = await fetch(`${creds.immich_url}/api/albums/${albumId}`, { + const resp = await fetch(`${creds.immich_url}/api/albums/${response.data}`, { headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' }, signal: AbortSignal.timeout(15000), }); @@ -334,12 +335,12 @@ export async function syncAlbumAssets( asset_ids: assets.map((a: any) => a.id), }; - const addResult = addTripPhotos(tripId, userId, true, [selection]); - if ('error' in addResult) return { error: addResult.error, status: addResult.status }; + const result = await addTripPhotos(tripId, userId, true, [selection]); + if ('error' in result) return { error: result.error.message, status: result.error.status }; updateSyncTimeForAlbumLink(linkId); - return { success: true, added: addResult.added, total: assets.length }; + return { success: true, added: result.data.added, total: assets.length }; } catch { return { error: 'Could not reach Immich', status: 502 }; } diff --git a/server/src/services/memoriesService.ts b/server/src/services/memoriesService.ts index 0470559..03ac465 100644 --- a/server/src/services/memoriesService.ts +++ b/server/src/services/memoriesService.ts @@ -1,14 +1,29 @@ import { db, canAccessTrip } from '../db/database'; import { notifyTripMembers } from './notifications'; - -type ServiceError = { error: string; status: number }; +import { broadcast } from '../websocket'; -/** - * Verify that requestingUserId can access a shared photo belonging to ownerUserId. - * The asset must be shared (shared=1) and the requesting user must be a member of - * the same trip that contains the photo. - */ +type ServiceError = { success: false; error: { message: string; status: number } }; +type ServiceResult = { success: true; data: T } | ServiceError; + +function fail(error: string, status: number): ServiceError { + return { success: false, error: { message: error, status }}; +} + +function success(data: T): ServiceResult { + return { success: true, data: data }; +} + +function mapDbError(error: unknown, fallbackMessage: string): ServiceError { + if (error instanceof Error && /unique|constraint/i.test(error.message)) { + return fail('Resource already exists', 409); + } + return fail(fallbackMessage, 500); +} + +//----------------------------------------------- +//access check helper + export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number, tripId: string, assetId: string, provider: string): boolean { if (requestingUserId === ownerUserId) { return true; @@ -27,15 +42,70 @@ export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number if (!sharedAsset) { return false; } - return !!canAccessTrip(String(tripId), requestingUserId); + return !!canAccessTrip(tripId, requestingUserId); } - -function accessDeniedIfMissing(tripId: string, userId: number): ServiceError | null { - if (!canAccessTrip(tripId, userId)) { - return { error: 'Trip not found', status: 404 }; +export function listTripPhotos(tripId: string, userId: number): ServiceResult { + const access = canAccessTrip(tripId, userId); + if (!access) { + return fail('Trip not found or access denied', 404); } - return null; + + try { + const photos = db.prepare(` + SELECT tp.asset_id, tp.provider, tp.user_id, tp.shared, tp.added_at, + u.username, u.avatar + FROM trip_photos tp + JOIN users u ON tp.user_id = u.id + WHERE tp.trip_id = ? + AND (tp.user_id = ? OR tp.shared = 1) + ORDER BY tp.added_at ASC + `).all(tripId, userId) as any[]; + + return success(photos); + } catch (error) { + return mapDbError(error, 'Failed to list trip photos'); + } +} + +export function listTripAlbumLinks(tripId: string, userId: number): ServiceResult { + const access = canAccessTrip(tripId, userId); + if (!access) { + return fail('Trip not found or access denied', 404); + } + + try { + const links = db.prepare(` + SELECT tal.id, + tal.trip_id, + tal.user_id, + tal.provider, + tal.album_id, + tal.album_name, + tal.sync_enabled, + tal.last_synced_at, + tal.created_at, + u.username + FROM trip_album_links tal + JOIN users u ON tal.user_id = u.id + WHERE tal.trip_id = ? + ORDER BY tal.created_at ASC + `).all(tripId); + + return success(links); + } catch (error) { + return mapDbError(error, 'Failed to list trip album links'); + } +} + +//----------------------------------------------- +// managing photos in trip + +function _addTripPhoto(tripId: string, userId: number, provider: string, assetId: string, shared: boolean): boolean { + const result = db.prepare( + 'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, ?, ?)' + ).run(tripId, userId, assetId, provider, shared ? 1 : 0); + return result.changes > 0; } export type Selection = { @@ -43,234 +113,198 @@ export type Selection = { asset_ids: string[]; }; - -//fallback for old clients that don't send selections as an array of provider/asset_id groups -export function normalizeSelections(selectionsRaw: unknown, providerRaw: unknown, assetIdsRaw: unknown): Selection[] { - const selectionsFromBody = Array.isArray(selectionsRaw) ? selectionsRaw : null; - const provider = String(providerRaw || '').toLowerCase(); - - if (selectionsFromBody && selectionsFromBody.length > 0) { - return selectionsFromBody - .map((selection: any) => ({ - provider: String(selection?.provider || '').toLowerCase(), - asset_ids: Array.isArray(selection?.asset_ids) ? selection.asset_ids : [], - })) - .filter((selection: Selection) => selection.provider && selection.asset_ids.length > 0); - } - - if (provider && Array.isArray(assetIdsRaw) && assetIdsRaw.length > 0) { - return [{ provider, asset_ids: assetIdsRaw }]; - } - - return []; -} - -export function listTripPhotos(tripId: string, userId: number): { photos: any[] } | ServiceError { - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return denied; - - const photos = db.prepare(` - SELECT tp.asset_id, tp.provider, tp.user_id, tp.shared, tp.added_at, - u.username, u.avatar - FROM trip_photos tp - JOIN users u ON tp.user_id = u.id - WHERE tp.trip_id = ? - AND (tp.user_id = ? OR tp.shared = 1) - ORDER BY tp.added_at ASC - `).all(tripId, userId) as any[]; - - return { photos }; -} - -export function listTripAlbumLinks(tripId: string, userId: number): { links: any[] } | ServiceError { - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return denied; - - const links = db.prepare(` - SELECT tal.id, - tal.trip_id, - tal.user_id, - tal.provider, - tal.album_id, - tal.album_name, - tal.sync_enabled, - tal.last_synced_at, - tal.created_at, - u.username - FROM trip_album_links tal - JOIN users u ON tal.user_id = u.id - WHERE tal.trip_id = ? - ORDER BY tal.created_at ASC - `).all(tripId); - - return { links }; -} - -export function createTripAlbumLink( +export async function addTripPhotos( tripId: string, userId: number, - providerRaw: unknown, - albumIdRaw: unknown, - albumNameRaw: unknown, -): { success: true } | ServiceError { - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return denied; + shared: boolean, + selections: Selection[], + sid?: string, +): Promise> { + const access = canAccessTrip(tripId, userId); + if (!access) { + return fail('Trip not found or access denied', 404); + } + + if (selections.length === 0) { + return fail('No photos selected', 400); + } + + try { + let added = 0; + for (const selection of selections) { + for (const raw of selection.asset_ids) { + const assetId = String(raw || '').trim(); + if (!assetId) continue; + if (_addTripPhoto(tripId, userId, selection.provider, assetId, shared)) { + added++; + } + } + } + + await _notifySharedTripPhotos(tripId, userId, added); + broadcast(tripId, 'memories:updated', { userId }, sid); + return success({ added, shared }); + } catch (error) { + return mapDbError(error, 'Failed to add trip photos'); + } +} + + +export async function setTripPhotoSharing( + tripId: string, + userId: number, + provider: string, + assetId: string, + shared: boolean, + sid?: string, +): Promise> { + const access = canAccessTrip(tripId, userId); + if (!access) { + return fail('Trip not found or access denied', 404); + } + + try { + db.prepare(` + UPDATE trip_photos + SET shared = ? + WHERE trip_id = ? + AND user_id = ? + AND asset_id = ? + AND provider = ? + `).run(shared ? 1 : 0, tripId, userId, assetId, provider); + + await _notifySharedTripPhotos(tripId, userId, 1); + broadcast(tripId, 'memories:updated', { userId }, sid); + return success(true); + } catch (error) { + return mapDbError(error, 'Failed to update photo sharing'); + } +} + + +export function removeTripPhoto( + tripId: string, + userId: number, + provider: string, + assetId: string, + sid?: string, +): ServiceResult { + const access = canAccessTrip(tripId, userId); + if (!access) { + return fail('Trip not found or access denied', 404); + } + + try { + db.prepare(` + DELETE FROM trip_photos + WHERE trip_id = ? + AND user_id = ? + AND asset_id = ? + AND provider = ? + `).run(tripId, userId, assetId, provider); + + broadcast(tripId, 'memories:updated', { userId }, sid); + + return success(true); + } catch (error) { + return mapDbError(error, 'Failed to remove trip photo'); + } +} + +// ---------------------------------------------- +// managing album links in trip + +export function createTripAlbumLink(tripId: string, userId: number, providerRaw: unknown, albumIdRaw: unknown, albumNameRaw: unknown): ServiceResult { + const access = canAccessTrip(tripId, userId); + if (!access) { + return fail('Trip not found or access denied', 404); + } const provider = String(providerRaw || '').toLowerCase(); const albumId = String(albumIdRaw || '').trim(); const albumName = String(albumNameRaw || '').trim(); if (!provider) { - return { error: 'provider is required', status: 400 }; + return fail('provider is required', 400); } if (!albumId) { - return { error: 'album_id required', status: 400 }; + return fail('album_id required', 400); } try { - db.prepare( + const result = db.prepare( 'INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)' ).run(tripId, userId, provider, albumId, albumName); - return { success: true }; - } catch { - return { error: 'Album already linked', status: 400 }; - } -} -export function removeAlbumLink(tripId: string, linkId: string, userId: number): { success: true } | ServiceError { - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return denied; - - db.prepare('DELETE FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?') - .run(linkId, tripId, userId); - - return { success: true }; -} - -function addTripPhoto(tripId: string, userId: number, provider: string, assetId: string, shared: boolean): boolean { - const result = db.prepare( - 'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, ?, ?)' - ).run(tripId, userId, assetId, provider, shared ? 1 : 0); - return result.changes > 0; -} - -export function addTripPhotos( - tripId: string, - userId: number, - shared: boolean, - selections: Selection[], -): { success: true; added: number; shared: boolean } | ServiceError { - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return denied; - - if (selections.length === 0) { - return { error: 'No photos selected', status: 400 }; - } - - let added = 0; - for (const selection of selections) { - for (const raw of selection.asset_ids) { - const assetId = String(raw || '').trim(); - if (!assetId) continue; - if (addTripPhoto(tripId, userId, selection.provider, assetId, shared)) { - added++; - } + if (result.changes === 0) { + return fail('Album already linked', 409); } + + return success(true); + } catch (error) { + return mapDbError(error, 'Failed to link album'); } - return { success: true, added, shared }; } -export function getAlbumIdFromLink(tripId: string, linkId: string, userId: number): string { - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return null; +export function removeAlbumLink(tripId: string, linkId: string, userId: number): ServiceResult { + const access = canAccessTrip(tripId, userId); + if (!access) { + return fail('Trip not found or access denied', 404); + } - const { album_id } = db.prepare('SELECT album_id FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?').get(linkId, tripId, userId) as { album_id: string } | null; + try { + db.prepare('DELETE FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?') + .run(linkId, tripId, userId); - return album_id; + return success(true); + } catch (error) { + return mapDbError(error, 'Failed to remove album link'); + } +} + +//helpers for album link syncing + +export function getAlbumIdFromLink(tripId: string, linkId: string, userId: number): ServiceResult { + const access = canAccessTrip(tripId, userId); + if (!access) return fail('Trip not found or access denied', 404); + + try { + const row = db.prepare('SELECT album_id FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?') + .get(linkId, tripId, userId) as { album_id: string } | null; + + return row ? success(row.album_id) : fail('Album link not found', 404); + } catch { + return fail('Failed to retrieve album link', 500); + } } export function updateSyncTimeForAlbumLink(linkId: string): void { db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId); } +//----------------------------------------------- +// notifications helper - -export function removeTripPhoto( - tripId: string, - userId: number, - providerRaw: unknown, - assetIdRaw: unknown, -): { success: true } | ServiceError { - const assetId = String(assetIdRaw || ''); - const provider = String(providerRaw || '').toLowerCase(); - - if (!assetId) { - return { error: 'asset_id is required', status: 400 }; - } - if (!provider) { - return { error: 'provider is required', status: 400 }; - } - - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return denied; - - db.prepare(` - DELETE FROM trip_photos - WHERE trip_id = ? - AND user_id = ? - AND asset_id = ? - AND provider = ? - `).run(tripId, userId, assetId, provider); - - return { success: true }; -} - -export function setTripPhotoSharing( - tripId: string, - userId: number, - providerRaw: unknown, - assetIdRaw: unknown, - sharedRaw: unknown, -): { success: true } | ServiceError { - const assetId = String(assetIdRaw || ''); - const provider = String(providerRaw || '').toLowerCase(); - - if (!assetId) { - return { error: 'asset_id is required', status: 400 }; - } - if (!provider) { - return { error: 'provider is required', status: 400 }; - } - - const denied = accessDeniedIfMissing(tripId, userId); - if (denied) return denied; - - db.prepare(` - UPDATE trip_photos - SET shared = ? - WHERE trip_id = ? - AND user_id = ? - AND asset_id = ? - AND provider = ? - `).run(sharedRaw ? 1 : 0, tripId, userId, assetId, provider); - - return { success: true }; -} - -export async function notifySharedTripPhotos( +async function _notifySharedTripPhotos( tripId: string, actorUserId: number, - actorName: string, added: number, -): Promise { - if (added <= 0) return; +): Promise> { + if (added <= 0) return fail('No photos shared, skipping notifications', 200); - const tripInfo = db.prepare('SELECT title FROM trips WHERE id = ?').get(tripId) as { title: string } | undefined; - await notifyTripMembers(Number(tripId), actorUserId, 'photos_shared', { - trip: tripInfo?.title || 'Untitled', - actor: actorName, - count: String(added), - }); + try { + const actorRow = db.prepare('SELECT username FROM users WHERE id = ?').get(actorUserId) as { username: string | null }; + + const tripInfo = db.prepare('SELECT title FROM trips WHERE id = ?').get(tripId) as { title: string } | undefined; + await notifyTripMembers(Number(tripId), actorUserId, 'photos_shared', { + trip: tripInfo?.title || 'Untitled', + actor: actorRow?.username || 'Unknown', + count: String(added), + }); + return success(undefined); + } catch { + return fail('Failed to send notifications', 500); + } } diff --git a/server/src/services/synologyService.ts b/server/src/services/synologyService.ts index 7a01c1c..4f66fbd 100644 --- a/server/src/services/synologyService.ts +++ b/server/src/services/synologyService.ts @@ -6,6 +6,7 @@ import { decrypt_api_key, maybe_encrypt_api_key } from './apiKeyCrypto'; import { checkSsrf } from '../utils/ssrfGuard'; import { addTripPhotos, getAlbumIdFromLink, Selection, updateSyncTimeForAlbumLink } from './memoriesService'; import { error } from 'node:console'; +import { th } from 'zod/locales'; const SYNOLOGY_API_TIMEOUT_MS = 30000; const SYNOLOGY_PROVIDER = 'synologyphotos'; @@ -403,8 +404,8 @@ export async function listSynologyAlbums(userId: number): Promise<{ albums: Arra export async function syncSynologyAlbumLink(userId: number, tripId: string, linkId: string): Promise<{ added: number; total: number }> { - const albumId = getAlbumIdFromLink(tripId, linkId, userId); - if (!albumId) { + const response = getAlbumIdFromLink(tripId, linkId, userId); + if (!response.success) { throw new SynologyServiceError(404, 'Album link not found'); } @@ -417,7 +418,7 @@ export async function syncSynologyAlbumLink(userId: number, tripId: string, link api: 'SYNO.Foto.Browse.Item', method: 'list', version: 1, - album_id: Number(albumId), + album_id: Number(response.data), offset, limit: pageSize, additional: ['thumbnail'], @@ -438,12 +439,12 @@ export async function syncSynologyAlbumLink(userId: number, tripId: string, link asset_ids: allItems.map(item => String(item.additional?.thumbnail?.cache_key || '')).filter(id => id), }; - const addResult = addTripPhotos(tripId, userId, true, [selection]); - if ('error' in addResult) throw new SynologyServiceError(addResult.status, addResult.error); - updateSyncTimeForAlbumLink(linkId); - return { added: addResult.added, total: allItems.length }; + const result = await addTripPhotos(tripId, userId, true, [selection]); + if ('error' in result) throw new SynologyServiceError(result.error.status, result.error.message); + + return { added: result.data.added, total: allItems.length }; } export async function searchSynologyPhotos(userId: number, from?: string, to?: string, offset = 0, limit = 300): Promise<{ assets: SynologyPhotoInfo[]; total: number; hasMore: boolean }> {