145 lines
6.2 KiB
TypeScript
145 lines
6.2 KiB
TypeScript
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>): number | null {
|
|
const present = values.filter((value): value is number => value !== null);
|
|
return present.length > 0 ? Math.max(...present) : null;
|
|
}
|