/* =========================================
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.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();
}