mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 00:44:41 +08:00
feat: skip intro & outro
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { Favorite, IStorage, PlayRecord } from './types';
|
||||
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -340,6 +340,9 @@ export class D1Storage implements IStorage {
|
||||
db
|
||||
.prepare('DELETE FROM search_history WHERE username = ?')
|
||||
.bind(userName),
|
||||
db
|
||||
.prepare('DELETE FROM skip_configs WHERE username = ?')
|
||||
.bind(userName),
|
||||
];
|
||||
|
||||
await db.batch(statements);
|
||||
@@ -473,4 +476,82 @@ export class D1Storage implements IStorage {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 跳过片头片尾配置 ----------
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
const result = await db
|
||||
.prepare(
|
||||
'SELECT * FROM skip_configs WHERE username = ? AND source = ? AND id_video = ?'
|
||||
)
|
||||
.bind(userName, source, id)
|
||||
.first<any>();
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
return {
|
||||
enable: Boolean(result.enable),
|
||||
intro_time: result.intro_time,
|
||||
outro_time: result.outro_time,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to get skip config:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
await db
|
||||
.prepare(
|
||||
`
|
||||
INSERT OR REPLACE INTO skip_configs
|
||||
(username, source, id_video, enable, intro_time, outro_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
source,
|
||||
id,
|
||||
config.enable ? 1 : 0,
|
||||
config.intro_time,
|
||||
config.outro_time
|
||||
)
|
||||
.run();
|
||||
} catch (err) {
|
||||
console.error('Failed to set skip config:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
await db
|
||||
.prepare(
|
||||
'DELETE FROM skip_configs WHERE username = ? AND source = ? AND id_video = ?'
|
||||
)
|
||||
.bind(userName, source, id)
|
||||
.run();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete skip config:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { getAuthInfoFromBrowserCookie } from './auth';
|
||||
import { SkipConfig } from './types';
|
||||
|
||||
// ---- 类型 ----
|
||||
export interface PlayRecord {
|
||||
@@ -52,6 +53,7 @@ interface UserCacheStore {
|
||||
playRecords?: CacheData<Record<string, PlayRecord>>;
|
||||
favorites?: CacheData<Record<string, Favorite>>;
|
||||
searchHistory?: CacheData<string[]>;
|
||||
skipConfigs?: CacheData<Record<string, SkipConfig>>;
|
||||
}
|
||||
|
||||
// ---- 常量 ----
|
||||
@@ -248,6 +250,35 @@ class HybridCacheManager {
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的跳过片头片尾配置
|
||||
*/
|
||||
getCachedSkipConfigs(): Record<string, SkipConfig> | null {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return null;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
const cached = userCache.skipConfigs;
|
||||
|
||||
if (cached && this.isCacheValid(cached)) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存跳过片头片尾配置
|
||||
*/
|
||||
cacheSkipConfigs(data: Record<string, SkipConfig>): void {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
userCache.skipConfigs = this.createCacheData(data);
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定用户的所有缓存
|
||||
*/
|
||||
@@ -1122,11 +1153,13 @@ export async function refreshAllCache(): Promise<void> {
|
||||
|
||||
try {
|
||||
// 并行刷新所有数据
|
||||
const [playRecords, favorites, searchHistory] = await Promise.allSettled([
|
||||
fetchFromApi<Record<string, PlayRecord>>(`/api/playrecords`),
|
||||
fetchFromApi<Record<string, Favorite>>(`/api/favorites`),
|
||||
fetchFromApi<string[]>(`/api/searchhistory`),
|
||||
]);
|
||||
const [playRecords, favorites, searchHistory, skipConfigs] =
|
||||
await Promise.allSettled([
|
||||
fetchFromApi<Record<string, PlayRecord>>(`/api/playrecords`),
|
||||
fetchFromApi<Record<string, Favorite>>(`/api/favorites`),
|
||||
fetchFromApi<string[]>(`/api/searchhistory`),
|
||||
fetchFromApi<Record<string, SkipConfig>>(`/api/skipconfigs`),
|
||||
]);
|
||||
|
||||
if (playRecords.status === 'fulfilled') {
|
||||
cacheManager.cachePlayRecords(playRecords.value);
|
||||
@@ -1154,6 +1187,15 @@ export async function refreshAllCache(): Promise<void> {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (skipConfigs.status === 'fulfilled') {
|
||||
cacheManager.cacheSkipConfigs(skipConfigs.value);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('skipConfigsUpdated', {
|
||||
detail: skipConfigs.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('刷新缓存失败:', err);
|
||||
}
|
||||
@@ -1167,6 +1209,7 @@ export function getCacheStatus(): {
|
||||
hasPlayRecords: boolean;
|
||||
hasFavorites: boolean;
|
||||
hasSearchHistory: boolean;
|
||||
hasSkipConfigs: boolean;
|
||||
username: string | null;
|
||||
} {
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
@@ -1174,6 +1217,7 @@ export function getCacheStatus(): {
|
||||
hasPlayRecords: false,
|
||||
hasFavorites: false,
|
||||
hasSearchHistory: false,
|
||||
hasSkipConfigs: false,
|
||||
username: null,
|
||||
};
|
||||
}
|
||||
@@ -1183,6 +1227,7 @@ export function getCacheStatus(): {
|
||||
hasPlayRecords: !!cacheManager.getCachedPlayRecords(),
|
||||
hasFavorites: !!cacheManager.getCachedFavorites(),
|
||||
hasSearchHistory: !!cacheManager.getCachedSearchHistory(),
|
||||
hasSkipConfigs: !!cacheManager.getCachedSkipConfigs(),
|
||||
username: authInfo?.username || null,
|
||||
};
|
||||
}
|
||||
@@ -1192,7 +1237,8 @@ export function getCacheStatus(): {
|
||||
export type CacheUpdateEvent =
|
||||
| 'playRecordsUpdated'
|
||||
| 'favoritesUpdated'
|
||||
| 'searchHistoryUpdated';
|
||||
| 'searchHistoryUpdated'
|
||||
| 'skipConfigsUpdated';
|
||||
|
||||
/**
|
||||
* 用于 React 组件监听数据更新的事件监听器
|
||||
@@ -1233,7 +1279,12 @@ export async function preloadUserData(): Promise<void> {
|
||||
|
||||
// 检查是否已有有效缓存,避免重复请求
|
||||
const status = getCacheStatus();
|
||||
if (status.hasPlayRecords && status.hasFavorites && status.hasSearchHistory) {
|
||||
if (
|
||||
status.hasPlayRecords &&
|
||||
status.hasFavorites &&
|
||||
status.hasSearchHistory &&
|
||||
status.hasSkipConfigs
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1242,3 +1293,198 @@ export async function preloadUserData(): Promise<void> {
|
||||
console.warn('预加载用户数据失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- 跳过片头片尾配置相关 API ----------------
|
||||
|
||||
/**
|
||||
* 获取跳过片头片尾配置。
|
||||
* 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。
|
||||
*/
|
||||
export async function getSkipConfig(
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null> {
|
||||
// 服务器端渲染阶段直接返回空
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = generateStorageKey(source, id);
|
||||
|
||||
// 数据库存储模式:使用混合缓存策略(包括 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[key] || null;
|
||||
} else {
|
||||
// 缓存为空,直接从 API 获取并缓存
|
||||
try {
|
||||
const freshData = await fetchFromApi<Record<string, SkipConfig>>(
|
||||
`/api/skipconfigs`
|
||||
);
|
||||
cacheManager.cacheSkipConfigs(freshData);
|
||||
return freshData[key] || null;
|
||||
} catch (err) {
|
||||
console.error('获取跳过片头片尾配置失败:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// localStorage 模式
|
||||
try {
|
||||
const raw = localStorage.getItem('moontv_skip_configs');
|
||||
if (!raw) return null;
|
||||
const configs = JSON.parse(raw) as Record<string, SkipConfig>;
|
||||
return configs[key] || null;
|
||||
} catch (err) {
|
||||
console.error('读取跳过片头片尾配置失败:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存跳过片头片尾配置。
|
||||
* 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。
|
||||
*/
|
||||
export async function saveSkipConfig(
|
||||
source: string,
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
const key = generateStorageKey(source, id);
|
||||
|
||||
// 数据库存储模式:乐观更新策略(包括 redis、d1、upstash)
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
// 立即更新缓存
|
||||
const cachedConfigs = cacheManager.getCachedSkipConfigs() || {};
|
||||
cachedConfigs[key] = config;
|
||||
cacheManager.cacheSkipConfigs(cachedConfigs);
|
||||
|
||||
// 触发立即更新事件
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('skipConfigsUpdated', {
|
||||
detail: cachedConfigs,
|
||||
})
|
||||
);
|
||||
|
||||
// 异步同步到数据库
|
||||
try {
|
||||
const res = await fetch('/api/skipconfigs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ key, config }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`保存跳过片头片尾配置失败: ${res.status}`);
|
||||
} catch (err) {
|
||||
console.error('保存跳过片头片尾配置失败:', err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// localStorage 模式
|
||||
if (typeof window === 'undefined') {
|
||||
console.warn('无法在服务端保存跳过片头片尾配置到 localStorage');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem('moontv_skip_configs');
|
||||
const configs = raw ? (JSON.parse(raw) as Record<string, SkipConfig>) : {};
|
||||
configs[key] = config;
|
||||
localStorage.setItem('moontv_skip_configs', JSON.stringify(configs));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('skipConfigsUpdated', {
|
||||
detail: configs,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('保存跳过片头片尾配置失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除跳过片头片尾配置。
|
||||
* 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。
|
||||
*/
|
||||
export async function deleteSkipConfig(
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const key = generateStorageKey(source, id);
|
||||
|
||||
// 数据库存储模式:乐观更新策略(包括 redis、d1、upstash)
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
// 立即更新缓存
|
||||
const cachedConfigs = cacheManager.getCachedSkipConfigs() || {};
|
||||
delete cachedConfigs[key];
|
||||
cacheManager.cacheSkipConfigs(cachedConfigs);
|
||||
|
||||
// 触发立即更新事件
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('skipConfigsUpdated', {
|
||||
detail: cachedConfigs,
|
||||
})
|
||||
);
|
||||
|
||||
// 异步同步到数据库
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/skipconfigs?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error(`删除跳过片头片尾配置失败: ${res.status}`);
|
||||
} catch (err) {
|
||||
console.error('删除跳过片头片尾配置失败:', err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// localStorage 模式
|
||||
if (typeof window === 'undefined') {
|
||||
console.warn('无法在服务端删除跳过片头片尾配置到 localStorage');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem('moontv_skip_configs');
|
||||
if (raw) {
|
||||
const configs = JSON.parse(raw) as Record<string, SkipConfig>;
|
||||
delete configs[key];
|
||||
localStorage.setItem('moontv_skip_configs', JSON.stringify(configs));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('skipConfigsUpdated', {
|
||||
detail: configs,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除跳过片头片尾配置失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { D1Storage } from './d1.db';
|
||||
import { RedisStorage } from './redis.db';
|
||||
import { Favorite, IStorage, PlayRecord } from './types';
|
||||
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||
import { UpstashRedisStorage } from './upstash.db';
|
||||
|
||||
// storage type 常量: 'localstorage' | 'redis' | 'd1' | 'upstash',默认 'localstorage'
|
||||
@@ -181,6 +181,39 @@ export class DbManager {
|
||||
await (this.storage as any).setAdminConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 跳过片头片尾配置 ----------
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null> {
|
||||
if (typeof (this.storage as any).getSkipConfig === 'function') {
|
||||
return (this.storage as any).getSkipConfig(userName, source, id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
if (typeof (this.storage as any).setSkipConfig === 'function') {
|
||||
await (this.storage as any).setSkipConfig(userName, source, id, config);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
if (typeof (this.storage as any).deleteSkipConfig === 'function') {
|
||||
await (this.storage as any).deleteSkipConfig(userName, source, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { Favorite, IStorage, PlayRecord } from './types';
|
||||
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -223,6 +223,15 @@ export class RedisStorage implements IStorage {
|
||||
if (favoriteKeys.length > 0) {
|
||||
await withRetry(() => this.client.del(favoriteKeys));
|
||||
}
|
||||
|
||||
// 删除跳过片头片尾配置
|
||||
const skipConfigPattern = `u:${userName}:skip:*`;
|
||||
const skipConfigKeys = await withRetry(() =>
|
||||
this.client.keys(skipConfigPattern)
|
||||
);
|
||||
if (skipConfigKeys.length > 0) {
|
||||
await withRetry(() => this.client.del(skipConfigKeys));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
@@ -283,6 +292,46 @@ export class RedisStorage implements IStorage {
|
||||
this.client.set(this.adminConfigKey(), JSON.stringify(config))
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 跳过片头片尾配置 ----------
|
||||
private skipConfigKey(user: string, source: string, id: string) {
|
||||
return `u:${user}:skip:${source}+${id}`;
|
||||
}
|
||||
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null> {
|
||||
const val = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, source, id))
|
||||
);
|
||||
return val ? (JSON.parse(val) as SkipConfig) : null;
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(
|
||||
this.skipConfigKey(userName, source, id),
|
||||
JSON.stringify(config)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async deleteSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.del(this.skipConfigKey(userName, source, id))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 单例 Redis 客户端
|
||||
|
||||
@@ -64,6 +64,20 @@ export interface IStorage {
|
||||
// 管理员配置相关
|
||||
getAdminConfig(): Promise<AdminConfig | null>;
|
||||
setAdminConfig(config: AdminConfig): Promise<void>;
|
||||
|
||||
// 跳过片头片尾配置相关
|
||||
getSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null>;
|
||||
setSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void>;
|
||||
deleteSkipConfig(userName: string, source: string, id: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 搜索结果数据结构
|
||||
@@ -95,3 +109,10 @@ export interface DoubanResult {
|
||||
message: string;
|
||||
list: DoubanItem[];
|
||||
}
|
||||
|
||||
// 跳过片头片尾配置数据结构
|
||||
export interface SkipConfig {
|
||||
enable: boolean; // 是否启用跳过片头片尾
|
||||
intro_time: number; // 片头时间(秒)
|
||||
outro_time: number; // 片尾时间(秒)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Redis } from '@upstash/redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { Favorite, IStorage, PlayRecord } from './types';
|
||||
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -209,6 +209,15 @@ export class UpstashRedisStorage implements IStorage {
|
||||
if (favoriteKeys.length > 0) {
|
||||
await withRetry(() => this.client.del(...favoriteKeys));
|
||||
}
|
||||
|
||||
// 删除跳过片头片尾配置
|
||||
const skipConfigPattern = `u:${userName}:skip:*`;
|
||||
const skipConfigKeys = await withRetry(() =>
|
||||
this.client.keys(skipConfigPattern)
|
||||
);
|
||||
if (skipConfigKeys.length > 0) {
|
||||
await withRetry(() => this.client.del(...skipConfigKeys));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
@@ -267,6 +276,43 @@ export class UpstashRedisStorage implements IStorage {
|
||||
async setAdminConfig(config: AdminConfig): Promise<void> {
|
||||
await withRetry(() => this.client.set(this.adminConfigKey(), config));
|
||||
}
|
||||
|
||||
// ---------- 跳过片头片尾配置 ----------
|
||||
private skipConfigKey(user: string, source: string, id: string) {
|
||||
return `u:${user}:skip:${source}+${id}`;
|
||||
}
|
||||
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null> {
|
||||
const val = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, source, id))
|
||||
);
|
||||
return val ? (val as SkipConfig) : null;
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(this.skipConfigKey(userName, source, id), config)
|
||||
);
|
||||
}
|
||||
|
||||
async deleteSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.del(this.skipConfigKey(userName, source, id))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 单例 Upstash Redis 客户端
|
||||
|
||||
Reference in New Issue
Block a user