Initial commit: Flask internet radio player

This commit is contained in:
2026-04-30 17:21:10 +02:00
commit b8fb9de3b1
5 changed files with 501 additions and 0 deletions
+185
View File
@@ -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();
+213
View File
@@ -0,0 +1,213 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #1a1a2e;
color: #e0e0e0;
font-family: 'Segoe UI', system-ui, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.player-container {
background: #16213e;
border-radius: 16px;
padding: 2rem;
width: 100%;
max-width: 480px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.player-header h1 {
text-align: center;
font-size: 1.8rem;
color: #e94560;
margin-bottom: 1.5rem;
letter-spacing: 2px;
}
.now-playing {
background: #0f3460;
border-radius: 12px;
padding: 1rem 1.2rem;
margin-bottom: 1.2rem;
min-height: 64px;
display: flex;
align-items: center;
}
.track-title {
font-size: 1rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track-index {
font-size: 0.75rem;
color: #7a8ba0;
margin-top: 4px;
}
.progress-container {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.2rem;
font-size: 0.8rem;
color: #7a8ba0;
}
.progress-container input[type="range"] {
flex: 1;
}
.controls {
display: flex;
justify-content: center;
gap: 0.8rem;
margin-bottom: 1.2rem;
}
.controls button {
background: #0f3460;
border: none;
border-radius: 50%;
color: #e0e0e0;
cursor: pointer;
font-size: 1.1rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, transform 0.1s;
}
.controls button:hover {
background: #e94560;
color: #fff;
}
.controls button:active {
transform: scale(0.93);
}
#btn-play {
background: #e94560;
width: 60px;
height: 60px;
font-size: 1.4rem;
}
#btn-play:hover {
background: #c73652;
}
.controls button.toggle.active {
background: #e94560;
color: #fff;
}
.volume-container {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.5rem;
}
.volume-container span {
font-size: 1.1rem;
}
.volume-container input[type="range"] {
flex: 1;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 4px;
background: #0f3460;
border-radius: 2px;
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #e94560;
border-radius: 50%;
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #e94560;
border-radius: 50%;
border: none;
}
.playlist-container {
background: #0f3460;
border-radius: 12px;
overflow: hidden;
max-height: 280px;
display: flex;
flex-direction: column;
}
.playlist-header {
padding: 0.6rem 1rem;
font-size: 0.8rem;
color: #7a8ba0;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #16213e;
}
#playlist {
list-style: none;
overflow-y: auto;
flex: 1;
}
#playlist::-webkit-scrollbar {
width: 4px;
}
#playlist::-webkit-scrollbar-thumb {
background: #e94560;
border-radius: 2px;
}
#playlist li {
padding: 0.6rem 1rem;
cursor: pointer;
font-size: 0.88rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-bottom: 1px solid #16213e;
transition: background 0.15s;
}
#playlist li:hover {
background: #16213e;
}
#playlist li.active {
background: #1a1a2e;
color: #e94560;
font-weight: 600;
}