adding helper functions for syncing albums
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
removeTripPhoto,
|
||||
setTripPhotoSharing,
|
||||
notifySharedTripPhotos,
|
||||
normalizeSelections,
|
||||
} from '../services/memoriesService';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -44,13 +45,14 @@ router.delete('/trips/:tripId/album-links/:linkId', authenticate, (req: Request,
|
||||
router.post('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
|
||||
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(
|
||||
tripId,
|
||||
authReq.user.id,
|
||||
req.body?.shared,
|
||||
req.body?.selections,
|
||||
req.body?.provider,
|
||||
req.body?.asset_ids,
|
||||
shared,
|
||||
selections,
|
||||
);
|
||||
if ('error' in result) return res.status(result.status).json({ error: result.error });
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from '../db/database';
|
||||
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';
|
||||
|
||||
// ── Credentials ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -313,15 +314,14 @@ export async function syncAlbumAssets(
|
||||
linkId: string,
|
||||
userId: number
|
||||
): Promise<{ success?: boolean; added?: number; total?: number; error?: string; status?: number }> {
|
||||
const link = db.prepare('SELECT * FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ? AND provider = ?')
|
||||
.get(linkId, tripId, userId, 'immich') as any;
|
||||
if (!link) return { error: 'Album link not found', status: 404 };
|
||||
const albumId = getAlbumIdFromLink(tripId, linkId, userId);
|
||||
if (!albumId) 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/${link.album_id}`, {
|
||||
const resp = await fetch(`${creds.immich_url}/api/albums/${albumId}`, {
|
||||
headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
@@ -329,16 +329,17 @@ export async function syncAlbumAssets(
|
||||
const albumData = await resp.json() as { assets?: any[] };
|
||||
const assets = (albumData.assets || []).filter((a: any) => a.type === 'IMAGE');
|
||||
|
||||
const insert = db.prepare("INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, 'immich', 1)");
|
||||
let added = 0;
|
||||
for (const asset of assets) {
|
||||
const r = insert.run(tripId, userId, asset.id);
|
||||
if (r.changes > 0) added++;
|
||||
}
|
||||
const selection: Selection = {
|
||||
provider: 'immich',
|
||||
asset_ids: assets.map((a: any) => a.id),
|
||||
};
|
||||
|
||||
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
||||
const addResult = addTripPhotos(tripId, userId, true, [selection]);
|
||||
if ('error' in addResult) return { error: addResult.error, status: addResult.status };
|
||||
|
||||
return { success: true, added, total: assets.length };
|
||||
updateSyncTimeForAlbumLink(linkId);
|
||||
|
||||
return { success: true, added: addResult.added, total: assets.length };
|
||||
} catch {
|
||||
return { error: 'Could not reach Immich', status: 502 };
|
||||
}
|
||||
|
||||
@@ -38,12 +38,14 @@ function accessDeniedIfMissing(tripId: string, userId: number): ServiceError | n
|
||||
return null;
|
||||
}
|
||||
|
||||
type Selection = {
|
||||
export type Selection = {
|
||||
provider: string;
|
||||
asset_ids: unknown[];
|
||||
asset_ids: string[];
|
||||
};
|
||||
|
||||
function normalizeSelections(selectionsRaw: unknown, providerRaw: unknown, assetIdsRaw: unknown): Selection[] {
|
||||
|
||||
//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();
|
||||
|
||||
@@ -145,40 +147,54 @@ export function removeAlbumLink(tripId: string, linkId: string, userId: number):
|
||||
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,
|
||||
sharedRaw: unknown,
|
||||
selectionsRaw: unknown,
|
||||
providerRaw: unknown,
|
||||
assetIdsRaw: unknown,
|
||||
shared: boolean,
|
||||
selections: Selection[],
|
||||
): { success: true; added: number; shared: boolean } | ServiceError {
|
||||
const denied = accessDeniedIfMissing(tripId, userId);
|
||||
if (denied) return denied;
|
||||
|
||||
const shared = sharedRaw === undefined ? true : !!sharedRaw;
|
||||
const selections = normalizeSelections(selectionsRaw, providerRaw, assetIdsRaw);
|
||||
if (selections.length === 0) {
|
||||
return { error: 'selections required', status: 400 };
|
||||
return { error: 'No photos selected', status: 400 };
|
||||
}
|
||||
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
|
||||
let added = 0;
|
||||
for (const selection of selections) {
|
||||
for (const raw of selection.asset_ids) {
|
||||
const assetId = String(raw || '').trim();
|
||||
if (!assetId) continue;
|
||||
const result = insert.run(tripId, userId, assetId, selection.provider, shared ? 1 : 0);
|
||||
if (result.changes > 0) added++;
|
||||
if (addTripPhoto(tripId, userId, selection.provider, assetId, shared)) {
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, added, shared };
|
||||
}
|
||||
|
||||
export function getAlbumIdFromLink(tripId: string, linkId: string, userId: number): string {
|
||||
const denied = accessDeniedIfMissing(tripId, userId);
|
||||
if (denied) return null;
|
||||
|
||||
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;
|
||||
|
||||
return album_id;
|
||||
}
|
||||
|
||||
export function updateSyncTimeForAlbumLink(linkId: string): void {
|
||||
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function removeTripPhoto(
|
||||
tripId: string,
|
||||
userId: number,
|
||||
@@ -256,3 +272,5 @@ export async function notifySharedTripPhotos(
|
||||
count: String(added),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Readable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { Request, Response as ExpressResponse } from 'express';
|
||||
import { Response as ExpressResponse } from 'express';
|
||||
import { db } from '../db/database';
|
||||
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';
|
||||
|
||||
const SYNOLOGY_API_TIMEOUT_MS = 30000;
|
||||
const SYNOLOGY_PROVIDER = 'synologyphotos';
|
||||
@@ -401,10 +403,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 link = db.prepare(`SELECT * FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ? AND provider = ?`)
|
||||
.get(linkId, tripId, userId, SYNOLOGY_PROVIDER) as { album_id?: string | number } | undefined;
|
||||
|
||||
if (!link) {
|
||||
const albumId = getAlbumIdFromLink(tripId, linkId, userId);
|
||||
if (!albumId) {
|
||||
throw new SynologyServiceError(404, 'Album link not found');
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ export async function syncSynologyAlbumLink(userId: number, tripId: string, link
|
||||
api: 'SYNO.Foto.Browse.Item',
|
||||
method: 'list',
|
||||
version: 1,
|
||||
album_id: Number(link.album_id),
|
||||
album_id: Number(albumId),
|
||||
offset,
|
||||
limit: pageSize,
|
||||
additional: ['thumbnail'],
|
||||
@@ -433,22 +433,17 @@ export async function syncSynologyAlbumLink(userId: number, tripId: string, link
|
||||
offset += pageSize;
|
||||
}
|
||||
|
||||
const insert = db.prepare(
|
||||
"INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, 'synologyphotos', 1)"
|
||||
);
|
||||
const selection: Selection = {
|
||||
provider: SYNOLOGY_PROVIDER,
|
||||
asset_ids: allItems.map(item => String(item.additional?.thumbnail?.cache_key || '')).filter(id => id),
|
||||
};
|
||||
|
||||
let added = 0;
|
||||
for (const item of allItems) {
|
||||
const transformed = normalizeSynologyPhotoInfo(item);
|
||||
const assetId = String(transformed?.id || '').trim();
|
||||
if (!assetId) continue;
|
||||
const result = insert.run(tripId, userId, assetId);
|
||||
if (result.changes > 0) added++;
|
||||
}
|
||||
const addResult = addTripPhotos(tripId, userId, true, [selection]);
|
||||
if ('error' in addResult) throw new SynologyServiceError(addResult.status, addResult.error);
|
||||
|
||||
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
||||
updateSyncTimeForAlbumLink(linkId);
|
||||
|
||||
return { added, total: allItems.length };
|
||||
return { added: addResult.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 }> {
|
||||
|
||||
Reference in New Issue
Block a user