Files
RedditBot/src/web/routes/media.ts
T

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;
}