commit 05cb63fecd2c487909e4535e30aa849406b49a71
parent 96ad20dc64f168cc4ca2b0d052fd48023caf4959
Author: Hunter
Date: Tue, 28 Oct 2025 15:18:08 -0400
preload resources/tracks to improve responsiveness
Diffstat:
| M | host.py | | | 10 | +++++++++- |
| M | script.js | | | 270 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
2 files changed, 277 insertions(+), 3 deletions(-)
diff --git a/host.py b/host.py
@@ -57,11 +57,19 @@ def main():
# Create server
Handler = http.server.SimpleHTTPRequestHandler
- # Suppress default logging
+ # Suppress default logging and broken pipe errors
class QuietHandler(Handler):
def log_message(self, format, *args):
pass
+ def handle(self):
+ """Handle requests and suppress broken pipe errors"""
+ try:
+ super().handle()
+ except (BrokenPipeError, ConnectionResetError):
+ # Browser cancelled the request (normal for media streaming/preloading)
+ pass
+
try:
with socketserver.TCPServer(("", port), QuietHandler) as httpd:
local_url = f"http://localhost:{port}"
diff --git a/script.js b/script.js
@@ -14,6 +14,10 @@ let playerReady = false;
let songs = [];
let animationFrameId = null;
let prePlaySeekTime = 0;
+let preloadedAudio = {}; // Cache for preloaded audio elements
+let currentPreloadIndex = 0;
+let priorityPreloadQueue = []; // Songs requested by user that need priority preloading
+let isPreloadingPriority = false;
// Load tracks from tracks.json
fetch('tracks.json')
@@ -29,6 +33,10 @@ fetch('tracks.json')
playerReady = true;
updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} - ${songs[0].title}`);
renderPlaylist();
+ // Pre-cache resources first, then songs
+ preloadResources().then(() => {
+ startPreloadingSongs();
+ });
} else {
updateCurrentSongDisplay('No tracks found');
}
@@ -162,7 +170,16 @@ function playSong(index) {
currentSongIndex = index;
const song = songs[currentSongIndex];
- audio.src = `tracks/${song.filename}`;
+
+ // Use preloaded blob if available, otherwise load from server
+ if (preloadedAudio[song.filename]) {
+ audio.src = preloadedAudio[song.filename].blobUrl;
+ } else {
+ audio.src = `tracks/${song.filename}`;
+ // Request priority preloading for this song
+ requestPriorityPreload(song.filename);
+ }
+
audio.play();
isPlaying = true;
updatePlayPauseButton();
@@ -268,7 +285,13 @@ function applySeek(clickPercentage) {
// If audio hasn't been loaded yet, load it but don't play
if (!audio.src || audio.src === '') {
const song = songs[currentSongIndex];
- audio.src = `tracks/${song.filename}`;
+
+ // Use preloaded blob if available, otherwise load from server
+ if (preloadedAudio[song.filename]) {
+ audio.src = preloadedAudio[song.filename].blobUrl;
+ } else {
+ audio.src = `tracks/${song.filename}`;
+ }
// Wait for metadata to be loaded before seeking
audio.addEventListener('loadedmetadata', function setInitialTime() {
@@ -472,3 +495,246 @@ navigator.mediaSession.setActionHandler('nexttrack', () => {
nextSong();
}
});
+
+// Pre-caching system
+function preloadResources() {
+ console.log('Preloading UI resources...');
+
+ const resources = [
+ 'resources/play.png',
+ 'resources/pause.png',
+ 'resources/prev.png',
+ 'resources/next.png'
+ ];
+
+ const imagePromises = resources.map(src => {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ console.log(`Preloaded: ${src}`);
+ resolve();
+ };
+ img.onerror = () => {
+ console.error(`Failed to preload: ${src}`);
+ resolve(); // Resolve anyway to not block other resources
+ };
+ img.src = src;
+ });
+ });
+
+ return Promise.all(imagePromises).then(() => {
+ console.log('All UI resources preloaded');
+ });
+}
+
+function startPreloadingSongs() {
+ // Start with the first song
+ currentPreloadIndex = 0;
+ preloadNextSong();
+}
+
+function preloadNextSong() {
+ if (currentPreloadIndex >= songs.length) {
+ console.log('All songs preloaded');
+ return;
+ }
+
+ const song = songs[currentPreloadIndex];
+ const filename = song.filename;
+
+ // Skip if already preloaded
+ if (preloadedAudio[filename]) {
+ currentPreloadIndex++;
+ preloadNextSong();
+ return;
+ }
+
+ console.log(`Preloading: ${song.artist} - ${song.title}`);
+
+ // Use fetch to force full download of the entire file
+ fetch(`tracks/${filename}`)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ // Get total file size for progress tracking
+ const contentLength = response.headers.get('content-length');
+ console.log(`Downloading ${song.title} (${(contentLength / 1024 / 1024).toFixed(2)} MB)...`);
+
+ // Read the entire response as a blob
+ return response.blob();
+ })
+ .then(blob => {
+ // Create a blob URL that will persist in memory
+ const blobUrl = URL.createObjectURL(blob);
+
+ // Create audio element with the fully downloaded blob
+ const preloadAudio = new Audio();
+ preloadAudio.preload = 'auto';
+ preloadAudio.src = blobUrl;
+
+ // Wait for the audio element to fully buffer the blob
+ return new Promise((resolve) => {
+ const checkBuffered = () => {
+ // Check if the entire duration is buffered
+ if (preloadAudio.duration > 0 && preloadAudio.buffered.length > 0) {
+ const bufferedEnd = preloadAudio.buffered.end(preloadAudio.buffered.length - 1);
+ if (bufferedEnd >= preloadAudio.duration - 0.1) {
+ // Fully buffered!
+ resolve({ preloadAudio, blobUrl, blob });
+ return;
+ }
+ }
+ // Not fully buffered yet, check again soon
+ setTimeout(checkBuffered, 100);
+ };
+
+ // Start checking once metadata is loaded
+ preloadAudio.addEventListener('loadedmetadata', () => {
+ checkBuffered();
+ }, { once: true });
+
+ // Trigger the loading
+ preloadAudio.load();
+ });
+ })
+ .then(({ preloadAudio, blobUrl, blob }) => {
+ // Store both the audio element and blob URL
+ preloadedAudio[filename] = {
+ audio: preloadAudio,
+ blobUrl: blobUrl,
+ blob: blob
+ };
+
+ console.log(`✓ Fully preloaded: ${song.artist} - ${song.title}`);
+
+ // Move to next song
+ currentPreloadIndex++;
+ setTimeout(() => preloadNextSong(), 100);
+ })
+ .catch(error => {
+ console.error(`Failed to preload ${filename}:`, error);
+ currentPreloadIndex++;
+ preloadNextSong();
+ });
+}
+
+// Priority preloading system
+function requestPriorityPreload(filename) {
+ // Skip if already preloaded or already in priority queue
+ if (preloadedAudio[filename] || priorityPreloadQueue.includes(filename)) {
+ return;
+ }
+
+ console.log(`🔥 Priority preload requested: ${filename}`);
+ priorityPreloadQueue.push(filename);
+
+ // Start priority preloading if not already running
+ if (!isPreloadingPriority) {
+ processPriorityPreload();
+ }
+}
+
+function processPriorityPreload() {
+ if (priorityPreloadQueue.length === 0) {
+ isPreloadingPriority = false;
+ return;
+ }
+
+ isPreloadingPriority = true;
+ const filename = priorityPreloadQueue.shift();
+
+ // Check if already preloaded (might have finished during normal preloading)
+ if (preloadedAudio[filename]) {
+ processPriorityPreload();
+ return;
+ }
+
+ // Find the song info
+ const song = songs.find(s => s.filename === filename);
+ if (!song) {
+ processPriorityPreload();
+ return;
+ }
+
+ console.log(`🔥 Priority preloading: ${song.artist} - ${song.title}`);
+
+ // Use fetch to force full download of the entire file
+ fetch(`tracks/${filename}`)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.blob();
+ })
+ .then(blob => {
+ // Create a blob URL that will persist in memory
+ const blobUrl = URL.createObjectURL(blob);
+
+ // Create audio element with the fully downloaded blob
+ const preloadAudio = new Audio();
+ preloadAudio.preload = 'auto';
+ preloadAudio.src = blobUrl;
+
+ // Wait for the audio element to fully buffer the blob
+ return new Promise((resolve) => {
+ const checkBuffered = () => {
+ // Check if the entire duration is buffered
+ if (preloadAudio.duration > 0 && preloadAudio.buffered.length > 0) {
+ const bufferedEnd = preloadAudio.buffered.end(preloadAudio.buffered.length - 1);
+ if (bufferedEnd >= preloadAudio.duration - 0.1) {
+ // Fully buffered!
+ resolve({ preloadAudio, blobUrl, blob });
+ return;
+ }
+ }
+ // Not fully buffered yet, check again soon
+ setTimeout(checkBuffered, 100);
+ };
+
+ // Start checking once metadata is loaded
+ preloadAudio.addEventListener('loadedmetadata', () => {
+ checkBuffered();
+ }, { once: true });
+
+ // Trigger the loading
+ preloadAudio.load();
+ });
+ })
+ .then(({ preloadAudio, blobUrl, blob }) => {
+ // Store both the audio element and blob URL
+ preloadedAudio[filename] = {
+ audio: preloadAudio,
+ blobUrl: blobUrl,
+ blob: blob
+ };
+
+ // Switch to the preloaded version if this is the current song
+ if (songs[currentSongIndex].filename === filename && audio.src !== blobUrl) {
+ const currentTime = audio.currentTime;
+ const wasPlaying = !audio.paused;
+ audio.src = blobUrl;
+ audio.currentTime = currentTime;
+ if (wasPlaying) {
+ audio.play().catch(err => {
+ // Ignore play interruption errors (expected during source switching)
+ if (err.name !== 'AbortError') {
+ console.error('Error resuming playback:', err);
+ }
+ });
+ }
+ }
+
+ // Process next priority request
+ processPriorityPreload();
+ })
+ .catch(error => {
+ // Ignore abort errors (happens when normal preload finishes first)
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ // This is expected - normal preloading probably finished first
+ } else {
+ console.error(`Failed to priority preload ${filename}:`, error);
+ }
+ processPriorityPreload();
+ });
+}