191 lines
5.2 KiB
JavaScript
191 lines
5.2 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");
|
|
|
|
|
|
// ===============================
|
|
// URL SANITIZER
|
|
// ===============================
|
|
function sanitizeMp3Url(raw) {
|
|
if (!raw) return null;
|
|
|
|
// Reject invalid placeholders
|
|
if (raw === "#" || raw === "/" || raw === "./" || raw === "../") return null;
|
|
|
|
// Reject URLs that are not MP3 files
|
|
if (!raw.endsWith(".mp3")) return null;
|
|
|
|
// If JSON gives absolute URL
|
|
if (raw.startsWith("http")) return raw;
|
|
|
|
// If 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
|
|
// ===============================
|
|
async function loadMusicData() {
|
|
const response = await fetch("https://frutigeraeroarchive.org/data/music.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 = sanitizeMp3Url(item.a["@data-src-mp3"]);
|
|
if (!fixedUrl) return null;
|
|
|
|
return {
|
|
title: item.a.title,
|
|
url: fixedUrl,
|
|
background: item.a["@data-background"] || "",
|
|
artist: item.a["@data-artist"] || "",
|
|
spotify: item.a["@data-spotify"] || "",
|
|
youtube: item.a["@data-youtube"] || "",
|
|
description: item.a["@data-description"] || "",
|
|
copyright: item.a["@data-copyright"] || "",
|
|
date: item.a["@data-date"] || ""
|
|
};
|
|
})
|
|
.filter(Boolean); // remove null entries
|
|
}
|
|
|
|
|
|
// ===============================
|
|
// 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 in EXACT JSON ORDER
|
|
album.li.forEach((song, index) => {
|
|
const li = document.createElement("li");
|
|
const a = document.createElement("a");
|
|
|
|
a.textContent = song.a.title;
|
|
a.href = "#";
|
|
|
|
// Apply metadata
|
|
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);
|
|
});
|
|
});
|
|
|
|
// ALWAYS START AT SONG 0
|
|
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 }))
|
|
});
|
|
}
|
|
});
|