增加漫画展馆功能

This commit is contained in:
mtvpls
2026-04-16 16:01:47 +08:00
parent 4fe9314c57
commit cb225c3291
33 changed files with 3242 additions and 5 deletions
+265
View File
@@ -18,6 +18,7 @@ import {
MovieRequest,
} from './types';
import { AdminConfig } from './admin.types';
import { MangaReadRecord, MangaShelfItem } from './manga.types';
import { DatabaseAdapter } from './d1-adapter';
import { MusicV2HistoryRecord, MusicV2PlaylistItem, MusicV2PlaylistRecord } from './music-v2';
@@ -1747,6 +1748,268 @@ export class PostgresStorage implements IStorage {
}
}
// ==================== 漫画书架 ====================
async getMangaShelf(userName: string, key: string): Promise<MangaShelfItem | null> {
try {
const result = await this.db
.prepare('SELECT * FROM manga_shelf WHERE username = $1 AND key = $2')
.bind(userName, key)
.first();
if (!result) return null;
return {
title: result.title as string,
cover: (result.cover as string) || '',
sourceId: result.source_id as string,
sourceName: result.source_name as string,
mangaId: result.manga_id as string,
saveTime: Number(result.save_time || 0),
description: (result.description as string) || undefined,
author: (result.author as string) || undefined,
status: (result.status as string) || undefined,
lastChapterId: (result.last_chapter_id as string) || undefined,
lastChapterName: (result.last_chapter_name as string) || undefined,
};
} catch (err) {
console.error('PostgresStorage.getMangaShelf error:', err);
throw err;
}
}
async setMangaShelf(userName: string, key: string, item: MangaShelfItem): Promise<void> {
try {
await this.db
.prepare(`
INSERT INTO manga_shelf (
username, key, source_id, source_name, manga_id, title, cover, save_time,
description, author, status, last_chapter_id, last_chapter_name
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (username, key) DO UPDATE SET
source_id = EXCLUDED.source_id,
source_name = EXCLUDED.source_name,
manga_id = EXCLUDED.manga_id,
title = EXCLUDED.title,
cover = EXCLUDED.cover,
save_time = EXCLUDED.save_time,
description = EXCLUDED.description,
author = EXCLUDED.author,
status = EXCLUDED.status,
last_chapter_id = EXCLUDED.last_chapter_id,
last_chapter_name = EXCLUDED.last_chapter_name
`)
.bind(
userName,
key,
item.sourceId,
item.sourceName,
item.mangaId,
item.title,
item.cover || '',
item.saveTime,
item.description || null,
item.author || null,
item.status || null,
item.lastChapterId || null,
item.lastChapterName || null
)
.run();
} catch (err) {
console.error('PostgresStorage.setMangaShelf error:', err);
throw err;
}
}
async getAllMangaShelf(userName: string): Promise<{ [key: string]: MangaShelfItem }> {
try {
const results = await this.db
.prepare('SELECT * FROM manga_shelf WHERE username = $1 ORDER BY save_time DESC')
.bind(userName)
.all();
const shelves: { [key: string]: MangaShelfItem } = {};
if (!results.results) return shelves;
for (const row of results.results) {
shelves[row.key as string] = {
title: row.title as string,
cover: (row.cover as string) || '',
sourceId: row.source_id as string,
sourceName: row.source_name as string,
mangaId: row.manga_id as string,
saveTime: Number(row.save_time || 0),
description: (row.description as string) || undefined,
author: (row.author as string) || undefined,
status: (row.status as string) || undefined,
lastChapterId: (row.last_chapter_id as string) || undefined,
lastChapterName: (row.last_chapter_name as string) || undefined,
};
}
return shelves;
} catch (err) {
console.error('PostgresStorage.getAllMangaShelf error:', err);
throw err;
}
}
async deleteMangaShelf(userName: string, key: string): Promise<void> {
try {
await this.db
.prepare('DELETE FROM manga_shelf WHERE username = $1 AND key = $2')
.bind(userName, key)
.run();
} catch (err) {
console.error('PostgresStorage.deleteMangaShelf error:', err);
throw err;
}
}
// ==================== 漫画阅读历史 ====================
async getMangaReadRecord(userName: string, key: string): Promise<MangaReadRecord | null> {
try {
const result = await this.db
.prepare('SELECT * FROM manga_read_records WHERE username = $1 AND key = $2')
.bind(userName, key)
.first();
if (!result) return null;
return {
title: result.title as string,
cover: (result.cover as string) || '',
sourceId: result.source_id as string,
sourceName: result.source_name as string,
mangaId: result.manga_id as string,
chapterId: result.chapter_id as string,
chapterName: result.chapter_name as string,
pageIndex: Number(result.page_index || 0),
pageCount: Number(result.page_count || 0),
saveTime: Number(result.save_time || 0),
};
} catch (err) {
console.error('PostgresStorage.getMangaReadRecord error:', err);
throw err;
}
}
async setMangaReadRecord(userName: string, key: string, record: MangaReadRecord): Promise<void> {
try {
await this.db
.prepare(`
INSERT INTO manga_read_records (
username, key, source_id, source_name, manga_id, title, cover,
chapter_id, chapter_name, page_index, page_count, save_time
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (username, key) DO UPDATE SET
source_id = EXCLUDED.source_id,
source_name = EXCLUDED.source_name,
manga_id = EXCLUDED.manga_id,
title = EXCLUDED.title,
cover = EXCLUDED.cover,
chapter_id = EXCLUDED.chapter_id,
chapter_name = EXCLUDED.chapter_name,
page_index = EXCLUDED.page_index,
page_count = EXCLUDED.page_count,
save_time = EXCLUDED.save_time
`)
.bind(
userName,
key,
record.sourceId,
record.sourceName,
record.mangaId,
record.title,
record.cover || '',
record.chapterId,
record.chapterName,
record.pageIndex,
record.pageCount,
record.saveTime
)
.run();
} catch (err) {
console.error('PostgresStorage.setMangaReadRecord error:', err);
throw err;
}
}
async getAllMangaReadRecords(userName: string): Promise<{ [key: string]: MangaReadRecord }> {
try {
const results = await this.db
.prepare('SELECT * FROM manga_read_records WHERE username = $1 ORDER BY save_time DESC')
.bind(userName)
.all();
const records: { [key: string]: MangaReadRecord } = {};
if (!results.results) return records;
for (const row of results.results) {
records[row.key as string] = {
title: row.title as string,
cover: (row.cover as string) || '',
sourceId: row.source_id as string,
sourceName: row.source_name as string,
mangaId: row.manga_id as string,
chapterId: row.chapter_id as string,
chapterName: row.chapter_name as string,
pageIndex: Number(row.page_index || 0),
pageCount: Number(row.page_count || 0),
saveTime: Number(row.save_time || 0),
};
}
return records;
} catch (err) {
console.error('PostgresStorage.getAllMangaReadRecords error:', err);
throw err;
}
}
async deleteMangaReadRecord(userName: string, key: string): Promise<void> {
try {
await this.db
.prepare('DELETE FROM manga_read_records WHERE username = $1 AND key = $2')
.bind(userName, key)
.run();
} catch (err) {
console.error('PostgresStorage.deleteMangaReadRecord error:', err);
throw err;
}
}
async cleanupOldMangaReadRecords(userName: string): Promise<void> {
try {
const maxRecords = parseInt(process.env.MAX_MANGA_HISTORY_PER_USER || '100', 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;
await this.db
.prepare(`
DELETE FROM manga_read_records
WHERE username = $1
AND key NOT IN (
SELECT key FROM manga_read_records
WHERE username = $1
ORDER BY save_time DESC
LIMIT $2
)
`)
.bind(userName, maxRecords)
.run();
} catch (err) {
console.error('PostgresStorage.cleanupOldMangaReadRecords error:', err);
throw err;
}
}
// ==================== 跳过配置 ====================
async getSkipConfig(userName: string, source: string, id: string): Promise<SkipConfig | null> {
@@ -2205,6 +2468,8 @@ export class PostgresStorage implements IStorage {
'play_records',
'favorites',
'search_history',
'manga_shelf',
'manga_read_records',
'skip_configs',
'music_play_records',
'music_playlists',