/* ========================================= DOM ELEMENTS ========================================= */ const playlistElement = document.getElementById("playlist"); const songCountElement = document.getElementById("songCount"); const totalSongsElement = document.getElementById("totalSongs"); const currentTitle = document.getElementById("currentTitle"); const currentArtist = document.getElementById("currentArtist"); const coverImage = document.getElementById("coverImage"); const playBtn = document.getElementById("playBtn"); const nextBtn = document.getElementById("nextBtn"); const prevBtn = document.getElementById("prevBtn"); const shuffleBtn = document.getElementById("shuffleBtn"); const repeatBtn = document.getElementById("repeatBtn"); const progressBar = document.getElementById("progressBar"); const currentTimeElement = document.getElementById("currentTime"); const durationElement = document.getElementById("duration"); const volumeBar = document.getElementById("volumeBar"); const menuBtn = document.getElementById("menuBtn"); const closeMenu = document.getElementById("closeMenu"); const sideMenu = document.getElementById("sideMenu"); const overlay = document.getElementById("overlay"); const shareBtn = document.getElementById("shareBtn"); const randomBtn = document.getElementById("randomBtn"); const startWorkoutBtn = document.getElementById("startWorkoutBtn"); const toast = document.getElementById("toast"); const installBtn = document.getElementById("install-btn"); /* ========================================= APPLICATION STATE ========================================= */ let currentSongIndex = 0; let filteredSongs = [...songs]; let currentFilter = "all"; let isPlaying = false; let isShuffle = false; let isRepeat = false; let youtubePlayer = null; let youtubeReady = false; let deferredInstallPrompt = null; const failedSongIds = new Set(); /* ========================================= INSTALL / PWA ========================================= */ /* This checks whether the CURRENT app instance is running as an installed standalone application. Important: If the PWA is installed but the user manually opens localhost:5500 in Chrome, this will return false. That is expected because Chrome is running in browser mode. */ function isRunningAsInstalledApp() { return ( window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true || document.referrer.startsWith("android-app://") ); } /* Always hide initially. The button will only become visible when the browser fires beforeinstallprompt, meaning installation is actually available. */ function updateInstallButton() { if (!installBtn) { return; } installBtn.hidden = true; if (isRunningAsInstalledApp()) { console.log("Running as installed application"); } } updateInstallButton(); /* Browser tells us that the app can be installed. */ window.addEventListener("beforeinstallprompt", (event) => { event.preventDefault(); deferredInstallPrompt = event; if (installBtn && !isRunningAsInstalledApp()) { installBtn.hidden = false; console.log("PWA installation available"); } }); /* Install button click. */ installBtn?.addEventListener("click", async () => { if (!deferredInstallPrompt) { return; } deferredInstallPrompt.prompt(); const result = await deferredInstallPrompt.userChoice; console.log("Install result:", result.outcome); /* Hide after the user responds. If the user dismisses it, Chrome may later fire beforeinstallprompt again when installation becomes available again. */ installBtn.hidden = true; deferredInstallPrompt = null; }); /* Fired after successful installation. */ window.addEventListener("appinstalled", () => { console.log("BEAST MODE installed successfully"); deferredInstallPrompt = null; if (installBtn) { installBtn.hidden = true; } }); /* ========================================= YOUTUBE PLAYER ========================================= */ window.onYouTubeIframeAPIReady = function () { if (!songs.length) { console.error("No songs available"); return; } youtubePlayer = new YT.Player("youtube-player", { width: "1", height: "1", videoId: songs[currentSongIndex].id, playerVars: { autoplay: 0, controls: 0, rel: 0, playsinline: 1, enablejsapi: 1, origin: window.location.origin, }, events: { onReady(event) { youtubeReady = true; event.target.setVolume(Number(volumeBar.value)); loadSong(currentSongIndex, false); console.log("YouTube Player Ready"); }, onStateChange(event) { console.log("YouTube Player State:", event.data); /* PLAYING */ if (event.data === YT.PlayerState.PLAYING) { isPlaying = true; updatePlayButton(); renderPlaylist(); updateMediaSessionPlaybackState("playing"); return; } /* PAUSED */ if (event.data === YT.PlayerState.PAUSED) { isPlaying = false; updatePlayButton(); renderPlaylist(); updateMediaSessionPlaybackState("paused"); return; } /* ENDED */ if (event.data === YT.PlayerState.ENDED) { handleSongEnd(); } }, /* YouTube errors: 2 = Invalid parameter 5 = HTML5 player error 100 = Video unavailable 101 = Embedding not allowed 150 = Embedding not allowed */ onError(event) { handleYouTubeError(event); }, }, }); }; /* ========================================= RENDER PLAYLIST ========================================= */ function renderPlaylist() { if (!playlistElement) { return; } playlistElement.innerHTML = ""; filteredSongs.forEach((song, index) => { const realIndex = songs.findIndex((item) => item.id === song.id); const element = document.createElement("div"); element.className = "song"; if (realIndex === currentSongIndex) { element.classList.add("active"); } element.innerHTML = `
${String(index + 1).padStart(2, "0")}
${escapeHTML(song.title)}

${escapeHTML(song.title)}

${escapeHTML(song.artist)}

${escapeHTML(song.category.toUpperCase())}
${song.energy}%
`; element.addEventListener("click", () => { if (realIndex === currentSongIndex) { togglePlay(); return; } loadSong(realIndex, true); }); playlistElement.appendChild(element); }); if (songCountElement) { songCountElement.textContent = `${String(filteredSongs.length).padStart( 2, "0", )} TRACKS`; } } /* ========================================= LOAD SONG ========================================= */ function loadSong(index, autoplay = false) { const song = songs[index]; if (!song) { console.warn("Song not found:", index); return; } currentSongIndex = index; /* Update UI */ if (currentTitle) { currentTitle.textContent = song.title; } if (currentArtist) { currentArtist.textContent = song.artist; } if (coverImage) { coverImage.src = getThumbnail(song); coverImage.alt = song.title; } /* Update lock-screen / Media Session metadata */ updateMediaSession(song); /* Update playlist active state */ renderPlaylist(); /* YouTube player not ready yet. */ if (!youtubeReady || !youtubePlayer) { return; } /* Load or cue video. */ if (autoplay) { youtubePlayer.loadVideoById(song.id); } else { youtubePlayer.cueVideoById(song.id); } } /* ========================================= PLAY / PAUSE ========================================= */ function togglePlay() { if (!youtubeReady || !youtubePlayer) { console.log("YouTube player is not ready"); return; } /* Let onStateChange update isPlaying. Do not manually change isPlaying here, because YouTube is the source of truth. */ if (isPlaying) { youtubePlayer.pauseVideo(); } else { youtubePlayer.playVideo(); } } function updatePlayButton() { if (!playBtn) { return; } playBtn.textContent = isPlaying ? "❚❚" : "▶"; } /* ========================================= NEXT SONG ========================================= */ function nextSong() { if (!songs.length) { return; } let nextIndex; /* SHUFFLE */ if (isShuffle) { if (songs.length === 1) { nextIndex = 0; } else { do { nextIndex = Math.floor(Math.random() * songs.length); } while (nextIndex === currentSongIndex); } } else { nextIndex = (currentSongIndex + 1) % songs.length; } loadSong(nextIndex, true); } /* ========================================= PREVIOUS SONG ========================================= */ function previousSong() { if (!songs.length) { return; } const previousIndex = (currentSongIndex - 1 + songs.length) % songs.length; loadSong(previousIndex, true); } /* ========================================= SONG END ========================================= */ function handleSongEnd() { if (!youtubePlayer) { return; } /* Repeat current song. */ if (isRepeat) { youtubePlayer.seekTo(0, true); youtubePlayer.playVideo(); return; } nextSong(); } /* ========================================= YOUTUBE ERROR HANDLING ========================================= */ function handleYouTubeError(event) { const song = songs[currentSongIndex]; console.warn("YouTube playback failed:", { song: song?.title, videoId: song?.id, errorCode: event.data, }); /* Mark failed song so we don't keep retrying it in a loop. */ if (song) { failedSongIds.add(song.id); } const nextIndex = findNextPlayableSong(); /* No playable songs remain. */ if (nextIndex === -1) { isPlaying = false; updatePlayButton(); renderPlaylist(); updateMediaSessionPlaybackState("none"); console.error("No playable songs available"); return; } /* Wait briefly before moving to the next song. */ setTimeout(() => { loadSong(nextIndex, true); }, 500); } function findNextPlayableSong() { for (let i = 1; i <= songs.length; i++) { const index = (currentSongIndex + i) % songs.length; if (!failedSongIds.has(songs[index].id)) { return index; } } return -1; } /* ========================================= FILTERS ========================================= */ document.querySelectorAll(".filter").forEach((button) => { button.addEventListener("click", () => { currentFilter = button.dataset.filter; document .querySelectorAll(".filter") .forEach((item) => item.classList.remove("active")); button.classList.add("active"); if (currentFilter === "all") { filteredSongs = [...songs]; } else { filteredSongs = songs.filter((song) => song.category === currentFilter); } renderPlaylist(); }); }); /* ========================================= PROGRESS ========================================= */ setInterval(() => { if (!youtubeReady || !youtubePlayer) { return; } const duration = youtubePlayer.getDuration(); const currentTime = youtubePlayer.getCurrentTime(); if (duration && !Number.isNaN(duration)) { if (progressBar) { progressBar.value = (currentTime / duration) * 100; } if (currentTimeElement) { currentTimeElement.textContent = formatTime(currentTime); } if (durationElement) { durationElement.textContent = formatTime(duration); } } }, 500); /* ========================================= SEEK ========================================= */ progressBar?.addEventListener("input", () => { if (!youtubePlayer) { return; } const duration = youtubePlayer.getDuration(); if (duration && !Number.isNaN(duration)) { const seekTime = (Number(progressBar.value) / 100) * duration; youtubePlayer.seekTo(seekTime, true); } }); /* ========================================= VOLUME ========================================= */ volumeBar?.addEventListener("input", () => { if (youtubeReady && youtubePlayer) { youtubePlayer.setVolume(Number(volumeBar.value)); } }); /* ========================================= SHUFFLE ========================================= */ shuffleBtn?.addEventListener("click", () => { isShuffle = !isShuffle; shuffleBtn.classList.toggle("active", isShuffle); }); /* ========================================= REPEAT ========================================= */ repeatBtn?.addEventListener("click", () => { isRepeat = !isRepeat; repeatBtn.classList.toggle("active", isRepeat); }); /* ========================================= PLAYER CONTROLS ========================================= */ playBtn?.addEventListener("click", togglePlay); nextBtn?.addEventListener("click", nextSong); prevBtn?.addEventListener("click", previousSong); /* ========================================= START WORKOUT ========================================= */ startWorkoutBtn?.addEventListener("click", () => { if (!youtubeReady || !youtubePlayer) { console.log("YouTube is still loading"); return; } /* Always start from the currently selected song. */ youtubePlayer.playVideo(); }); /* ========================================= RANDOM SONG ========================================= */ randomBtn?.addEventListener("click", () => { if (!songs.length) { return; } let randomIndex; if (songs.length === 1) { randomIndex = 0; } else { do { randomIndex = Math.floor(Math.random() * songs.length); } while (randomIndex === currentSongIndex); } loadSong(randomIndex, true); }); /* ========================================= SIDE MENU ========================================= */ menuBtn?.addEventListener("click", openMenu); closeMenu?.addEventListener("click", closeSideMenu); overlay?.addEventListener("click", closeSideMenu); function openMenu() { sideMenu?.classList.add("open"); overlay?.classList.add("show"); } function closeSideMenu() { sideMenu?.classList.remove("open"); overlay?.classList.remove("show"); } /* ========================================= SHARE ========================================= */ shareBtn?.addEventListener("click", async () => { const shareData = { title: "BEAST MODE", text: "Hindi Gym Workout Music", url: window.location.href, }; try { if (navigator.share) { await navigator.share(shareData); } else { await navigator.clipboard.writeText(window.location.href); showToast(); } } catch (error) { /* User may simply cancel the native share dialog. */ console.log("Share cancelled"); } }); /* ========================================= KEYBOARD CONTROLS ========================================= */ document.addEventListener("keydown", (event) => { /* Don't hijack keyboard controls while using an input/range field. */ const target = event.target; const isFormElement = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement; if (event.code === "Space" && !isFormElement) { event.preventDefault(); togglePlay(); return; } if (event.code === "ArrowRight" && !isFormElement) { nextSong(); return; } if (event.code === "ArrowLeft" && !isFormElement) { previousSong(); } }); /* ========================================= MEDIA SESSION ========================================= */ function updateMediaSession(song) { if (!("mediaSession" in navigator)) { return; } try { navigator.mediaSession.metadata = new MediaMetadata({ title: song.title, artist: song.artist, album: "BEAST MODE • Gym Music", artwork: [ { src: getThumbnail(song), sizes: "480x360", type: "image/jpeg", }, ], }); } catch (error) { console.warn("Unable to update Media Session:", error); } } function updateMediaSessionPlaybackState(state) { if (!("mediaSession" in navigator)) { return; } try { navigator.mediaSession.playbackState = state; } catch (error) { console.warn("Unable to update Media Session state:", error); } } /* Lock-screen / headset controls */ if ("mediaSession" in navigator) { try { navigator.mediaSession.setActionHandler("play", () => { youtubePlayer?.playVideo(); }); navigator.mediaSession.setActionHandler("pause", () => { youtubePlayer?.pauseVideo(); }); navigator.mediaSession.setActionHandler("previoustrack", () => { previousSong(); }); navigator.mediaSession.setActionHandler("nexttrack", () => { nextSong(); }); } catch (error) { console.warn("Media Session action handlers unavailable:", error); } } /* ========================================= HELPERS ========================================= */ function getThumbnail(song) { if (song.cover) { return song.cover; } return `https://i.ytimg.com/vi/` + `${song.id}/hqdefault.jpg`; } function formatTime(seconds) { if (!seconds || Number.isNaN(seconds)) { return "0:00"; } const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`; } function escapeHTML(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function showToast() { if (!toast) { return; } toast.classList.add("show"); setTimeout(() => { toast.classList.remove("show"); }, 2500); } /* ========================================= INITIALIZE APP ========================================= */ if (!songs.length) { console.error("songs.js does not contain any songs"); } else { /* Total song count. */ if (totalSongsElement) { totalSongsElement.textContent = String(songs.length).padStart(2, "0"); } /* Initial song UI. */ const firstSong = songs[currentSongIndex]; if (currentTitle) { currentTitle.textContent = firstSong.title; } if (currentArtist) { currentArtist.textContent = firstSong.artist; } if (coverImage) { coverImage.src = getThumbnail(firstSong); coverImage.alt = firstSong.title; } updateMediaSession(firstSong); renderPlaylist(); }