handle seeking in unbuffered tracks

Hunter
Tuesday, October 28th, 2025 at 2:41 PM

script.js+103-5

script.js
 57  
 58  audio.addEventListener('pause', () => {
 59  	stopProgressBar();
    -	// Only show "Paused" if the user actually paused (not during track transitions)
    -	if (!audio.ended) {
 60 +	// Only show "Paused" if the user actually paused (not during track transitions or seeking)
 61 +	if (!audio.ended && !isSeeking) {
 62  		const song = songs[currentSongIndex];
 63  		updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`);
 64  	}

233  }
234  
235  function updateProgressBar() {
    -	if (audio.duration && !isDragging) {
236 +	if (audio.duration && !isDragging && !isSeeking) {
237  		const currentTime = audio.currentTime;
238  		const duration = audio.duration;
239  		const progressPercentage = (currentTime / duration) * 100;

259  	return clickPercentage;
260  }
261  
262 +let isSeeking = false;
263 +let targetSeekTime = null;
264 +
265  function applySeek(clickPercentage) {
266  	if (!playerReady) return;
267  

274  		audio.addEventListener('loadedmetadata', function setInitialTime() {
275  			const duration = audio.duration;
276  			const seekTime = duration * clickPercentage;
    -			audio.currentTime = seekTime;
277 +			attemptSeekWithRetry(seekTime, clickPercentage);
278  			prePlaySeekTime = seekTime;
279  			audio.removeEventListener('loadedmetadata', setInitialTime);
280  		}, { once: true });
281  	} else if (audio.duration) {
282  		const duration = audio.duration;
283  		const seekTime = duration * clickPercentage;
    -		audio.currentTime = seekTime;
284 +		attemptSeekWithRetry(seekTime, clickPercentage);
285  		prePlaySeekTime = seekTime;
286  	}
287  }
288  
289 +function isTimeBuffered(time) {
290 +	// Check if the given time is within any buffered time range
291 +	for (let i = 0; i < audio.buffered.length; i++) {
292 +		if (time >= audio.buffered.start(i) && time <= audio.buffered.end(i)) {
293 +			return true;
294 +		}
295 +	}
296 +	return false;
297 +}
298 +
299 +function attemptSeekWithRetry(seekTime, targetPercentage) {
300 +	targetSeekTime = seekTime;
301 +	isSeeking = true;
302 +
303 +	// Lock the progress bar at the target position
304 +	progressBar.style.setProperty('--progress', targetPercentage * 100);
305 +
306 +	const wasPlaying = !audio.paused;
307 +
308 +	// Try to seek
309 +	audio.currentTime = seekTime;
310 +
311 +	// Handler to check if we reached the target after seeking completes
312 +	function checkSeekSuccess() {
313 +		// Allow small tolerance for floating point comparison
314 +		if (Math.abs(audio.currentTime - targetSeekTime) > 0.5) {
315 +			// Browser clamped to buffered range - need to wait for more data
316 +			// Now pause and show seeking status
317 +			if (wasPlaying) {
318 +				audio.pause();
319 +			}
320 +			const song = songs[currentSongIndex];
321 +			updateCurrentSongDisplay(`Seeking: ${song.artist} - ${song.title}`);
322 +			continueSeekingToTarget(wasPlaying);
323 +		} else {
324 +			// Successfully reached target immediately (was already buffered)
325 +			isSeeking = false;
326 +			targetSeekTime = null;
327 +			// No need to update display - the seek was instant and playback continues normally
328 +		}
329 +	}
330 +
331 +	audio.addEventListener('seeked', checkSeekSuccess, { once: true });
332 +}
333 +
334 +function continueSeekingToTarget(wasPlaying) {
335 +	const song = songs[currentSongIndex];
336 +
337 +	// Keep showing seeking status
338 +	updateCurrentSongDisplay(`Seeking: ${song.artist} - ${song.title}`);
339 +
340 +	// Handler for when more data loads
341 +	function retrySeek() {
342 +		if (!isSeeking || targetSeekTime === null) {
343 +			return; // Seeking was cancelled
344 +		}
345 +
346 +		audio.currentTime = targetSeekTime;
347 +
348 +		// Check again after this seek completes
349 +		function checkAgain() {
350 +			if (!isSeeking || targetSeekTime === null) {
351 +				return;
352 +			}
353 +
354 +			if (Math.abs(audio.currentTime - targetSeekTime) > 0.5) {
355 +				// Still not there, keep trying
356 +				continueSeekingToTarget(wasPlaying);
357 +			} else {
358 +				// Success!
359 +				isSeeking = false;
360 +				targetSeekTime = null;
361 +
362 +				if (wasPlaying) {
363 +					audio.play();
364 +				} else {
365 +					updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`);
366 +				}
367 +			}
368 +		}
369 +
370 +		audio.addEventListener('seeked', checkAgain, { once: true });
371 +	}
372 +
373 +	// Wait for more data to load, then try again
374 +	audio.addEventListener('progress', retrySeek, { once: true });
375 +
376 +	// Also set a timeout fallback in case progress doesn't fire
377 +	setTimeout(() => {
378 +		if (isSeeking && targetSeekTime !== null && Math.abs(audio.currentTime - targetSeekTime) > 0.5) {
379 +			retrySeek();
380 +		}
381 +	}, 1000);
382 +}
383 +
384  function onProgressMouseDown(event) {
385  	if (!playerReady) return;
386  	isDragging = true;