Fix: Audio player skip behavior and range requests

This commit is contained in:
Hördle Bot
2025-11-26 10:58:04 +01:00
parent b66bab48bd
commit ec9a86ef4d
2 changed files with 119 additions and 39 deletions

View File

@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { readFile, stat } from 'fs/promises';
import { stat } from 'fs/promises';
import { createReadStream } from 'fs';
import path from 'path';
export async function GET(
@@ -30,24 +31,59 @@ export async function GET(
return new NextResponse('Forbidden', { status: 403 });
}
// Check if file exists
try {
await stat(filePath);
} catch {
return new NextResponse('File not found', { status: 404 });
const stats = await stat(filePath);
const fileSize = stats.size;
const range = request.headers.get('range');
if (range) {
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunksize = (end - start) + 1;
const stream = createReadStream(filePath, { start, end });
// Convert Node stream to Web stream
const readable = new ReadableStream({
start(controller) {
stream.on('data', (chunk: any) => controller.enqueue(chunk));
stream.on('end', () => controller.close());
stream.on('error', (err: any) => controller.error(err));
}
});
// Read file
const fileBuffer = await readFile(filePath);
// Return with proper headers
return new NextResponse(fileBuffer, {
return new NextResponse(readable, {
status: 206,
headers: {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize.toString(),
'Content-Type': 'audio/mpeg',
'Cache-Control': 'public, max-age=3600, must-revalidate',
},
});
} else {
const stream = createReadStream(filePath);
// Convert Node stream to Web stream
const readable = new ReadableStream({
start(controller) {
stream.on('data', (chunk: any) => controller.enqueue(chunk));
stream.on('end', () => controller.close());
stream.on('error', (err: any) => controller.error(err));
}
});
return new NextResponse(readable, {
status: 200,
headers: {
'Content-Length': fileSize.toString(),
'Content-Type': 'audio/mpeg',
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=3600, must-revalidate',
},
});
}
} catch (error) {
console.error('Error serving audio file:', error);
return new NextResponse('Internal Server Error', { status: 500 });

View File

@@ -22,17 +22,57 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
const [progress, setProgress] = useState(0);
const [hasPlayedOnce, setHasPlayedOnce] = useState(false);
const [processedSrc, setProcessedSrc] = useState(src);
const [processedUnlockedSeconds, setProcessedUnlockedSeconds] = useState(unlockedSeconds);
useEffect(() => {
console.log('[AudioPlayer] MOUNTED');
return () => console.log('[AudioPlayer] UNMOUNTED');
}, []);
useEffect(() => {
if (audioRef.current) {
// Check if props changed compared to what we last processed
const hasChanged = src !== processedSrc || unlockedSeconds !== processedUnlockedSeconds;
if (hasChanged) {
audioRef.current.pause();
audioRef.current.currentTime = startTime;
let startPos = startTime;
// If same song but more time unlocked, start from where previous segment ended
if (src === processedSrc && unlockedSeconds > processedUnlockedSeconds) {
startPos = startTime + processedUnlockedSeconds;
}
const targetPos = startPos;
audioRef.current.currentTime = targetPos;
// Ensure position is set correctly even if browser resets it
setTimeout(() => {
if (audioRef.current && Math.abs(audioRef.current.currentTime - targetPos) > 0.5) {
audioRef.current.currentTime = targetPos;
}
}, 50);
setIsPlaying(false);
setProgress(0);
// Calculate initial progress
const initialElapsed = startPos - startTime;
const initialPercent = unlockedSeconds > 0 ? (initialElapsed / unlockedSeconds) * 100 : 0;
setProgress(Math.min(initialPercent, 100));
setHasPlayedOnce(false); // Reset for new segment
onHasPlayedChange?.(false); // Notify parent
// Update processed state
setProcessedSrc(src);
setProcessedUnlockedSeconds(unlockedSeconds);
if (autoPlay) {
const playPromise = audioRef.current.play();
// Delay play slightly to ensure currentTime sticks
setTimeout(() => {
const playPromise = audioRef.current?.play();
if (playPromise !== undefined) {
playPromise
.then(() => {
@@ -46,9 +86,11 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
setIsPlaying(false);
});
}
}, 150);
}
}
}, [src, unlockedSeconds, startTime, autoPlay]);
}
}, [src, unlockedSeconds, startTime, autoPlay, processedSrc, processedUnlockedSeconds]);
// Expose play method to parent component
useImperativeHandle(ref, () => ({
@@ -148,4 +190,6 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
AudioPlayer.displayName = 'AudioPlayer';
export default AudioPlayer;