import { Router } from 'express'; import type { WebContext } from '../context.js'; import { resolveRedditUsername } from '../reddit-identities.js'; export function createMediaRouter(ctx: WebContext): Router { const router = Router(); router.get('/media', async (_req, res) => { if (!ctx.services.media) { res.json([]); return; } try { const runtimes = ctx.services.accountRuntimes?.list().filter((runtime) => runtime.state === 'running') ?? []; const items = runtimes.length > 0 ? (await Promise.all(runtimes.map((snapshot) => ctx.services.accountRuntimes!.get(snapshot.accountId)!.resources.mediaService.getMediaItems(), ))).flat() : await ctx.services.media.getMediaItems(); // Build a roomId → current chat lookup so we can override stale // stored usernames (e.g. items captured before username enrichment ran) // and surface the recipient even for rooms that were originally // ingested as `unknown`. const liveChats = runtimes.length > 0 ? (await Promise.all(runtimes.map((snapshot) => ctx.services.accountRuntimes!.get(snapshot.accountId)!.resources.chatService.getCachedConversations() .then((chats) => chats.map((chat) => ({ ...chat, accountId: snapshot.accountId }))), ))).flat() : (ctx.services.chat ? await ctx.services.chat.getCachedConversations() : []).map((chat) => ({ ...chat, accountId: '' })); const byRoom = new Map(liveChats.map((c) => [`${c.accountId}:${c.roomId ?? ''}`, c] as const)); res.json(items.map((item) => { const live = item.chatRoomId ? byRoom.get(`${item.accountId ?? ''}:${item.chatRoomId}`) : undefined; const username = resolveRedditUsername( live?.username ?? item.chatUsername, ctx.services.chat, ); const sender = /^(me|you)$/i.test(item.sender) ? 'You' : resolveRedditUsername(item.sender, ctx.services.chat) ?? username ?? 'Unknown Reddit user'; const account = item.accountId ? ctx.services.redditAccounts?.get(item.accountId) : null; return { ...item, chatUsername: username, sender, urlPath: `/media/${item.relativePath.split(/[\\/]/).map(encodeURIComponent).join('/')}`, owner: account ? { accountId: account.accountId, redditUsername: account.redditUsername } : undefined, }; })); } catch (error) { ctx.logger.error('Failed to fetch media index:', error); res.status(500).json({ error: String(error) }); } }); router.get('/media/sync', (_req, res) => { const runtimes = ctx.services.accountRuntimes?.list().filter((runtime) => runtime.state === 'running') ?? []; if (runtimes.length > 0) { const statuses = runtimes.map((snapshot) => ctx.services.accountRuntimes!.get(snapshot.accountId)!.resources.mediaSyncService.getStatus(), ); res.json({ isSyncing: statuses.some((status) => status.isSyncing), lastSyncAt: newest(statuses.map((status) => status.lastSyncAt)), lastSyncDurationMs: newest(statuses.map((status) => status.lastSyncDurationMs)), lastSyncChats: statuses.reduce((total, status) => total + (status.lastSyncChats ?? 0), 0) || null, lastError: statuses.find((status) => status.lastError)?.lastError ?? null, nextSyncAt: newest(statuses.map((status) => status.nextSyncAt)), }); return; } if (!ctx.services.mediaSync) { res.status(503).json({ error: 'Media sync service not initialized' }); return; } res.json(ctx.services.mediaSync.getStatus()); }); router.post('/media/sync', async (req, res) => { const runtimes = ctx.services.accountRuntimes?.list().filter((runtime) => runtime.state === 'running') ?? []; if (runtimes.length > 0) { const full = req.body?.full === true; const results = await Promise.all(runtimes.map((snapshot) => ctx.services.accountRuntimes!.get(snapshot.accountId)!.resources.mediaSyncService.triggerSync(full), )); res.json({ success: results.some(Boolean), reason: results.some(Boolean) ? undefined : 'Sync already in progress' }); return; } if (!ctx.services.mediaSync) { res.status(503).json({ error: 'Media sync service not initialized' }); return; } try { const full = req.body?.full === true; const started = await ctx.services.mediaSync.triggerSync(full); if (!started) { res.json({ success: false, reason: 'Sync already in progress' }); return; } ctx.broadcaster.push('info', `Media sync triggered from dashboard (${full ? 'full' : 'incremental'})`); res.json({ success: true }); } catch (error) { ctx.logger.error('Media sync trigger failed:', error); res.status(500).json({ error: String(error) }); } }); router.post('/media/sync/full', async (_req, res) => { const runtimes = ctx.services.accountRuntimes?.list().filter((runtime) => runtime.state === 'running') ?? []; if (runtimes.length > 0) { const results = await Promise.all(runtimes.map((snapshot) => ctx.services.accountRuntimes!.get(snapshot.accountId)!.resources.mediaSyncService.triggerSync(true), )); res.json({ success: results.some(Boolean), reason: results.some(Boolean) ? undefined : 'Sync already in progress' }); return; } if (!ctx.services.mediaSync) { res.status(503).json({ error: 'Media sync service not initialized' }); return; } try { const started = await ctx.services.mediaSync.triggerSync(true); if (!started) { res.json({ success: false, reason: 'Sync already in progress' }); return; } ctx.broadcaster.push('info', 'Full media sync triggered from dashboard (all conversations)'); res.json({ success: true }); } catch (error) { ctx.logger.error('Full media sync trigger failed:', error); res.status(500).json({ error: String(error) }); } }); return router; } function newest(values: Array): number | null { const present = values.filter((value): value is number => value !== null); return present.length > 0 ? Math.max(...present) : null; }