新增电子书架

This commit is contained in:
mtvpls
2026-04-29 02:24:29 +08:00
parent 6d9afd8072
commit 0d99953e99
38 changed files with 3826 additions and 6 deletions
+247 -2
View File
@@ -17,6 +17,7 @@ import {
} from './types';
import { AdminConfig } from './admin.types';
import { MangaReadRecord, MangaShelfItem } from './manga.types';
import { BookReadRecord, BookShelfItem } from './book.types';
import { DatabaseAdapter } from './d1-adapter';
import { MusicV2HistoryRecord, MusicV2PlaylistItem, MusicV2PlaylistRecord } from './music-v2';
import { userInfoCache } from './user-cache';
@@ -784,7 +785,7 @@ export class D1Storage implements IStorage {
username, song_id, source, songmid, name, artist, album, cover, duration_text, duration_sec,
play_progress_sec, last_played_at, play_count, last_quality, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(username, song_id) DO UPDATE SET
source = excluded.source,
songmid = excluded.songmid,
@@ -2096,6 +2097,248 @@ export class D1Storage implements IStorage {
}
}
// ==================== 电子书书架 ====================
async getBookShelf(userName: string, key: string): Promise<BookShelfItem | null> {
try {
const result = await this.db.prepare('SELECT * FROM book_shelf WHERE username = ? AND key = ?').bind(userName, key).first();
if (!result) return null;
return {
sourceId: result.source_id as string,
sourceName: result.source_name as string,
bookId: result.book_id as string,
title: result.title as string,
author: (result.author as string) || undefined,
cover: (result.cover as string) || undefined,
format: (result.format as 'epub' | 'pdf' | null) || undefined,
detailHref: (result.detail_href as string) || undefined,
acquisitionHref: (result.acquisition_href as string) || undefined,
progressPercent: result.progress_percent === null || result.progress_percent === undefined ? undefined : Number(result.progress_percent),
lastReadTime: result.last_read_time === null || result.last_read_time === undefined ? undefined : Number(result.last_read_time),
lastLocatorType: (result.last_locator_type as BookShelfItem['lastLocatorType']) || undefined,
lastLocatorValue: (result.last_locator_value as string) || undefined,
lastChapterTitle: (result.last_chapter_title as string) || undefined,
saveTime: Number(result.save_time || 0),
};
} catch (err) {
console.error('D1Storage.getBookShelf error:', err);
throw err;
}
}
async setBookShelf(userName: string, key: string, item: BookShelfItem): Promise<void> {
try {
await this.db.prepare(`
INSERT INTO book_shelf (
username, key, source_id, source_name, book_id, title, author, cover, format, detail_href, acquisition_href,
progress_percent, last_read_time, last_locator_type, last_locator_value, last_chapter_title, save_time
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(username, key) DO UPDATE SET
source_id = excluded.source_id,
source_name = excluded.source_name,
book_id = excluded.book_id,
title = excluded.title,
author = excluded.author,
cover = excluded.cover,
format = excluded.format,
detail_href = excluded.detail_href,
acquisition_href = excluded.acquisition_href,
progress_percent = excluded.progress_percent,
last_read_time = excluded.last_read_time,
last_locator_type = excluded.last_locator_type,
last_locator_value = excluded.last_locator_value,
last_chapter_title = excluded.last_chapter_title,
save_time = excluded.save_time
`).bind(
userName, key, item.sourceId, item.sourceName, item.bookId, item.title, item.author || null,
item.cover || null, item.format || null, item.detailHref || null, item.acquisitionHref || null, item.progressPercent ?? null,
item.lastReadTime ?? null, item.lastLocatorType || null, item.lastLocatorValue || null,
item.lastChapterTitle || null, item.saveTime
).run();
} catch (err) {
console.error('D1Storage.setBookShelf error:', err);
throw err;
}
}
async getAllBookShelf(userName: string): Promise<{ [key: string]: BookShelfItem }> {
try {
const results = await this.db.prepare('SELECT * FROM book_shelf WHERE username = ? ORDER BY COALESCE(last_read_time, save_time) DESC').bind(userName).all();
const shelves: { [key: string]: BookShelfItem } = {};
if (!results.results) return shelves;
for (const row of results.results) {
shelves[row.key as string] = {
sourceId: row.source_id as string,
sourceName: row.source_name as string,
bookId: row.book_id as string,
title: row.title as string,
author: (row.author as string) || undefined,
cover: (row.cover as string) || undefined,
format: (row.format as 'epub' | 'pdf' | null) || undefined,
detailHref: (row.detail_href as string) || undefined,
acquisitionHref: (row.acquisition_href as string) || undefined,
progressPercent: row.progress_percent === null || row.progress_percent === undefined ? undefined : Number(row.progress_percent),
lastReadTime: row.last_read_time === null || row.last_read_time === undefined ? undefined : Number(row.last_read_time),
lastLocatorType: (row.last_locator_type as BookShelfItem['lastLocatorType']) || undefined,
lastLocatorValue: (row.last_locator_value as string) || undefined,
lastChapterTitle: (row.last_chapter_title as string) || undefined,
saveTime: Number(row.save_time || 0),
};
}
return shelves;
} catch (err) {
console.error('D1Storage.getAllBookShelf error:', err);
throw err;
}
}
async deleteBookShelf(userName: string, key: string): Promise<void> {
try {
await this.db.prepare('DELETE FROM book_shelf WHERE username = ? AND key = ?').bind(userName, key).run();
} catch (err) {
console.error('D1Storage.deleteBookShelf error:', err);
throw err;
}
}
// ==================== 电子书阅读历史 ====================
async getBookReadRecord(userName: string, key: string): Promise<BookReadRecord | null> {
try {
const result = await this.db.prepare('SELECT * FROM book_read_records WHERE username = ? AND key = ?').bind(userName, key).first();
if (!result) return null;
return {
sourceId: result.source_id as string,
sourceName: result.source_name as string,
bookId: result.book_id as string,
title: result.title as string,
author: (result.author as string) || undefined,
cover: (result.cover as string) || undefined,
format: result.format as 'epub' | 'pdf',
detailHref: (result.detail_href as string) || undefined,
acquisitionHref: (result.acquisition_href as string) || undefined,
locator: {
type: result.locator_type as BookReadRecord['locator']['type'],
value: result.locator_value as string,
href: (result.chapter_href as string) || undefined,
chapterTitle: (result.chapter_title as string) || undefined,
},
progressPercent: Number(result.progress_percent || 0),
chapterTitle: (result.chapter_title as string) || undefined,
chapterHref: (result.chapter_href as string) || undefined,
saveTime: Number(result.save_time || 0),
};
} catch (err) {
console.error('D1Storage.getBookReadRecord error:', err);
throw err;
}
}
async setBookReadRecord(userName: string, key: string, record: BookReadRecord): Promise<void> {
try {
await this.db.prepare(`
INSERT INTO book_read_records (
username, key, source_id, source_name, book_id, title, author, cover, format, detail_href, acquisition_href,
locator_type, locator_value, chapter_title, chapter_href, progress_percent, save_time
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(username, key) DO UPDATE SET
source_id = excluded.source_id,
source_name = excluded.source_name,
book_id = excluded.book_id,
title = excluded.title,
author = excluded.author,
cover = excluded.cover,
format = excluded.format,
detail_href = excluded.detail_href,
acquisition_href = excluded.acquisition_href,
locator_type = excluded.locator_type,
locator_value = excluded.locator_value,
chapter_title = excluded.chapter_title,
chapter_href = excluded.chapter_href,
progress_percent = excluded.progress_percent,
save_time = excluded.save_time
`).bind(
userName, key, record.sourceId, record.sourceName, record.bookId, record.title, record.author || null,
record.cover || null, record.format, record.detailHref || null, record.acquisitionHref || null, record.locator.type, record.locator.value,
record.chapterTitle || record.locator.chapterTitle || null, record.chapterHref || record.locator.href || null,
record.progressPercent, record.saveTime
).run();
} catch (err) {
console.error('D1Storage.setBookReadRecord error:', err);
throw err;
}
}
async getAllBookReadRecords(userName: string): Promise<{ [key: string]: BookReadRecord }> {
try {
const results = await this.db.prepare('SELECT * FROM book_read_records WHERE username = ? ORDER BY save_time DESC').bind(userName).all();
const records: { [key: string]: BookReadRecord } = {};
if (!results.results) return records;
for (const row of results.results) {
records[row.key as string] = {
sourceId: row.source_id as string,
sourceName: row.source_name as string,
bookId: row.book_id as string,
title: row.title as string,
author: (row.author as string) || undefined,
cover: (row.cover as string) || undefined,
format: row.format as 'epub' | 'pdf',
detailHref: (row.detail_href as string) || undefined,
acquisitionHref: (row.acquisition_href as string) || undefined,
locator: {
type: row.locator_type as BookReadRecord['locator']['type'],
value: row.locator_value as string,
href: (row.chapter_href as string) || undefined,
chapterTitle: (row.chapter_title as string) || undefined,
},
progressPercent: Number(row.progress_percent || 0),
chapterTitle: (row.chapter_title as string) || undefined,
chapterHref: (row.chapter_href as string) || undefined,
saveTime: Number(row.save_time || 0),
};
}
return records;
} catch (err) {
console.error('D1Storage.getAllBookReadRecords error:', err);
throw err;
}
}
async deleteBookReadRecord(userName: string, key: string): Promise<void> {
try {
await this.db.prepare('DELETE FROM book_read_records WHERE username = ? AND key = ?').bind(userName, key).run();
} catch (err) {
console.error('D1Storage.deleteBookReadRecord error:', err);
throw err;
}
}
async cleanupOldBookReadRecords(userName: string): Promise<void> {
try {
const maxRecords = parseInt(process.env.MAX_BOOK_HISTORY_PER_USER || '100', 10);
const threshold = maxRecords + 10;
const countResult = await this.db.prepare('SELECT COUNT(*) as count FROM book_read_records WHERE username = ?').bind(userName).first();
const count = Number(countResult?.count || 0);
if (count <= threshold) return;
await this.db.prepare(`
DELETE FROM book_read_records
WHERE username = ?
AND key NOT IN (
SELECT key FROM book_read_records
WHERE username = ?
ORDER BY save_time DESC
LIMIT ?
)
`).bind(userName, userName, maxRecords).run();
} catch (err) {
console.error('D1Storage.cleanupOldBookReadRecords error:', err);
throw err;
}
}
// ==================== 跳过配置 ====================
async getSkipConfig(userName: string, source: string, id: string): Promise<SkipConfig | null> {
@@ -2373,7 +2616,7 @@ export class D1Storage implements IStorage {
requested_by, request_count, status, created_at, updated_at,
fulfilled_at, fulfilled_source, fulfilled_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
.bind(
request.id,
@@ -2558,6 +2801,8 @@ export class D1Storage implements IStorage {
'search_history',
'manga_shelf',
'manga_read_records',
'book_shelf',
'book_read_records',
'skip_configs',
'music_play_records',
'music_playlists',