From b5e351bb9ce979358344eec7946af19b57c785a5 Mon Sep 17 00:00:00 2001 From: shinya Date: Tue, 29 Jul 2025 21:30:20 +0800 Subject: [PATCH] fix: cross devices skip configs sync --- VERSION.txt | 2 +- src/app/api/skipconfigs/route.ts | 5 ++- src/lib/d1.db.ts | 30 ++++++++++++++++ src/lib/db.client.ts | 61 ++++++++++++++++++++++++++++++++ src/lib/db.ts | 9 +++++ src/lib/redis.db.ts | 30 ++++++++++++++++ src/lib/types.ts | 1 + src/lib/upstash.db.ts | 30 ++++++++++++++++ src/lib/version.ts | 2 +- 9 files changed, 165 insertions(+), 5 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index da0ffa6..df38e6b 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -20250729193611 \ No newline at end of file +20250729213020 \ No newline at end of file diff --git a/src/app/api/skipconfigs/route.ts b/src/app/api/skipconfigs/route.ts index 9109459..b14c525 100644 --- a/src/app/api/skipconfigs/route.ts +++ b/src/app/api/skipconfigs/route.ts @@ -25,9 +25,8 @@ export async function GET(request: NextRequest) { return NextResponse.json(config); } else { // 获取所有配置 - // 注意:这里需要实现获取所有跳过片头片尾配置的方法 - // 由于当前接口设计是按source+id获取单个配置,这里返回空对象 - return NextResponse.json({}); + const configs = await db.getAllSkipConfigs(authInfo.username); + return NextResponse.json(configs); } } catch (error) { console.error('获取跳过片头片尾配置失败:', error); diff --git a/src/lib/d1.db.ts b/src/lib/d1.db.ts index 87ce2a2..4c82b95 100644 --- a/src/lib/d1.db.ts +++ b/src/lib/d1.db.ts @@ -554,4 +554,34 @@ export class D1Storage implements IStorage { throw err; } } + + async getAllSkipConfigs( + userName: string + ): Promise<{ [key: string]: SkipConfig }> { + try { + const db = await this.getDatabase(); + const result = await db + .prepare( + 'SELECT source, id_video, enable, intro_time, outro_time FROM skip_configs WHERE username = ?' + ) + .bind(userName) + .all(); + + const configs: { [key: string]: SkipConfig } = {}; + + result.results.forEach((row) => { + const key = `${row.source}+${row.id_video}`; + configs[key] = { + enable: Boolean(row.enable), + intro_time: row.intro_time, + outro_time: row.outro_time, + }; + }); + + return configs; + } catch (err) { + console.error('Failed to get all skip configs:', err); + throw err; + } + } } diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index 1860fdb..a551524 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -1426,6 +1426,67 @@ export async function saveSkipConfig( } } +/** + * 获取所有跳过片头片尾配置。 + * 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。 + */ +export async function getAllSkipConfigs(): Promise> { + // 服务器端渲染阶段直接返回空 + if (typeof window === 'undefined') { + return {}; + } + + // 数据库存储模式:使用混合缓存策略(包括 redis、d1、upstash) + if (STORAGE_TYPE !== 'localstorage') { + // 优先从缓存获取数据 + const cachedData = cacheManager.getCachedSkipConfigs(); + + if (cachedData) { + // 返回缓存数据,同时后台异步更新 + fetchFromApi>(`/api/skipconfigs`) + .then((freshData) => { + // 只有数据真正不同时才更新缓存 + if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) { + cacheManager.cacheSkipConfigs(freshData); + // 触发数据更新事件 + window.dispatchEvent( + new CustomEvent('skipConfigsUpdated', { + detail: freshData, + }) + ); + } + }) + .catch((err) => { + console.warn('后台同步跳过片头片尾配置失败:', err); + }); + + return cachedData; + } else { + // 缓存为空,直接从 API 获取并缓存 + try { + const freshData = await fetchFromApi>( + `/api/skipconfigs` + ); + cacheManager.cacheSkipConfigs(freshData); + return freshData; + } catch (err) { + console.error('获取跳过片头片尾配置失败:', err); + return {}; + } + } + } + + // localStorage 模式 + try { + const raw = localStorage.getItem('moontv_skip_configs'); + if (!raw) return {}; + return JSON.parse(raw) as Record; + } catch (err) { + console.error('读取跳过片头片尾配置失败:', err); + return {}; + } +} + /** * 删除跳过片头片尾配置。 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 diff --git a/src/lib/db.ts b/src/lib/db.ts index 751d2ae..ab97b99 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -214,6 +214,15 @@ export class DbManager { await (this.storage as any).deleteSkipConfig(userName, source, id); } } + + async getAllSkipConfigs( + userName: string + ): Promise<{ [key: string]: SkipConfig }> { + if (typeof (this.storage as any).getAllSkipConfigs === 'function') { + return (this.storage as any).getAllSkipConfigs(userName); + } + return {}; + } } // 导出默认实例 diff --git a/src/lib/redis.db.ts b/src/lib/redis.db.ts index 7bc9368..0cebfe6 100644 --- a/src/lib/redis.db.ts +++ b/src/lib/redis.db.ts @@ -332,6 +332,36 @@ export class RedisStorage implements IStorage { this.client.del(this.skipConfigKey(userName, source, id)) ); } + + async getAllSkipConfigs( + userName: string + ): Promise<{ [key: string]: SkipConfig }> { + const pattern = `u:${userName}:skip:*`; + const keys = await withRetry(() => this.client.keys(pattern)); + + if (keys.length === 0) { + return {}; + } + + const configs: { [key: string]: SkipConfig } = {}; + + // 批量获取所有配置 + const values = await withRetry(() => this.client.mGet(keys)); + + keys.forEach((key, index) => { + const value = values[index]; + if (value) { + // 从key中提取source+id + const match = key.match(/^u:.+?:skip:(.+)$/); + if (match) { + const sourceAndId = match[1]; + configs[sourceAndId] = JSON.parse(value as string) as SkipConfig; + } + } + }); + + return configs; + } } // 单例 Redis 客户端 diff --git a/src/lib/types.ts b/src/lib/types.ts index 24b8e3a..a3abc8d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -78,6 +78,7 @@ export interface IStorage { config: SkipConfig ): Promise; deleteSkipConfig(userName: string, source: string, id: string): Promise; + getAllSkipConfigs(userName: string): Promise<{ [key: string]: SkipConfig }>; } // 搜索结果数据结构 diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts index 945c0e3..c7d4285 100644 --- a/src/lib/upstash.db.ts +++ b/src/lib/upstash.db.ts @@ -313,6 +313,36 @@ export class UpstashRedisStorage implements IStorage { this.client.del(this.skipConfigKey(userName, source, id)) ); } + + async getAllSkipConfigs( + userName: string + ): Promise<{ [key: string]: SkipConfig }> { + const pattern = `u:${userName}:skip:*`; + const keys = await withRetry(() => this.client.keys(pattern)); + + if (keys.length === 0) { + return {}; + } + + const configs: { [key: string]: SkipConfig } = {}; + + // 批量获取所有配置 + const values = await withRetry(() => this.client.mget(keys)); + + keys.forEach((key, index) => { + const value = values[index]; + if (value) { + // 从key中提取source+id + const match = key.match(/^u:.+?:skip:(.+)$/); + if (match) { + const sourceAndId = match[1]; + configs[sourceAndId] = value as SkipConfig; + } + } + }); + + return configs; + } } // 单例 Upstash Redis 客户端 diff --git a/src/lib/version.ts b/src/lib/version.ts index e8da238..8a61b86 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -2,7 +2,7 @@ 'use client'; -const CURRENT_VERSION = '20250729193611'; +const CURRENT_VERSION = '20250729213020'; // 版本检查结果枚举 export enum UpdateStatus {