Download index.php from druvx13/gtavc: direct link, hf CLI and curl.
- Browser
- Download file 30.4 kB
-
https://huggingface.co/spaces/druvx13/gtavc/resolve/main/index.php
- Command line
-
hf download hf://spaces/druvx13/gtavc/index.php
-
curl -L -o index.php https://huggingface.co/spaces/druvx13/gtavc/resolve/main/index.php
30.4 kB
| // index.php | |
| // Database configuration | |
| $host = "sql111.byetcluster.com"; | |
| $db = "ssmm_32394582_music"; | |
| $user = "ssmm_32394582"; | |
| $pass = "13qrny65"; | |
| // Create connection using MySQLi | |
| $conn = new mysqli($host, $user, $pass, $db); | |
| if($conn->connect_error){ | |
| die("Connection failed: " . $conn->connect_error); | |
| } | |
| // API endpoints if "action" parameter is provided | |
| if (isset($_GET['action'])) { | |
| $action = $_GET['action']; | |
| if ($action === 'getPlaylist') { | |
| header("Content-Type: application/json"); | |
| $sql = "SELECT * FROM songs ORDER BY uploaded_at DESC"; | |
| $result = $conn->query($sql); | |
| $songs = array(); | |
| while ($row = $result->fetch_assoc()) { | |
| $songs[] = $row; | |
| } | |
| echo json_encode($songs); | |
| $conn->close(); | |
| exit; | |
| } | |
| if ($action === 'uploadSong') { | |
| header("Content-Type: application/json"); | |
| // Process the song file upload | |
| if (isset($_FILES['song'])) { | |
| $uploadDir = 'uploads/'; | |
| if (!is_dir($uploadDir)) { | |
| mkdir($uploadDir, 0777, true); | |
| } | |
| $songName = basename($_FILES['song']['name']); | |
| $targetSong = $uploadDir . $songName; | |
| $songType = strtolower(pathinfo($targetSong, PATHINFO_EXTENSION)); | |
| // Allow only MP3 files | |
| if ($songType !== "mp3") { | |
| echo json_encode(array("error" => "Only MP3 files are allowed for the song.")); | |
| exit; | |
| } | |
| if (move_uploaded_file($_FILES['song']['tmp_name'], $targetSong)) { | |
| $songURL = $targetSong; | |
| } else { | |
| echo json_encode(array("error" => "Error uploading the song file.")); | |
| exit; | |
| } | |
| } else { | |
| echo json_encode(array("error" => "No song file received.")); | |
| exit; | |
| } | |
| // Process optional cover image upload | |
| $coverURL = ''; | |
| if (isset($_FILES['cover']) && $_FILES['cover']['error'] == UPLOAD_ERR_OK) { | |
| $coverName = basename($_FILES['cover']['name']); | |
| $targetCover = $uploadDir . $coverName; | |
| $coverType = strtolower(pathinfo($targetCover, PATHINFO_EXTENSION)); | |
| // Allow only specific image types | |
| $allowed = array("jpg", "jpeg", "png", "gif"); | |
| if (!in_array($coverType, $allowed)) { | |
| echo json_encode(array("error" => "Cover image must be jpg, jpeg, png, or gif.")); | |
| exit; | |
| } | |
| if (move_uploaded_file($_FILES['cover']['tmp_name'], $targetCover)) { | |
| $coverURL = $targetCover; | |
| } | |
| } | |
| // Get additional data from the POST request | |
| $title = $conn->real_escape_string($_POST['title'] ?? $songName); | |
| $artist = $conn->real_escape_string($_POST['artist'] ?? 'Uploaded Artist'); | |
| $lyrics = $conn->real_escape_string($_POST['lyrics'] ?? ''); | |
| $stmt = $conn->prepare("INSERT INTO songs (title, file, cover, artist, lyrics) VALUES (?, ?, ?, ?, ?)"); | |
| $stmt->bind_param("sssss", $title, $songURL, $coverURL, $artist, $lyrics); | |
| if ($stmt->execute()){ | |
| echo json_encode(array( | |
| "success" => "File uploaded successfully", | |
| "song" => array( | |
| "id" => $stmt->insert_id, | |
| "title" => $title, | |
| "file" => $songURL, | |
| "cover" => $coverURL, | |
| "artist" => $artist, | |
| "lyrics" => $lyrics | |
| ) | |
| )); | |
| } else { | |
| echo json_encode(array("error" => "Database error: " . $conn->error)); | |
| } | |
| $stmt->close(); | |
| $conn->close(); | |
| exit; | |
| } | |
| } | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Music Player</title> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Material Icons --> | |
| <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |
| <!-- External CSS file --> | |
| <link rel="stylesheet" href="style.css"> | |
| </head> | |
| <body class="bg-gray-900 text-white min-h-screen"> | |
| <!-- Main container --> | |
| <div class="container mx-auto px-4 py-8"> | |
| <div class="flex flex-col lg:flex-row gap-8"> | |
| <!-- Playlist section --> | |
| <div class="w-full lg:w-1/3 bg-gray-800 rounded-xl p-6 shadow-lg"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-bold">Playlist</h2> | |
| <div class="flex gap-2"> | |
| <button id="refreshBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="material-icons">refresh</i> | |
| <span>Refresh</span> | |
| </button> | |
| <button id="uploadBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="material-icons">upload</i> | |
| <span>Upload</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <input type="text" id="searchInput" placeholder="Search songs..." class="w-full bg-gray-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div id="playlist" class="max-h-[500px] overflow-y-auto"> | |
| <!-- Playlist items will be added here --> | |
| </div> | |
| <div id="emptyState" class="text-center py-12 text-gray-400"> | |
| <i class="material-icons text-5xl mb-4">music_off</i> | |
| <p class="text-lg mb-4">Your playlist is empty</p> | |
| <button id="uploadBtnEmpty" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 mx-auto transition"> | |
| <i class="material-icons">upload</i> | |
| <span>Upload MP3 files</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Player section --> | |
| <div class="w-full lg:w-2/3"> | |
| <div class="player-container rounded-xl p-6 relative overflow-hidden"> | |
| <canvas class="visualizer" id="visualizer"></canvas> | |
| <div class="flex flex-col md:flex-row gap-6 items-center"> | |
| <!-- Album art --> | |
| <div class="album-art w-48 h-48 rounded-lg flex items-center justify-center"> | |
| <i class="material-icons text-6xl text-gray-500" id="albumArtIcon">music_note</i> | |
| <img id="albumArtImage" src="" alt="Album Art" class="w-full h-full object-cover rounded-lg hidden"> | |
| </div> | |
| <!-- Song info --> | |
| <div class="flex-1"> | |
| <h1 class="text-2xl font-bold truncate" id="songTitle">No song selected</h1> | |
| <p class="text-gray-400 mb-4" id="songArtist">Unknown artist</p> | |
| <!-- Progress bar --> | |
| <div class="mb-2"> | |
| <div class="progress-bar rounded-full w-full"> | |
| <div class="progress-filled rounded-full" id="progressBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between text-sm text-gray-400 mb-6"> | |
| <span id="currentTime">0:00</span> | |
| <span id="durationTime">0:00</span> | |
| </div> | |
| <!-- Controls --> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center gap-2"> | |
| <button id="shuffleBtn" class="control-btn" title="Shuffle"> | |
| <i class="material-icons">shuffle</i> | |
| </button> | |
| <button id="prevBtn" class="control-btn" title="Previous"> | |
| <i class="material-icons">skip_previous</i> | |
| </button> | |
| </div> | |
| <button id="playBtn" class="control-btn bg-blue-600 hover:bg-blue-700 w-14 h-14" title="Play/Pause"> | |
| <i class="material-icons text-2xl" id="playIcon">play_arrow</i> | |
| <i class="material-icons text-2xl hidden" id="pauseIcon">pause</i> | |
| </button> | |
| <div class="flex items-center gap-2"> | |
| <button id="nextBtn" class="control-btn" title="Next"> | |
| <i class="material-icons">skip_next</i> | |
| </button> | |
| <button id="repeatBtn" class="control-btn" title="Repeat"> | |
| <i class="material-icons">repeat</i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Additional controls --> | |
| <div class="flex justify-between items-center mt-6"> | |
| <div class="flex items-center gap-4"> | |
| <div class="relative"> | |
| <button id="volumeBtn" class="control-btn" title="Volume"> | |
| <i class="material-icons" id="volumeIcon">volume_up</i> | |
| </button> | |
| <div id="volumeControl" class="absolute bottom-full left-1/2 transform -translate-x-1/2 bg-gray-800 p-3 rounded-lg shadow-lg mb-2 hidden"> | |
| <div class="volume-bar rounded-full"> | |
| <div class="volume-filled rounded-full" id="volumeBar"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="relative"> | |
| <button id="timerBtn" class="control-btn" title="Sleep Timer"> | |
| <i class="material-icons">timer</i> | |
| </button> | |
| <div id="timerContainer" class="absolute bottom-full left-0 bg-gray-800 p-3 rounded-lg shadow-lg mb-2 w-40 hidden"> | |
| <div class="text-sm mb-2">Sleep Timer</div> | |
| <div class="flex flex-col gap-1"> | |
| <div class="timer-option" data-minutes="0">Off</div> | |
| <div class="timer-option" data-minutes="5">5 minutes</div> | |
| <div class="timer-option" data-minutes="15">15 minutes</div> | |
| <div class="timer-option" data-minutes="30">30 minutes</div> | |
| <div class="timer-option" data-minutes="60">1 hour</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="eq-bars" id="eqBars"> | |
| <div class="eq-bar"></div> | |
| <div class="eq-bar"></div> | |
| <div class="eq-bar"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Lyrics section --> | |
| <div class="mt-6 bg-gray-800 rounded-xl p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-bold">Lyrics</h3> | |
| </div> | |
| <div id="lyricsContent" class="text-center text-gray-400"> | |
| No lyrics available for the current song | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast notification --> | |
| <div class="toast" id="toast"></div> | |
| <!-- Upload form modal --> | |
| <div id="uploadModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 hidden"> | |
| <div class="bg-gray-800 rounded-xl p-6 w-11/12 md:w-1/2 relative"> | |
| <h2 class="text-2xl mb-4">Upload New Song</h2> | |
| <form id="uploadForm"> | |
| <div class="mb-4"> | |
| <label class="block mb-1">Song Title</label> | |
| <input type="text" id="songTitleInput" class="w-full p-2 rounded bg-gray-700" placeholder="Enter song title"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block mb-1">Artist</label> | |
| <input type="text" id="songArtistInput" class="w-full p-2 rounded bg-gray-700" placeholder="Enter artist name"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block mb-1">Cover Image (Optional)</label> | |
| <input type="file" id="coverFileInput" accept="image/*" class="w-full p-2 rounded bg-gray-700"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block mb-1">Lyrics</label> | |
| <textarea id="songLyricsInput" class="w-full p-2 rounded bg-gray-700" placeholder="Enter lyrics"></textarea> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block mb-1">Song File (MP3)</label> | |
| <input type="file" id="songFileInput" accept=".mp3,audio/mp3" class="w-full p-2 rounded bg-gray-700"> | |
| </div> | |
| <div class="flex justify-end gap-4"> | |
| <button type="button" id="cancelUploadBtn" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded">Cancel</button> | |
| <button type="submit" id="submitUploadBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded">Upload</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Audio player state and DOM elements | |
| const state = { | |
| playlist: [], | |
| filteredPlaylist: [], | |
| currentIndex: -1, | |
| isPlaying: false, | |
| isShuffled: false, | |
| isRepeating: false, | |
| volume: 0.7, | |
| timer: null, | |
| searchQuery: '', | |
| audioContext: null, | |
| analyser: null, | |
| dataArray: null, | |
| animationId: null | |
| }; | |
| const elements = { | |
| audio: new Audio(), | |
| playBtn: document.getElementById('playBtn'), | |
| playIcon: document.getElementById('playIcon'), | |
| pauseIcon: document.getElementById('pauseIcon'), | |
| prevBtn: document.getElementById('prevBtn'), | |
| nextBtn: document.getElementById('nextBtn'), | |
| shuffleBtn: document.getElementById('shuffleBtn'), | |
| repeatBtn: document.getElementById('repeatBtn'), | |
| volumeBtn: document.getElementById('volumeBtn'), | |
| volumeIcon: document.getElementById('volumeIcon'), | |
| volumeControl: document.getElementById('volumeControl'), | |
| volumeBar: document.getElementById('volumeBar'), | |
| timerBtn: document.getElementById('timerBtn'), | |
| timerContainer: document.getElementById('timerContainer'), | |
| progressBar: document.getElementById('progressBar'), | |
| currentTime: document.getElementById('currentTime'), | |
| durationTime: document.getElementById('durationTime'), | |
| songTitle: document.getElementById('songTitle'), | |
| songArtist: document.getElementById('songArtist'), | |
| albumArtIcon: document.getElementById('albumArtIcon'), | |
| albumArtImage: document.getElementById('albumArtImage'), | |
| playlist: document.getElementById('playlist'), | |
| emptyState: document.getElementById('emptyState'), | |
| uploadBtn: document.getElementById('uploadBtn'), | |
| uploadBtnEmpty: document.getElementById('uploadBtnEmpty'), | |
| refreshBtn: document.getElementById('refreshBtn'), | |
| searchInput: document.getElementById('searchInput'), | |
| lyricsContent: document.getElementById('lyricsContent'), | |
| eqBars: document.getElementById('eqBars'), | |
| visualizer: document.getElementById('visualizer'), | |
| toast: document.getElementById('toast'), | |
| uploadModal: document.getElementById('uploadModal'), | |
| uploadForm: document.getElementById('uploadForm'), | |
| cancelUploadBtn: document.getElementById('cancelUploadBtn'), | |
| submitUploadBtn: document.getElementById('submitUploadBtn'), | |
| songTitleInput: document.getElementById('songTitleInput'), | |
| songArtistInput: document.getElementById('songArtistInput'), | |
| songLyricsInput: document.getElementById('songLyricsInput'), | |
| songFileInput: document.getElementById('songFileInput'), | |
| coverFileInput: document.getElementById('coverFileInput') | |
| }; | |
| // Initialize the player | |
| function init() { | |
| elements.audio.volume = state.volume; | |
| elements.volumeBar.style.height = `${state.volume * 100}%`; | |
| loadPlaylist(); | |
| setupEventListeners(); | |
| initVisualizer(); | |
| } | |
| // Fetch playlist via AJAX from the PHP endpoint | |
| function loadPlaylist() { | |
| fetch('index.php?action=getPlaylist') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (Array.isArray(data)) { | |
| state.playlist = data; | |
| state.filteredPlaylist = [...data]; | |
| updatePlaylistUI(); | |
| elements.emptyState.style.display = data.length > 0 ? 'none' : 'block'; | |
| if(data.length > 0) { | |
| state.currentIndex = 0; | |
| } | |
| } else { | |
| console.error("Error fetching playlist:", data.error); | |
| } | |
| }) | |
| .catch(error => console.error("Fetch error:", error)); | |
| } | |
| // Update the playlist UI | |
| function updatePlaylistUI() { | |
| elements.playlist.innerHTML = ''; | |
| if (state.filteredPlaylist.length === 0) { | |
| const item = document.createElement('div'); | |
| item.className = 'text-center py-4 text-gray-400'; | |
| item.textContent = 'No songs found'; | |
| elements.playlist.appendChild(item); | |
| return; | |
| } | |
| state.filteredPlaylist.forEach((song, index) => { | |
| const item = document.createElement('div'); | |
| item.className = `flex items-center justify-between p-3 rounded-lg cursor-pointer playlist-item ${state.currentIndex === index ? 'current-song' : ''}`; | |
| item.dataset.index = index; | |
| item.innerHTML = ` | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 bg-gray-700 rounded flex items-center justify-center"> | |
| <i class="material-icons text-lg">music_note</i> | |
| </div> | |
| <div> | |
| <div class="font-medium">${song.title}</div> | |
| <div class="text-sm text-gray-400">${song.artist}</div> | |
| </div> | |
| </div> | |
| <div class="text-gray-400 text-sm">3:45</div> | |
| `; | |
| item.addEventListener('click', () => playSong(index)); | |
| elements.playlist.appendChild(item); | |
| }); | |
| } | |
| // Play song by index | |
| function playSong(index) { | |
| if (index < 0 || index >= state.playlist.length) return; | |
| state.currentIndex = index; | |
| const song = state.playlist[index]; | |
| elements.songTitle.textContent = song.title; | |
| elements.songArtist.textContent = song.artist; | |
| if (song.cover) { | |
| elements.albumArtIcon.style.display = 'none'; | |
| elements.albumArtImage.src = song.cover; | |
| elements.albumArtImage.style.display = 'block'; | |
| } else { | |
| elements.albumArtIcon.style.display = 'block'; | |
| elements.albumArtImage.style.display = 'none'; | |
| } | |
| if (song.lyrics) { | |
| elements.lyricsContent.innerHTML = song.lyrics.split('\n').map(line => | |
| `<div class="mb-2">${line}</div>` | |
| ).join(''); | |
| } else { | |
| elements.lyricsContent.innerHTML = 'No lyrics available for this song'; | |
| } | |
| elements.audio.src = song.file; | |
| elements.audio.play().then(() => { | |
| state.isPlaying = true; | |
| updatePlayButton(); | |
| updatePlaylistUI(); | |
| }).catch(error => { | |
| showNotification('Error playing song', true); | |
| console.error('Play error:', error); | |
| }); | |
| } | |
| // Update play/pause button display | |
| function updatePlayButton() { | |
| if (state.isPlaying) { | |
| elements.playIcon.classList.add('hidden'); | |
| elements.pauseIcon.classList.remove('hidden'); | |
| } else { | |
| elements.playIcon.classList.remove('hidden'); | |
| elements.pauseIcon.classList.add('hidden'); | |
| } | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| elements.playBtn.addEventListener('click', () => { | |
| if (state.currentIndex === -1 && state.playlist.length > 0) { | |
| playSong(0); | |
| } else if (state.isPlaying) { | |
| elements.audio.pause(); | |
| state.isPlaying = false; | |
| updatePlayButton(); | |
| } else { | |
| elements.audio.play().then(() => { | |
| state.isPlaying = true; | |
| updatePlayButton(); | |
| }); | |
| } | |
| }); | |
| elements.prevBtn.addEventListener('click', () => { | |
| if (state.currentIndex > 0) { | |
| playSong(state.currentIndex - 1); | |
| } else { | |
| playSong(state.playlist.length - 1); | |
| } | |
| }); | |
| elements.nextBtn.addEventListener('click', () => { | |
| if (state.currentIndex < state.playlist.length - 1) { | |
| playSong(state.currentIndex + 1); | |
| } else { | |
| playSong(0); | |
| } | |
| }); | |
| elements.shuffleBtn.addEventListener('click', () => { | |
| state.isShuffled = !state.isShuffled; | |
| elements.shuffleBtn.classList.toggle('active-btn', state.isShuffled); | |
| showNotification(state.isShuffled ? 'Shuffle enabled' : 'Shuffle disabled'); | |
| }); | |
| elements.repeatBtn.addEventListener('click', () => { | |
| state.isRepeating = !state.isRepeating; | |
| elements.repeatBtn.classList.toggle('active-btn', state.isRepeating); | |
| showNotification(state.isRepeating ? 'Repeat enabled' : 'Repeat disabled'); | |
| }); | |
| elements.volumeBtn.addEventListener('click', () => { | |
| elements.volumeControl.classList.toggle('hidden'); | |
| }); | |
| elements.volumeBar.addEventListener('click', (e) => { | |
| const rect = e.target.getBoundingClientRect(); | |
| const volume = 1 - (e.clientY - rect.top) / rect.height; | |
| setVolume(Math.max(0, Math.min(1, volume))); | |
| }); | |
| elements.timerBtn.addEventListener('click', () => { | |
| elements.timerContainer.classList.toggle('hidden'); | |
| }); | |
| document.querySelectorAll('.timer-option').forEach(option => { | |
| option.addEventListener('click', (e) => { | |
| const minutes = parseInt(e.target.dataset.minutes); | |
| setTimer(minutes); | |
| }); | |
| }); | |
| elements.searchInput.addEventListener('input', (e) => { | |
| state.searchQuery = e.target.value.toLowerCase(); | |
| filterPlaylist(); | |
| }); | |
| // Show the upload modal when upload buttons are clicked | |
| elements.uploadBtn.addEventListener('click', showUploadModal); | |
| elements.uploadBtnEmpty.addEventListener('click', showUploadModal); | |
| // Cancel button hides the modal | |
| elements.cancelUploadBtn.addEventListener('click', hideUploadModal); | |
| // Handle form submission | |
| elements.uploadForm.addEventListener('submit', handleUploadSubmit); | |
| elements.refreshBtn.addEventListener('click', () => { | |
| showNotification('Playlist refreshed'); | |
| loadPlaylist(); | |
| }); | |
| elements.audio.addEventListener('timeupdate', updateProgress); | |
| elements.audio.addEventListener('ended', handleSongEnd); | |
| elements.audio.addEventListener('durationchange', updateDuration); | |
| elements.audio.addEventListener('volumechange', updateVolumeIcon); | |
| } | |
| function filterPlaylist() { | |
| if (!state.searchQuery) { | |
| state.filteredPlaylist = [...state.playlist]; | |
| } else { | |
| state.filteredPlaylist = state.playlist.filter(song => | |
| song.title.toLowerCase().includes(state.searchQuery) || | |
| song.artist.toLowerCase().includes(state.searchQuery) | |
| ); | |
| } | |
| updatePlaylistUI(); | |
| } | |
| function updateProgress() { | |
| const currentTime = elements.audio.currentTime; | |
| const duration = elements.audio.duration || 1; | |
| const progressPercent = (currentTime / duration) * 100; | |
| elements.progressBar.style.width = `${progressPercent}%`; | |
| elements.currentTime.textContent = formatTime(currentTime); | |
| } | |
| function updateDuration() { | |
| elements.durationTime.textContent = formatTime(elements.audio.duration || 0); | |
| } | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins}:${secs < 10 ? '0' : ''}${secs}`; | |
| } | |
| function handleSongEnd() { | |
| if (state.isRepeating) { | |
| elements.audio.currentTime = 0; | |
| elements.audio.play(); | |
| } else if (state.isShuffled) { | |
| const nextIndex = Math.floor(Math.random() * state.playlist.length); | |
| playSong(nextIndex); | |
| } else { | |
| if (state.currentIndex < state.playlist.length - 1) { | |
| playSong(state.currentIndex + 1); | |
| } else { | |
| state.isPlaying = false; | |
| updatePlayButton(); | |
| } | |
| } | |
| } | |
| function setVolume(volume) { | |
| state.volume = volume; | |
| elements.audio.volume = volume; | |
| elements.volumeBar.style.height = `${volume * 100}%`; | |
| elements.volumeControl.classList.add('hidden'); | |
| } | |
| function updateVolumeIcon() { | |
| if (elements.audio.muted || elements.audio.volume === 0) { | |
| elements.volumeIcon.textContent = 'volume_off'; | |
| } else if (elements.audio.volume < 0.5) { | |
| elements.volumeIcon.textContent = 'volume_down'; | |
| } else { | |
| elements.volumeIcon.textContent = 'volume_up'; | |
| } | |
| } | |
| function setTimer(minutes) { | |
| if (state.timer) { | |
| clearTimeout(state.timer); | |
| state.timer = null; | |
| } | |
| if (minutes > 0) { | |
| state.timer = setTimeout(() => { | |
| elements.audio.pause(); | |
| state.isPlaying = false; | |
| updatePlayButton(); | |
| showNotification('Sleep timer stopped playback'); | |
| }, minutes * 60 * 1000); | |
| document.querySelectorAll('.timer-option').forEach(option => { | |
| option.classList.toggle('active', parseInt(option.dataset.minutes) === minutes); | |
| }); | |
| showNotification(`Sleep timer set for ${minutes} minutes`); | |
| } else { | |
| document.querySelectorAll('.timer-option').forEach(option => { | |
| option.classList.remove('active'); | |
| }); | |
| showNotification('Sleep timer disabled'); | |
| } | |
| elements.timerContainer.classList.add('hidden'); | |
| } | |
| function showNotification(message, isError = false) { | |
| elements.toast.textContent = message; | |
| elements.toast.className = `toast ${isError ? 'error' : ''} show`; | |
| setTimeout(() => { | |
| elements.toast.className = 'toast'; | |
| }, 3000); | |
| } | |
| // Show the upload modal form | |
| function showUploadModal() { | |
| // Reset form fields | |
| elements.uploadForm.reset(); | |
| elements.uploadModal.classList.remove('hidden'); | |
| } | |
| // Hide the upload modal form | |
| function hideUploadModal() { | |
| elements.uploadModal.classList.add('hidden'); | |
| } | |
| // Handle form submission and file upload | |
| function handleUploadSubmit(event) { | |
| event.preventDefault(); | |
| // Check that a song file is selected | |
| if (elements.songFileInput.files.length === 0) { | |
| showNotification("No song file selected", true); | |
| return; | |
| } | |
| const songFile = elements.songFileInput.files[0]; | |
| const formData = new FormData(); | |
| formData.append('song', songFile); | |
| // Append cover file if provided | |
| if (elements.coverFileInput.files.length > 0) { | |
| formData.append('cover', elements.coverFileInput.files[0]); | |
| } | |
| formData.append('title', elements.songTitleInput.value || songFile.name.replace('.mp3', '')); | |
| formData.append('artist', elements.songArtistInput.value || 'Uploaded Artist'); | |
| formData.append('lyrics', elements.songLyricsInput.value || ''); | |
| // Show uploading process | |
| showNotification("Uploading..."); | |
| fetch('index.php?action=uploadSong', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(result => { | |
| if (result.success) { | |
| showNotification(result.success); | |
| state.playlist.unshift(result.song); | |
| state.filteredPlaylist = [...state.playlist]; | |
| updatePlaylistUI(); | |
| elements.emptyState.style.display = 'none'; | |
| hideUploadModal(); | |
| } else { | |
| showNotification(result.error, true); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error("Upload error:", error); | |
| showNotification("Upload failed", true); | |
| }); | |
| } | |
| // Initialize visualizer | |
| function initVisualizer() { | |
| try { | |
| const AudioContext = window.AudioContext || window.webkitAudioContext; | |
| state.audioContext = new AudioContext(); | |
| state.analyser = state.audioContext.createAnalyser(); | |
| state.analyser.fftSize = 256; | |
| const source = state.audioContext.createMediaElementSource(elements.audio); | |
| source.connect(state.analyser); | |
| state.analyser.connect(state.audioContext.destination); | |
| const bufferLength = state.analyser.frequencyBinCount; | |
| state.dataArray = new Uint8Array(bufferLength); | |
| visualize(); | |
| } catch (e) { | |
| console.error('Audio visualization error:', e); | |
| } | |
| } | |
| // Visualize audio on canvas | |
| function visualize() { | |
| if (!state.analyser) return; | |
| state.analyser.getByteFrequencyData(state.dataArray); | |
| const canvas = elements.visualizer; | |
| const ctx = canvas.getContext('2d'); | |
| const width = canvas.width = canvas.offsetWidth; | |
| const height = canvas.height = canvas.offsetHeight; | |
| ctx.clearRect(0, 0, width, height); | |
| const barWidth = (width / state.dataArray.length) * 2.5; | |
| let x = 0; | |
| for (let i = 0; i < state.dataArray.length; i++) { | |
| const barHeight = (state.dataArray[i] / 255) * height; | |
| ctx.fillStyle = `rgb(${50 + state.dataArray[i]}, 100, 200)`; | |
| ctx.fillRect(x, height - barHeight, barWidth, barHeight); | |
| x += barWidth + 1; | |
| } | |
| state.animationId = requestAnimationFrame(visualize); | |
| } | |
| init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |