// =============================== // 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"); 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; } // Otherwise treat as relative filename return "https://frutigeraeroarchive.org/music/" + raw; } // =============================== // LOAD MUSIC DATA (CORS PROXY) // =============================== async function loadMusicData() { const response = await fetch( "https://corsproxy.io/?url=https%3A%2F%2Ffrutigeraeroarchive.org%2Fdata%2Fmusic.min.json" ); const data = await response.json(); const section = data.ols.find(item => item["@id"] === "frutigerAeroBliss"); if (!section) return []; // 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); } // =============================== // CREATE PLAYLIST // =============================== function createAlbum(album) { if (!PLAYLIST) return; const existing = PLAYLIST.querySelector("ol"); if (existing) existing.remove(); const list = document.createElement("ol"); list.id = album["@id"]; const title = document.createElement("span"); title.textContent = album.span; list.appendChild(title); const small = document.createElement("small"); small.textContent = album.small; list.appendChild(small); // Build playlist ONLY from valid songs album.li.forEach((song, index) => { const li = document.createElement("li"); const a = document.createElement("a"); a.textContent = song.a.title; a.href = "#"; for (const key in song.a) { if (key.startsWith("@data-")) { a.setAttribute(key.replace("@", ""), song.a[key]); } } 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 })) }); } });