添加跳过配置功能,包括数据库和API支持,更新播放器以处理跳过片段
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { EpisodeSkipConfig } from '@/lib/types';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { action, key, config, username } = body;
|
||||
|
||||
// 验证请求参数
|
||||
if (!action) {
|
||||
return NextResponse.json({ error: '缺少操作类型' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取认证信息
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
|
||||
// 如果是直接传入的认证信息(客户端模式),使用传入的信息
|
||||
const finalUsername = username || authInfo?.username;
|
||||
|
||||
if (!finalUsername) {
|
||||
return NextResponse.json({ error: '用户未登录' }, { status: 401 });
|
||||
}
|
||||
|
||||
// 创建存储实例
|
||||
const storage = getStorage();
|
||||
|
||||
switch (action) {
|
||||
case 'get': {
|
||||
if (!key) {
|
||||
return NextResponse.json({ error: '缺少配置键' }, { status: 400 });
|
||||
}
|
||||
|
||||
const skipConfig = await storage.getSkipConfig(finalUsername, key);
|
||||
return NextResponse.json({ config: skipConfig });
|
||||
}
|
||||
|
||||
case 'set': {
|
||||
if (!key || !config) {
|
||||
return NextResponse.json({ error: '缺少配置键或配置数据' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 验证配置数据结构
|
||||
if (!config.source || !config.id || !config.title || !Array.isArray(config.segments)) {
|
||||
return NextResponse.json({ error: '配置数据格式错误' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 验证片段数据
|
||||
for (const segment of config.segments) {
|
||||
if (
|
||||
typeof segment.start !== 'number' ||
|
||||
typeof segment.end !== 'number' ||
|
||||
segment.start >= segment.end ||
|
||||
!['opening', 'ending'].includes(segment.type)
|
||||
) {
|
||||
return NextResponse.json({ error: '片段数据格式错误' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
await storage.setSkipConfig(finalUsername, key, config as EpisodeSkipConfig);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
case 'getAll': {
|
||||
const allConfigs = await storage.getAllSkipConfigs(finalUsername);
|
||||
return NextResponse.json({ configs: allConfigs });
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
if (!key) {
|
||||
return NextResponse.json({ error: '缺少配置键' }, { status: 400 });
|
||||
}
|
||||
|
||||
await storage.deleteSkipConfig(finalUsername, key);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: '不支持的操作类型' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('跳过配置 API 错误:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '服务器内部错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||
|
||||
import EpisodeSelector from '@/components/EpisodeSelector';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import SkipController from '@/components/SkipController';
|
||||
|
||||
// 扩展 HTMLVideoElement 类型以支持 hls 属性
|
||||
declare global {
|
||||
@@ -163,6 +164,10 @@ function PlayPageClient() {
|
||||
const saveIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastSaveTimeRef = useRef<number>(0);
|
||||
|
||||
// 播放器时间状态(用于跳过功能)
|
||||
const [currentPlayTime, setCurrentPlayTime] = useState<number>(0);
|
||||
const [videoDuration, setVideoDuration] = useState<number>(0);
|
||||
|
||||
const artPlayerRef = useRef<any>(null);
|
||||
const artRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -1200,12 +1205,27 @@ function PlayPageClient() {
|
||||
// 监听播放器事件
|
||||
artPlayerRef.current.on('ready', () => {
|
||||
setError(null);
|
||||
// 更新视频时长
|
||||
const duration = artPlayerRef.current.duration || 0;
|
||||
setVideoDuration(duration);
|
||||
});
|
||||
|
||||
artPlayerRef.current.on('video:volumechange', () => {
|
||||
lastVolumeRef.current = artPlayerRef.current.volume;
|
||||
});
|
||||
|
||||
// 监听播放时间更新(用于跳过功能)
|
||||
artPlayerRef.current.on('video:timeupdate', () => {
|
||||
const currentTime = artPlayerRef.current.currentTime || 0;
|
||||
setCurrentPlayTime(currentTime);
|
||||
|
||||
// 同时更新时长(防止ready事件中获取不到)
|
||||
const duration = artPlayerRef.current.duration || 0;
|
||||
if (duration > 0 && videoDuration !== duration) {
|
||||
setVideoDuration(duration);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听视频可播放事件,这时恢复播放进度更可靠
|
||||
artPlayerRef.current.on('video:canplay', () => {
|
||||
// 若存在需要恢复的播放进度,则跳转
|
||||
@@ -1531,6 +1551,18 @@ function PlayPageClient() {
|
||||
className='bg-black w-full h-full rounded-xl overflow-hidden shadow-lg'
|
||||
></div>
|
||||
|
||||
{/* 跳过片头片尾控制器 */}
|
||||
{currentSource && currentId && videoTitle && (
|
||||
<SkipController
|
||||
source={currentSource}
|
||||
id={currentId}
|
||||
title={videoTitle}
|
||||
artPlayerRef={artPlayerRef}
|
||||
currentTime={currentPlayTime}
|
||||
_duration={videoDuration}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 换源加载蒙层 */}
|
||||
{isVideoLoading && (
|
||||
<div className='absolute inset-0 bg-black/85 backdrop-blur-sm rounded-xl flex items-center justify-center z-[500] transition-all duration-300'>
|
||||
|
||||
Reference in New Issue
Block a user