fix: cross devices skip configs sync

This commit is contained in:
shinya
2025-07-29 21:30:20 +08:00
parent 10f2dc1904
commit b5e351bb9c
9 changed files with 165 additions and 5 deletions

View File

@@ -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<any>();
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;
}
}
}

View File

@@ -1426,6 +1426,67 @@ export async function saveSkipConfig(
}
}
/**
* 获取所有跳过片头片尾配置。
* 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。
*/
export async function getAllSkipConfigs(): Promise<Record<string, SkipConfig>> {
// 服务器端渲染阶段直接返回空
if (typeof window === 'undefined') {
return {};
}
// 数据库存储模式:使用混合缓存策略(包括 redis、d1、upstash
if (STORAGE_TYPE !== 'localstorage') {
// 优先从缓存获取数据
const cachedData = cacheManager.getCachedSkipConfigs();
if (cachedData) {
// 返回缓存数据,同时后台异步更新
fetchFromApi<Record<string, SkipConfig>>(`/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<Record<string, SkipConfig>>(
`/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<string, SkipConfig>;
} catch (err) {
console.error('读取跳过片头片尾配置失败:', err);
return {};
}
}
/**
* 删除跳过片头片尾配置。
* 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。

View File

@@ -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 {};
}
}
// 导出默认实例

View File

@@ -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 客户端

View File

@@ -78,6 +78,7 @@ export interface IStorage {
config: SkipConfig
): Promise<void>;
deleteSkipConfig(userName: string, source: string, id: string): Promise<void>;
getAllSkipConfigs(userName: string): Promise<{ [key: string]: SkipConfig }>;
}
// 搜索结果数据结构

View File

@@ -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 客户端

View File

@@ -2,7 +2,7 @@
'use client';
const CURRENT_VERSION = '20250729193611';
const CURRENT_VERSION = '20250729213020';
// 版本检查结果枚举
export enum UpdateStatus {