feat: support concurrent Reddit accounts

This commit is contained in:
jjbrunton committed 2026-08-08 19:00:38 +01:00
1 parent d819a0f1e6
commit 16a31335d0
51 files changed
+3241 -250

No files matched your search

+56 -7
View File
@@ -11,19 +11,27 @@ export function createMediaRouter(ctx: WebContext): Router {
return;
}
try {
const items = await ctx.services.media.getMediaItems();
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 = ctx.services.chat
? await ctx.services.chat.getCachedConversations()
: [];
const byRoom = new Map(liveChats.map((c) => [c.roomId ?? '', c] as const));
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.chatRoomId) : undefined;
const live = item.chatRoomId ? byRoom.get(`${item.accountId ?? ''}:${item.chatRoomId}`) : undefined;
const username = resolveRedditUsername(
live?.username ?? item.chatUsername,
ctx.services.chat,
@@ -31,11 +39,15 @@ export function createMediaRouter(ctx: WebContext): Router {
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/${encodeURIComponent(item.relativePath)}`,
urlPath: `/media/${item.relativePath.split(/[\\/]/).map(encodeURIComponent).join('/')}`,
owner: account ? { accountId: account.accountId, redditUsername: account.redditUsername } : undefined,
};
}));
} catch (error) {
@@ -45,6 +57,21 @@ export function createMediaRouter(ctx: WebContext): Router {
});
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;
@@ -53,6 +80,15 @@ export function createMediaRouter(ctx: WebContext): Router {
});
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;
@@ -73,6 +109,14 @@ export function createMediaRouter(ctx: WebContext): Router {
});
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;
@@ -93,3 +137,8 @@ export function createMediaRouter(ctx: WebContext): Router {
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;
}