添加跳过配置功能,包括数据库和API支持,更新播放器以处理跳过片段
This commit is contained in:
+101
-1
@@ -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 { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -473,4 +473,104 @@ export class D1Storage implements IStorage {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 跳过配置相关
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
key: string
|
||||
): Promise<EpisodeSkipConfig | null> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
const result = await db
|
||||
.prepare('SELECT * FROM skip_configs WHERE username = ? AND key = ?')
|
||||
.bind(userName, key)
|
||||
.first<any>();
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
return {
|
||||
source: result.source,
|
||||
id: result.video_id,
|
||||
title: result.title,
|
||||
segments: JSON.parse(result.segments),
|
||||
updated_time: result.updated_time,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to get skip config:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
userName: string,
|
||||
key: string,
|
||||
config: EpisodeSkipConfig
|
||||
): Promise<void> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
await db
|
||||
.prepare(
|
||||
`
|
||||
INSERT OR REPLACE INTO skip_configs
|
||||
(username, key, source, video_id, title, segments, updated_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
)
|
||||
.bind(
|
||||
userName,
|
||||
key,
|
||||
config.source,
|
||||
config.id,
|
||||
config.title,
|
||||
JSON.stringify(config.segments),
|
||||
config.updated_time
|
||||
)
|
||||
.run();
|
||||
} catch (err) {
|
||||
console.error('Failed to set skip config:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getAllSkipConfigs(
|
||||
userName: string
|
||||
): Promise<{ [key: string]: EpisodeSkipConfig }> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
const result = await db
|
||||
.prepare('SELECT * FROM skip_configs WHERE username = ?')
|
||||
.bind(userName)
|
||||
.all<any>();
|
||||
|
||||
const configs: { [key: string]: EpisodeSkipConfig } = {};
|
||||
|
||||
for (const row of result.results) {
|
||||
configs[row.key] = {
|
||||
source: row.source,
|
||||
id: row.video_id,
|
||||
title: row.title,
|
||||
segments: JSON.parse(row.segments),
|
||||
updated_time: row.updated_time,
|
||||
};
|
||||
}
|
||||
|
||||
return configs;
|
||||
} catch (err) {
|
||||
console.error('Failed to get all skip configs:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSkipConfig(userName: string, key: string): Promise<void> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
await db
|
||||
.prepare('DELETE FROM skip_configs WHERE username = ? AND key = ?')
|
||||
.bind(userName, key)
|
||||
.run();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete skip config:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,22 @@ export interface Favorite {
|
||||
search_title?: string;
|
||||
}
|
||||
|
||||
// ---- 片头片尾跳过配置类型 ----
|
||||
export interface SkipSegment {
|
||||
start: number; // 开始时间(秒)
|
||||
end: number; // 结束时间(秒)
|
||||
type: 'opening' | 'ending'; // 片头或片尾
|
||||
title?: string; // 可选的描述
|
||||
}
|
||||
|
||||
export interface EpisodeSkipConfig {
|
||||
source: string; // 资源站标识
|
||||
id: string; // 剧集ID
|
||||
title: string; // 剧集标题
|
||||
segments: SkipSegment[]; // 跳过片段列表
|
||||
updated_time: number; // 最后更新时间
|
||||
}
|
||||
|
||||
// ---- 缓存数据结构 ----
|
||||
interface CacheData<T> {
|
||||
data: T;
|
||||
@@ -52,6 +68,7 @@ interface UserCacheStore {
|
||||
playRecords?: CacheData<Record<string, PlayRecord>>;
|
||||
favorites?: CacheData<Record<string, Favorite>>;
|
||||
searchHistory?: CacheData<string[]>;
|
||||
skipConfigs?: CacheData<Record<string, EpisodeSkipConfig>>;
|
||||
}
|
||||
|
||||
// ---- 常量 ----
|
||||
@@ -59,6 +76,7 @@ interface UserCacheStore {
|
||||
const PLAY_RECORDS_KEY = 'katelyatv_play_records';
|
||||
const FAVORITES_KEY = 'katelyatv_favorites';
|
||||
const SEARCH_HISTORY_KEY = 'katelyatv_search_history';
|
||||
const SKIP_CONFIGS_KEY = 'katelyatv_skip_configs';
|
||||
const LEGACY_PLAY_RECORDS_KEY = 'moontv_play_records';
|
||||
const LEGACY_FAVORITES_KEY = 'moontv_favorites';
|
||||
const LEGACY_SEARCH_HISTORY_KEY = 'moontv_search_history';
|
||||
@@ -253,6 +271,35 @@ class HybridCacheManager {
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的跳过配置
|
||||
*/
|
||||
getCachedSkipConfigs(): Record<string, EpisodeSkipConfig> | 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, EpisodeSkipConfig>): void {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
userCache.skipConfigs = this.createCacheData(data);
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定用户的所有缓存
|
||||
*/
|
||||
@@ -1255,3 +1302,244 @@ export async function preloadUserData(): Promise<void> {
|
||||
console.warn('预加载用户数据失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- 片头片尾跳过配置管理 ----------------
|
||||
|
||||
/**
|
||||
* 生成跳过配置的存储 key
|
||||
*/
|
||||
export function generateSkipConfigKey(source: string, id: string): string {
|
||||
return `${source}_${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个跳过配置
|
||||
*/
|
||||
export async function getSkipConfig(
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<EpisodeSkipConfig | null> {
|
||||
try {
|
||||
const key = generateSkipConfigKey(source, id);
|
||||
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
// localStorage 模式
|
||||
const allConfigs = JSON.parse(
|
||||
localStorage.getItem(SKIP_CONFIGS_KEY) || '{}'
|
||||
);
|
||||
return allConfigs[key] || null;
|
||||
} else {
|
||||
// 数据库模式:先查缓存
|
||||
const cacheManager = HybridCacheManager.getInstance();
|
||||
const cachedConfigs = cacheManager.getCachedSkipConfigs();
|
||||
|
||||
if (cachedConfigs && cachedConfigs[key]) {
|
||||
return cachedConfigs[key];
|
||||
}
|
||||
|
||||
// 缓存未命中,从服务器获取
|
||||
const authInfo = getAuthInfoFromBrowserCookie();
|
||||
if (!authInfo?.username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/skip-configs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'get',
|
||||
key,
|
||||
username: authInfo.username,
|
||||
signature: authInfo.signature,
|
||||
timestamp: authInfo.timestamp,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const config = data.config;
|
||||
|
||||
// 更新缓存
|
||||
if (config) {
|
||||
const allConfigs = cachedConfigs || {};
|
||||
allConfigs[key] = config;
|
||||
cacheManager.cacheSkipConfigs(allConfigs);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取跳过配置失败:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存跳过配置
|
||||
*/
|
||||
export async function saveSkipConfig(
|
||||
source: string,
|
||||
id: string,
|
||||
config: EpisodeSkipConfig
|
||||
): Promise<void> {
|
||||
try {
|
||||
const key = generateSkipConfigKey(source, id);
|
||||
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
// localStorage 模式
|
||||
const allConfigs = JSON.parse(
|
||||
localStorage.getItem(SKIP_CONFIGS_KEY) || '{}'
|
||||
);
|
||||
allConfigs[key] = config;
|
||||
localStorage.setItem(SKIP_CONFIGS_KEY, JSON.stringify(allConfigs));
|
||||
} else {
|
||||
// 数据库模式
|
||||
const authInfo = getAuthInfoFromBrowserCookie();
|
||||
if (!authInfo?.username) {
|
||||
throw new Error('用户未登录');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/skip-configs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'set',
|
||||
key,
|
||||
config,
|
||||
username: authInfo.username,
|
||||
signature: authInfo.signature,
|
||||
timestamp: authInfo.timestamp,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('保存跳过配置失败');
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
const cacheManager = HybridCacheManager.getInstance();
|
||||
const cachedConfigs = cacheManager.getCachedSkipConfigs() || {};
|
||||
cachedConfigs[key] = config;
|
||||
cacheManager.cacheSkipConfigs(cachedConfigs);
|
||||
}
|
||||
|
||||
console.log('跳过配置已保存:', key);
|
||||
} catch (err) {
|
||||
console.error('保存跳过配置失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有跳过配置
|
||||
*/
|
||||
export async function getAllSkipConfigs(): Promise<Record<string, EpisodeSkipConfig>> {
|
||||
try {
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
// localStorage 模式
|
||||
return JSON.parse(localStorage.getItem(SKIP_CONFIGS_KEY) || '{}');
|
||||
} else {
|
||||
// 数据库模式:先查缓存
|
||||
const cacheManager = HybridCacheManager.getInstance();
|
||||
const cachedConfigs = cacheManager.getCachedSkipConfigs();
|
||||
|
||||
if (cachedConfigs) {
|
||||
return cachedConfigs;
|
||||
}
|
||||
|
||||
// 缓存未命中,从服务器获取
|
||||
const authInfo = getAuthInfoFromBrowserCookie();
|
||||
if (!authInfo?.username) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const response = await fetch('/api/skip-configs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'getAll',
|
||||
username: authInfo.username,
|
||||
signature: authInfo.signature,
|
||||
timestamp: authInfo.timestamp,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const configs = data.configs || {};
|
||||
|
||||
// 更新缓存
|
||||
cacheManager.cacheSkipConfigs(configs);
|
||||
|
||||
return configs;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取所有跳过配置失败:', err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除跳过配置
|
||||
*/
|
||||
export async function deleteSkipConfig(source: string, id: string): Promise<void> {
|
||||
try {
|
||||
const key = generateSkipConfigKey(source, id);
|
||||
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
// localStorage 模式
|
||||
const allConfigs = JSON.parse(
|
||||
localStorage.getItem(SKIP_CONFIGS_KEY) || '{}'
|
||||
);
|
||||
delete allConfigs[key];
|
||||
localStorage.setItem(SKIP_CONFIGS_KEY, JSON.stringify(allConfigs));
|
||||
} else {
|
||||
// 数据库模式
|
||||
const authInfo = getAuthInfoFromBrowserCookie();
|
||||
if (!authInfo?.username) {
|
||||
throw new Error('用户未登录');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/skip-configs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete',
|
||||
key,
|
||||
username: authInfo.username,
|
||||
signature: authInfo.signature,
|
||||
timestamp: authInfo.timestamp,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('删除跳过配置失败');
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
const cacheManager = HybridCacheManager.getInstance();
|
||||
const cachedConfigs = cacheManager.getCachedSkipConfigs() || {};
|
||||
delete cachedConfigs[key];
|
||||
cacheManager.cacheSkipConfigs(cachedConfigs);
|
||||
}
|
||||
|
||||
console.log('跳过配置已删除:', key);
|
||||
} catch (err) {
|
||||
console.error('删除跳过配置失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
+66
-1
@@ -3,7 +3,7 @@
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { Favorite, IStorage, PlayRecord } from './types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -283,6 +283,71 @@ export class RedisStorage implements IStorage {
|
||||
this.client.set(this.adminConfigKey(), JSON.stringify(config))
|
||||
);
|
||||
}
|
||||
|
||||
// 跳过配置相关
|
||||
private skipConfigKey(userName: string, key: string): string {
|
||||
return `katelyatv:skip_config:${userName}:${key}`;
|
||||
}
|
||||
|
||||
private skipConfigsKey(userName: string): string {
|
||||
return `katelyatv:skip_configs:${userName}`;
|
||||
}
|
||||
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
key: string
|
||||
): Promise<EpisodeSkipConfig | null> {
|
||||
const data = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, key))
|
||||
);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
userName: string,
|
||||
key: string,
|
||||
config: EpisodeSkipConfig
|
||||
): Promise<void> {
|
||||
await withRetry(async () => {
|
||||
// 保存到独立的key
|
||||
await this.client.set(
|
||||
this.skipConfigKey(userName, key),
|
||||
JSON.stringify(config)
|
||||
);
|
||||
// 同时加入到用户的跳过配置集合中
|
||||
await this.client.sAdd(this.skipConfigsKey(userName), key);
|
||||
});
|
||||
}
|
||||
|
||||
async getAllSkipConfigs(
|
||||
userName: string
|
||||
): Promise<{ [key: string]: EpisodeSkipConfig }> {
|
||||
const keys = await withRetry(() =>
|
||||
this.client.sMembers(this.skipConfigsKey(userName))
|
||||
);
|
||||
|
||||
const configs: { [key: string]: EpisodeSkipConfig } = {};
|
||||
|
||||
for (const key of keys) {
|
||||
const data = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, key))
|
||||
);
|
||||
if (data) {
|
||||
configs[key] = JSON.parse(data);
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
async deleteSkipConfig(userName: string, key: string): Promise<void> {
|
||||
await withRetry(async () => {
|
||||
// 删除独立的key
|
||||
await this.client.del(this.skipConfigKey(userName, key));
|
||||
// 从用户的跳过配置集合中移除
|
||||
await this.client.sRem(this.skipConfigsKey(userName), key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 单例 Redis 客户端
|
||||
|
||||
@@ -14,6 +14,23 @@ export interface PlayRecord {
|
||||
search_title: string; // 搜索时使用的标题
|
||||
}
|
||||
|
||||
// 片头片尾数据结构
|
||||
export interface SkipSegment {
|
||||
start: number; // 开始时间(秒)
|
||||
end: number; // 结束时间(秒)
|
||||
type: 'opening' | 'ending'; // 片头或片尾
|
||||
title?: string; // 可选的描述
|
||||
}
|
||||
|
||||
// 剧集跳过配置
|
||||
export interface EpisodeSkipConfig {
|
||||
source: string; // 资源站标识
|
||||
id: string; // 剧集ID
|
||||
title: string; // 剧集标题
|
||||
segments: SkipSegment[]; // 跳过片段列表
|
||||
updated_time: number; // 最后更新时间
|
||||
}
|
||||
|
||||
// 收藏数据结构
|
||||
export interface Favorite {
|
||||
source_name: string;
|
||||
@@ -58,6 +75,12 @@ export interface IStorage {
|
||||
addSearchHistory(userName: string, keyword: string): Promise<void>;
|
||||
deleteSearchHistory(userName: string, keyword?: string): Promise<void>;
|
||||
|
||||
// 片头片尾跳过配置相关
|
||||
getSkipConfig(userName: string, key: string): Promise<EpisodeSkipConfig | null>;
|
||||
setSkipConfig(userName: string, key: string, config: EpisodeSkipConfig): Promise<void>;
|
||||
getAllSkipConfigs(userName: string): Promise<{ [key: string]: EpisodeSkipConfig }>;
|
||||
deleteSkipConfig(userName: string, key: string): Promise<void>;
|
||||
|
||||
// 用户列表
|
||||
getAllUsers(): Promise<string[]>;
|
||||
|
||||
|
||||
+63
-1
@@ -3,7 +3,7 @@
|
||||
import { Redis } from '@upstash/redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { Favorite, IStorage, PlayRecord } from './types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -267,6 +267,68 @@ export class UpstashRedisStorage implements IStorage {
|
||||
async setAdminConfig(config: AdminConfig): Promise<void> {
|
||||
await withRetry(() => this.client.set(this.adminConfigKey(), config));
|
||||
}
|
||||
|
||||
// 跳过配置相关
|
||||
private skipConfigKey(userName: string, key: string): string {
|
||||
return `katelyatv:skip_config:${userName}:${key}`;
|
||||
}
|
||||
|
||||
private skipConfigsKey(userName: string): string {
|
||||
return `katelyatv:skip_configs:${userName}`;
|
||||
}
|
||||
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
key: string
|
||||
): Promise<EpisodeSkipConfig | null> {
|
||||
const data = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, key))
|
||||
);
|
||||
return data ? (data as EpisodeSkipConfig) : null;
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
userName: string,
|
||||
key: string,
|
||||
config: EpisodeSkipConfig
|
||||
): Promise<void> {
|
||||
await withRetry(async () => {
|
||||
// 保存到独立的key
|
||||
await this.client.set(this.skipConfigKey(userName, key), config);
|
||||
// 同时加入到用户的跳过配置集合中
|
||||
await this.client.sadd(this.skipConfigsKey(userName), key);
|
||||
});
|
||||
}
|
||||
|
||||
async getAllSkipConfigs(
|
||||
userName: string
|
||||
): Promise<{ [key: string]: EpisodeSkipConfig }> {
|
||||
const keys = await withRetry(() =>
|
||||
this.client.smembers(this.skipConfigsKey(userName))
|
||||
);
|
||||
|
||||
const configs: { [key: string]: EpisodeSkipConfig } = {};
|
||||
|
||||
for (const key of ensureStringArray(keys || [])) {
|
||||
const data = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, key))
|
||||
);
|
||||
if (data) {
|
||||
configs[key] = data as EpisodeSkipConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
async deleteSkipConfig(userName: string, key: string): Promise<void> {
|
||||
await withRetry(async () => {
|
||||
// 删除独立的key
|
||||
await this.client.del(this.skipConfigKey(userName, key));
|
||||
// 从用户的跳过配置集合中移除
|
||||
await this.client.srem(this.skipConfigsKey(userName), key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 单例 Upstash Redis 客户端
|
||||
|
||||
Reference in New Issue
Block a user