增加播放记录批量删除功能
This commit is contained in:
+682
-229
@@ -21,7 +21,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';
|
||||
|
||||
/**
|
||||
* Vercel Postgres 存储实现
|
||||
@@ -60,7 +64,10 @@ export class PostgresStorage implements IStorage {
|
||||
try {
|
||||
const result = await this.db.prepare(statement).run();
|
||||
if (!result.success && result.error) {
|
||||
console.warn('PostgresStorage.ensureMangaShelfColumns warning:', result.error);
|
||||
console.warn(
|
||||
'PostgresStorage.ensureMangaShelfColumns warning:',
|
||||
result.error
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('PostgresStorage.ensureMangaShelfColumns warning:', err);
|
||||
@@ -70,7 +77,10 @@ export class PostgresStorage 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 = $1 AND key = $2')
|
||||
@@ -85,10 +95,15 @@ export class PostgresStorage 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,
|
||||
@@ -107,7 +122,8 @@ export class PostgresStorage implements IStorage {
|
||||
save_time = EXCLUDED.save_time,
|
||||
search_title = EXCLUDED.search_title,
|
||||
new_episodes = EXCLUDED.new_episodes
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
key,
|
||||
@@ -130,10 +146,14 @@ export class PostgresStorage 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 = $1 ORDER BY save_time DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM play_records WHERE username = $1 ORDER BY save_time DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -163,14 +183,39 @@ export class PostgresStorage 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((_, index) => `$${index + 2}`)
|
||||
.join(',');
|
||||
await this.db
|
||||
.prepare(
|
||||
`DELETE FROM play_records WHERE username = $1 AND key IN (${placeholders})`
|
||||
)
|
||||
.bind(userName, ...uniqueKeys)
|
||||
.run();
|
||||
} catch (err) {
|
||||
console.error('PostgresStorage.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 = $1')
|
||||
.prepare(
|
||||
'SELECT COUNT(*) as count FROM play_records WHERE username = $1'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
|
||||
@@ -179,7 +224,8 @@ export class PostgresStorage implements IStorage {
|
||||
|
||||
// 删除超出限制的旧记录
|
||||
await this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
DELETE FROM play_records
|
||||
WHERE username = $1
|
||||
AND key NOT IN (
|
||||
@@ -188,11 +234,14 @@ export class PostgresStorage implements IStorage {
|
||||
ORDER BY save_time DESC
|
||||
LIMIT $2
|
||||
)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(userName, maxRecords)
|
||||
.run();
|
||||
|
||||
console.log(`PostgresStorage: Cleaned up old play records for user ${userName}`);
|
||||
console.log(
|
||||
`PostgresStorage: Cleaned up old play records for user ${userName}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('PostgresStorage.cleanupOldPlayRecords error:', err);
|
||||
throw err;
|
||||
@@ -227,10 +276,15 @@ export class PostgresStorage 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,
|
||||
@@ -248,7 +302,8 @@ export class PostgresStorage implements IStorage {
|
||||
origin = EXCLUDED.origin,
|
||||
is_completed = EXCLUDED.is_completed,
|
||||
vod_remarks = EXCLUDED.vod_remarks
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
key,
|
||||
@@ -270,10 +325,14 @@ export class PostgresStorage 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 = $1 ORDER BY save_time DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM favorites WHERE username = $1 ORDER BY save_time DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -360,12 +419,17 @@ export class PostgresStorage 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 = $1 AND banned = 0')
|
||||
.prepare(
|
||||
'SELECT password_hash FROM users WHERE username = $1 AND banned = 0'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
|
||||
@@ -461,12 +525,16 @@ export class PostgresStorage 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,
|
||||
};
|
||||
@@ -491,13 +559,15 @@ export class PostgresStorage 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 ($1, $2, $3, 0, $4, 1, 1, 1)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
'', // 站长不需要密码哈希
|
||||
@@ -536,14 +606,16 @@ export class PostgresStorage 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 ($1, $2, $3, 0, $4, $5, $6, $7, 1, 1, 1)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
passwordHash,
|
||||
@@ -584,7 +656,9 @@ export class PostgresStorage implements IStorage {
|
||||
// 获取总数
|
||||
const countQuery = trimmedSearch
|
||||
? this.db
|
||||
.prepare('SELECT COUNT(*) as total FROM users WHERE username LIKE $1')
|
||||
.prepare(
|
||||
'SELECT COUNT(*) as total FROM users WHERE username LIKE $1'
|
||||
)
|
||||
.bind(searchPattern)
|
||||
: this.db.prepare('SELECT COUNT(*) as total FROM users');
|
||||
const countResult = await countQuery.first();
|
||||
@@ -627,21 +701,25 @@ export class PostgresStorage 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 $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`)
|
||||
`
|
||||
)
|
||||
.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 $1 OFFSET $2
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(actualLimit, actualOffset);
|
||||
const result = await listQuery.all();
|
||||
|
||||
@@ -678,7 +756,9 @@ export class PostgresStorage 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,
|
||||
});
|
||||
}
|
||||
@@ -749,7 +829,11 @@ export class PostgresStorage implements IStorage {
|
||||
values.push(userName);
|
||||
|
||||
await this.db
|
||||
.prepare(`UPDATE users SET ${fields.join(', ')} WHERE username = $${paramIndex}`)
|
||||
.prepare(
|
||||
`UPDATE users SET ${fields.join(
|
||||
', '
|
||||
)} WHERE username = $${paramIndex}`
|
||||
)
|
||||
.bind(...values)
|
||||
.run();
|
||||
|
||||
@@ -825,10 +909,12 @@ export class PostgresStorage implements IStorage {
|
||||
try {
|
||||
// Postgres 支持 JSON 查询
|
||||
const result = await this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
SELECT username FROM users
|
||||
WHERE tags::jsonb ? $1
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(tagName)
|
||||
.all();
|
||||
|
||||
@@ -855,7 +941,10 @@ export class PostgresStorage 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 = $1 WHERE username = $2')
|
||||
@@ -879,14 +968,16 @@ export class PostgresStorage 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, $2, $3, $4, $5, $6, $7, $8, 1, 1, 1)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
passwordHash,
|
||||
@@ -943,15 +1034,23 @@ export class PostgresStorage implements IStorage {
|
||||
|
||||
return result?.email_notifications === 1;
|
||||
} catch (err) {
|
||||
console.error('PostgresStorage.getEmailNotificationPreference error:', err);
|
||||
console.error(
|
||||
'PostgresStorage.getEmailNotificationPreference error:',
|
||||
err
|
||||
);
|
||||
return true; // 默认开启
|
||||
}
|
||||
}
|
||||
|
||||
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 = $1 WHERE username = $2')
|
||||
.prepare(
|
||||
'UPDATE users SET email_notifications = $1 WHERE username = $2'
|
||||
)
|
||||
.bind(enabled ? 1 : 0, userName)
|
||||
.run();
|
||||
|
||||
@@ -959,7 +1058,10 @@ export class PostgresStorage implements IStorage {
|
||||
const { userInfoCache } = await import('./user-cache');
|
||||
userInfoCache.delete(userName);
|
||||
} catch (err) {
|
||||
console.error('PostgresStorage.setEmailNotificationPreference error:', err);
|
||||
console.error(
|
||||
'PostgresStorage.setEmailNotificationPreference error:',
|
||||
err
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -969,7 +1071,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getTvboxSubscribeToken(userName: string): Promise<string | null> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.prepare('SELECT tvbox_subscribe_token FROM users_v2 WHERE username = $1')
|
||||
.prepare(
|
||||
'SELECT tvbox_subscribe_token FROM users_v2 WHERE username = $1'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
|
||||
@@ -983,7 +1087,9 @@ export class PostgresStorage implements IStorage {
|
||||
async setTvboxSubscribeToken(userName: string, token: string): Promise<void> {
|
||||
try {
|
||||
await this.db
|
||||
.prepare('UPDATE users_v2 SET tvbox_subscribe_token = $1 WHERE username = $2')
|
||||
.prepare(
|
||||
'UPDATE users_v2 SET tvbox_subscribe_token = $1 WHERE username = $2'
|
||||
)
|
||||
.bind(token, userName)
|
||||
.run();
|
||||
|
||||
@@ -999,7 +1105,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getUsernameByTvboxToken(token: string): Promise<string | null> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.prepare('SELECT username FROM users_v2 WHERE tvbox_subscribe_token = $1')
|
||||
.prepare(
|
||||
'SELECT username FROM users_v2 WHERE tvbox_subscribe_token = $1'
|
||||
)
|
||||
.bind(token)
|
||||
.first();
|
||||
|
||||
@@ -1015,7 +1123,9 @@ export class PostgresStorage 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 = $1 AND key = $2')
|
||||
.prepare(
|
||||
'SELECT * FROM music_play_records WHERE username = $1 AND key = $2'
|
||||
)
|
||||
.bind(userName, key)
|
||||
.first();
|
||||
|
||||
@@ -1038,10 +1148,15 @@ export class PostgresStorage 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
ON CONFLICT(username, key) DO UPDATE SET
|
||||
@@ -1052,7 +1167,8 @@ export class PostgresStorage implements IStorage {
|
||||
play_time = EXCLUDED.play_time,
|
||||
duration = EXCLUDED.duration,
|
||||
save_time = EXCLUDED.save_time
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
key,
|
||||
@@ -1073,14 +1189,18 @@ export class PostgresStorage 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;
|
||||
|
||||
try {
|
||||
// 使用批量插入,Postgres 支持 batch 操作
|
||||
const statements = records.map(({ key, record }) =>
|
||||
this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO music_play_records (username, key, platform, song_id, name, artist, album, pic, play_time, duration, save_time)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
ON CONFLICT(username, key) DO UPDATE SET
|
||||
@@ -1093,7 +1213,8 @@ export class PostgresStorage implements IStorage {
|
||||
play_time = EXCLUDED.play_time,
|
||||
duration = EXCLUDED.duration,
|
||||
save_time = EXCLUDED.save_time
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
key,
|
||||
@@ -1118,10 +1239,14 @@ export class PostgresStorage 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 = $1 ORDER BY save_time DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM music_play_records WHERE username = $1 ORDER BY save_time DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -1151,7 +1276,9 @@ export class PostgresStorage implements IStorage {
|
||||
async deleteMusicPlayRecord(userName: string, key: string): Promise<void> {
|
||||
try {
|
||||
await this.db
|
||||
.prepare('DELETE FROM music_play_records WHERE username = $1 AND key = $2')
|
||||
.prepare(
|
||||
'DELETE FROM music_play_records WHERE username = $1 AND key = $2'
|
||||
)
|
||||
.bind(userName, key)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -1174,19 +1301,24 @@ export class PostgresStorage 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 ($1, $2, $3, $4, $5, $6, $7)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
playlist.id,
|
||||
userName,
|
||||
@@ -1230,7 +1362,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getUserMusicPlaylists(userName: string): Promise<any[]> {
|
||||
try {
|
||||
const results = await this.db
|
||||
.prepare('SELECT * FROM music_playlists WHERE username = $1 ORDER BY created_at DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM music_playlists WHERE username = $1 ORDER BY created_at DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -1251,11 +1385,14 @@ export class PostgresStorage 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 setClauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
@@ -1282,7 +1419,11 @@ export class PostgresStorage implements IStorage {
|
||||
values.push(playlistId);
|
||||
|
||||
await this.db
|
||||
.prepare(`UPDATE music_playlists SET ${setClauses.join(', ')} WHERE id = $${paramIndex}`)
|
||||
.prepare(
|
||||
`UPDATE music_playlists SET ${setClauses.join(
|
||||
', '
|
||||
)} WHERE id = $${paramIndex}`
|
||||
)
|
||||
.bind(...values)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -1303,28 +1444,34 @@ export class PostgresStorage 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 maxSortResult = await this.db
|
||||
.prepare('SELECT MAX(sort_order) as max_sort FROM music_playlist_songs WHERE playlist_id = $1')
|
||||
.prepare(
|
||||
'SELECT MAX(sort_order) as max_sort FROM music_playlist_songs WHERE playlist_id = $1'
|
||||
)
|
||||
.bind(playlistId)
|
||||
.first();
|
||||
|
||||
const nextSortOrder = (maxSortResult?.max_sort as number || 0) + 1;
|
||||
const nextSortOrder = ((maxSortResult?.max_sort 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)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT(playlist_id, platform, song_id) DO UPDATE SET
|
||||
@@ -1333,7 +1480,8 @@ export class PostgresStorage implements IStorage {
|
||||
album = EXCLUDED.album,
|
||||
pic = EXCLUDED.pic,
|
||||
duration = EXCLUDED.duration
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
playlistId,
|
||||
song.platform,
|
||||
@@ -1359,10 +1507,16 @@ export class PostgresStorage 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 = $1 AND platform = $2 AND song_id = $3')
|
||||
.prepare(
|
||||
'DELETE FROM music_playlist_songs WHERE playlist_id = $1 AND platform = $2 AND song_id = $3'
|
||||
)
|
||||
.bind(playlistId, platform, songId)
|
||||
.run();
|
||||
|
||||
@@ -1380,7 +1534,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getPlaylistSongs(playlistId: string): Promise<any[]> {
|
||||
try {
|
||||
const results = await this.db
|
||||
.prepare('SELECT * FROM music_playlist_songs WHERE playlist_id = $1 ORDER BY sort_order ASC')
|
||||
.prepare(
|
||||
'SELECT * FROM music_playlist_songs WHERE playlist_id = $1 ORDER BY sort_order ASC'
|
||||
)
|
||||
.bind(playlistId)
|
||||
.all();
|
||||
|
||||
@@ -1403,11 +1559,16 @@ export class PostgresStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
async updatePlaylistSongOrder(playlistId: string, songOrders: Array<{ platform: string; songId: string; sortOrder: number }>): Promise<void> {
|
||||
async updatePlaylistSongOrder(
|
||||
playlistId: string,
|
||||
songOrders: Array<{ platform: string; songId: string; sortOrder: number }>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const statements = songOrders.map(({ platform, songId, sortOrder }) =>
|
||||
this.db
|
||||
.prepare('UPDATE music_playlist_songs SET sort_order = $1 WHERE playlist_id = $2 AND platform = $3 AND song_id = $4')
|
||||
.prepare(
|
||||
'UPDATE music_playlist_songs SET sort_order = $1 WHERE playlist_id = $2 AND platform = $3 AND song_id = $4'
|
||||
)
|
||||
.bind(sortOrder, playlistId, platform, songId)
|
||||
);
|
||||
|
||||
@@ -1432,7 +1593,9 @@ export class PostgresStorage implements IStorage {
|
||||
try {
|
||||
const results = await this.db
|
||||
// 按队列顺序返回;当前播放项由最大 last_played_at 决定
|
||||
.prepare('SELECT * FROM music_v2_history WHERE username = $1 ORDER BY created_at ASC, id ASC')
|
||||
.prepare(
|
||||
'SELECT * FROM music_v2_history WHERE username = $1 ORDER BY created_at ASC, id ASC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -1461,10 +1624,14 @@ export class PostgresStorage 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
|
||||
@@ -1484,7 +1651,8 @@ export class PostgresStorage implements IStorage {
|
||||
play_count = EXCLUDED.play_count,
|
||||
last_quality = EXCLUDED.last_quality,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
record.songId,
|
||||
@@ -1510,7 +1678,10 @@ export class PostgresStorage 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);
|
||||
}
|
||||
@@ -1518,7 +1689,9 @@ export class PostgresStorage implements IStorage {
|
||||
|
||||
async deleteMusicV2History(userName: string, songId: string): Promise<void> {
|
||||
await this.db
|
||||
.prepare('DELETE FROM music_v2_history WHERE username = $1 AND song_id = $2')
|
||||
.prepare(
|
||||
'DELETE FROM music_v2_history WHERE username = $1 AND song_id = $2'
|
||||
)
|
||||
.bind(userName, songId)
|
||||
.run();
|
||||
}
|
||||
@@ -1532,23 +1705,39 @@ export class PostgresStorage 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 ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`)
|
||||
.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 = $1')
|
||||
.bind(playlistId)
|
||||
@@ -1566,9 +1755,13 @@ export class PostgresStorage 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 = $1 ORDER BY updated_at DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM music_v2_playlists WHERE username = $1 ORDER BY updated_at DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
if (!results.results) return [];
|
||||
@@ -1584,12 +1777,15 @@ export class PostgresStorage 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 clauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
let index = 1;
|
||||
@@ -1613,7 +1809,11 @@ export class PostgresStorage implements IStorage {
|
||||
values.push(Date.now());
|
||||
values.push(playlistId);
|
||||
await this.db
|
||||
.prepare(`UPDATE music_v2_playlists SET ${clauses.join(', ')} WHERE id = $${index}`)
|
||||
.prepare(
|
||||
`UPDATE music_v2_playlists SET ${clauses.join(
|
||||
', '
|
||||
)} WHERE id = $${index}`
|
||||
)
|
||||
.bind(...values)
|
||||
.run();
|
||||
}
|
||||
@@ -1625,20 +1825,29 @@ export class PostgresStorage 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 maxSort: any = await this.db
|
||||
.prepare('SELECT MAX(sort_order) as max_sort FROM music_v2_playlist_items WHERE playlist_id = $1')
|
||||
.prepare(
|
||||
'SELECT MAX(sort_order) as max_sort FROM music_v2_playlist_items WHERE playlist_id = $1'
|
||||
)
|
||||
.bind(playlistId)
|
||||
.first();
|
||||
const nextOrder = Math.max(item.sortOrder || 0, (maxSort?.max_sort as number || 0) + 1);
|
||||
const nextOrder = Math.max(
|
||||
item.sortOrder || 0,
|
||||
((maxSort?.max_sort 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
|
||||
)
|
||||
@@ -1653,7 +1862,8 @@ export class PostgresStorage implements IStorage {
|
||||
duration_text = EXCLUDED.duration_text,
|
||||
duration_sec = EXCLUDED.duration_sec,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
playlistId,
|
||||
playlist.username,
|
||||
@@ -1679,9 +1889,14 @@ export class PostgresStorage 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 = $1 AND song_id = $2')
|
||||
.prepare(
|
||||
'DELETE FROM music_v2_playlist_items WHERE playlist_id = $1 AND song_id = $2'
|
||||
)
|
||||
.bind(playlistId, songId)
|
||||
.run();
|
||||
const items = await this.listMusicV2PlaylistItems(playlistId);
|
||||
@@ -1691,9 +1906,13 @@ export class PostgresStorage 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 = $1 ORDER BY sort_order ASC, added_at ASC')
|
||||
.prepare(
|
||||
'SELECT * FROM music_v2_playlist_items WHERE playlist_id = $1 ORDER BY sort_order ASC, added_at ASC'
|
||||
)
|
||||
.bind(playlistId)
|
||||
.all();
|
||||
if (!results.results) return [];
|
||||
@@ -1714,9 +1933,14 @@ export class PostgresStorage 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 = $1 AND song_id = $2 LIMIT 1')
|
||||
.prepare(
|
||||
'SELECT 1 FROM music_v2_playlist_items WHERE playlist_id = $1 AND song_id = $2 LIMIT 1'
|
||||
)
|
||||
.bind(playlistId, songId)
|
||||
.first();
|
||||
return row !== null;
|
||||
@@ -1727,7 +1951,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getSearchHistory(userName: string): Promise<string[]> {
|
||||
try {
|
||||
const results = await this.db
|
||||
.prepare('SELECT keyword FROM search_history WHERE username = $1 ORDER BY timestamp DESC LIMIT 20')
|
||||
.prepare(
|
||||
'SELECT keyword FROM search_history WHERE username = $1 ORDER BY timestamp DESC LIMIT 20'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -1745,24 +1971,29 @@ export class PostgresStorage implements IStorage {
|
||||
|
||||
// 插入或更新时间戳
|
||||
await this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO search_history (username, keyword, timestamp)
|
||||
VALUES ($1, $2, $3)
|
||||
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 = $1')
|
||||
.prepare(
|
||||
'SELECT COUNT(*) as count FROM search_history WHERE username = $1'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
|
||||
const count = (countResult?.count as number) || 0;
|
||||
if (count > 20) {
|
||||
await this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
DELETE FROM search_history
|
||||
WHERE username = $1
|
||||
AND id NOT IN (
|
||||
@@ -1771,7 +2002,8 @@ export class PostgresStorage implements IStorage {
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 20
|
||||
)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(userName)
|
||||
.run();
|
||||
}
|
||||
@@ -1785,7 +2017,9 @@ export class PostgresStorage implements IStorage {
|
||||
try {
|
||||
if (keyword) {
|
||||
await this.db
|
||||
.prepare('DELETE FROM search_history WHERE username = $1 AND keyword = $2')
|
||||
.prepare(
|
||||
'DELETE FROM search_history WHERE username = $1 AND keyword = $2'
|
||||
)
|
||||
.bind(userName, keyword)
|
||||
.run();
|
||||
} else {
|
||||
@@ -1802,7 +2036,10 @@ export class PostgresStorage 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
|
||||
@@ -1826,11 +2063,13 @@ export class PostgresStorage 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),
|
||||
};
|
||||
@@ -1840,11 +2079,16 @@ export class PostgresStorage 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,
|
||||
@@ -1867,7 +2111,8 @@ export class PostgresStorage 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,
|
||||
@@ -1894,11 +2139,15 @@ export class PostgresStorage 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 = $1 ORDER BY save_time DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM manga_shelf WHERE username = $1 ORDER BY save_time DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -1921,11 +2170,13 @@ export class PostgresStorage 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),
|
||||
};
|
||||
@@ -1952,10 +2203,15 @@ export class PostgresStorage 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 = $1 AND key = $2')
|
||||
.prepare(
|
||||
'SELECT * FROM manga_read_records WHERE username = $1 AND key = $2'
|
||||
)
|
||||
.bind(userName, key)
|
||||
.first();
|
||||
|
||||
@@ -1978,10 +2234,15 @@ export class PostgresStorage 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
|
||||
@@ -1998,7 +2259,8 @@ export class PostgresStorage implements IStorage {
|
||||
page_index = EXCLUDED.page_index,
|
||||
page_count = EXCLUDED.page_count,
|
||||
save_time = EXCLUDED.save_time
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
key,
|
||||
@@ -2020,10 +2282,14 @@ export class PostgresStorage 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 = $1 ORDER BY save_time DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM manga_read_records WHERE username = $1 ORDER BY save_time DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -2055,7 +2321,9 @@ export class PostgresStorage implements IStorage {
|
||||
async deleteMangaReadRecord(userName: string, key: string): Promise<void> {
|
||||
try {
|
||||
await this.db
|
||||
.prepare('DELETE FROM manga_read_records WHERE username = $1 AND key = $2')
|
||||
.prepare(
|
||||
'DELETE FROM manga_read_records WHERE username = $1 AND key = $2'
|
||||
)
|
||||
.bind(userName, key)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2066,10 +2334,15 @@ export class PostgresStorage 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 = $1')
|
||||
.prepare(
|
||||
'SELECT COUNT(*) as count FROM manga_read_records WHERE username = $1'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
|
||||
@@ -2077,7 +2350,8 @@ export class PostgresStorage implements IStorage {
|
||||
if (count <= threshold) return;
|
||||
|
||||
await this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
DELETE FROM manga_read_records
|
||||
WHERE username = $1
|
||||
AND key NOT IN (
|
||||
@@ -2086,7 +2360,8 @@ export class PostgresStorage implements IStorage {
|
||||
ORDER BY save_time DESC
|
||||
LIMIT $2
|
||||
)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(userName, maxRecords)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2095,10 +2370,12 @@ export class PostgresStorage 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 = $1 AND key = $2')
|
||||
@@ -2116,9 +2393,18 @@ export class PostgresStorage 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),
|
||||
@@ -2129,10 +2415,15 @@ export class PostgresStorage 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(`
|
||||
.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
|
||||
@@ -2154,12 +2445,26 @@ export class PostgresStorage 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
|
||||
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) {
|
||||
@@ -2168,10 +2473,14 @@ export class PostgresStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
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 = $1 ORDER BY COALESCE(last_read_time, save_time) DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM book_shelf WHERE username = $1 ORDER BY COALESCE(last_read_time, save_time) DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
const shelves: { [key: string]: BookShelfItem } = {};
|
||||
@@ -2187,9 +2496,17 @@ export class PostgresStorage 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),
|
||||
@@ -2204,7 +2521,10 @@ export class PostgresStorage implements IStorage {
|
||||
|
||||
async deleteBookShelf(userName: string, key: string): Promise<void> {
|
||||
try {
|
||||
await this.db.prepare('DELETE FROM book_shelf WHERE username = $1 AND key = $2').bind(userName, key).run();
|
||||
await this.db
|
||||
.prepare('DELETE FROM book_shelf WHERE username = $1 AND key = $2')
|
||||
.bind(userName, key)
|
||||
.run();
|
||||
} catch (err) {
|
||||
console.error('PostgresStorage.deleteBookShelf error:', err);
|
||||
throw err;
|
||||
@@ -2213,10 +2533,15 @@ export class PostgresStorage 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 = $1 AND key = $2')
|
||||
.prepare(
|
||||
'SELECT * FROM book_read_records WHERE username = $1 AND key = $2'
|
||||
)
|
||||
.bind(userName, key)
|
||||
.first();
|
||||
if (!result) return null;
|
||||
@@ -2247,10 +2572,15 @@ export class PostgresStorage 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(`
|
||||
.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
|
||||
@@ -2272,12 +2602,26 @@ export class PostgresStorage 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
|
||||
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) {
|
||||
@@ -2286,10 +2630,14 @@ export class PostgresStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
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 = $1 ORDER BY save_time DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM book_read_records WHERE username = $1 ORDER BY save_time DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
const records: { [key: string]: BookReadRecord } = {};
|
||||
@@ -2326,7 +2674,12 @@ export class PostgresStorage implements IStorage {
|
||||
|
||||
async deleteBookReadRecord(userName: string, key: string): Promise<void> {
|
||||
try {
|
||||
await this.db.prepare('DELETE FROM book_read_records WHERE username = $1 AND key = $2').bind(userName, key).run();
|
||||
await this.db
|
||||
.prepare(
|
||||
'DELETE FROM book_read_records WHERE username = $1 AND key = $2'
|
||||
)
|
||||
.bind(userName, key)
|
||||
.run();
|
||||
} catch (err) {
|
||||
console.error('PostgresStorage.deleteBookReadRecord error:', err);
|
||||
throw err;
|
||||
@@ -2335,16 +2688,22 @@ export class PostgresStorage 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 = $1')
|
||||
.prepare(
|
||||
'SELECT COUNT(*) as count FROM book_read_records WHERE username = $1'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
const count = Number(countResult?.count || 0);
|
||||
if (count <= threshold) return;
|
||||
await this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
DELETE FROM book_read_records
|
||||
WHERE username = $1
|
||||
AND key NOT IN (
|
||||
@@ -2353,7 +2712,8 @@ export class PostgresStorage implements IStorage {
|
||||
ORDER BY save_time DESC
|
||||
LIMIT $2
|
||||
)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(userName, maxRecords)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2364,7 +2724,11 @@ export class PostgresStorage 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
|
||||
@@ -2384,19 +2748,32 @@ export class PostgresStorage 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 ($1, $2, $3, $4, $5)
|
||||
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('PostgresStorage.setSkipConfig error:', err);
|
||||
@@ -2404,7 +2781,11 @@ export class PostgresStorage 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
|
||||
@@ -2417,7 +2798,9 @@ export class PostgresStorage 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 = $1')
|
||||
@@ -2454,7 +2837,9 @@ export class PostgresStorage 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 = $1')
|
||||
@@ -2469,14 +2854,19 @@ export class PostgresStorage 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 ($1, $2)
|
||||
ON CONFLICT (username) DO UPDATE SET rules = EXCLUDED.rules
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(userName, JSON.stringify(config))
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2502,7 +2892,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getNotifications(userName: string): Promise<Notification[]> {
|
||||
try {
|
||||
const results = await this.db
|
||||
.prepare('SELECT * FROM notifications WHERE username = $1 ORDER BY timestamp DESC')
|
||||
.prepare(
|
||||
'SELECT * FROM notifications WHERE username = $1 ORDER BY timestamp DESC'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -2522,13 +2914,18 @@ export class PostgresStorage 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 ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
notification.id,
|
||||
userName,
|
||||
@@ -2546,10 +2943,15 @@ export class PostgresStorage 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 = $1 AND id = $2')
|
||||
.prepare(
|
||||
'UPDATE notifications SET read = 1 WHERE username = $1 AND id = $2'
|
||||
)
|
||||
.bind(userName, notificationId)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2558,7 +2960,10 @@ export class PostgresStorage 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 = $1 AND id = $2')
|
||||
@@ -2585,7 +2990,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getUnreadNotificationCount(userName: string): Promise<number> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.prepare('SELECT COUNT(*) as count FROM notifications WHERE username = $1 AND read = 0')
|
||||
.prepare(
|
||||
'SELECT COUNT(*) as count FROM notifications WHERE username = $1 AND read = 0'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
|
||||
@@ -2630,14 +3037,16 @@ export class PostgresStorage 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
request.id,
|
||||
request.tmdbId || null,
|
||||
@@ -2663,7 +3072,10 @@ export class PostgresStorage 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[] = [];
|
||||
@@ -2700,7 +3112,11 @@ export class PostgresStorage implements IStorage {
|
||||
values.push(requestId);
|
||||
|
||||
await this.db
|
||||
.prepare(`UPDATE movie_requests SET ${fields.join(', ')} WHERE id = $${paramIndex}`)
|
||||
.prepare(
|
||||
`UPDATE movie_requests SET ${fields.join(
|
||||
', '
|
||||
)} WHERE id = $${paramIndex}`
|
||||
)
|
||||
.bind(...values)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2724,7 +3140,9 @@ export class PostgresStorage implements IStorage {
|
||||
async getUserMovieRequests(userName: string): Promise<string[]> {
|
||||
try {
|
||||
const results = await this.db
|
||||
.prepare('SELECT request_id FROM user_movie_requests WHERE username = $1')
|
||||
.prepare(
|
||||
'SELECT request_id FROM user_movie_requests WHERE username = $1'
|
||||
)
|
||||
.bind(userName)
|
||||
.all();
|
||||
|
||||
@@ -2736,10 +3154,15 @@ export class PostgresStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
async addUserMovieRequest(userName: string, requestId: string): Promise<void> {
|
||||
async addUserMovieRequest(
|
||||
userName: string,
|
||||
requestId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.db
|
||||
.prepare('INSERT INTO user_movie_requests (username, request_id) VALUES ($1, $2) ON CONFLICT (username, request_id) DO NOTHING')
|
||||
.prepare(
|
||||
'INSERT INTO user_movie_requests (username, request_id) VALUES ($1, $2) ON CONFLICT (username, request_id) DO NOTHING'
|
||||
)
|
||||
.bind(userName, requestId)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2748,10 +3171,15 @@ export class PostgresStorage 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 = $1 AND request_id = $2')
|
||||
.prepare(
|
||||
'DELETE FROM user_movie_requests WHERE username = $1 AND request_id = $2'
|
||||
)
|
||||
.bind(userName, requestId)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2800,11 +3228,13 @@ export class PostgresStorage implements IStorage {
|
||||
async setAdminConfig(config: AdminConfig): Promise<void> {
|
||||
try {
|
||||
await this.db
|
||||
.prepare(`
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO admin_config (id, config, updated_at)
|
||||
VALUES (1, $1, $2)
|
||||
ON CONFLICT (id) DO UPDATE SET config = EXCLUDED.config, updated_at = EXCLUDED.updated_at
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(JSON.stringify(config), Date.now())
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2841,8 +3271,15 @@ export class PostgresStorage 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')) {
|
||||
console.warn('PostgresStorage.clearAllData warning:', table, message);
|
||||
if (
|
||||
message.includes('no such table') ||
|
||||
message.includes('does not exist')
|
||||
) {
|
||||
console.warn(
|
||||
'PostgresStorage.clearAllData warning:',
|
||||
table,
|
||||
message
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
@@ -2871,11 +3308,13 @@ export class PostgresStorage 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 ($1, $2, $3)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(key, value, Date.now())
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2899,7 +3338,9 @@ export class PostgresStorage 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 = $1')
|
||||
.prepare(
|
||||
'SELECT last_check_time FROM favorite_check_times WHERE username = $1'
|
||||
)
|
||||
.bind(userName)
|
||||
.first();
|
||||
|
||||
@@ -2910,14 +3351,19 @@ export class PostgresStorage 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 ($1, $2)
|
||||
ON CONFLICT (username) DO UPDATE SET last_check_time = EXCLUDED.last_check_time
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(userName, timestamp)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2926,10 +3372,15 @@ export class PostgresStorage 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 = $1 WHERE username = $2')
|
||||
.prepare(
|
||||
'UPDATE users SET last_movie_request_time = $1 WHERE username = $2'
|
||||
)
|
||||
.bind(timestamp, userName)
|
||||
.run();
|
||||
} catch (err) {
|
||||
@@ -2952,11 +3403,13 @@ class PostgresRedisHashAdapter {
|
||||
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 ($1, $2, $3)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at
|
||||
`)
|
||||
`
|
||||
)
|
||||
.bind(key, value, Date.now())
|
||||
.run();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user