preload resources/tracks to improve responsiveness
Hunter
Tuesday, October 28th, 2025 at 3:18 PM
57 # Create server
58 Handler = http.server.SimpleHTTPRequestHandler
59
- # Suppress default logging
60 + # Suppress default logging and broken pipe errors
61 class QuietHandler(Handler):
62 def log_message(self, format, *args):
63 pass
64
65 + def handle(self):
66 + """Handle requests and suppress broken pipe errors"""
67 + try:
68 + super().handle()
69 + except (BrokenPipeError, ConnectionResetError):
70 + # Browser cancelled the request (normal for media streaming/preloading)
71 + pass
72 +
73 try:
74 with socketserver.TCPServer(("", port), QuietHandler) as httpd:
75 local_url = f"http://localhost:{port}"
14 let songs = [];
15 let animationFrameId = null;
16 let prePlaySeekTime = 0;
17 +let preloadedAudio = {}; // Cache for preloaded audio elements
18 +let currentPreloadIndex = 0;
19 +let priorityPreloadQueue = []; // Songs requested by user that need priority preloading
20 +let isPreloadingPriority = false;
21
22 // Load tracks from tracks.json
23 fetch('tracks.json')
33 playerReady = true;
34 updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} - ${songs[0].title}`);
35 renderPlaylist();
36 + // Pre-cache resources first, then songs
37 + preloadResources().then(() => {
38 + startPreloadingSongs();
39 + });
40 } else {
41 updateCurrentSongDisplay('No tracks found');
42 }
170
171 currentSongIndex = index;
172 const song = songs[currentSongIndex];
- audio.src = `tracks/${song.filename}`;
173 +
174 + // Use preloaded blob if available, otherwise load from server
175 + if (preloadedAudio[song.filename]) {
176 + audio.src = preloadedAudio[song.filename].blobUrl;
177 + } else {
178 + audio.src = `tracks/${song.filename}`;
179 + // Request priority preloading for this song
180 + requestPriorityPreload(song.filename);
181 + }
182 +
183 audio.play();
184 isPlaying = true;
185 updatePlayPauseButton();
285 // If audio hasn't been loaded yet, load it but don't play
286 if (!audio.src || audio.src === '') {
287 const song = songs[currentSongIndex];
- audio.src = `tracks/${song.filename}`;
288 +
289 + // Use preloaded blob if available, otherwise load from server
290 + if (preloadedAudio[song.filename]) {
291 + audio.src = preloadedAudio[song.filename].blobUrl;
292 + } else {
293 + audio.src = `tracks/${song.filename}`;
294 + }
295
296 // Wait for metadata to be loaded before seeking
297 audio.addEventListener('loadedmetadata', function setInitialTime() {
495 nextSong();
496 }
497 });
498 +
499 +// Pre-caching system
500 +function preloadResources() {
501 + console.log('Preloading UI resources...');
502 +
503 + const resources = [
504 + 'resources/play.png',
505 + 'resources/pause.png',
506 + 'resources/prev.png',
507 + 'resources/next.png'
508 + ];
509 +
510 + const imagePromises = resources.map(src => {
511 + return new Promise((resolve, reject) => {
512 + const img = new Image();
513 + img.onload = () => {
514 + console.log(`Preloaded: ${src}`);
515 + resolve();
516 + };
517 + img.onerror = () => {
518 + console.error(`Failed to preload: ${src}`);
519 + resolve(); // Resolve anyway to not block other resources
520 + };
521 + img.src = src;
522 + });
523 + });
524 +
525 + return Promise.all(imagePromises).then(() => {
526 + console.log('All UI resources preloaded');
527 + });
528 +}
529 +
530 +function startPreloadingSongs() {
531 + // Start with the first song
532 + currentPreloadIndex = 0;
533 + preloadNextSong();
534 +}
535 +
536 +function preloadNextSong() {
537 + if (currentPreloadIndex >= songs.length) {
538 + console.log('All songs preloaded');
539 + return;
540 + }
541 +
542 + const song = songs[currentPreloadIndex];
543 + const filename = song.filename;
544 +
545 + // Skip if already preloaded
546 + if (preloadedAudio[filename]) {
547 + currentPreloadIndex++;
548 + preloadNextSong();
549 + return;
550 + }
551 +
552 + console.log(`Preloading: ${song.artist} - ${song.title}`);
553 +
554 + // Use fetch to force full download of the entire file
555 + fetch(`tracks/${filename}`)
556 + .then(response => {
557 + if (!response.ok) {
558 + throw new Error(`HTTP error! status: ${response.status}`);
559 + }
560 + // Get total file size for progress tracking
561 + const contentLength = response.headers.get('content-length');
562 + console.log(`Downloading ${song.title} (${(contentLength / 1024 / 1024).toFixed(2)} MB)...`);
563 +
564 + // Read the entire response as a blob
565 + return response.blob();
566 + })
567 + .then(blob => {
568 + // Create a blob URL that will persist in memory
569 + const blobUrl = URL.createObjectURL(blob);
570 +
571 + // Create audio element with the fully downloaded blob
572 + const preloadAudio = new Audio();
573 + preloadAudio.preload = 'auto';
574 + preloadAudio.src = blobUrl;
575 +
576 + // Wait for the audio element to fully buffer the blob
577 + return new Promise((resolve) => {
578 + const checkBuffered = () => {
579 + // Check if the entire duration is buffered
580 + if (preloadAudio.duration > 0 && preloadAudio.buffered.length > 0) {
581 + const bufferedEnd = preloadAudio.buffered.end(preloadAudio.buffered.length - 1);
582 + if (bufferedEnd >= preloadAudio.duration - 0.1) {
583 + // Fully buffered!
584 + resolve({ preloadAudio, blobUrl, blob });
585 + return;
586 + }
587 + }
588 + // Not fully buffered yet, check again soon
589 + setTimeout(checkBuffered, 100);
590 + };
591 +
592 + // Start checking once metadata is loaded
593 + preloadAudio.addEventListener('loadedmetadata', () => {
594 + checkBuffered();
595 + }, { once: true });
596 +
597 + // Trigger the loading
598 + preloadAudio.load();
599 + });
600 + })
601 + .then(({ preloadAudio, blobUrl, blob }) => {
602 + // Store both the audio element and blob URL
603 + preloadedAudio[filename] = {
604 + audio: preloadAudio,
605 + blobUrl: blobUrl,
606 + blob: blob
607 + };
608 +
609 + console.log(`✓ Fully preloaded: ${song.artist} - ${song.title}`);
610 +
611 + // Move to next song
612 + currentPreloadIndex++;
613 + setTimeout(() => preloadNextSong(), 100);
614 + })
615 + .catch(error => {
616 + console.error(`Failed to preload ${filename}:`, error);
617 + currentPreloadIndex++;
618 + preloadNextSong();
619 + });
620 +}
621 +
622 +// Priority preloading system
623 +function requestPriorityPreload(filename) {
624 + // Skip if already preloaded or already in priority queue
625 + if (preloadedAudio[filename] || priorityPreloadQueue.includes(filename)) {
626 + return;
627 + }
628 +
629 + console.log(`🔥 Priority preload requested: ${filename}`);
630 + priorityPreloadQueue.push(filename);
631 +
632 + // Start priority preloading if not already running
633 + if (!isPreloadingPriority) {
634 + processPriorityPreload();
635 + }
636 +}
637 +
638 +function processPriorityPreload() {
639 + if (priorityPreloadQueue.length === 0) {
640 + isPreloadingPriority = false;
641 + return;
642 + }
643 +
644 + isPreloadingPriority = true;
645 + const filename = priorityPreloadQueue.shift();
646 +
647 + // Check if already preloaded (might have finished during normal preloading)
648 + if (preloadedAudio[filename]) {
649 + processPriorityPreload();
650 + return;
651 + }
652 +
653 + // Find the song info
654 + const song = songs.find(s => s.filename === filename);
655 + if (!song) {
656 + processPriorityPreload();
657 + return;
658 + }
659 +
660 + console.log(`🔥 Priority preloading: ${song.artist} - ${song.title}`);
661 +
662 + // Use fetch to force full download of the entire file
663 + fetch(`tracks/${filename}`)
664 + .then(response => {
665 + if (!response.ok) {
666 + throw new Error(`HTTP error! status: ${response.status}`);
667 + }
668 + return response.blob();
669 + })
670 + .then(blob => {
671 + // Create a blob URL that will persist in memory
672 + const blobUrl = URL.createObjectURL(blob);
673 +
674 + // Create audio element with the fully downloaded blob
675 + const preloadAudio = new Audio();
676 + preloadAudio.preload = 'auto';
677 + preloadAudio.src = blobUrl;
678 +
679 + // Wait for the audio element to fully buffer the blob
680 + return new Promise((resolve) => {
681 + const checkBuffered = () => {
682 + // Check if the entire duration is buffered
683 + if (preloadAudio.duration > 0 && preloadAudio.buffered.length > 0) {
684 + const bufferedEnd = preloadAudio.buffered.end(preloadAudio.buffered.length - 1);
685 + if (bufferedEnd >= preloadAudio.duration - 0.1) {
686 + // Fully buffered!
687 + resolve({ preloadAudio, blobUrl, blob });
688 + return;
689 + }
690 + }
691 + // Not fully buffered yet, check again soon
692 + setTimeout(checkBuffered, 100);
693 + };
694 +
695 + // Start checking once metadata is loaded
696 + preloadAudio.addEventListener('loadedmetadata', () => {
697 + checkBuffered();
698 + }, { once: true });
699 +
700 + // Trigger the loading
701 + preloadAudio.load();
702 + });
703 + })
704 + .then(({ preloadAudio, blobUrl, blob }) => {
705 + // Store both the audio element and blob URL
706 + preloadedAudio[filename] = {
707 + audio: preloadAudio,
708 + blobUrl: blobUrl,
709 + blob: blob
710 + };
711 +
712 + // Switch to the preloaded version if this is the current song
713 + if (songs[currentSongIndex].filename === filename && audio.src !== blobUrl) {
714 + const currentTime = audio.currentTime;
715 + const wasPlaying = !audio.paused;
716 + audio.src = blobUrl;
717 + audio.currentTime = currentTime;
718 + if (wasPlaying) {
719 + audio.play().catch(err => {
720 + // Ignore play interruption errors (expected during source switching)
721 + if (err.name !== 'AbortError') {
722 + console.error('Error resuming playback:', err);
723 + }
724 + });
725 + }
726 + }
727 +
728 + // Process next priority request
729 + processPriorityPreload();
730 + })
731 + .catch(error => {
732 + // Ignore abort errors (happens when normal preload finishes first)
733 + if (error.name === 'AbortError' || error.message.includes('aborted')) {
734 + // This is expected - normal preloading probably finished first
735 + } else {
736 + console.error(`Failed to priority preload ${filename}:`, error);
737 + }
738 + processPriorityPreload();
739 + });
740 +}