#3822 Online "Radio Station" Playing MP3s
#3822 Online "Radio Station" Playing MP3s
#3822 I have 25 mp3's to upload to my server. How can I create a "radios station" to stream this set of mp3's in a loop?
You can create a "radio station" in two main ways. The simplest method is a client-side playlist that loops for each user. The "true" method is a server-side stream where all users hear the same part of the song at the same time, just like a real radio broadcast.
Here’s how to do both.
🎵 Option 1: The Simple Playlist (Client-Side)
This method uses simple HTML and JavaScript. It doesn't create a single "stream" for everyone; rather, each visitor gets their own personal player that plays the 25 songs in a loop.
Pros: Very easy to set up. Requires no special server software.
Cons: Not a real broadcast. Two users visiting the page at the same time will hear different things (one might be on song 1, the other on song 5).
How to Do It
Upload your 25 MP3s to a folder on your server (e.g.,
/music/).Create an HTML file with the following code.
This code uses the HTML5 <audio> tag and some JavaScript to automatically play the next song when the current one ends.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My MP3 Loop</title>
<style>
body { font-family: sans-serif; text-align: center; padding-top: 50px; }
audio { width: 80%; max-width: 600px; margin-top: 20px; }
</style>
</head>
<body>
<h2>My 25-Song Radio</h2>
<p>Now Playing: <strong id="now-playing">Loading...</strong></p>
<audio id="player" controls autoplay>
Your browser does not support the audio element.
</audio>
<script>
// --- Your 25 MP3s ---
// List all your MP3 filenames here
const playlist = [
'song1.mp3',
'song2.mp3',
'song3.mp3',
// ... add all 25 songs
'song25.mp3'
];
// ---------------------
const musicFolder = '/music/'; // The folder where you uploaded your MP3s
let currentTrack = 0;
const player = document.getElementById('player');
const nowPlaying = document.getElementById('now-playing');
function playTrack(trackIndex) {
// Update the source and "now playing" text
let trackName = playlist[trackIndex];
player.src = musicFolder + trackName;
nowPlaying.textContent = trackName;
// Play the track
player.play();
}
// Event listener for when a track ends
player.addEventListener('ended', function() {
// Move to the next track
currentTrack++;
// If we're at the end of the playlist, loop back to the start
if (currentTrack >= playlist.length) {
currentTrack = 0;
}
// Play the new track
playTrack(currentTrack);
});
// Start playing the first track
playTrack(currentTrack);
</script>
</body>
</html>
📻 Option 2: The "True" Radio Station (Server-Side)
This is the method for a real broadcast. A program on your server continuously plays the 25 MP3s in a loop, creating a single audio stream. Anyone who connects hears whatever is playing right now.
This requires specialized streaming software.
Pros: A real, 24/7 broadcast. All listeners are in sync.
Cons: More complex to set up. Requires installing software on your server.
Recommended: Use an All-in-One Platform
The easiest way to do this is with a "radio-in-a-box" platform. The most popular open-source option is AzuraCast.
What it is: AzuraCast is a free, open-source web application that you install on your server (often via Docker). It gives you a powerful web interface to:
Upload your 25 MP3s.
Create playlists (e.g., a "main" playlist with all 25 songs, set to loop).
It automatically manages the backend streaming software (like Icecast and Liquidsoap).
It provides you with a public stream URL (like
http://your-server.com:8000/stream.mp3) that people can listen to.
The Manual (DIY) Setup: Icecast + Liquidsoap
If you are comfortable with the command line and don't want a full web panel, you can install the components yourself. You need two pieces of software:
Icecast: This is the "radio tower." It's the server that takes an audio stream and broadcasts it to multiple listeners.
A "Source Client": This is the "DJ." It's a program that reads your MP3s, puts them in order, and sends them as a single stream to Icecast. The most powerful one is Liquidsoap. A simpler one is ezstream.
Example with Liquidsoap:
Liquidsoap is extremely powerful. You would create a small script (e.g., station.liq) to tell it what to do.
Install Icecast and Liquidsoap on your server (e.g.,
sudo apt install icecast2 liquidsoap).Configure Icecast (usually at
/etc/icecast2/icecast.xml) to set passwords.Create a Liquidsoap script (
station.liq):# --- Configuration --- set("log.file.path", "/var/log/liquidsoap/station.log") # --- Your Music --- # Point this to the folder with your 25 MP3s music = playlist("/path/to/your/music/", mode="random") # --- The Stream --- # This creates a looping, cross-fading stream from your playlist radio = music # --- Output to Icecast --- # Send the "radio" stream to your Icecast server output.icecast( %mp3, host = "127.0.0.1", port = 8000, password = "your-icecast-password", mount = "stream.mp3", radio )Run Liquidsoap as a background service. It will connect to Icecast, start playing your MP3s in a random loop, and broadcast it.
Users could then listen by opening http://your-server-ip:8000/stream.mp3 in a browser, VLC, or any internet radio player.
How to Choose
Go with Option 1 (Simple Playlist): If you just want a simple way for people to listen to your 25 songs on a webpage, and you don't care if they are all in sync.
Go with Option 2 (AzuraCast): If you want a real radio station with a professional web interface for managing your music, and you're comfortable installing server software (like with Docker).
Go with Option 2 (Manual Setup): If you are a command-line expert, want a very lightweight solution, and enjoy configuring services manually.
For your goal of a "radio station," Option 2 (using AzuraCast) is likely the best balance of power and ease of use.
Provide JavaScript to read a directory and replace the mp3 list
This is a great follow-up. You can't use only client-side JavaScript to read a server's directory. That would be a major security hole, as it would let any website scan a server's file system.
Instead, you need a two-part solution:
A simple server-side script that reads the directory and prints the list of MP3s.
Your client-side JavaScript (from the previous example) which fetches that list from your script.
Here’s how to do it.
1. The Server-Side Script (Choose One)
You only need one of these. Create a file on your server with one of the following snippets.
Example in PHP (Most Common)
If your server runs PHP (most web hosts do), create a file named get_music.php and put it in the same directory as your HTML file.
File: get_music.php
<?php
// The folder where your music is.
// Make sure this path is correct from the script's location.
$music_dir = 'music/';
// Read all files from the directory
$files = scandir($music_dir);
$mp3_list = [];
foreach($files as $file) {
// Find files that end in .mp3
if (pathinfo($file, PATHINFO_EXTENSION) == 'mp3') {
// Add the filename to our list
$mp3_list[] = $file;
}
}
// Set the content type to JSON and print the list
header('Content-Type: application/json');
echo json_encode($mp3_list);
?>
Example in Node.js (with Express)
If you're running a Node.js server, you'd add a new route.
// Add these to your server.js
const fs = require('fs');
const path = require('path');
// Route to get the list of MP3s
app.get('/get-music', (req, res) => {
// Path to your music directory (e.g., inside a 'public' folder)
const musicDir = path.join(__dirname, 'public', 'music');
fs.readdir(musicDir, (err, files) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Failed to read directory' });
}
// Filter for MP3 files
const mp3s = files.filter(file =>
path.extname(file).toLowerCase() === '.mp3'
);
res.json(mp3s);
});
});
2. The Updated Client-Side JavaScript
Now, you replace the hard-coded const playlist = [...] in your HTML file with JavaScript that fetches the list from your new script.
This code uses async/await to make the request.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Dynamic Radio</title>
<style>
body { font-family: sans-serif; text-align: center; padding-top: 50px; }
audio { width: 80%; max-width: 600px; margin-top: 20px; }
</style>
</head>
<body>
<h2>My Dynamic Radio</h2>
<p>Now Playing: <strong id="now-playing">Loading...</strong></p>
<audio id="player" controls>
Your browser does not support the audio element.
</audio>
<script>
// --- Configuration ---
const musicFolder = '/music/'; // The public URL folder for your music
// This is the URL to the script you created in Step 1
// (Use '/get-music' for the Node.js example)
const playlistUrl = '/get_music.php';
// ---------------------
let playlist = []; // This will be filled from the server
let currentTrack = 0;
const player = document.getElementById('player');
const nowPlaying = document.getElementById('now-playing');
function playTrack(trackIndex) {
if (playlist.length === 0) {
nowPlaying.textContent = "Playlist is empty.";
return;
}
let trackName = playlist[trackIndex];
player.src = musicFolder + trackName;
nowPlaying.textContent = trackName;
player.play();
}
// Event listener for when a track ends
player.addEventListener('ended', function() {
currentTrack++;
if (currentTrack >= playlist.length) {
currentTrack = 0; // Loop back to the start
}
playTrack(currentTrack);
});
// --- NEW: Load the playlist from the server ---
async function loadPlaylist() {
try {
const response = await fetch(playlistUrl);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
playlist = await response.json(); // Our array of MP3s
if (playlist.length > 0) {
// Start playing the first track
player.autoplay = true; // Start playback automatically
playTrack(currentTrack);
} else {
nowPlaying.textContent = "No MP3s found in directory.";
}
} catch (error) {
console.error("Could not fetch playlist:", error);
nowPlaying.textContent = "Error loading playlist.";
}
}
// Run the function to load everything
loadPlaylist();
</script>
</body>
</html>
How This Works
The page loads.
The
loadPlaylist()JavaScript function runs.It calls your server script (
get_music.php).The PHP script scans the
/music/directory, finds all.mp3files, and returns them as a JSON array like:["song1.mp3", "song2.mp3", "song3.mp3"].The JavaScript receives this array, saves it to the
playlistvariable, and then starts playing the first song.
Now, any time you upload a new MP3 to your /music/ folder, it will automatically be included in the radio station the next time someone loads the page.
From <https://gemini.google.com/app/ca4bbff4d27f9255> Google Gemini 2.5 Pro
Songer.co: "Cicada's Song"
Provide HTML, JavaScript, CSS and PHP to get a list of MP3s from the server and loop them either in sequence or randomly. The user should be able to pick from several file lists (like sports, community, nature, military, etc.), each from a separate folder. Add the capability to upload an MP3 to a specific file list (folder). Add the capability to add a folder.
Here is a complete solution with HTML, CSS, JavaScript, and PHP to create the MP3 player you've described.
This solution is broken down into separate files. You will need a web server (like Apache or NGINX) with PHP enabled to run this.
File Structure
For this to work, create your files and folders as follows:
/ (your website root)
|
+-- index.html
+-- style.css
+-- script.js
|
+-- php/
| +-- get_folders.php
| +-- get_files.php
| +-- upload.php
| +-- create_folder.php
|
+-- music/
+-- sports/
| +-- (put sports MP3s here)
+-- nature/
| +-- (put nature MP3s here)
+-- community/
| +-- (put community MP3s here)
+-- military/
+-- (put military MP3s here)
1. HTML (index.html)
This file creates the user interface: the player, controls, and forms.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web MP3 Player</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>MP3 Jukebox</h1>
<div class="player-box">
<h2>Player</h2>
<div class="form-group">
<label for="folder-select">Choose a List:</label>
<select id="folder-select"></select>
</div>
<audio id="audio-player" controls></audio>
<div id="now-playing">Now Playing: <span>...</span></div>
<div class="controls">
<button id="prev-btn">Previous</button>
<button id="play-pause-btn">Play</button>
<button id="next-btn">Next</button>
<div class="toggle-switch">
<input type="checkbox" id="random-toggle" class="checkbox">
<label for="random-toggle" class="label">
<span class="inner"></span>
<span class="switch"></span>
</label>
</div>
<label for="random-toggle" class="toggle-label">Random</label>
</div>
</div>
<div class="management-box">
<h2>Manage Music</h2>
<form id="upload-form" class="form-section">
<h3>Upload an MP3</h3>
<div class="form-group">
<label for="upload-folder-select">Upload to List:</label>
<select id="upload-folder-select"></select>
</div>
<div class="form-group">
<label for="mp3-file">Select MP3 File:</label>
<input type="file" id="mp3-file" name="mp3file" accept="audio/mpeg" required>
</div>
<button type="submit">Upload File</button>
<div id="upload-status" class="status-message"></div>
</form>
<form id="new-folder-form" class="form-section">
<h3>Add a New List (Folder)</h3>
<div class="form-group">
<label for="new-folder-name">New List Name:</label>
<input type="text" id="new-folder-name" name="new_folder_name" placeholder="e.g., 'ambient'" required>
</div>
<button type="submit">Create List</button>
<div id="folder-status" class="status-message"></div>
</form>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
2. CSS (style.css)
This provides styling to make the player functional and look clean.
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f0f2f5;
color: #333;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
box-sizing: border-box;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 20px;
width: 100%;
max-width: 900px;
}
.player-box, .management-box {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
padding: 25px;
flex: 1;
min-width: 300px;
}
.player-box {
flex-basis: 400px;
}
.management-box {
flex-basis: 300px;
display: flex;
flex-direction: column;
gap: 20px;
}
h1 {
text-align: center;
color: #1c1e21;
width: 100%;
margin-bottom: 10px;
}
h2 {
border-bottom: 2px solid #e0e0e0;
padding-bottom: 10px;
margin-top: 0;
}
h3 {
margin-top: 0;
color: #333;
}
.form-section {
border: 1px solid #ddd;
border-radius: 6px;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
select, input[type="text"], input[type="file"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
}
#audio-player {
width: 100%;
margin-top: 15px;
}
#now-playing {
margin-top: 10px;
font-style: italic;
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#now-playing span {
font-weight: bold;
color: #000;
}
.controls {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
}
.controls button {
background-color: #6c757d;
}
.controls button:hover {
background-color: #5a6268;
}
.status-message {
margin-top: 10px;
font-size: 0.9em;
}
.status-message.success {
color: green;
}
.status-message.error {
color: red;
}
/* Toggle Switch CSS */
.toggle-label {
font-weight: normal;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.toggle-switch .checkbox {
opacity: 0;
width: 0;
height: 0;
}
.label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.label .inner {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
.checkbox:checked + .label {
background-color: #007bff;
}
.checkbox:checked + .label .inner {
transform: translateX(22px);
}
3. JavaScript (script.js)
This file contains all the client-side logic to fetch data, handle user interactions, and control the player.
document.addEventListener('DOMContentLoaded', () => {
// Player elements
const folderSelect = document.getElementById('folder-select');
const audioPlayer = document.getElementById('audio-player');
const nowPlayingSpan = document.querySelector('#now-playing span');
const playPauseBtn = document.getElementById('play-pause-btn');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const randomToggle = document.getElementById('random-toggle');
// Upload form elements
const uploadForm = document.getElementById('upload-form');
const uploadFolderSelect = document.getElementById('upload-folder-select');
const mp3FileInput = document.getElementById('mp3-file');
const uploadStatus = document.getElementById('upload-status');
// New folder form elements
const newFolderForm = document.getElementById('new-folder-form');
const newFolderNameInput = document.getElementById('new-folder-name');
const folderStatus = document.getElementById('folder-status');
// Player state
let currentTracklist = [];
let currentTrackIndex = 0;
let isPlaying = false;
let isRandom = false;
// --- Initialization ---
/**
* Fetches the list of folders from the server and populates the dropdowns.
*/
async function loadFolders() {
try {
const response = await fetch('php/get_folders.php');
const folders = await response.json();
// Clear existing options
folderSelect.innerHTML = '<option value="">Select a list...</option>';
uploadFolderSelect.innerHTML = '<option value="">Select a list...</option>';
if (folders.error) {
throw new Error(folders.error);
}
folders.forEach(folder => {
const option1 = new Option(folder, folder);
const option2 = new Option(folder, folder);
folderSelect.add(option1);
uploadFolderSelect.add(option2);
});
} catch (error) {
console.error('Error loading folders:', error);
folderStatus.textContent = `Error loading folders: ${error.message}`;
folderStatus.className = 'status-message error';
}
}
/**
* Fetches the list of MP3s for a given folder.
*/
async function loadTracklist(folder) {
if (!folder) {
currentTracklist = [];
audioPlayer.src = '';
nowPlayingSpan.textContent = '...';
return;
}
try {
const response = await fetch(`php/get_files.php?folder=${encodeURIComponent(folder)}`);
const files = await response.json();
if (files.error) {
throw new Error(files.error);
}
currentTracklist = files;
currentTrackIndex = 0;
if (currentTracklist.length > 0) {
loadTrack(currentTrackIndex);
} else {
audioPlayer.src = '';
nowPlayingSpan.textContent = 'No tracks in this list.';
}
} catch (error) {
console.error('Error loading files:', error);
nowPlayingSpan.textContent = `Error: ${error.message}`;
}
}
/**
* Loads a specific track into the audio player.
*/
function loadTrack(index) {
if (currentTracklist.length === 0) return;
currentTrackIndex = index;
const trackPath = currentTracklist[currentTrackIndex];
const trackName = trackPath.split('/').pop(); // Get just the filename
audioPlayer.src = trackPath;
nowPlayingSpan.textContent = trackName;
if (isPlaying) {
audioPlayer.play();
}
}
// --- Player Controls ---
function playTrack() {
if (currentTracklist.length === 0) return;
audioPlayer.play();
isPlaying = true;
playPauseBtn.textContent = 'Pause';
}
function pauseTrack() {
audioPlayer.pause();
isPlaying = false;
playPauseBtn.textContent = 'Play';
}
function playPauseToggle() {
if (isPlaying) {
pauseTrack();
} else {
playTrack();
}
}
function nextTrack() {
if (currentTracklist.length === 0) return;
if (isRandom) {
// Play a random track, but not the same one twice in a row
let newIndex;
do {
newIndex = Math.floor(Math.random() * currentTracklist.length);
} while (currentTracklist.length > 1 && newIndex === currentTrackIndex);
currentTrackIndex = newIndex;
} else {
// Play next track in sequence
currentTrackIndex = (currentTrackIndex + 1) % currentTracklist.length;
}
loadTrack(currentTrackIndex);
playTrack();
}
function prevTrack() {
if (currentTracklist.length === 0) return;
if (isRandom) {
// Just play another random track
nextTrack();
} else {
// Play previous track in sequence
currentTrackIndex = (currentTrackIndex - 1 + currentTracklist.length) % currentTracklist.length;
loadTrack(currentTrackIndex);
playTrack();
}
}
// --- Form Handling ---
/**
* Handles the new folder creation form submission.
*/
async function handleNewFolderSubmit(event) {
event.preventDefault();
const folderName = newFolderNameInput.value.trim();
if (!folderName) {
folderStatus.textContent = 'Please enter a folder name.';
folderStatus.className = 'status-message error';
return;
}
const formData = new FormData();
formData.append('new_folder_name', folderName);
try {
const response = await fetch('php/create_folder.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
folderStatus.textContent = result.success;
folderStatus.className = 'status-message success';
newFolderNameInput.value = '';
await loadFolders(); // Refresh folder lists
} else {
throw new Error(result.error);
}
} catch (error) {
folderStatus.textContent = `Error: ${error.message}`;
folderStatus.className = 'status-message error';
}
}
/**
* Handles the MP3 upload form submission.
*/
async function handleUploadSubmit(event) {
event.preventDefault();
const folder = uploadFolderSelect.value;
const file = mp3FileInput.files[0];
if (!folder) {
uploadStatus.textContent = 'Please select a list to upload to.';
uploadStatus.className = 'status-message error';
return;
}
if (!file) {
uploadStatus.textContent = 'Please select an MP3 file.';
uploadStatus.className = 'status-message error';
return;
}
const formData = new FormData();
formData.append('folder', folder);
formData.append('mp3file', file);
uploadStatus.textContent = 'Uploading...';
uploadStatus.className = 'status-message';
try {
const response = await fetch('php/upload.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
uploadStatus.textContent = result.success;
uploadStatus.className = 'status-message success';
uploadForm.reset();
// If the upload was to the currently active folder, refresh the tracklist
if (folder === folderSelect.value) {
await loadTracklist(folder);
}
} else {
throw new Error(result.error);
}
} catch (error) {
uploadStatus.textContent = `Error: ${error.message}`;
uploadStatus.className = 'status-message error';
}
}
// --- Event Listeners ---
// Player
folderSelect.addEventListener('change', (e) => loadTracklist(e.target.value));
playPauseBtn.addEventListener('click', playPauseToggle);
nextBtn.addEventListener('click', nextTrack);
prevBtn.addEventListener('click', prevTrack);
randomToggle.addEventListener('change', (e) => {
isRandom = e.target.checked;
});
audioPlayer.addEventListener('ended', nextTrack); // Auto-play next track
audioPlayer.addEventListener('pause', () => {
isPlaying = false;
playPauseBtn.textContent = 'Play';
});
audioPlayer.addEventListener('play', () => {
isPlaying = true;
playPauseBtn.textContent = 'Pause';
});
// Forms
newFolderForm.addEventListener('submit', handleNewFolderSubmit);
uploadForm.addEventListener('submit', handleUploadSubmit);
// --- Initial Load ---
loadFolders();
});
4. PHP Scripts (in the php/ folder)
These scripts handle the server-side logic.
php/get_folders.php
Scans the music/ directory and returns a list of subfolders.
<?php
header('Content-Type: application/json');
$baseDir = '../music/';
$folders = [];
// Check if the base directory exists
if (!is_dir($baseDir)) {
echo json_encode(['error' => 'Base music directory not found.']);
exit;
}
// Scan the base directory for subdirectories
$items = scandir($baseDir);
foreach ($items as $item) {
// Ignore '.' and '..' and any files
if ($item[0] !== '.' && is_dir($baseDir . $item)) {
$folders[] = $item;
}
}
echo json_encode($folders);
?>
php/get_files.php
Scans a specific folder (e.g., music/nature/) and returns a list of MP3 files.
<?php
header('Content-Type: application/json');
$baseDir = '../music/';
// 1. Get and validate the folder parameter
if (!isset($_GET['folder']) || empty($_GET['folder'])) {
echo json_encode(['error' => 'No folder specified.']);
exit;
}
$folder = $_GET['folder'];
// 2. Security: Sanitize the folder name to prevent directory traversal
// This allows only alphanumeric characters, hyphens, and underscores
$safeFolder = preg_replace('/[^a-zA-Z0-9_-]/', '', $folder);
if ($safeFolder !== $folder) {
echo json_encode(['error' => 'Invalid folder name.']);
exit;
}
// 3. Construct the full path and check if it's a valid directory
$fullPath = $baseDir . $safeFolder;
if (!is_dir($fullPath)) {
echo json_encode(['error' => 'Folder not found.']);
exit;
}
// 4. Scan the directory for .mp3 files
$files = [];
$items = scandir($fullPath);
foreach ($items as $item) {
// Check for .mp3 extension
if (pathinfo($item, PATHINFO_EXTENSION) === 'mp3') {
// Return the web-accessible path
$files[] = 'music/' . $safeFolder . '/' . $item;
}
}
echo json_encode($files);
?>
php/create_folder.php
Creates a new, empty folder inside the music/ directory.
<?php
header('Content-Type: application/json');
$baseDir = '../music/';
$response = [];
// 1. Check if the form data is set
if (!isset($_POST['new_folder_name']) || empty(trim($_POST['new_folder_name']))) {
$response['error'] = 'Folder name is required.';
echo json_encode($response);
exit;
}
$folderName = trim($_POST['new_folder_name']);
// 2. Security: Sanitize the folder name
// Allow only alphanumeric, spaces, hyphens, and underscores
// Replace spaces with underscores for better compatibility
$folderName = str_replace(' ', '_', $folderName);
$safeFolder = preg_replace('/[^a-zA-Z0-9_-]/', '', $folderName);
if (empty($safeFolder)) {
$response['error'] = 'Invalid folder name. Use only letters, numbers, hyphens, and underscores.';
echo json_encode($response);
exit;
}
// 3. Construct the full path
$fullPath = $baseDir . $safeFolder;
// 4. Check if the directory already exists
if (is_dir($fullPath)) {
$response['error'] = 'A folder with this name already exists.';
} else {
// 5. Try to create the new directory
// 0755 permissions are standard (read/write/execute for owner, read/execute for group/others)
if (mkdir($fullPath, 0755, true)) {
$response['success'] = "Folder '$safeFolder' created successfully.";
} else {
$response['error'] = 'Failed to create the folder. Check server permissions.';
}
}
echo json_encode($response);
?>
php/upload.php
Handles the file upload, validates it, and moves it to the correct folder.
<?php
header('Content-Type: application/json');
$baseDir = '../music/';
$response = [];
// 1. Check for basic errors
if (!isset($_FILES['mp3file']) || $_FILES['mp3file']['error'] !== UPLOAD_ERR_OK) {
$response['error'] = 'File upload error. Code: ' . $_FILES['mp3file']['error'];
echo json_encode($response);
exit;
}
if (!isset($_POST['folder']) || empty($_POST['folder'])) {
$response['error'] = 'No destination folder specified.';
echo json_encode($response);
exit;
}
// 2. File and folder data
$file = $_FILES['mp3file'];
$folder = $_POST['folder'];
// 3. Security: Sanitize the destination folder name
$safeFolder = preg_replace('/[^a-zA-Z0-9_-]/', '', $folder);
$destinationDir = $baseDir . $safeFolder;
if ($safeFolder !== $folder || !is_dir($destinationDir)) {
$response['error'] = 'Invalid or non-existent destination folder.';
echo json_encode($response);
exit;
}
// 4. Validate file type (check MIME type)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if ($mimeType !== 'audio/mpeg') {
$response['error'] = 'Invalid file type. Only MP3 files (audio/mpeg) are allowed.';
echo json_encode($response);
exit;
}
// 5. Validate file size (e.g., max 20MB)
$maxSize = 20 * 1024 * 1024; // 20 MB
if ($file['size'] > $maxSize) {
$response['error'] = 'File is too large. Maximum size is 20MB.';
echo json_encode($response);
exit;
}
// 6. Security: Sanitize the filename
$originalName = $file['name'];
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
$filename = pathinfo($originalName, PATHINFO_FILENAME);
// Remove special characters, replace spaces with underscores
$safeFilename = preg_replace('/[^a-zA-Z0-9_-]/', '', str_replace(' ', '_', $filename));
$safeFilename = $safeFilename . '.' . $extension;
// 7. Construct final destination path
$destinationPath = $destinationDir . '/' . $safeFilename;
// 8. Prevent overwriting existing files
$i = 1;
$baseFilename = pathinfo($safeFilename, PATHINFO_FILENAME);
while (file_exists($destinationPath)) {
$safeFilename = $baseFilename . '_' . $i . '.' . $extension;
$destinationPath = $destinationDir . '/' . $safeFilename;
$i++;
}
// 9. Move the file
if (move_uploaded_file($file['tmp_name'], $destinationPath)) {
$response['success'] = "File '$safeFilename' uploaded successfully to '$safeFolder'.";
} else {
$response['error'] = 'Failed to move the uploaded file. Check server permissions for the music folder.';
}
echo json_encode($response);
?>
Would you like me to explain any of these sections in more detail?
Add a list with radio buttons, partitioned by folder, list the songs available to play. When the user clicks on a radio button that song should begin to play
Here are the updates to integrate the clickable song list.
I've provided the new and modified sections for each file. You just need to add/replace the code in your existing files.
1. HTML (index.html)
Add the new song list container inside the player-box, right after the controls div.
<div class="controls">
</div>
<div class="song-list-container">
<h3>Tracks</h3>
<div id="song-list">
<em class="empty-list-message">Select a list to see tracks...</em>
</div>
</div>
</div>
```
---
### 2. CSS (`style.css`)
Add these new styles to the end of your `style.css` file to format the track list.
```css
/* ... at the end of style.css ... */
.song-list-container {
margin-top: 20px;
}
.song-list-container h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.1em;
color: #333;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
#song-list {
max-height: 280px; /* Adjust as needed */
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fdfdfd;
}
.song-item {
display: block;
padding: 10px 12px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
}
.song-item:last-child {
border-bottom: none;
}
.song-item:hover {
background-color: #f5f5f5;
}
.song-item input[type="radio"] {
margin-right: 10px;
vertical-align: middle;
/* Make the radio button easier to click */
transform: scale(1.1);
}
.song-item label {
display: inline; /* Keep it next to the radio */
font-weight: normal; /* Override the default bold label style */
cursor: pointer;
vertical-align: middle;
/* Prevent text selection when clicking */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.empty-list-message {
display: block;
padding: 20px;
text-align: center;
color: #888;
font-style: italic;
}
3. JavaScript (script.js)
This involves three changes:
Add the new
songListDivelement to your variables.Update the
loadTracklistfunction to build the radio buttons.Update the
loadTrackfunction to check the correct radio button when the song changes.
// ... near the top with other "Player elements" ...
const songListDiv = document.getElementById('song-list'); // ◄◄◄ ADD THIS
// ...
/**
* Fetches the list of MP3s for a given folder.
*/
// ▼▼▼ REPLACE this entire function ▼▼▼
async function loadTracklist(folder) {
// Clear old list
songListDiv.innerHTML = '<em class="empty-list-message">Loading...</em>';
if (!folder) {
currentTracklist = [];
audioPlayer.src = '';
nowPlayingSpan.textContent = '...';
songListDiv.innerHTML = '<em class="empty-list-message">Select a list to see tracks...</em>';
return;
}
try {
const response = await fetch(`php/get_files.php?folder=${encodeURIComponent(folder)}`);
const files = await response.json();
if (files.error) {
throw new Error(files.error);
}
currentTracklist = files;
currentTrackIndex = 0;
songListDiv.innerHTML = ''; // Clear "Loading..."
if (currentTracklist.length > 0) {
// Populate the song list UI
currentTracklist.forEach((trackPath, index) => {
const trackName = trackPath.split('/').pop();
const songItem = document.createElement('div');
songItem.className = 'song-item';
const radioBtn = document.createElement('input');
radioBtn.type = 'radio';
radioBtn.name = 'song-track';
radioBtn.id = `track-${index}`;
radioBtn.value = index;
const label = document.createElement('label');
label.htmlFor = `track-${index}`;
label.textContent = trackName;
// *** This is the new functionality ***
// Add listener to play the song when radio is clicked
radioBtn.addEventListener('click', () => {
loadTrack(index); // Load the track
playTrack(); // And play it immediately
});
// Also allow clicking the label
label.addEventListener('click', (e) => {
// This just makes sure the radio click fires
// in case the 'for' attribute is slow.
if (!radioBtn.checked) {
radioBtn.click();
}
});
songItem.appendChild(radioBtn);
songItem.appendChild(label);
songListDiv.appendChild(songItem);
});
// Load the first track (this will also check the first radio button)
loadTrack(currentTrackIndex);
} else {
audioPlayer.src = '';
nowPlayingSpan.textContent = 'No tracks in this list.';
songListDiv.innerHTML = '<em class="empty-list-message">No tracks found in this list.</em>';
}
} catch (error) {
console.error('Error loading files:', error);
nowPlayingSpan.textContent = `Error: ${error.message}`;
songListDiv.innerHTML = `<em class"empty-list-message">Error loading tracks.</em>`;
}
}
// ▲▲▲ END REPLACED FUNCTION ▲▲▲
/**
* Loads a specific track into the audio player.
*/
// ▼▼▼ REPLACE this entire function ▼▼▼
function loadTrack(index) {
if (currentTracklist.length === 0) return;
currentTrackIndex = index;
const trackPath = currentTracklist[currentTrackIndex];
const trackName = trackPath.split('/').pop(); // Get just the filename
audioPlayer.src = trackPath;
nowPlayingSpan.textContent = trackName;
// ** NEW: Update the radio button selection **
const radioToSelect = document.getElementById(`track-${index}`);
if (radioToSelect) {
radioToSelect.checked = true;
}
if (isPlaying) {
audioPlayer.play();
}
}
// ▲▲▲ END REPLACED FUNCTION ▲▲▲
That's it! When you select a folder, loadTracklist will now fetch the files and build the list of radio buttons. Clicking any radio button will call loadTrack and playTrack to start it immediately. The "Next" and "Prev" buttons will also correctly update which radio button is checked.
From <https://gemini.google.com/app/1e9f9f08f0a5e965> Google Gemini 2.5 Pro
Comments
Post a Comment