Initial commit: Flask internet radio player
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
const audio = document.getElementById("audio-player");
|
||||
const btnPlay = document.getElementById("btn-play");
|
||||
const btnPrev = document.getElementById("btn-prev");
|
||||
const btnNext = document.getElementById("btn-next");
|
||||
const btnShuffle = document.getElementById("btn-shuffle");
|
||||
const btnRepeat = document.getElementById("btn-repeat");
|
||||
const progressBar = document.getElementById("progress-bar");
|
||||
const currentTimeEl = document.getElementById("current-time");
|
||||
const durationEl = document.getElementById("duration");
|
||||
const volumeSlider = document.getElementById("volume");
|
||||
const playlistEl = document.getElementById("playlist");
|
||||
const trackTitle = document.getElementById("track-title");
|
||||
const trackIndex = document.getElementById("track-index");
|
||||
const playlistCount = document.getElementById("playlist-count");
|
||||
|
||||
let playlist = [];
|
||||
let currentIndex = 0;
|
||||
let shuffle = false;
|
||||
let repeat = false;
|
||||
let shuffleOrder = [];
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (isNaN(seconds)) return "0:00";
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60).toString().padStart(2, "0");
|
||||
return `${m}:${s}`;
|
||||
}
|
||||
|
||||
function stripExtension(filename) {
|
||||
return filename.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
|
||||
function buildShuffleOrder() {
|
||||
shuffleOrder = [...Array(playlist.length).keys()];
|
||||
for (let i = shuffleOrder.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffleOrder[i], shuffleOrder[j]] = [shuffleOrder[j], shuffleOrder[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function loadTrack(index, autoplay = false) {
|
||||
currentIndex = index;
|
||||
const filename = playlist[index];
|
||||
audio.src = `/audio/${encodeURIComponent(filename)}`;
|
||||
trackTitle.textContent = stripExtension(filename);
|
||||
trackIndex.textContent = `Track ${index + 1} of ${playlist.length}`;
|
||||
document.querySelectorAll("#playlist li").forEach((li, i) => {
|
||||
li.classList.toggle("active", i === index);
|
||||
if (i === index) li.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
progressBar.value = 0;
|
||||
currentTimeEl.textContent = "0:00";
|
||||
durationEl.textContent = "0:00";
|
||||
if (autoplay) audio.play();
|
||||
}
|
||||
|
||||
function playNext() {
|
||||
if (playlist.length === 0) return;
|
||||
if (shuffle) {
|
||||
const pos = shuffleOrder.indexOf(currentIndex);
|
||||
const nextPos = (pos + 1) % shuffleOrder.length;
|
||||
loadTrack(shuffleOrder[nextPos], true);
|
||||
} else {
|
||||
loadTrack((currentIndex + 1) % playlist.length, true);
|
||||
}
|
||||
}
|
||||
|
||||
function playPrev() {
|
||||
if (playlist.length === 0) return;
|
||||
if (audio.currentTime > 3) {
|
||||
audio.currentTime = 0;
|
||||
return;
|
||||
}
|
||||
if (shuffle) {
|
||||
const pos = shuffleOrder.indexOf(currentIndex);
|
||||
const prevPos = (pos - 1 + shuffleOrder.length) % shuffleOrder.length;
|
||||
loadTrack(shuffleOrder[prevPos], true);
|
||||
} else {
|
||||
loadTrack((currentIndex - 1 + playlist.length) % playlist.length, true);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlaylist() {
|
||||
playlistEl.innerHTML = "";
|
||||
playlistCount.textContent = `${playlist.length} track${playlist.length !== 1 ? "s" : ""}`;
|
||||
playlist.forEach((file, i) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = stripExtension(file);
|
||||
li.title = file;
|
||||
li.addEventListener("click", () => loadTrack(i, true));
|
||||
playlistEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPlaylist() {
|
||||
try {
|
||||
const res = await fetch("/api/playlist");
|
||||
playlist = await res.json();
|
||||
renderPlaylist();
|
||||
if (playlist.length > 0) {
|
||||
buildShuffleOrder();
|
||||
loadTrack(0);
|
||||
} else {
|
||||
trackTitle.textContent = "No audio files found";
|
||||
trackIndex.textContent = "Drop .mp3 / .ogg / .wav files into the audio/ folder";
|
||||
}
|
||||
} catch (e) {
|
||||
trackTitle.textContent = "Error loading playlist";
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
btnPlay.addEventListener("click", () => {
|
||||
if (audio.paused) {
|
||||
if (!audio.src) loadTrack(0, true);
|
||||
else audio.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
});
|
||||
|
||||
btnNext.addEventListener("click", playNext);
|
||||
btnPrev.addEventListener("click", playPrev);
|
||||
|
||||
btnShuffle.addEventListener("click", () => {
|
||||
shuffle = !shuffle;
|
||||
btnShuffle.classList.toggle("active", shuffle);
|
||||
if (shuffle) buildShuffleOrder();
|
||||
});
|
||||
|
||||
btnRepeat.addEventListener("click", () => {
|
||||
repeat = !repeat;
|
||||
btnRepeat.classList.toggle("active", repeat);
|
||||
});
|
||||
|
||||
// Audio events
|
||||
audio.addEventListener("play", () => { btnPlay.textContent = "⏸"; });
|
||||
audio.addEventListener("pause", () => { btnPlay.textContent = "▶"; });
|
||||
|
||||
audio.addEventListener("timeupdate", () => {
|
||||
if (audio.duration) {
|
||||
progressBar.value = (audio.currentTime / audio.duration) * 100;
|
||||
currentTimeEl.textContent = formatTime(audio.currentTime);
|
||||
}
|
||||
});
|
||||
|
||||
audio.addEventListener("loadedmetadata", () => {
|
||||
durationEl.textContent = formatTime(audio.duration);
|
||||
});
|
||||
|
||||
audio.addEventListener("ended", () => {
|
||||
if (repeat) {
|
||||
audio.currentTime = 0;
|
||||
audio.play();
|
||||
} else {
|
||||
playNext();
|
||||
}
|
||||
});
|
||||
|
||||
progressBar.addEventListener("input", () => {
|
||||
if (audio.duration) {
|
||||
audio.currentTime = (progressBar.value / 100) * audio.duration;
|
||||
}
|
||||
});
|
||||
|
||||
volumeSlider.addEventListener("input", () => {
|
||||
audio.volume = volumeSlider.value / 100;
|
||||
});
|
||||
|
||||
// Set initial volume
|
||||
audio.volume = volumeSlider.value / 100;
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.target.tagName === "INPUT") return;
|
||||
switch (e.key) {
|
||||
case " ": e.preventDefault(); btnPlay.click(); break;
|
||||
case "ArrowRight": playNext(); break;
|
||||
case "ArrowLeft": playPrev(); break;
|
||||
case "ArrowUp": volumeSlider.value = Math.min(100, +volumeSlider.value + 5); audio.volume = volumeSlider.value / 100; break;
|
||||
case "ArrowDown": volumeSlider.value = Math.max(0, +volumeSlider.value - 5); audio.volume = volumeSlider.value / 100; break;
|
||||
}
|
||||
});
|
||||
|
||||
loadPlaylist();
|
||||
Reference in New Issue
Block a user