diff --git a/README.md b/README.md index 5e7da18..ac3d8d6 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能 | VIDEOINFO_CACHE_MINUTES | 私人影库视频信息在内存中的缓存时长(分钟) | 正整数 | 1440(1天) | | NEXT_PUBLIC_ENABLE_SOURCE_SEARCH | 是否开启源站寻片功能 | true/false | true | | MAX_PLAY_RECORDS_PER_USER | 单个用户播放记录清理阈值(超过此数量将自动清理旧记录) | 正整数 | 100 | +| MAX_MANGA_HISTORY_PER_USER | 单个用户漫画阅读历史保留上限 | 正整数 | 100 | | INIT_CONFIG | 初始配置(JSON 格式,包含 api_site、custom_category、lives 等) | JSON 字符串 | (空) | | CONFIG_SUBSCRIPTION_URL | 配置订阅 URL(Base58 编码的配置文件地址,优先级高于 INIT_CONFIG) | URL | (空) | | TMDB_API_KEY | TMDB API 密钥 | 任意字符串 | (空) | diff --git a/src/lib/d1.db.ts b/src/lib/d1.db.ts index 6f9138f..507c1ec 100644 --- a/src/lib/d1.db.ts +++ b/src/lib/d1.db.ts @@ -2068,13 +2068,14 @@ export class D1Storage implements IStorage { async cleanupOldMangaReadRecords(userName: string): Promise { try { const maxRecords = parseInt(process.env.MAX_MANGA_HISTORY_PER_USER || '100', 10); + const threshold = maxRecords + 10; const countResult = await this.db .prepare('SELECT COUNT(*) as count FROM manga_read_records WHERE username = ?') .bind(userName) .first(); const count = Number(countResult?.count || 0); - if (count <= maxRecords) return; + if (count <= threshold) return; await this.db .prepare(` diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index 7e8654b..7913bd7 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -95,6 +95,8 @@ const PLAY_RECORDS_KEY = 'moontv_play_records'; const FAVORITES_KEY = 'moontv_favorites'; const MANGA_SHELF_KEY = 'moontv_manga_shelf'; const MANGA_HISTORY_KEY = 'moontv_manga_history'; +const DEFAULT_MAX_MANGA_HISTORY_RECORDS = 100; +const DEFAULT_MAX_MANGA_HISTORY_THRESHOLD = DEFAULT_MAX_MANGA_HISTORY_RECORDS + 10; const SEARCH_HISTORY_KEY = 'moontv_search_history'; const MUSIC_PLAY_RECORDS_KEY = 'moontv_music_play_records'; @@ -1701,6 +1703,17 @@ export async function clearAllMangaShelf(): Promise { window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: {} })); } +function trimMangaReadRecords(records: Record): Record { + const entries = Object.entries(records); + if (entries.length <= DEFAULT_MAX_MANGA_HISTORY_THRESHOLD) return records; + + return Object.fromEntries( + entries + .sort(([, a], [, b]) => b.saveTime - a.saveTime) + .slice(0, DEFAULT_MAX_MANGA_HISTORY_RECORDS) + ); +} + export async function getAllMangaReadRecords(): Promise> { if (typeof window === 'undefined') return {}; @@ -1748,8 +1761,9 @@ export async function saveMangaReadRecord(sourceId: string, mangaId: string, rec if (STORAGE_TYPE !== 'localstorage') { const cached = cacheManager.getCachedMangaReadRecords() || {}; cached[key] = record; - cacheManager.cacheMangaReadRecords(cached); - window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: cached })); + const trimmedRecords = trimMangaReadRecords(cached); + cacheManager.cacheMangaReadRecords(trimmedRecords); + window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: trimmedRecords })); try { await fetchWithAuth('/api/manga/history', { @@ -1766,8 +1780,9 @@ export async function saveMangaReadRecord(sourceId: string, mangaId: string, rec const allRecords = await getAllMangaReadRecords(); allRecords[key] = record; - localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(allRecords)); - window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: allRecords })); + const trimmedRecords = trimMangaReadRecords(allRecords); + localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(trimmedRecords)); + window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: trimmedRecords })); } export async function deleteMangaReadRecord(sourceId: string, mangaId: string): Promise { diff --git a/src/lib/postgres.db.ts b/src/lib/postgres.db.ts index bbf3b15..00cde42 100644 --- a/src/lib/postgres.db.ts +++ b/src/lib/postgres.db.ts @@ -2037,13 +2037,14 @@ export class PostgresStorage implements IStorage { async cleanupOldMangaReadRecords(userName: string): Promise { try { const maxRecords = parseInt(process.env.MAX_MANGA_HISTORY_PER_USER || '100', 10); + const threshold = maxRecords + 10; const countResult = await this.db .prepare('SELECT COUNT(*) as count FROM manga_read_records WHERE username = $1') .bind(userName) .first(); const count = Number(countResult?.count || 0); - if (count <= maxRecords) return; + if (count <= threshold) return; await this.db .prepare(` diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts index b98b51b..18fae9d 100644 --- a/src/lib/redis-base.db.ts +++ b/src/lib/redis-base.db.ts @@ -1557,6 +1557,8 @@ export abstract class BaseRedisStorage implements IStorage { async cleanupOldMangaReadRecords(userName: string): Promise { const records = await this.getAllMangaReadRecords(userName); const maxRecords = parseInt(process.env.MAX_MANGA_HISTORY_PER_USER || '100', 10); + const threshold = maxRecords + 10; + if (Object.keys(records).length <= threshold) return; const keys = Object.entries(records) .sort(([, a], [, b]) => b.saveTime - a.saveTime) .slice(maxRecords)