diff --git a/javascript/nav-music.js b/javascript/nav-music.js index c746919..0a89266 100644 --- a/javascript/nav-music.js +++ b/javascript/nav-music.js @@ -1,164 +1,197 @@ -// Volume adjustment for audio elements -document.querySelectorAll("audio").forEach((audio) => { - audio.volume = 0.5; // Volume range is from 0.0 (silent) to 1.0 (maximum) -}); +// =============================== +// INITIAL SETUP +// =============================== +let currentSongIndex = 0; +let SONG_LINKS = []; +let PLAYLIST = document.getElementById("playlistLeft"); +let AUDIO_PLAYER = document.getElementById("navMusic"); +let TRACK_NAME_DISPLAY = document.getElementById("currentSongInfo"); -// Shuffle Playlist (Fisher-Yates) -function fisherYatesShuffle(songs) { - for (let i = songs.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [songs[i], songs[j]] = [songs[j], songs[i]]; +let NAV_PLAY = document.getElementById("navMusicPlay"); +let NAV_PAUSE = document.getElementById("navMusicPause"); +let NAV_NEXT = document.getElementById("navMusicNext"); +let NAV_PREV = document.getElementById("navMusicPrevious"); + + +// =============================== +// UNIVERSAL AUDIO URL SANITIZER +// =============================== +function sanitizeAudioUrl(raw) { + if (!raw) return null; + + // Reject placeholders or HTML anchors + if (raw === "#" || raw === "/" || raw === "./" || raw === "../") return null; + + // Allowed audio extensions + const audioExtensions = [ + ".mp3", ".ogg", ".wav", ".m4a", ".aac", ".flac", ".opus", ".webm" + ]; + + const lower = raw.toLowerCase(); + const isAudio = audioExtensions.some(ext => lower.endsWith(ext)); + if (!isAudio) return null; + + // Absolute URL + if (raw.startsWith("http")) return raw; + + // JSON gives /music/... or /audio/... + if (raw.startsWith("/")) { + return "https://frutigeraeroarchive.org" + raw; } - return songs; + + // Otherwise treat as relative filename + return "https://frutigeraeroarchive.org/music/" + raw; } -// Load the JSON file containing music data (using CORS proxy) + +// =============================== +// LOAD MUSIC DATA (CORS PROXY) +// =============================== async function loadMusicData() { - const corsProxyUrl = 'https://corsproxy.io/?url='; - const targetUrl = 'https://frutigeraeroarchive.org/data/music.min.json'; - const response = await fetch(corsProxyUrl + encodeURIComponent(targetUrl)); + const response = await fetch( + "http://chat.veltron.net" + ); + const data = await response.json(); - // Get the "frutigerAeroBliss" section from the JSON - const section = data.ols.find(item => item["@id"] === "frutigerAeroBliss"); - - if (!section) { - console.error('Section "frutigerAeroBliss" not found.'); + // Safety check to prevent crashes + if (!data || !data.ols) { + console.error("Music JSON is invalid or blocked by CORS:", data); return []; } - // Map the songs into a simplified format with direct audio URLs - const baseUrl = "https://frutigeraeroarchive.org/music/"; // Base URL for the music files - const playlist = section.li.map(item => ({ - title: item.a.title, - url: baseUrl + item.a["@data-src-mp3"].split("/").pop(), // Build the full URL from the base path - background: item.a["@data-background"] || '', // Optional background if available - artist: item.a["@data-artist"] || '', - spotifyLink: item.a["@data-spotify"] || '', - youtubeLink: item.a["@data-youtube"] || '' - })); + const section = data.ols.find(item => item["@id"] === "frutigerAeroBliss"); + if (!section) return []; - // Shuffle the playlist - return fisherYatesShuffle(playlist); + // KEEP JSON ORDER EXACTLY, but filter invalid URLs + return section.li + .map(item => { + const fixedUrl = sanitizeAudioUrl(item.a["@data-src-mp3"]); + if (!fixedUrl) return null; + + return { + ...item.a, + "@data-src-mp3": fixedUrl + }; + }) + .filter(Boolean); } -// Define Music Player -const NAV_MUSIC = document.getElementById("navMusic"); -const BUTTON_PLAY = document.getElementById("navMusicPlay"); -const BUTTON_PAUSE = document.getElementById("navMusicPause"); -const BUTTON_PREVIOUS = document.getElementById("navMusicPrevious"); -const BUTTON_NEXT = document.getElementById("navMusicNext"); -const CURRENT_SONG_INFO = document.getElementById("currentSongInfo"); -let songs = []; -let currentIndex = 0; -let currentTime = 0; -let isPlaying = false; // Track play/pause state +// =============================== +// CREATE PLAYLIST +// =============================== +function createAlbum(album) { + if (!PLAYLIST) return; -// Save state to sessionStorage -function saveState() { - sessionStorage.setItem("isPlaying", isPlaying); -} + const existing = PLAYLIST.querySelector("ol"); + if (existing) existing.remove(); -// Load state from sessionStorage -function loadState() { - const savedIsPlaying = sessionStorage.getItem("isPlaying"); + const list = document.createElement("ol"); + list.id = album["@id"]; - if (savedIsPlaying !== null) { - isPlaying = savedIsPlaying === "true"; - } -} + const title = document.createElement("span"); + title.textContent = album.span; + list.appendChild(title); -// Load the current song -function loadSong(index) { - if (index < 0 || index >= songs.length) return; - NAV_MUSIC.src = songs[index].url; // Directly set the audio URL (no proxy needed) - CURRENT_SONG_INFO.innerText = `Featured song: ${songs[index].title}`; - NAV_MUSIC.load(); -} + const small = document.createElement("small"); + small.textContent = album.small; + list.appendChild(small); -// Update button states based on `isPlaying` -function updateButtonStates() { - if (isPlaying) { - BUTTON_PLAY.classList.add("active"); - BUTTON_PAUSE.classList.remove("active"); - } else { - BUTTON_PAUSE.classList.add("active"); - BUTTON_PLAY.classList.remove("active"); - } -} + // Build playlist ONLY from valid songs + album.li.forEach((song, index) => { + const li = document.createElement("li"); + const a = document.createElement("a"); -// Event listeners for playback controls -BUTTON_PLAY.addEventListener("click", function () { - isPlaying = true; - NAV_MUSIC.play(); - updateButtonStates(); - saveState(); -}); + a.textContent = song.a.title; + a.href = "#"; -BUTTON_PAUSE.addEventListener("click", function () { - isPlaying = false; - NAV_MUSIC.pause(); - updateButtonStates(); - saveState(); -}); - -BUTTON_PREVIOUS.addEventListener("click", function () { - currentIndex = currentIndex === 0 ? songs.length - 1 : currentIndex - 1; - currentTime = 0; // Reset time when switching songs - loadSong(currentIndex); - if (isPlaying) { - NAV_MUSIC.play(); - } - updateButtonStates(); - saveState(); -}); - -BUTTON_NEXT.addEventListener("click", function () { - currentIndex = (currentIndex + 1) % songs.length; - currentTime = 0; // Reset time when switching songs - loadSong(currentIndex); - if (isPlaying) { - NAV_MUSIC.play(); - } - updateButtonStates(); - saveState(); -}); - -// Autoplay next song when current ends -NAV_MUSIC.addEventListener("ended", function () { - currentIndex = (currentIndex + 1) % songs.length; - currentTime = 0; // Reset time when a new song starts - loadSong(currentIndex); - if (isPlaying) { - NAV_MUSIC.play(); - } - saveState(); -}); - -// Initial load -window.addEventListener("load", async () => { - songs = await loadMusicData(); - if (songs.length > 0) { - loadState(); - loadSong(currentIndex); - if (isPlaying) { - NAV_MUSIC.play(); - } - updateButtonStates(); - } -}); - -// Pause all audio if user presses play on another audio element -document.addEventListener( - "play", - function (e) { - var audios = document.getElementsByTagName("audio"); - for (var i = 0, len = audios.length; i < len; i++) { - if (audios[i] != e.target) { - audios[i].pause(); + for (const key in song.a) { + if (key.startsWith("@data-")) { + a.setAttribute(key.replace("@", ""), song.a[key]); } } - }, - true -); + + li.appendChild(a); + list.appendChild(li); + }); + + PLAYLIST.appendChild(list); + + SONG_LINKS = PLAYLIST.querySelectorAll("a[data-src-mp3]"); + + SONG_LINKS.forEach((song, index) => { + song.addEventListener("click", (e) => { + e.preventDefault(); + currentSongIndex = index; + updateSong(index); + }); + }); + + currentSongIndex = 0; + updateSong(0); +} + + +// =============================== +// UPDATE SONG +// =============================== +function updateSong(index) { + SONG_LINKS.forEach((song, i) => { + song.classList.toggle("activeSong", i === index); + }); + + const song = SONG_LINKS[index]; + const src = song.getAttribute("data-src-mp3"); + + if (!src) { + console.warn("Invalid song URL, skipping."); + return; + } + + AUDIO_PLAYER.src = src; + TRACK_NAME_DISPLAY.textContent = song.getAttribute("data-title"); + + AUDIO_PLAYER.play().catch(console.error); +} + + +// =============================== +// NEXT / PREVIOUS +// =============================== +function nextTrack() { + currentSongIndex = (currentSongIndex + 1) % SONG_LINKS.length; + updateSong(currentSongIndex); +} + +function previousTrack() { + currentSongIndex = (currentSongIndex - 1 + SONG_LINKS.length) % SONG_LINKS.length; + updateSong(currentSongIndex); +} + +AUDIO_PLAYER.addEventListener("ended", nextTrack); + + +// =============================== +// NAV BUTTONS +// =============================== +NAV_PLAY.addEventListener("click", () => AUDIO_PLAYER.play()); +NAV_PAUSE.addEventListener("click", () => AUDIO_PLAYER.pause()); +NAV_NEXT.addEventListener("click", nextTrack); +NAV_PREV.addEventListener("click", previousTrack); + + +// =============================== +// LOAD ON STARTUP +// =============================== +window.addEventListener("load", async () => { + const songs = await loadMusicData(); + if (songs.length > 0) { + createAlbum({ + "@id": "frutigerAeroBliss", + span: "Frutiger Aero Bliss", + small: "Music that fits well with Frutiger Aero.", + li: songs.map(song => ({ a: song })) + }); + } +});