asteroid/template/player.chtml

440 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>