191 lines
5.1 KiB
JavaScript
191 lines
5.1 KiB
JavaScript
// ===============================
|
|
// 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 }))
|
|
});
|
|
}
|
|
});
|