- Added hunt/feed duck mechanics (80% hunt, 20% feed) - Implemented persistent scoring system - Added channel control commands (\!stopducks/\!startducks) - Enhanced duck hunt with wrong action penalties - Organized bot structure with botmain.js as main file - Added comprehensive documentation (README.md) - Included 17 plugins with various games and utilities 🦆 Duck Hunt Features: - Hunt ducks with \!shoot/\!bang (80% of spawns) - Feed ducks with \!feed (20% of spawns) - Persistent scores saved to JSON - Channel-specific controls for #bakedbeans - Reaction time tracking and special achievements 🎮 Other Games: - Casino games (slots, coinflip, hi-lo, scratch cards) - Multiplayer games (pigs, zombie dice, quiplash) - Text generation (babble, conspiracy, drunk historian) - Interactive features (story writing, emojify, combos) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
509 lines
No EOL
16 KiB
HTML
509 lines
No EOL
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Server Music Player</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
|
min-height: 100vh;
|
|
color: white;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 800px;
|
|
width: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
border-radius: 20px;
|
|
padding: 40px;
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
|
backdrop-filter: blur(10px);
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
h1 {
|
|
font-size: 3em;
|
|
margin-bottom: 10px;
|
|
text-align: center;
|
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.subtitle {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
opacity: 0.8;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.player-container {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 15px;
|
|
padding: 30px;
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.track-info {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.track-title {
|
|
font-size: 2.2em;
|
|
margin-bottom: 10px;
|
|
color: #4ecdc4;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.track-details {
|
|
font-size: 1.1em;
|
|
opacity: 0.8;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.audio-player {
|
|
width: 100%;
|
|
margin: 20px 0;
|
|
border-radius: 10px;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 15px;
|
|
margin: 30px 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.control-btn {
|
|
background: linear-gradient(45deg, #4ecdc4, #44a08d);
|
|
color: white;
|
|
padding: 15px 25px;
|
|
border: none;
|
|
border-radius: 25px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
font-size: 1em;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.control-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
|
background: linear-gradient(45deg, #44a08d, #4ecdc4);
|
|
}
|
|
|
|
.control-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.status {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin: 20px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.status.loading {
|
|
color: #ffd700;
|
|
}
|
|
|
|
.status.error {
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
.status.success {
|
|
color: #4ecdc4;
|
|
}
|
|
|
|
.playlist {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
margin-top: 30px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.playlist h3 {
|
|
color: #4ecdc4;
|
|
margin-bottom: 15px;
|
|
text-align: center;
|
|
}
|
|
|
|
.track-item {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-bottom: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
border-left: 4px solid transparent;
|
|
}
|
|
|
|
.track-item:hover {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-left-color: #4ecdc4;
|
|
}
|
|
|
|
.track-item.current {
|
|
background: rgba(78, 205, 196, 0.3);
|
|
border-left-color: #4ecdc4;
|
|
}
|
|
|
|
.visualizer {
|
|
height: 60px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 10px;
|
|
margin: 20px 0;
|
|
display: flex;
|
|
align-items: end;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.bar {
|
|
width: 4px;
|
|
background: linear-gradient(to top, #4ecdc4, #667eea);
|
|
border-radius: 2px;
|
|
transition: height 0.1s ease;
|
|
height: 5px;
|
|
}
|
|
|
|
.settings {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.settings h4 {
|
|
color: #4ecdc4;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.setting-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.setting-input {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 5px;
|
|
padding: 8px 12px;
|
|
color: white;
|
|
width: 200px;
|
|
}
|
|
|
|
.setting-input:focus {
|
|
outline: none;
|
|
border-color: #4ecdc4;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.container { padding: 20px; }
|
|
h1 { font-size: 2em; }
|
|
.track-title { font-size: 1.5em; }
|
|
.controls { flex-direction: column; align-items: center; }
|
|
.control-btn { width: 200px; }
|
|
.setting-item { flex-direction: column; gap: 10px; }
|
|
.setting-input { width: 100%; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🎵 Server Music Player</h1>
|
|
<p class="subtitle">Random FLAC player from server directory</p>
|
|
|
|
<div class="settings">
|
|
<h4>⚙️ Server Configuration</h4>
|
|
<div class="setting-item">
|
|
<label>Server URL:</label>
|
|
<input type="text" id="serverUrl" class="setting-input" value="http://localhost:3000" placeholder="http://localhost:3000">
|
|
</div>
|
|
<div class="setting-item">
|
|
<label>Music Directory:</label>
|
|
<input type="text" id="musicDir" class="setting-input" value="/music" placeholder="/path/to/music">
|
|
</div>
|
|
<button class="control-btn" onclick="connectToServer()">🔗 Connect</button>
|
|
</div>
|
|
|
|
<div class="status" id="status">
|
|
Click "Connect" to start browsing your music library
|
|
</div>
|
|
|
|
<div class="player-container" id="playerContainer" style="display: none;">
|
|
<div class="track-info">
|
|
<div class="track-title" id="trackTitle">No track loaded</div>
|
|
<div class="track-details" id="trackPath">Path: N/A</div>
|
|
<div class="track-details" id="trackSize">Size: N/A</div>
|
|
<div class="track-details" id="trackFormat">Format: FLAC</div>
|
|
</div>
|
|
|
|
<div class="visualizer" id="visualizer"></div>
|
|
|
|
<audio id="audioPlayer" class="audio-player" controls>
|
|
Your browser does not support the audio element.
|
|
</audio>
|
|
|
|
<div class="controls">
|
|
<button class="control-btn" onclick="previousTrack()">⏮️ Previous</button>
|
|
<button class="control-btn" onclick="getRandomTrack()">🎲 Random</button>
|
|
<button class="control-btn" onclick="nextTrack()">⏭️ Next</button>
|
|
<button class="control-btn" onclick="refreshLibrary()">🔄 Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="playlist" id="playlist" style="display: none;">
|
|
<h3>🎵 Music Library</h3>
|
|
<div id="trackList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let musicLibrary = [];
|
|
let currentTrackIndex = 0;
|
|
let audioPlayer = null;
|
|
let serverUrl = 'http://localhost:3000';
|
|
let musicDirectory = '/music';
|
|
|
|
function updateStatus(message, type = 'info') {
|
|
const status = document.getElementById('status');
|
|
status.textContent = message;
|
|
status.className = `status ${type}`;
|
|
}
|
|
|
|
async function connectToServer() {
|
|
serverUrl = document.getElementById('serverUrl').value;
|
|
musicDirectory = document.getElementById('musicDir').value;
|
|
|
|
updateStatus('Connecting to server...', 'loading');
|
|
|
|
try {
|
|
const response = await fetch(`${serverUrl}/api/tracks?dir=${encodeURIComponent(musicDirectory)}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server responded with ${response.status}`);
|
|
}
|
|
|
|
musicLibrary = await response.json();
|
|
|
|
if (musicLibrary.length === 0) {
|
|
updateStatus('No FLAC files found in the specified directory', 'error');
|
|
return;
|
|
}
|
|
|
|
updateStatus(`Connected! Found ${musicLibrary.length} tracks`, 'success');
|
|
document.getElementById('playerContainer').style.display = 'block';
|
|
document.getElementById('playlist').style.display = 'block';
|
|
|
|
setupPlayer();
|
|
generatePlaylist();
|
|
getRandomTrack();
|
|
|
|
} catch (error) {
|
|
updateStatus(`Connection failed: ${error.message}`, 'error');
|
|
console.error('Connection error:', error);
|
|
}
|
|
}
|
|
|
|
function setupPlayer() {
|
|
audioPlayer = document.getElementById('audioPlayer');
|
|
|
|
audioPlayer.addEventListener('loadstart', () => {
|
|
document.getElementById('trackTitle').textContent = 'Loading...';
|
|
});
|
|
|
|
audioPlayer.addEventListener('loadedmetadata', () => {
|
|
updateTrackInfo();
|
|
animateVisualizer();
|
|
});
|
|
|
|
audioPlayer.addEventListener('ended', () => {
|
|
nextTrack();
|
|
});
|
|
|
|
audioPlayer.addEventListener('play', () => {
|
|
animateVisualizer();
|
|
});
|
|
|
|
audioPlayer.addEventListener('pause', () => {
|
|
stopVisualizer();
|
|
});
|
|
|
|
createVisualizer();
|
|
}
|
|
|
|
function generatePlaylist() {
|
|
const trackList = document.getElementById('trackList');
|
|
trackList.innerHTML = '';
|
|
|
|
musicLibrary.forEach((track, index) => {
|
|
const trackItem = document.createElement('div');
|
|
trackItem.className = 'track-item';
|
|
trackItem.onclick = () => playTrack(index);
|
|
|
|
trackItem.innerHTML = `
|
|
<div><strong>${track.name}</strong></div>
|
|
<div style="font-size: 0.9em; opacity: 0.7;">${track.path}</div>
|
|
<div style="font-size: 0.8em; opacity: 0.6;">Size: ${(track.size / (1024 * 1024)).toFixed(1)} MB</div>
|
|
`;
|
|
|
|
trackList.appendChild(trackItem);
|
|
});
|
|
}
|
|
|
|
async function playTrack(index) {
|
|
if (index < 0 || index >= musicLibrary.length) return;
|
|
|
|
currentTrackIndex = index;
|
|
const track = musicLibrary[index];
|
|
|
|
updateStatus('Loading track...', 'loading');
|
|
|
|
try {
|
|
// Get the audio stream URL from server
|
|
const streamUrl = `${serverUrl}/api/stream/${encodeURIComponent(track.path)}`;
|
|
audioPlayer.src = streamUrl;
|
|
|
|
updateTrackInfo();
|
|
updatePlaylistHighlight();
|
|
updateStatus('Track loaded successfully', 'success');
|
|
|
|
} catch (error) {
|
|
updateStatus(`Failed to load track: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function updateTrackInfo() {
|
|
const track = musicLibrary[currentTrackIndex];
|
|
document.getElementById('trackTitle').textContent = track.name;
|
|
document.getElementById('trackPath').textContent = `Path: ${track.path}`;
|
|
document.getElementById('trackSize').textContent = `Size: ${(track.size / (1024 * 1024)).toFixed(1)} MB`;
|
|
document.getElementById('trackFormat').textContent = 'Format: FLAC';
|
|
}
|
|
|
|
function updatePlaylistHighlight() {
|
|
const trackItems = document.querySelectorAll('.track-item');
|
|
trackItems.forEach((item, index) => {
|
|
item.classList.toggle('current', index === currentTrackIndex);
|
|
});
|
|
}
|
|
|
|
function getRandomTrack() {
|
|
if (musicLibrary.length === 0) return;
|
|
|
|
let randomIndex;
|
|
do {
|
|
randomIndex = Math.floor(Math.random() * musicLibrary.length);
|
|
} while (randomIndex === currentTrackIndex && musicLibrary.length > 1);
|
|
|
|
playTrack(randomIndex);
|
|
}
|
|
|
|
function nextTrack() {
|
|
if (musicLibrary.length === 0) return;
|
|
const nextIndex = (currentTrackIndex + 1) % musicLibrary.length;
|
|
playTrack(nextIndex);
|
|
}
|
|
|
|
function previousTrack() {
|
|
if (musicLibrary.length === 0) return;
|
|
const prevIndex = currentTrackIndex > 0 ? currentTrackIndex - 1 : musicLibrary.length - 1;
|
|
playTrack(prevIndex);
|
|
}
|
|
|
|
function refreshLibrary() {
|
|
connectToServer();
|
|
}
|
|
|
|
function createVisualizer() {
|
|
const visualizer = document.getElementById('visualizer');
|
|
visualizer.innerHTML = '';
|
|
|
|
for (let i = 0; i < 50; i++) {
|
|
const bar = document.createElement('div');
|
|
bar.className = 'bar';
|
|
visualizer.appendChild(bar);
|
|
}
|
|
}
|
|
|
|
function animateVisualizer() {
|
|
const bars = document.querySelectorAll('.bar');
|
|
|
|
function animate() {
|
|
if (audioPlayer && !audioPlayer.paused) {
|
|
bars.forEach((bar, index) => {
|
|
const height = Math.random() * 40 + 5;
|
|
bar.style.height = height + 'px';
|
|
bar.style.animationDelay = `${index * 0.05}s`;
|
|
});
|
|
setTimeout(() => requestAnimationFrame(animate), 100);
|
|
}
|
|
}
|
|
animate();
|
|
}
|
|
|
|
function stopVisualizer() {
|
|
const bars = document.querySelectorAll('.bar');
|
|
bars.forEach(bar => {
|
|
bar.style.height = '5px';
|
|
});
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.target.tagName !== 'INPUT') {
|
|
switch(e.code) {
|
|
case 'Space':
|
|
e.preventDefault();
|
|
if (audioPlayer && audioPlayer.src) {
|
|
if (audioPlayer.paused) {
|
|
audioPlayer.play();
|
|
} else {
|
|
audioPlayer.pause();
|
|
}
|
|
}
|
|
break;
|
|
case 'ArrowRight':
|
|
e.preventDefault();
|
|
nextTrack();
|
|
break;
|
|
case 'ArrowLeft':
|
|
e.preventDefault();
|
|
previousTrack();
|
|
break;
|
|
case 'KeyR':
|
|
e.preventDefault();
|
|
getRandomTrack();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |