440 lines
15 KiB
Plaintext
440 lines
15 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<title data-text="title">Asteroid Radio - Web Player</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🎵 WEB PLAYER</h1>
|
||
<div class="nav">
|
||
<a href="/asteroid/">← Back to Main</a>
|
||
<a href="/asteroid/admin">Admin Dashboard</a>
|
||
</div>
|
||
|
||
<!-- Live Stream Section -->
|
||
<div class="player-section">
|
||
<h2>🔴 Live Radio Stream</h2>
|
||
<div class="live-player">
|
||
<p><strong>Now Playing:</strong> <span id="live-now-playing">Loading...</span></p>
|
||
<p><strong>Listeners:</strong> <span id="live-listeners">0</span></p>
|
||
<audio controls style="width: 100%; margin: 10px 0;">
|
||
<source src="http://localhost:8000/asteroid.mp3" type="audio/mpeg">
|
||
Your browser does not support the audio element.
|
||
</audio>
|
||
<p><em>Listen to the live Asteroid Radio stream</em></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Track Browser -->
|
||
<div class="player-section">
|
||
<h2>Personal Track Library</h2>
|
||
<div class="track-browser">
|
||
<input type="text" id="search-tracks" placeholder="Search tracks..." class="search-input">
|
||
<div id="track-list" class="track-list">
|
||
<div class="loading">Loading tracks...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audio Player Widget -->
|
||
<div class="player-section">
|
||
<h2>Audio Player</h2>
|
||
<div class="audio-player">
|
||
<div class="now-playing">
|
||
<div class="track-art">🎵</div>
|
||
<div class="track-details">
|
||
<div class="track-title" id="current-title">No track selected</div>
|
||
<div class="track-artist" id="current-artist">Unknown Artist</div>
|
||
<div class="track-album" id="current-album">Unknown Album</div>
|
||
</div>
|
||
</div>
|
||
|
||
<audio id="audio-player" controls preload="none" style="width: 100%; margin: 20px 0;">
|
||
Your browser does not support the audio element.
|
||
</audio>
|
||
|
||
<div class="player-controls">
|
||
<button id="prev-btn" class="btn btn-secondary">⏮️ Previous</button>
|
||
<button id="play-pause-btn" class="btn btn-primary">▶️ Play</button>
|
||
<button id="next-btn" class="btn btn-secondary">⏭️ Next</button>
|
||
<button id="shuffle-btn" class="btn btn-info">🔀 Shuffle</button>
|
||
<button id="repeat-btn" class="btn btn-warning">🔁 Repeat</button>
|
||
</div>
|
||
|
||
<div class="player-info">
|
||
<div class="time-display">
|
||
<span id="current-time">0:00</span> / <span id="total-time">0:00</span>
|
||
</div>
|
||
<div class="volume-control">
|
||
<label for="volume-slider">🔊</label>
|
||
<input type="range" id="volume-slider" min="0" max="100" value="50" class="volume-slider">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Playlist Management -->
|
||
<div class="player-section">
|
||
<h2>Playlists</h2>
|
||
<div class="playlist-controls">
|
||
<input type="text" id="new-playlist-name" placeholder="New playlist name..." class="playlist-input">
|
||
<button id="create-playlist" class="btn btn-success">➕ Create Playlist</button>
|
||
</div>
|
||
|
||
<div class="playlist-list">
|
||
<div id="playlists-container">
|
||
<div class="no-playlists">No playlists created yet.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Queue -->
|
||
<div class="player-section">
|
||
<h2>Play Queue</h2>
|
||
<div class="queue-controls">
|
||
<button id="clear-queue" class="btn btn-danger">🗑️ Clear Queue</button>
|
||
<button id="save-queue" class="btn btn-info">💾 Save as Playlist</button>
|
||
</div>
|
||
<div id="play-queue" class="play-queue">
|
||
<div class="empty-queue">Queue is empty</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Web Player JavaScript
|
||
let tracks = [];
|
||
let currentTrack = null;
|
||
let currentTrackIndex = -1;
|
||
let playQueue = [];
|
||
let isShuffled = false;
|
||
let isRepeating = false;
|
||
let audioPlayer = null;
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
audioPlayer = document.getElementById('audio-player');
|
||
loadTracks();
|
||
setupEventListeners();
|
||
updatePlayerDisplay();
|
||
});
|
||
|
||
function setupEventListeners() {
|
||
// Search
|
||
document.getElementById('search-tracks').addEventListener('input', filterTracks);
|
||
|
||
// Player controls
|
||
document.getElementById('play-pause-btn').addEventListener('click', togglePlayPause);
|
||
document.getElementById('prev-btn').addEventListener('click', playPrevious);
|
||
document.getElementById('next-btn').addEventListener('click', playNext);
|
||
document.getElementById('shuffle-btn').addEventListener('click', toggleShuffle);
|
||
document.getElementById('repeat-btn').addEventListener('click', toggleRepeat);
|
||
|
||
// Volume control
|
||
document.getElementById('volume-slider').addEventListener('input', updateVolume);
|
||
|
||
// Audio player events
|
||
if (audioPlayer) {
|
||
audioPlayer.addEventListener('loadedmetadata', updateTimeDisplay);
|
||
audioPlayer.addEventListener('timeupdate', updateTimeDisplay);
|
||
audioPlayer.addEventListener('ended', handleTrackEnd);
|
||
audioPlayer.addEventListener('play', () => updatePlayButton('⏸️ Pause'));
|
||
audioPlayer.addEventListener('pause', () => updatePlayButton('▶️ Play'));
|
||
}
|
||
|
||
// Playlist controls
|
||
document.getElementById('create-playlist').addEventListener('click', createPlaylist);
|
||
document.getElementById('clear-queue').addEventListener('click', clearQueue);
|
||
document.getElementById('save-queue').addEventListener('click', saveQueueAsPlaylist);
|
||
}
|
||
|
||
async function loadTracks() {
|
||
try {
|
||
const response = await fetch('/asteroid/api/tracks');
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success') {
|
||
tracks = data.tracks || [];
|
||
displayTracks(tracks);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading tracks:', error);
|
||
document.getElementById('track-list').innerHTML = '<div class="error">Error loading tracks</div>';
|
||
}
|
||
}
|
||
|
||
function displayTracks(trackList) {
|
||
const container = document.getElementById('track-list');
|
||
|
||
if (trackList.length === 0) {
|
||
container.innerHTML = '<div class="no-tracks">No tracks found</div>';
|
||
return;
|
||
}
|
||
|
||
const tracksHtml = trackList.map((track, index) => `
|
||
<div class="track-item" data-track-id="${track.id}" data-index="${index}">
|
||
<div class="track-info">
|
||
<div class="track-title">${track.title[0] || 'Unknown Title'}</div>
|
||
<div class="track-meta">${track.artist[0] || 'Unknown Artist'} • ${track.album[0] || 'Unknown Album'}</div>
|
||
</div>
|
||
<div class="track-actions">
|
||
<button onclick="playTrack(${index})" class="btn btn-sm btn-success">▶️</button>
|
||
<button onclick="addToQueue(${index})" class="btn btn-sm btn-info">➕</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
container.innerHTML = tracksHtml;
|
||
}
|
||
|
||
function filterTracks() {
|
||
const query = document.getElementById('search-tracks').value.toLowerCase();
|
||
const filtered = tracks.filter(track =>
|
||
(track.title[0] || '').toLowerCase().includes(query) ||
|
||
(track.artist[0] || '').toLowerCase().includes(query) ||
|
||
(track.album[0] || '').toLowerCase().includes(query)
|
||
);
|
||
displayTracks(filtered);
|
||
}
|
||
|
||
function playTrack(index) {
|
||
if (index < 0 || index >= tracks.length) return;
|
||
|
||
currentTrack = tracks[index];
|
||
currentTrackIndex = index;
|
||
|
||
// Load track into audio player
|
||
audioPlayer.src = `/asteroid/tracks/${currentTrack.id}/stream`;
|
||
audioPlayer.load();
|
||
audioPlayer.play().catch(error => {
|
||
console.error('Playback error:', error);
|
||
alert('Error playing track. The track may not be available.');
|
||
});
|
||
|
||
updatePlayerDisplay();
|
||
|
||
// Update server-side player state
|
||
fetch(`/api/play?track-id=${currentTrack.id}`, { method: 'POST' })
|
||
.catch(error => console.error('API update error:', error));
|
||
}
|
||
|
||
function togglePlayPause() {
|
||
if (!currentTrack) {
|
||
alert('Please select a track to play');
|
||
return;
|
||
}
|
||
|
||
if (audioPlayer.paused) {
|
||
audioPlayer.play();
|
||
} else {
|
||
audioPlayer.pause();
|
||
}
|
||
}
|
||
|
||
function playPrevious() {
|
||
if (playQueue.length > 0) {
|
||
// Play from queue
|
||
const prevIndex = Math.max(0, currentTrackIndex - 1);
|
||
playTrack(prevIndex);
|
||
} else {
|
||
// Play previous track in library
|
||
const prevIndex = currentTrackIndex > 0 ? currentTrackIndex - 1 : tracks.length - 1;
|
||
playTrack(prevIndex);
|
||
}
|
||
}
|
||
|
||
function playNext() {
|
||
if (playQueue.length > 0) {
|
||
// Play from queue
|
||
const nextTrack = playQueue.shift();
|
||
playTrack(tracks.findIndex(t => t.id === nextTrack.id));
|
||
updateQueueDisplay();
|
||
} else {
|
||
// Play next track in library
|
||
const nextIndex = isShuffled ?
|
||
Math.floor(Math.random() * tracks.length) :
|
||
(currentTrackIndex + 1) % tracks.length;
|
||
playTrack(nextIndex);
|
||
}
|
||
}
|
||
|
||
function handleTrackEnd() {
|
||
if (isRepeating) {
|
||
audioPlayer.currentTime = 0;
|
||
audioPlayer.play();
|
||
} else {
|
||
playNext();
|
||
}
|
||
}
|
||
|
||
function toggleShuffle() {
|
||
isShuffled = !isShuffled;
|
||
const btn = document.getElementById('shuffle-btn');
|
||
btn.textContent = isShuffled ? '🔀 Shuffle ON' : '🔀 Shuffle';
|
||
btn.classList.toggle('active', isShuffled);
|
||
}
|
||
|
||
function toggleRepeat() {
|
||
isRepeating = !isRepeating;
|
||
const btn = document.getElementById('repeat-btn');
|
||
btn.textContent = isRepeating ? '🔁 Repeat ON' : '🔁 Repeat';
|
||
btn.classList.toggle('active', isRepeating);
|
||
}
|
||
|
||
function updateVolume() {
|
||
const volume = document.getElementById('volume-slider').value / 100;
|
||
if (audioPlayer) {
|
||
audioPlayer.volume = volume;
|
||
}
|
||
}
|
||
|
||
function updateTimeDisplay() {
|
||
const current = formatTime(audioPlayer.currentTime);
|
||
const total = formatTime(audioPlayer.duration);
|
||
document.getElementById('current-time').textContent = current;
|
||
document.getElementById('total-time').textContent = total;
|
||
}
|
||
|
||
function formatTime(seconds) {
|
||
if (isNaN(seconds)) return '0:00';
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = Math.floor(seconds % 60);
|
||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
function updatePlayButton(text) {
|
||
document.getElementById('play-pause-btn').textContent = text;
|
||
}
|
||
|
||
function updatePlayerDisplay() {
|
||
if (currentTrack) {
|
||
document.getElementById('current-title').textContent = currentTrack.title[0] || 'Unknown Title';
|
||
document.getElementById('current-artist').textContent = currentTrack.artist[0] || 'Unknown Artist';
|
||
document.getElementById('current-album').textContent = currentTrack.album[0] || 'Unknown Album';
|
||
}
|
||
}
|
||
|
||
function addToQueue(index) {
|
||
if (index < 0 || index >= tracks.length) return;
|
||
|
||
playQueue.push(tracks[index]);
|
||
updateQueueDisplay();
|
||
}
|
||
|
||
function updateQueueDisplay() {
|
||
const container = document.getElementById('play-queue');
|
||
|
||
if (playQueue.length === 0) {
|
||
container.innerHTML = '<div class="empty-queue">Queue is empty</div>';
|
||
return;
|
||
}
|
||
|
||
const queueHtml = playQueue.map((track, index) => `
|
||
<div class="queue-item">
|
||
<div class="track-info">
|
||
<div class="track-title">${track.title[0] || 'Unknown Title'}</div>
|
||
<div class="track-meta">${track.artist[0] || 'Unknown Artist'}</div>
|
||
</div>
|
||
<button onclick="removeFromQueue(${index})" class="btn btn-sm btn-danger">✖️</button>
|
||
</div>
|
||
`).join('');
|
||
|
||
container.innerHTML = queueHtml;
|
||
}
|
||
|
||
function removeFromQueue(index) {
|
||
playQueue.splice(index, 1);
|
||
updateQueueDisplay();
|
||
}
|
||
|
||
function clearQueue() {
|
||
playQueue = [];
|
||
updateQueueDisplay();
|
||
}
|
||
|
||
function createPlaylist() {
|
||
const name = document.getElementById('new-playlist-name').value.trim();
|
||
if (!name) {
|
||
alert('Please enter a playlist name');
|
||
return;
|
||
}
|
||
|
||
// TODO: Implement playlist creation API
|
||
alert('Playlist creation not yet implemented');
|
||
document.getElementById('new-playlist-name').value = '';
|
||
}
|
||
|
||
function saveQueueAsPlaylist() {
|
||
if (playQueue.length === 0) {
|
||
alert('Queue is empty');
|
||
return;
|
||
}
|
||
|
||
const name = prompt('Enter playlist name:');
|
||
if (name) {
|
||
// TODO: Implement save queue as playlist
|
||
alert('Save queue as playlist not yet implemented');
|
||
}
|
||
}
|
||
|
||
// Initialize volume
|
||
updateVolume();
|
||
|
||
// Live stream functionality
|
||
function updateLiveStream() {
|
||
try {
|
||
fetch('/asteroid/api/icecast-status')
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
console.log('Live stream data:', data); // Debug log
|
||
|
||
if (data.icestats && data.icestats.source) {
|
||
const sources = Array.isArray(data.icestats.source) ? data.icestats.source : [data.icestats.source];
|
||
const mainStream = sources.find(s => s.listenurl && s.listenurl.includes('asteroid.mp3'));
|
||
|
||
if (mainStream && mainStream.title) {
|
||
const titleParts = mainStream.title.split(' - ');
|
||
const artist = titleParts.length > 1 ? titleParts[0] : 'Unknown Artist';
|
||
const track = titleParts.length > 1 ? titleParts.slice(1).join(' - ') : mainStream.title;
|
||
|
||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||
const listenersEl = document.getElementById('live-listeners');
|
||
|
||
if (nowPlayingEl) nowPlayingEl.textContent = `${artist} - ${track}`;
|
||
if (listenersEl) listenersEl.textContent = mainStream.listeners || '0';
|
||
|
||
console.log('Updated live stream info:', `${artist} - ${track}`, 'Listeners:', mainStream.listeners);
|
||
} else {
|
||
console.log('No main stream found or no title');
|
||
}
|
||
} else {
|
||
console.log('No icestats or source in response');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Live stream fetch error:', error);
|
||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream unavailable';
|
||
});
|
||
} catch (error) {
|
||
console.error('Live stream update error:', error);
|
||
}
|
||
}
|
||
|
||
// Update live stream info every 10 seconds
|
||
setTimeout(updateLiveStream, 1000); // Initial update after 1 second
|
||
setInterval(updateLiveStream, 10000);
|
||
</script>
|
||
</body>
|
||
</html>
|