feat: support concurrent Reddit accounts
This commit is contained in:
1 parent
d819a0f1e6
commit
16a31335d0
51 files changed
+3241
-250
No files matched your search
+59
-7
@@ -4,6 +4,7 @@ import crypto from 'crypto';
|
||||
import { z } from 'zod';
|
||||
import type { Logger } from '../utils/logger.js';
|
||||
import type { ActiveChat, ChatMedia, ChatMessage } from './chat.js';
|
||||
import type { AccountContentRepository } from '../database/account-repositories.js';
|
||||
|
||||
export interface MediaDownloadResult {
|
||||
buffer: Buffer;
|
||||
@@ -15,6 +16,7 @@ export interface MediaDownloader {
|
||||
}
|
||||
|
||||
export interface MediaRecord {
|
||||
accountId?: string;
|
||||
id: string;
|
||||
chatUsername: string | null;
|
||||
chatRoomId: string | null;
|
||||
@@ -59,13 +61,16 @@ export class MediaService {
|
||||
private downloader: MediaDownloader,
|
||||
private logger: Logger,
|
||||
mediaRoot?: string,
|
||||
private readonly databaseStore?: AccountContentRepository,
|
||||
private readonly getAccountId: () => string | null = () => null,
|
||||
) {
|
||||
this.mediaRoot = mediaRoot ?? path.join(process.cwd(), 'data', 'media');
|
||||
this.indexPath = path.join(this.mediaRoot, 'index.json');
|
||||
}
|
||||
|
||||
async attachMedia(chat: ActiveChat, messages: ScrapedChatMessage[]): Promise<ChatMessage[]> {
|
||||
await this.loadIndex();
|
||||
const databaseBacked = Boolean(this.databaseStore && this.getAccountId());
|
||||
if (!databaseBacked) await this.loadIndex();
|
||||
const updated: ChatMessage[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
@@ -79,11 +84,26 @@ export class MediaService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.saveIndex();
|
||||
if (!databaseBacked) await this.saveIndex();
|
||||
return updated;
|
||||
}
|
||||
|
||||
async getMediaItems(): Promise<MediaRecord[]> {
|
||||
const accountId = this.getAccountId();
|
||||
if (this.databaseStore && accountId) {
|
||||
return this.databaseStore.listMedia(accountId).map((item) => ({
|
||||
accountId: item.accountId,
|
||||
id: item.mediaId,
|
||||
chatUsername: item.chatUsername,
|
||||
chatRoomId: item.chatRoomId,
|
||||
sender: item.sender,
|
||||
timestamp: item.timestamp,
|
||||
originalUrl: item.originalUrl,
|
||||
fileName: item.fileName,
|
||||
relativePath: item.relativePath,
|
||||
createdAt: item.createdAt,
|
||||
}));
|
||||
}
|
||||
await this.loadIndex();
|
||||
return (this.index?.items || []).slice().sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
@@ -133,11 +153,23 @@ export class MediaService {
|
||||
|
||||
// Try the full-resolution URL first, fall back to original
|
||||
const fullUrl = this.resolveFullMediaUrl(url);
|
||||
const id = crypto.createHash('sha1').update(fullUrl).digest('hex');
|
||||
const accountId = this.getAccountId();
|
||||
const id = crypto.createHash('sha1').update(`${accountId ?? 'legacy'}\0${fullUrl}`).digest('hex');
|
||||
if (this.databaseStore && accountId) {
|
||||
const existingRecord = this.databaseStore.getMedia(accountId, id);
|
||||
if (existingRecord) {
|
||||
try {
|
||||
await fs.access(path.join(this.mediaRoot, existingRecord.relativePath));
|
||||
return { ...existingRecord, id: existingRecord.mediaId };
|
||||
} catch {
|
||||
this.databaseStore.deleteMedia(accountId, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
const existing = this.index?.items.find((item) => item.id === id);
|
||||
if (existing) {
|
||||
// Verify the file still exists on disk
|
||||
const existingPath = path.join(this.mediaRoot, existing.fileName);
|
||||
const existingPath = path.join(this.mediaRoot, existing.relativePath);
|
||||
try {
|
||||
await fs.access(existingPath);
|
||||
return existing;
|
||||
@@ -165,13 +197,17 @@ export class MediaService {
|
||||
}
|
||||
|
||||
const fileName = `${id}${extension}`;
|
||||
const relativePath = fileName;
|
||||
const fullPath = path.join(this.mediaRoot, fileName);
|
||||
const accountDirectory = accountId
|
||||
? crypto.createHash('sha256').update(accountId).digest('hex').slice(0, 16)
|
||||
: '';
|
||||
const relativePath = accountDirectory ? path.join(accountDirectory, fileName) : fileName;
|
||||
const fullPath = path.join(this.mediaRoot, relativePath);
|
||||
|
||||
await fs.mkdir(this.mediaRoot, { recursive: true });
|
||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||
await fs.writeFile(fullPath, download.buffer);
|
||||
|
||||
const record: MediaRecord = {
|
||||
accountId: accountId ?? undefined,
|
||||
id,
|
||||
chatUsername: chat.username || null,
|
||||
chatRoomId: chat.roomId || null,
|
||||
@@ -183,6 +219,22 @@ export class MediaService {
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
if (this.databaseStore && accountId) {
|
||||
this.databaseStore.upsertMedia({
|
||||
accountId,
|
||||
mediaId: id,
|
||||
chatUsername: record.chatUsername,
|
||||
chatRoomId: record.chatRoomId,
|
||||
sender: record.sender,
|
||||
timestamp: record.timestamp,
|
||||
originalUrl: record.originalUrl,
|
||||
fileName: record.fileName,
|
||||
relativePath: record.relativePath,
|
||||
createdAt: record.createdAt,
|
||||
});
|
||||
return record;
|
||||
}
|
||||
|
||||
if (!this.index) {
|
||||
this.index = { version: 1, savedAt: Date.now(), items: [record] };
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user