增加播放记录批量删除功能

This commit is contained in:
mtvpls
2026-06-01 17:52:16 +08:00
parent 2f1a763367
commit 0201891efa
8 changed files with 2721 additions and 828 deletions
+691 -240
View File
@@ -19,7 +19,11 @@ 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 {
MusicV2HistoryRecord,
MusicV2PlaylistItem,
MusicV2PlaylistRecord,
} from './music-v2';
import { userInfoCache } from './user-cache';
/**
@@ -58,8 +62,15 @@ export class D1Storage implements IStorage {
for (const statement of statements) {
try {
const result = await this.db.prepare(statement).run();
if (!result.success && result.error && !/duplicate column|already exists/i.test(result.error)) {
console.warn('D1Storage.ensureMangaShelfColumns warning:', result.error);
if (
!result.success &&
result.error &&
!/duplicate column|already exists/i.test(result.error)
) {
console.warn(
'D1Storage.ensureMangaShelfColumns warning:',
result.error
);
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
@@ -72,7 +83,10 @@ export class D1Storage implements IStorage {
// ==================== 播放记录 ====================
async getPlayRecord(userName: string, key: string): Promise<PlayRecord | null> {
async getPlayRecord(
userName: string,
key: string
): Promise<PlayRecord | null> {
try {
const result = await this.db
.prepare('SELECT * FROM play_records WHERE username = ? AND key = ?')
@@ -87,10 +101,15 @@ export class D1Storage implements IStorage {
}
}
async setPlayRecord(userName: string, key: string, record: PlayRecord): Promise<void> {
async setPlayRecord(
userName: string,
key: string,
record: PlayRecord
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO play_records (
username, key, title, source_name, cover, year,
episode_index, total_episodes, play_time, total_time,
@@ -109,7 +128,8 @@ export class D1Storage implements IStorage {
save_time = excluded.save_time,
search_title = excluded.search_title,
new_episodes = excluded.new_episodes
`)
`
)
.bind(
userName,
key,
@@ -132,10 +152,14 @@ export class D1Storage implements IStorage {
}
}
async getAllPlayRecords(userName: string): Promise<{ [key: string]: PlayRecord }> {
async getAllPlayRecords(
userName: string
): Promise<{ [key: string]: PlayRecord }> {
try {
const results = await this.db
.prepare('SELECT * FROM play_records WHERE username = ? ORDER BY save_time DESC')
.prepare(
'SELECT * FROM play_records WHERE username = ? ORDER BY save_time DESC'
)
.bind(userName)
.all();
@@ -165,14 +189,37 @@ export class D1Storage implements IStorage {
}
}
async deletePlayRecords(userName: string, keys: string[]): Promise<void> {
const uniqueKeys = Array.from(new Set(keys)).filter(Boolean);
if (uniqueKeys.length === 0) return;
try {
const placeholders = uniqueKeys.map(() => '?').join(',');
await this.db
.prepare(
`DELETE FROM play_records WHERE username = ? AND key IN (${placeholders})`
)
.bind(userName, ...uniqueKeys)
.run();
} catch (err) {
console.error('D1Storage.deletePlayRecords error:', err);
throw err;
}
}
async cleanupOldPlayRecords(userName: string): Promise<void> {
try {
const maxRecords = parseInt(process.env.MAX_PLAY_RECORDS_PER_USER || '100', 10);
const maxRecords = parseInt(
process.env.MAX_PLAY_RECORDS_PER_USER || '100',
10
);
const threshold = maxRecords + 10;
// 检查记录数量
const countResult = await this.db
.prepare('SELECT COUNT(*) as count FROM play_records WHERE username = ?')
.prepare(
'SELECT COUNT(*) as count FROM play_records WHERE username = ?'
)
.bind(userName)
.first();
@@ -181,7 +228,8 @@ export class D1Storage implements IStorage {
// 删除超出限制的旧记录
await this.db
.prepare(`
.prepare(
`
DELETE FROM play_records
WHERE username = ?
AND key NOT IN (
@@ -190,11 +238,14 @@ export class D1Storage implements IStorage {
ORDER BY save_time DESC
LIMIT ?
)
`)
`
)
.bind(userName, userName, maxRecords)
.run();
console.log(`D1Storage: Cleaned up old play records for user ${userName}`);
console.log(
`D1Storage: Cleaned up old play records for user ${userName}`
);
} catch (err) {
console.error('D1Storage.cleanupOldPlayRecords error:', err);
throw err;
@@ -234,10 +285,15 @@ export class D1Storage implements IStorage {
}
}
async setFavorite(userName: string, key: string, favorite: Favorite): Promise<void> {
async setFavorite(
userName: string,
key: string,
favorite: Favorite
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO favorites (
username, key, source_name, total_episodes, title,
year, cover, save_time, search_title, origin,
@@ -255,7 +311,8 @@ export class D1Storage implements IStorage {
origin = excluded.origin,
is_completed = excluded.is_completed,
vod_remarks = excluded.vod_remarks
`)
`
)
.bind(
userName,
key,
@@ -277,10 +334,14 @@ export class D1Storage implements IStorage {
}
}
async getAllFavorites(userName: string): Promise<{ [key: string]: Favorite }> {
async getAllFavorites(
userName: string
): Promise<{ [key: string]: Favorite }> {
try {
const results = await this.db
.prepare('SELECT * FROM favorites WHERE username = ? ORDER BY save_time DESC')
.prepare(
'SELECT * FROM favorites WHERE username = ? ORDER BY save_time DESC'
)
.bind(userName)
.all();
@@ -330,7 +391,9 @@ export class D1Storage implements IStorage {
async getMusicPlayRecord(userName: string, key: string): Promise<any | null> {
try {
const result = await this.db
.prepare('SELECT * FROM music_play_records WHERE username = ? AND key = ?')
.prepare(
'SELECT * FROM music_play_records WHERE username = ? AND key = ?'
)
.bind(userName, key)
.first();
@@ -353,10 +416,15 @@ export class D1Storage implements IStorage {
}
}
async setMusicPlayRecord(userName: string, key: string, record: any): Promise<void> {
async setMusicPlayRecord(
userName: string,
key: string,
record: any
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO music_play_records (username, key, platform, song_id, name, artist, album, pic, play_time, duration, save_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(username, key) DO UPDATE SET
@@ -367,7 +435,8 @@ export class D1Storage implements IStorage {
play_time = excluded.play_time,
duration = excluded.duration,
save_time = excluded.save_time
`)
`
)
.bind(
userName,
key,
@@ -388,15 +457,18 @@ export class D1Storage implements IStorage {
}
}
async batchSetMusicPlayRecords(userName: string, records: { key: string; record: any }[]): Promise<void> {
async batchSetMusicPlayRecords(
userName: string,
records: { key: string; record: any }[]
): Promise<void> {
if (records.length === 0) return;
if (!this.db) return;
try {
// 使用批量插入,D1 支持 batch 操作
const statements = records.map(({ key, record }) =>
this.db!
.prepare(`
this.db!.prepare(
`
INSERT INTO music_play_records (username, key, platform, song_id, name, artist, album, pic, play_time, duration, save_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(username, key) DO UPDATE SET
@@ -409,20 +481,20 @@ export class D1Storage implements IStorage {
play_time = excluded.play_time,
duration = excluded.duration,
save_time = excluded.save_time
`)
.bind(
userName,
key,
record.platform,
record.id,
record.name,
record.artist,
record.album || null,
record.pic || null,
record.play_time,
record.duration,
record.save_time
)
`
).bind(
userName,
key,
record.platform,
record.id,
record.name,
record.artist,
record.album || null,
record.pic || null,
record.play_time,
record.duration,
record.save_time
)
);
if (this.db.batch) {
@@ -434,10 +506,14 @@ export class D1Storage implements IStorage {
}
}
async getAllMusicPlayRecords(userName: string): Promise<{ [key: string]: any }> {
async getAllMusicPlayRecords(
userName: string
): Promise<{ [key: string]: any }> {
try {
const results = await this.db
.prepare('SELECT * FROM music_play_records WHERE username = ? ORDER BY save_time DESC')
.prepare(
'SELECT * FROM music_play_records WHERE username = ? ORDER BY save_time DESC'
)
.bind(userName)
.all();
@@ -467,7 +543,9 @@ export class D1Storage implements IStorage {
async deleteMusicPlayRecord(userName: string, key: string): Promise<void> {
try {
await this.db
.prepare('DELETE FROM music_play_records WHERE username = ? AND key = ?')
.prepare(
'DELETE FROM music_play_records WHERE username = ? AND key = ?'
)
.bind(userName, key)
.run();
} catch (err) {
@@ -490,19 +568,24 @@ export class D1Storage implements IStorage {
// ==================== 音乐歌单相关 ====================
async createMusicPlaylist(userName: string, playlist: {
id: string;
name: string;
description?: string;
cover?: string;
}): Promise<void> {
async createMusicPlaylist(
userName: string,
playlist: {
id: string;
name: string;
description?: string;
cover?: string;
}
): Promise<void> {
try {
const now = Date.now();
await this.db
.prepare(`
.prepare(
`
INSERT INTO music_playlists (id, username, name, description, cover, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)
`
)
.bind(
playlist.id,
userName,
@@ -546,7 +629,9 @@ export class D1Storage implements IStorage {
async getUserMusicPlaylists(userName: string): Promise<any[]> {
try {
const results = await this.db
.prepare('SELECT * FROM music_playlists WHERE username = ? ORDER BY created_at DESC')
.prepare(
'SELECT * FROM music_playlists WHERE username = ? ORDER BY created_at DESC'
)
.bind(userName)
.all();
@@ -567,11 +652,14 @@ export class D1Storage implements IStorage {
}
}
async updateMusicPlaylist(playlistId: string, updates: {
name?: string;
description?: string;
cover?: string;
}): Promise<void> {
async updateMusicPlaylist(
playlistId: string,
updates: {
name?: string;
description?: string;
cover?: string;
}
): Promise<void> {
try {
const fields: string[] = [];
const values: any[] = [];
@@ -618,28 +706,34 @@ export class D1Storage implements IStorage {
}
}
async addSongToPlaylist(playlistId: string, song: {
platform: string;
id: string;
name: string;
artist: string;
album?: string;
pic?: string;
duration: number;
}): Promise<void> {
async addSongToPlaylist(
playlistId: string,
song: {
platform: string;
id: string;
name: string;
artist: string;
album?: string;
pic?: string;
duration: number;
}
): Promise<void> {
try {
const now = Date.now();
// 获取当前最大的 sort_order
const maxOrderResult = await this.db
.prepare('SELECT MAX(sort_order) as max_order FROM music_playlist_songs WHERE playlist_id = ?')
.prepare(
'SELECT MAX(sort_order) as max_order FROM music_playlist_songs WHERE playlist_id = ?'
)
.bind(playlistId)
.first();
const nextOrder = (maxOrderResult?.max_order as number || 0) + 1;
const nextOrder = ((maxOrderResult?.max_order as number) || 0) + 1;
await this.db
.prepare(`
.prepare(
`
INSERT INTO music_playlist_songs (
playlist_id, platform, song_id, name, artist, album, pic, duration, added_at, sort_order
)
@@ -650,7 +744,8 @@ export class D1Storage implements IStorage {
album = excluded.album,
pic = excluded.pic,
duration = excluded.duration
`)
`
)
.bind(
playlistId,
song.platform,
@@ -667,7 +762,9 @@ export class D1Storage implements IStorage {
// 更新歌单的 updated_at 和封面(如果是第一首歌)
const songCount = await this.db
.prepare('SELECT COUNT(*) as count FROM music_playlist_songs WHERE playlist_id = ?')
.prepare(
'SELECT COUNT(*) as count FROM music_playlist_songs WHERE playlist_id = ?'
)
.bind(playlistId)
.first();
@@ -685,10 +782,16 @@ export class D1Storage implements IStorage {
}
}
async removeSongFromPlaylist(playlistId: string, platform: string, songId: string): Promise<void> {
async removeSongFromPlaylist(
playlistId: string,
platform: string,
songId: string
): Promise<void> {
try {
await this.db
.prepare('DELETE FROM music_playlist_songs WHERE playlist_id = ? AND platform = ? AND song_id = ?')
.prepare(
'DELETE FROM music_playlist_songs WHERE playlist_id = ? AND platform = ? AND song_id = ?'
)
.bind(playlistId, platform, songId)
.run();
@@ -706,7 +809,9 @@ export class D1Storage implements IStorage {
async getPlaylistSongs(playlistId: string): Promise<any[]> {
try {
const results = await this.db
.prepare('SELECT * FROM music_playlist_songs WHERE playlist_id = ? ORDER BY sort_order ASC')
.prepare(
'SELECT * FROM music_playlist_songs WHERE playlist_id = ? ORDER BY sort_order ASC'
)
.bind(playlistId)
.all();
@@ -729,10 +834,16 @@ export class D1Storage implements IStorage {
}
}
async isSongInPlaylist(playlistId: string, platform: string, songId: string): Promise<boolean> {
async isSongInPlaylist(
playlistId: string,
platform: string,
songId: string
): Promise<boolean> {
try {
const result = await this.db
.prepare('SELECT 1 FROM music_playlist_songs WHERE playlist_id = ? AND platform = ? AND song_id = ? LIMIT 1')
.prepare(
'SELECT 1 FROM music_playlist_songs WHERE playlist_id = ? AND platform = ? AND song_id = ? LIMIT 1'
)
.bind(playlistId, platform, songId)
.first();
@@ -749,7 +860,9 @@ export class D1Storage implements IStorage {
try {
const results = await this.db
// 按队列顺序返回;当前播放项由最大 last_played_at 决定
.prepare('SELECT * FROM music_v2_history WHERE username = ? ORDER BY created_at ASC, id ASC')
.prepare(
'SELECT * FROM music_v2_history WHERE username = ? ORDER BY created_at ASC, id ASC'
)
.bind(userName)
.all();
@@ -778,10 +891,14 @@ export class D1Storage implements IStorage {
}
}
async upsertMusicV2History(userName: string, record: MusicV2HistoryRecord): Promise<void> {
async upsertMusicV2History(
userName: string,
record: MusicV2HistoryRecord
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO music_v2_history (
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
@@ -801,7 +918,8 @@ export class D1Storage implements IStorage {
play_count = excluded.play_count,
last_quality = excluded.last_quality,
updated_at = excluded.updated_at
`)
`
)
.bind(
userName,
record.songId,
@@ -827,7 +945,10 @@ export class D1Storage implements IStorage {
}
}
async batchUpsertMusicV2History(userName: string, records: MusicV2HistoryRecord[]): Promise<void> {
async batchUpsertMusicV2History(
userName: string,
records: MusicV2HistoryRecord[]
): Promise<void> {
for (const record of records) {
await this.upsertMusicV2History(userName, record);
}
@@ -835,7 +956,9 @@ export class D1Storage implements IStorage {
async deleteMusicV2History(userName: string, songId: string): Promise<void> {
await this.db
.prepare('DELETE FROM music_v2_history WHERE username = ? AND song_id = ?')
.prepare(
'DELETE FROM music_v2_history WHERE username = ? AND song_id = ?'
)
.bind(userName, songId)
.run();
}
@@ -849,23 +972,39 @@ export class D1Storage implements IStorage {
// ==================== Music V2 歌单相关 ====================
async createMusicV2Playlist(userName: string, playlist: {
id: string;
name: string;
description?: string;
cover?: string;
}): Promise<void> {
async createMusicV2Playlist(
userName: string,
playlist: {
id: string;
name: string;
description?: string;
cover?: string;
}
): Promise<void> {
const now = Date.now();
await this.db
.prepare(`
.prepare(
`
INSERT INTO music_v2_playlists (id, username, name, description, cover, song_count, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)
.bind(playlist.id, userName, playlist.name, playlist.description || null, playlist.cover || null, 0, now, now)
`
)
.bind(
playlist.id,
userName,
playlist.name,
playlist.description || null,
playlist.cover || null,
0,
now,
now
)
.run();
}
async getMusicV2Playlist(playlistId: string): Promise<MusicV2PlaylistRecord | null> {
async getMusicV2Playlist(
playlistId: string
): Promise<MusicV2PlaylistRecord | null> {
const row: any = await this.db
.prepare('SELECT * FROM music_v2_playlists WHERE id = ?')
.bind(playlistId)
@@ -885,9 +1024,13 @@ export class D1Storage implements IStorage {
};
}
async listMusicV2Playlists(userName: string): Promise<MusicV2PlaylistRecord[]> {
async listMusicV2Playlists(
userName: string
): Promise<MusicV2PlaylistRecord[]> {
const results = await this.db
.prepare('SELECT * FROM music_v2_playlists WHERE username = ? ORDER BY updated_at DESC')
.prepare(
'SELECT * FROM music_v2_playlists WHERE username = ? ORDER BY updated_at DESC'
)
.bind(userName)
.all();
@@ -905,12 +1048,15 @@ export class D1Storage implements IStorage {
}));
}
async updateMusicV2Playlist(playlistId: string, updates: {
name?: string;
description?: string;
cover?: string;
song_count?: number;
}): Promise<void> {
async updateMusicV2Playlist(
playlistId: string,
updates: {
name?: string;
description?: string;
cover?: string;
song_count?: number;
}
): Promise<void> {
const fields: string[] = [];
const values: any[] = [];
@@ -936,7 +1082,9 @@ export class D1Storage implements IStorage {
values.push(playlistId);
await this.db
.prepare(`UPDATE music_v2_playlists SET ${fields.join(', ')} WHERE id = ?`)
.prepare(
`UPDATE music_v2_playlists SET ${fields.join(', ')} WHERE id = ?`
)
.bind(...values)
.run();
}
@@ -948,21 +1096,30 @@ export class D1Storage implements IStorage {
.run();
}
async addMusicV2PlaylistItem(playlistId: string, item: MusicV2PlaylistItem): Promise<void> {
async addMusicV2PlaylistItem(
playlistId: string,
item: MusicV2PlaylistItem
): Promise<void> {
const playlist = await this.getMusicV2Playlist(playlistId);
if (!playlist) {
throw new Error('歌单不存在');
}
const maxOrder: any = await this.db
.prepare('SELECT MAX(sort_order) as max_order FROM music_v2_playlist_items WHERE playlist_id = ?')
.prepare(
'SELECT MAX(sort_order) as max_order FROM music_v2_playlist_items WHERE playlist_id = ?'
)
.bind(playlistId)
.first();
const nextOrder = Math.max(item.sortOrder || 0, (maxOrder?.max_order as number || 0) + 1);
const nextOrder = Math.max(
item.sortOrder || 0,
((maxOrder?.max_order as number) || 0) + 1
);
const now = Date.now();
await this.db
.prepare(`
.prepare(
`
INSERT INTO music_v2_playlist_items (
playlist_id, username, song_id, source, songmid, name, artist, album, cover, duration_text, duration_sec, sort_order, added_at, updated_at
)
@@ -977,7 +1134,8 @@ export class D1Storage implements IStorage {
duration_text = excluded.duration_text,
duration_sec = excluded.duration_sec,
updated_at = excluded.updated_at
`)
`
)
.bind(
playlistId,
playlist.username,
@@ -1003,9 +1161,14 @@ export class D1Storage implements IStorage {
});
}
async removeMusicV2PlaylistItem(playlistId: string, songId: string): Promise<void> {
async removeMusicV2PlaylistItem(
playlistId: string,
songId: string
): Promise<void> {
await this.db
.prepare('DELETE FROM music_v2_playlist_items WHERE playlist_id = ? AND song_id = ?')
.prepare(
'DELETE FROM music_v2_playlist_items WHERE playlist_id = ? AND song_id = ?'
)
.bind(playlistId, songId)
.run();
@@ -1016,9 +1179,13 @@ export class D1Storage implements IStorage {
});
}
async listMusicV2PlaylistItems(playlistId: string): Promise<MusicV2PlaylistItem[]> {
async listMusicV2PlaylistItems(
playlistId: string
): Promise<MusicV2PlaylistItem[]> {
const results = await this.db
.prepare('SELECT * FROM music_v2_playlist_items WHERE playlist_id = ? ORDER BY sort_order ASC, added_at ASC')
.prepare(
'SELECT * FROM music_v2_playlist_items WHERE playlist_id = ? ORDER BY sort_order ASC, added_at ASC'
)
.bind(playlistId)
.all();
@@ -1041,9 +1208,14 @@ export class D1Storage implements IStorage {
}));
}
async hasMusicV2PlaylistItem(playlistId: string, songId: string): Promise<boolean> {
async hasMusicV2PlaylistItem(
playlistId: string,
songId: string
): Promise<boolean> {
const row = await this.db
.prepare('SELECT 1 FROM music_v2_playlist_items WHERE playlist_id = ? AND song_id = ? LIMIT 1')
.prepare(
'SELECT 1 FROM music_v2_playlist_items WHERE playlist_id = ? AND song_id = ? LIMIT 1'
)
.bind(playlistId, songId)
.first();
return row !== null;
@@ -1096,12 +1268,17 @@ export class D1Storage implements IStorage {
async verifyUser(userName: string, password: string): Promise<boolean> {
try {
// 检查是否是环境变量中的管理员
if (userName === process.env.USERNAME && password === process.env.PASSWORD) {
if (
userName === process.env.USERNAME &&
password === process.env.PASSWORD
) {
return true;
}
const user = await this.db
.prepare('SELECT password_hash FROM users WHERE username = ? AND banned = 0')
.prepare(
'SELECT password_hash FROM users WHERE username = ? AND banned = 0'
)
.bind(userName)
.first();
@@ -1196,12 +1373,16 @@ export class D1Storage implements IStorage {
banned: user.banned === 1,
tags: user.tags ? JSON.parse(user.tags as string) : undefined,
oidcSub: user.oidc_sub as string | undefined,
enabledApis: user.enabled_apis ? JSON.parse(user.enabled_apis as string) : undefined,
enabledApis: user.enabled_apis
? JSON.parse(user.enabled_apis as string)
: undefined,
created_at: user.created_at as number,
playrecord_migrated: user.playrecord_migrated === 1,
favorite_migrated: user.favorite_migrated === 1,
skip_migrated: user.skip_migrated === 1,
last_movie_request_time: user.last_movie_request_time as number | undefined,
last_movie_request_time: user.last_movie_request_time as
| number
| undefined,
email: user.email as string | undefined,
emailNotifications: user.email_notifications === 1,
};
@@ -1232,13 +1413,15 @@ export class D1Storage implements IStorage {
// 为站长创建数据库记录
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO users (
username, password_hash, role, banned, created_at,
playrecord_migrated, favorite_migrated, skip_migrated
)
VALUES (?, ?, ?, 0, ?, 1, 1, 1)
`)
`
)
.bind(
userName,
'', // 站长不需要密码哈希
@@ -1276,14 +1459,16 @@ export class D1Storage implements IStorage {
const passwordHash = await this.hashPassword(password);
await this.db
.prepare(`
.prepare(
`
INSERT INTO users (
username, password_hash, role, banned, tags, oidc_sub,
enabled_apis, created_at, playrecord_migrated,
favorite_migrated, skip_migrated
)
VALUES (?, ?, ?, 0, ?, ?, ?, ?, 1, 1, 1)
`)
`
)
.bind(
userName,
passwordHash,
@@ -1327,7 +1512,9 @@ export class D1Storage implements IStorage {
// 获取总数
const countQuery = trimmedSearch
? this.db
.prepare('SELECT COUNT(*) as total FROM users WHERE username LIKE ?')
.prepare(
'SELECT COUNT(*) as total FROM users WHERE username LIKE ?'
)
.bind(searchPattern)
: this.db.prepare('SELECT COUNT(*) as total FROM users');
const countResult = await countQuery.first();
@@ -1370,21 +1557,25 @@ export class D1Storage implements IStorage {
// 获取用户列表(按创建时间降序)
const listQuery = trimmedSearch
? this.db
.prepare(`
.prepare(
`
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
FROM users
WHERE username LIKE ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`)
`
)
.bind(searchPattern, actualLimit, actualOffset)
: this.db
.prepare(`
.prepare(
`
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
FROM users
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`)
`
)
.bind(actualLimit, actualOffset);
const result = await listQuery.all();
@@ -1421,7 +1612,9 @@ export class D1Storage implements IStorage {
banned: user.banned === 1,
tags: user.tags ? JSON.parse(user.tags as string) : undefined,
oidcSub: user.oidc_sub as string | undefined,
enabledApis: user.enabled_apis ? JSON.parse(user.enabled_apis as string) : undefined,
enabledApis: user.enabled_apis
? JSON.parse(user.enabled_apis as string)
: undefined,
created_at: user.created_at as number,
});
}
@@ -1568,10 +1761,12 @@ export class D1Storage implements IStorage {
try {
// SQLite 不支持 JSON 查询,需要使用 LIKE
const result = await this.db
.prepare(`
.prepare(
`
SELECT username FROM users
WHERE tags LIKE ?
`)
`
)
.bind(`%"${tagName}"%`)
.all();
@@ -1600,7 +1795,10 @@ export class D1Storage implements IStorage {
}
// 直接设置用户密码哈希(用于数据导入,不进行二次哈希)
async setUserPasswordHash(userName: string, passwordHash: string): Promise<void> {
async setUserPasswordHash(
userName: string,
passwordHash: string
): Promise<void> {
try {
await this.db
.prepare('UPDATE users SET password_hash = ? WHERE username = ?')
@@ -1625,14 +1823,16 @@ export class D1Storage implements IStorage {
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO users (
username, password_hash, role, banned, tags, oidc_sub,
enabled_apis, created_at, playrecord_migrated,
favorite_migrated, skip_migrated
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, 1, 1)
`)
`
)
.bind(
userName,
passwordHash,
@@ -1693,7 +1893,10 @@ export class D1Storage implements IStorage {
}
}
async setEmailNotificationPreference?(userName: string, enabled: boolean): Promise<void> {
async setEmailNotificationPreference?(
userName: string,
enabled: boolean
): Promise<void> {
try {
await this.db
.prepare('UPDATE users SET email_notifications = ? WHERE username = ?')
@@ -1724,10 +1927,15 @@ export class D1Storage implements IStorage {
}
}
async setTvboxSubscribeToken?(userName: string, token: string): Promise<void> {
async setTvboxSubscribeToken?(
userName: string,
token: string
): Promise<void> {
try {
await this.db
.prepare('UPDATE users SET tvbox_subscribe_token = ? WHERE username = ?')
.prepare(
'UPDATE users SET tvbox_subscribe_token = ? WHERE username = ?'
)
.bind(token, userName)
.run();
@@ -1758,7 +1966,9 @@ export class D1Storage implements IStorage {
async getSearchHistory(userName: string): Promise<string[]> {
try {
const results = await this.db
.prepare('SELECT keyword FROM search_history WHERE username = ? ORDER BY timestamp DESC LIMIT 20')
.prepare(
'SELECT keyword FROM search_history WHERE username = ? ORDER BY timestamp DESC LIMIT 20'
)
.bind(userName)
.all();
@@ -1776,24 +1986,29 @@ export class D1Storage implements IStorage {
// 插入或更新时间戳
await this.db
.prepare(`
.prepare(
`
INSERT INTO search_history (username, keyword, timestamp)
VALUES (?, ?, ?)
ON CONFLICT(username, keyword) DO UPDATE SET timestamp = excluded.timestamp
`)
`
)
.bind(userName, keyword, timestamp)
.run();
// 保持最多 20 条记录
const countResult = await this.db
.prepare('SELECT COUNT(*) as count FROM search_history WHERE username = ?')
.prepare(
'SELECT COUNT(*) as count FROM search_history WHERE username = ?'
)
.bind(userName)
.first();
const count = (countResult?.count as number) || 0;
if (count > 20) {
await this.db
.prepare(`
.prepare(
`
DELETE FROM search_history
WHERE username = ?
AND id NOT IN (
@@ -1802,7 +2017,8 @@ export class D1Storage implements IStorage {
ORDER BY timestamp DESC
LIMIT 20
)
`)
`
)
.bind(userName, userName)
.run();
}
@@ -1816,7 +2032,9 @@ export class D1Storage implements IStorage {
try {
if (keyword) {
await this.db
.prepare('DELETE FROM search_history WHERE username = ? AND keyword = ?')
.prepare(
'DELETE FROM search_history WHERE username = ? AND keyword = ?'
)
.bind(userName, keyword)
.run();
} else {
@@ -1833,7 +2051,10 @@ export class D1Storage implements IStorage {
// ==================== 漫画书架 ====================
async getMangaShelf(userName: string, key: string): Promise<MangaShelfItem | null> {
async getMangaShelf(
userName: string,
key: string
): Promise<MangaShelfItem | null> {
try {
await this.schemaReady;
const result = await this.db
@@ -1857,11 +2078,13 @@ export class D1Storage implements IStorage {
latestChapterId: (result.latest_chapter_id as string) || undefined,
latestChapterName: (result.latest_chapter_name as string) || undefined,
latestChapterCount:
result.latest_chapter_count === null || result.latest_chapter_count === undefined
result.latest_chapter_count === null ||
result.latest_chapter_count === undefined
? undefined
: Number(result.latest_chapter_count),
unreadChapterCount:
result.unread_chapter_count === null || result.unread_chapter_count === undefined
result.unread_chapter_count === null ||
result.unread_chapter_count === undefined
? undefined
: Number(result.unread_chapter_count),
};
@@ -1871,11 +2094,16 @@ export class D1Storage implements IStorage {
}
}
async setMangaShelf(userName: string, key: string, item: MangaShelfItem): Promise<void> {
async setMangaShelf(
userName: string,
key: string,
item: MangaShelfItem
): Promise<void> {
try {
await this.schemaReady;
await this.db
.prepare(`
.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,
@@ -1898,7 +2126,8 @@ export class D1Storage implements IStorage {
latest_chapter_name = excluded.latest_chapter_name,
latest_chapter_count = excluded.latest_chapter_count,
unread_chapter_count = excluded.unread_chapter_count
`)
`
)
.bind(
userName,
key,
@@ -1925,11 +2154,15 @@ export class D1Storage implements IStorage {
}
}
async getAllMangaShelf(userName: string): Promise<{ [key: string]: MangaShelfItem }> {
async getAllMangaShelf(
userName: string
): Promise<{ [key: string]: MangaShelfItem }> {
try {
await this.schemaReady;
const results = await this.db
.prepare('SELECT * FROM manga_shelf WHERE username = ? ORDER BY save_time DESC')
.prepare(
'SELECT * FROM manga_shelf WHERE username = ? ORDER BY save_time DESC'
)
.bind(userName)
.all();
@@ -1952,11 +2185,13 @@ export class D1Storage implements IStorage {
latestChapterId: (row.latest_chapter_id as string) || undefined,
latestChapterName: (row.latest_chapter_name as string) || undefined,
latestChapterCount:
row.latest_chapter_count === null || row.latest_chapter_count === undefined
row.latest_chapter_count === null ||
row.latest_chapter_count === undefined
? undefined
: Number(row.latest_chapter_count),
unreadChapterCount:
row.unread_chapter_count === null || row.unread_chapter_count === undefined
row.unread_chapter_count === null ||
row.unread_chapter_count === undefined
? undefined
: Number(row.unread_chapter_count),
};
@@ -1983,10 +2218,15 @@ export class D1Storage implements IStorage {
// ==================== 漫画阅读历史 ====================
async getMangaReadRecord(userName: string, key: string): Promise<MangaReadRecord | null> {
async getMangaReadRecord(
userName: string,
key: string
): Promise<MangaReadRecord | null> {
try {
const result = await this.db
.prepare('SELECT * FROM manga_read_records WHERE username = ? AND key = ?')
.prepare(
'SELECT * FROM manga_read_records WHERE username = ? AND key = ?'
)
.bind(userName, key)
.first();
@@ -2009,10 +2249,15 @@ export class D1Storage implements IStorage {
}
}
async setMangaReadRecord(userName: string, key: string, record: MangaReadRecord): Promise<void> {
async setMangaReadRecord(
userName: string,
key: string,
record: MangaReadRecord
): Promise<void> {
try {
await this.db
.prepare(`
.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
@@ -2029,7 +2274,8 @@ export class D1Storage implements IStorage {
page_index = excluded.page_index,
page_count = excluded.page_count,
save_time = excluded.save_time
`)
`
)
.bind(
userName,
key,
@@ -2051,10 +2297,14 @@ export class D1Storage implements IStorage {
}
}
async getAllMangaReadRecords(userName: string): Promise<{ [key: string]: MangaReadRecord }> {
async getAllMangaReadRecords(
userName: string
): Promise<{ [key: string]: MangaReadRecord }> {
try {
const results = await this.db
.prepare('SELECT * FROM manga_read_records WHERE username = ? ORDER BY save_time DESC')
.prepare(
'SELECT * FROM manga_read_records WHERE username = ? ORDER BY save_time DESC'
)
.bind(userName)
.all();
@@ -2086,7 +2336,9 @@ export class D1Storage implements IStorage {
async deleteMangaReadRecord(userName: string, key: string): Promise<void> {
try {
await this.db
.prepare('DELETE FROM manga_read_records WHERE username = ? AND key = ?')
.prepare(
'DELETE FROM manga_read_records WHERE username = ? AND key = ?'
)
.bind(userName, key)
.run();
} catch (err) {
@@ -2097,10 +2349,15 @@ export class D1Storage implements IStorage {
async cleanupOldMangaReadRecords(userName: string): Promise<void> {
try {
const maxRecords = parseInt(process.env.MAX_MANGA_HISTORY_PER_USER || '100', 10);
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 = ?')
.prepare(
'SELECT COUNT(*) as count FROM manga_read_records WHERE username = ?'
)
.bind(userName)
.first();
@@ -2108,7 +2365,8 @@ export class D1Storage implements IStorage {
if (count <= threshold) return;
await this.db
.prepare(`
.prepare(
`
DELETE FROM manga_read_records
WHERE username = ?
AND key NOT IN (
@@ -2117,7 +2375,8 @@ export class D1Storage implements IStorage {
ORDER BY save_time DESC
LIMIT ?
)
`)
`
)
.bind(userName, userName, maxRecords)
.run();
} catch (err) {
@@ -2126,12 +2385,17 @@ export class D1Storage implements IStorage {
}
}
// ==================== 电子书书架 ====================
async getBookShelf(userName: string, key: string): Promise<BookShelfItem | null> {
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();
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,
@@ -2143,9 +2407,18 @@ export class D1Storage implements IStorage {
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,
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),
@@ -2156,9 +2429,15 @@ export class D1Storage implements IStorage {
}
}
async setBookShelf(userName: string, key: string, item: BookShelfItem): Promise<void> {
async setBookShelf(
userName: string,
key: string,
item: BookShelfItem
): Promise<void> {
try {
await this.db.prepare(`
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
@@ -2180,21 +2459,44 @@ export class D1Storage implements IStorage {
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();
`
)
.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 }> {
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 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) {
@@ -2208,9 +2510,17 @@ export class D1Storage implements IStorage {
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,
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),
@@ -2225,7 +2535,10 @@ export class D1Storage implements IStorage {
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();
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;
@@ -2234,9 +2547,17 @@ export class D1Storage implements IStorage {
// ==================== 电子书阅读历史 ====================
async getBookReadRecord(userName: string, key: string): Promise<BookReadRecord | null> {
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();
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,
@@ -2265,9 +2586,15 @@ export class D1Storage implements IStorage {
}
}
async setBookReadRecord(userName: string, key: string, record: BookReadRecord): Promise<void> {
async setBookReadRecord(
userName: string,
key: string,
record: BookReadRecord
): Promise<void> {
try {
await this.db.prepare(`
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
@@ -2289,21 +2616,44 @@ export class D1Storage implements IStorage {
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();
`
)
.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 }> {
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 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) {
@@ -2338,7 +2688,10 @@ export class D1Storage implements IStorage {
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();
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;
@@ -2347,12 +2700,22 @@ export class D1Storage implements IStorage {
async cleanupOldBookReadRecords(userName: string): Promise<void> {
try {
const maxRecords = parseInt(process.env.MAX_BOOK_HISTORY_PER_USER || '100', 10);
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 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(`
await this.db
.prepare(
`
DELETE FROM book_read_records
WHERE username = ?
AND key NOT IN (
@@ -2361,7 +2724,10 @@ export class D1Storage implements IStorage {
ORDER BY save_time DESC
LIMIT ?
)
`).bind(userName, userName, maxRecords).run();
`
)
.bind(userName, userName, maxRecords)
.run();
} catch (err) {
console.error('D1Storage.cleanupOldBookReadRecords error:', err);
throw err;
@@ -2370,7 +2736,11 @@ export class D1Storage implements IStorage {
// ==================== 跳过配置 ====================
async getSkipConfig(userName: string, source: string, id: string): Promise<SkipConfig | null> {
async getSkipConfig(
userName: string,
source: string,
id: string
): Promise<SkipConfig | null> {
try {
const key = `${source}+${id}`;
const result = await this.db
@@ -2390,19 +2760,32 @@ export class D1Storage implements IStorage {
}
}
async setSkipConfig(userName: string, source: string, id: string, config: SkipConfig): Promise<void> {
async setSkipConfig(
userName: string,
source: string,
id: string,
config: SkipConfig
): Promise<void> {
try {
const key = `${source}+${id}`;
await this.db
.prepare(`
.prepare(
`
INSERT INTO skip_configs (username, key, enable, intro_time, outro_time)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(username, key) DO UPDATE SET
enable = excluded.enable,
intro_time = excluded.intro_time,
outro_time = excluded.outro_time
`)
.bind(userName, key, config.enable ? 1 : 0, config.intro_time, config.outro_time)
`
)
.bind(
userName,
key,
config.enable ? 1 : 0,
config.intro_time,
config.outro_time
)
.run();
} catch (err) {
console.error('D1Storage.setSkipConfig error:', err);
@@ -2410,7 +2793,11 @@ export class D1Storage implements IStorage {
}
}
async deleteSkipConfig(userName: string, source: string, id: string): Promise<void> {
async deleteSkipConfig(
userName: string,
source: string,
id: string
): Promise<void> {
try {
const key = `${source}+${id}`;
await this.db
@@ -2423,7 +2810,9 @@ export class D1Storage implements IStorage {
}
}
async getAllSkipConfigs(userName: string): Promise<{ [key: string]: SkipConfig }> {
async getAllSkipConfigs(
userName: string
): Promise<{ [key: string]: SkipConfig }> {
try {
const results = await this.db
.prepare('SELECT * FROM skip_configs WHERE username = ?')
@@ -2463,7 +2852,9 @@ export class D1Storage implements IStorage {
// ==================== 弹幕过滤配置 ====================
async getDanmakuFilterConfig(userName: string): Promise<DanmakuFilterConfig | null> {
async getDanmakuFilterConfig(
userName: string
): Promise<DanmakuFilterConfig | null> {
try {
const result = await this.db
.prepare('SELECT rules FROM danmaku_filter_configs WHERE username = ?')
@@ -2478,14 +2869,19 @@ export class D1Storage implements IStorage {
}
}
async setDanmakuFilterConfig(userName: string, config: DanmakuFilterConfig): Promise<void> {
async setDanmakuFilterConfig(
userName: string,
config: DanmakuFilterConfig
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO danmaku_filter_configs (username, rules)
VALUES (?, ?)
ON CONFLICT(username) DO UPDATE SET rules = excluded.rules
`)
`
)
.bind(userName, JSON.stringify(config))
.run();
} catch (err) {
@@ -2511,7 +2907,9 @@ export class D1Storage implements IStorage {
async getNotifications(userName: string): Promise<Notification[]> {
try {
const results = await this.db
.prepare('SELECT * FROM notifications WHERE username = ? ORDER BY timestamp DESC')
.prepare(
'SELECT * FROM notifications WHERE username = ? ORDER BY timestamp DESC'
)
.bind(userName)
.all();
@@ -2531,13 +2929,18 @@ export class D1Storage implements IStorage {
}
}
async addNotification(userName: string, notification: Notification): Promise<void> {
async addNotification(
userName: string,
notification: Notification
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO notifications (id, username, type, title, message, timestamp, read, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)
`
)
.bind(
notification.id,
userName,
@@ -2555,10 +2958,15 @@ export class D1Storage implements IStorage {
}
}
async markNotificationAsRead(userName: string, notificationId: string): Promise<void> {
async markNotificationAsRead(
userName: string,
notificationId: string
): Promise<void> {
try {
await this.db
.prepare('UPDATE notifications SET read = 1 WHERE username = ? AND id = ?')
.prepare(
'UPDATE notifications SET read = 1 WHERE username = ? AND id = ?'
)
.bind(userName, notificationId)
.run();
} catch (err) {
@@ -2567,7 +2975,10 @@ export class D1Storage implements IStorage {
}
}
async deleteNotification(userName: string, notificationId: string): Promise<void> {
async deleteNotification(
userName: string,
notificationId: string
): Promise<void> {
try {
await this.db
.prepare('DELETE FROM notifications WHERE username = ? AND id = ?')
@@ -2594,7 +3005,9 @@ export class D1Storage implements IStorage {
async getUnreadNotificationCount(userName: string): Promise<number> {
try {
const result = await this.db
.prepare('SELECT COUNT(*) as count FROM notifications WHERE username = ? AND read = 0')
.prepare(
'SELECT COUNT(*) as count FROM notifications WHERE username = ? AND read = 0'
)
.bind(userName)
.first();
@@ -2639,14 +3052,16 @@ export class D1Storage implements IStorage {
async createMovieRequest(request: MovieRequest): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO movie_requests (
id, tmdb_id, title, year, media_type, season, poster, overview,
requested_by, request_count, status, created_at, updated_at,
fulfilled_at, fulfilled_source, fulfilled_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
`
)
.bind(
request.id,
request.tmdbId || null,
@@ -2672,7 +3087,10 @@ export class D1Storage implements IStorage {
}
}
async updateMovieRequest(requestId: string, updates: Partial<MovieRequest>): Promise<void> {
async updateMovieRequest(
requestId: string,
updates: Partial<MovieRequest>
): Promise<void> {
try {
const fields: string[] = [];
const values: any[] = [];
@@ -2732,7 +3150,9 @@ export class D1Storage implements IStorage {
async getUserMovieRequests(userName: string): Promise<string[]> {
try {
const results = await this.db
.prepare('SELECT request_id FROM user_movie_requests WHERE username = ?')
.prepare(
'SELECT request_id FROM user_movie_requests WHERE username = ?'
)
.bind(userName)
.all();
@@ -2744,10 +3164,15 @@ export class D1Storage implements IStorage {
}
}
async addUserMovieRequest(userName: string, requestId: string): Promise<void> {
async addUserMovieRequest(
userName: string,
requestId: string
): Promise<void> {
try {
await this.db
.prepare('INSERT OR IGNORE INTO user_movie_requests (username, request_id) VALUES (?, ?)')
.prepare(
'INSERT OR IGNORE INTO user_movie_requests (username, request_id) VALUES (?, ?)'
)
.bind(userName, requestId)
.run();
} catch (err) {
@@ -2756,10 +3181,15 @@ export class D1Storage implements IStorage {
}
}
async removeUserMovieRequest(userName: string, requestId: string): Promise<void> {
async removeUserMovieRequest(
userName: string,
requestId: string
): Promise<void> {
try {
await this.db
.prepare('DELETE FROM user_movie_requests WHERE username = ? AND request_id = ?')
.prepare(
'DELETE FROM user_movie_requests WHERE username = ? AND request_id = ?'
)
.bind(userName, requestId)
.run();
} catch (err) {
@@ -2808,11 +3238,13 @@ export class D1Storage implements IStorage {
async setAdminConfig(config: AdminConfig): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO admin_config (id, config, updated_at)
VALUES (1, ?, ?)
ON CONFLICT(id) DO UPDATE SET config = excluded.config, updated_at = excluded.updated_at
`)
`
)
.bind(JSON.stringify(config), Date.now())
.run();
} catch (err) {
@@ -2849,7 +3281,10 @@ export class D1Storage implements IStorage {
await this.db.prepare(`DELETE FROM ${table}`).run();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('no such table') || message.includes('does not exist')) {
if (
message.includes('no such table') ||
message.includes('does not exist')
) {
console.warn('D1Storage.clearAllData warning:', table, message);
continue;
}
@@ -2879,11 +3314,13 @@ export class D1Storage implements IStorage {
async setGlobalValue(key: string, value: string): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO global_config (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
`)
`
)
.bind(key, value, Date.now())
.run();
} catch (err) {
@@ -2907,7 +3344,9 @@ export class D1Storage implements IStorage {
async getLastFavoriteCheckTime(userName: string): Promise<number> {
try {
const result = await this.db
.prepare('SELECT last_check_time FROM favorite_check_times WHERE username = ?')
.prepare(
'SELECT last_check_time FROM favorite_check_times WHERE username = ?'
)
.bind(userName)
.first();
@@ -2918,14 +3357,19 @@ export class D1Storage implements IStorage {
}
}
async setLastFavoriteCheckTime(userName: string, timestamp: number): Promise<void> {
async setLastFavoriteCheckTime(
userName: string,
timestamp: number
): Promise<void> {
try {
await this.db
.prepare(`
.prepare(
`
INSERT INTO favorite_check_times (username, last_check_time)
VALUES (?, ?)
ON CONFLICT(username) DO UPDATE SET last_check_time = excluded.last_check_time
`)
`
)
.bind(userName, timestamp)
.run();
} catch (err) {
@@ -2934,10 +3378,15 @@ export class D1Storage implements IStorage {
}
}
async updateLastMovieRequestTime(userName: string, timestamp: number): Promise<void> {
async updateLastMovieRequestTime(
userName: string,
timestamp: number
): Promise<void> {
try {
await this.db
.prepare('UPDATE users SET last_movie_request_time = ? WHERE username = ?')
.prepare(
'UPDATE users SET last_movie_request_time = ? WHERE username = ?'
)
.bind(timestamp, userName)
.run();
} catch (err) {
@@ -2965,11 +3414,13 @@ class RedisHashAdapter {
async hSet(hashKey: string, field: string, value: string): Promise<void> {
const key = `${hashKey}:${field}`;
await this.db
.prepare(`
.prepare(
`
INSERT INTO global_config (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
`)
`
)
.bind(key, value, Date.now())
.run();
}