feat: make cute design
This commit is contained in:
parent
79f72d0531
commit
a1fa762ea8
4 changed files with 159 additions and 97 deletions
148
index.html
Normal file
148
index.html
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Music Player</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; padding: 1rem; }
|
||||||
|
button { margin: 0.5rem; }
|
||||||
|
#queueList { margin-top: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="font-family: sans-serif; max-width: 600px; margin: auto;">
|
||||||
|
<h2>🎵 Dashdio</h2>
|
||||||
|
<div id="nowPlaying">
|
||||||
|
<strong>Artist:</strong> <span id="trackArtist">-</span><br>
|
||||||
|
<strong>Title:</strong> <span id="trackTitle">-</span><br>
|
||||||
|
<strong>Status:</strong> <span id="playbackState">-</span><br>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<progress id="progressBar" value="0" max="100" style="width: 100%; height: 20px;"></progress>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<span id="elapsedTime">0:00</span> / <span id="totalTime">0:00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<label for="volumeSlider"><strong>🔊 Volume:</strong> <span id="volumeValue">1</span></label><br>
|
||||||
|
<input type="range" min="0" max="1" step="0.01" id="volumeSlider" oninput="setVolume(this.value)">
|
||||||
|
<button onclick="play()">Play/Pause</button>
|
||||||
|
<button onclick="stop()">Stop</button>
|
||||||
|
<button onclick="skip()">Skip</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 30px;">📃 Queue</h3>
|
||||||
|
<input type="text" id="trackUrl" placeholder="Track URL">
|
||||||
|
<button onclick="addToQueue()">Add to Queue</button>
|
||||||
|
<table id="queueTable" style="width: 100%; border-collapse: collapse; font-size: 0.95em;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f0f0f0;">
|
||||||
|
<th style="text-align: left; padding: 8px;">#</th>
|
||||||
|
<th style="text-align: left; padding: 8px;">Artist</th>
|
||||||
|
<th style="text-align: left; padding: 8px;">Title</th>
|
||||||
|
<th style="text-align: right; padding: 8px;">Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="queueBody">
|
||||||
|
<!-- Filled by JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function formatTime(seconds) {
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = Math.floor(seconds % 60).toString().padStart(2, '0');
|
||||||
|
return `${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(position, duration) {
|
||||||
|
const progressBar = document.getElementById("progressBar");
|
||||||
|
const elapsedTime = document.getElementById("elapsedTime");
|
||||||
|
const totalTime = document.getElementById("totalTime");
|
||||||
|
|
||||||
|
progressBar.max = duration;
|
||||||
|
progressBar.value = position;
|
||||||
|
elapsedTime.textContent = formatTime(position);
|
||||||
|
totalTime.textContent = formatTime(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const api = (endpoint, options = {}) =>
|
||||||
|
fetch(endpoint, options).then(res => res.json()).catch(console.error);
|
||||||
|
|
||||||
|
const setVolume = async (val) => {
|
||||||
|
await api(`/player/volume?volume=${val}`, { method: "PUT" });
|
||||||
|
document.getElementById("volumeValue").textContent = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToQueue = async () => {
|
||||||
|
const url = document.getElementById("trackUrl").value;
|
||||||
|
await api(`/queue?url=${encodeURIComponent(url)}`, { method: "POST" });
|
||||||
|
updateQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = async () => { await api("/player/play", { method: "POST" });};
|
||||||
|
const stop = async () => { await api("/player/stop", { method: "POST" });};
|
||||||
|
const skip = async () => { await api("/player/skip", { method: "POST" });};
|
||||||
|
|
||||||
|
// WebSocket connections
|
||||||
|
let playerSocket, queueSocket;
|
||||||
|
|
||||||
|
function connectWebSockets() {
|
||||||
|
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||||
|
const base = `${proto}://${location.host}`;
|
||||||
|
|
||||||
|
playerSocket = new WebSocket(`${base}/player`);
|
||||||
|
queueSocket = new WebSocket(`${base}/queue`);
|
||||||
|
|
||||||
|
playerSocket.onopen = () => playerSocket.send("ping");
|
||||||
|
queueSocket.onopen = () => queueSocket.send("ping");
|
||||||
|
|
||||||
|
playerSocket.onmessage = (event) => {
|
||||||
|
const state = JSON.parse(event.data);
|
||||||
|
|
||||||
|
const { playback_state, track, position, volume } = state;
|
||||||
|
|
||||||
|
document.getElementById("trackArtist").textContent = track?.artist || "-";
|
||||||
|
document.getElementById("trackTitle").textContent = track?.title || "-";
|
||||||
|
document.getElementById("playbackState").textContent = playback_state;
|
||||||
|
|
||||||
|
document.getElementById("volumeSlider").value = volume;
|
||||||
|
document.getElementById("volumeValue").textContent = volume;
|
||||||
|
|
||||||
|
if (track) {
|
||||||
|
updateProgress(position, track.duration);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
queueSocket.onmessage = (event) => {
|
||||||
|
const queue = JSON.parse(event.data);
|
||||||
|
const queueBody = document.getElementById("queueBody");
|
||||||
|
queueBody.innerHTML = "";
|
||||||
|
|
||||||
|
(queue.items || queue).forEach((track, index) => {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td style="padding: 8px;">${index + 1}</td>
|
||||||
|
<td style="padding: 8px;">${track.artist}</td>
|
||||||
|
<td style="padding: 8px;">${track.title}</td>
|
||||||
|
<td style="padding: 8px; text-align: right;">${formatTime(track.duration)}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
queueBody.appendChild(row);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
playerSocket.onerror = queueSocket.onerror = console.error;
|
||||||
|
playerSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
||||||
|
queueSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWebSockets();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Music Player</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; padding: 1rem; }
|
|
||||||
button { margin: 0.5rem; }
|
|
||||||
#queueList { margin-top: 1rem; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Music Player</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>Player Controls</h2>
|
|
||||||
<button onclick="play()">Play/Pause</button>
|
|
||||||
<button onclick="stop()">Stop</button>
|
|
||||||
<button onclick="skip()">Skip</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>Volume</h2>
|
|
||||||
<input type="range" min="0" max="1" step="0.01" id="volumeSlider" oninput="setVolume(this.value)">
|
|
||||||
<span id="volumeValue">0</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>Queue</h2>
|
|
||||||
<input type="text" id="trackUrl" placeholder="Track URL">
|
|
||||||
<button onclick="addToQueue()">Add to Queue</button>
|
|
||||||
<div id="queueList"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>Player State</h2>
|
|
||||||
<pre id="playerState"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const api = (endpoint, options = {}) =>
|
|
||||||
fetch(endpoint, options).then(res => res.json()).catch(console.error);
|
|
||||||
|
|
||||||
const setVolume = async (val) => {
|
|
||||||
await api(`/player/volume?volume=${val}`, { method: "PUT" });
|
|
||||||
document.getElementById("volumeValue").textContent = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToQueue = async () => {
|
|
||||||
const url = document.getElementById("trackUrl").value;
|
|
||||||
await api(`/queue?url=${encodeURIComponent(url)}`, { method: "POST" });
|
|
||||||
updateQueue();
|
|
||||||
};
|
|
||||||
|
|
||||||
const play = async () => { await api("/player/play", { method: "POST" });};
|
|
||||||
const stop = async () => { await api("/player/stop", { method: "POST" });};
|
|
||||||
const skip = async () => { await api("/player/skip", { method: "POST" });};
|
|
||||||
|
|
||||||
// WebSocket connections
|
|
||||||
let playerSocket, queueSocket;
|
|
||||||
|
|
||||||
function connectWebSockets() {
|
|
||||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
||||||
const base = `${proto}://${location.host}`;
|
|
||||||
|
|
||||||
playerSocket = new WebSocket(`${base}/player`);
|
|
||||||
queueSocket = new WebSocket(`${base}/queue`);
|
|
||||||
|
|
||||||
playerSocket.onopen = () => playerSocket.send("ping");
|
|
||||||
queueSocket.onopen = () => queueSocket.send("ping");
|
|
||||||
|
|
||||||
playerSocket.onmessage = (event) => {
|
|
||||||
const state = JSON.parse(event.data);
|
|
||||||
document.getElementById("playerState").textContent = JSON.stringify(state, null, 2);
|
|
||||||
document.getElementById("volumeSlider").value = state.volume;
|
|
||||||
document.getElementById("volumeValue").textContent = state.volume;
|
|
||||||
};
|
|
||||||
|
|
||||||
queueSocket.onmessage = (event) => {
|
|
||||||
const queue = JSON.parse(event.data);
|
|
||||||
document.getElementById("queueList").textContent = JSON.stringify(queue.items, null, 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
playerSocket.onerror = queueSocket.onerror = console.error;
|
|
||||||
playerSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
|
||||||
queueSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectWebSockets();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
2
main.py
2
main.py
|
@ -71,7 +71,7 @@ ws_manager = ConnectionManager()
|
||||||
# Interface
|
# Interface
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def root():
|
async def root():
|
||||||
with open("interface.html") as f:
|
with open("index.html") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
@ -82,7 +83,7 @@ class MusicPlayer:
|
||||||
|
|
||||||
async def _update_position(self):
|
async def _update_position(self):
|
||||||
new_pos = pygame.mixer.music.get_pos() // 1000
|
new_pos = pygame.mixer.music.get_pos() // 1000
|
||||||
# Changes only of fulls seconds
|
# Changes only on fulls seconds
|
||||||
if new_pos != self._state.position:
|
if new_pos != self._state.position:
|
||||||
if new_pos == -1:
|
if new_pos == -1:
|
||||||
self._state.position = None
|
self._state.position = None
|
||||||
|
@ -104,9 +105,16 @@ class MusicPlayer:
|
||||||
|
|
||||||
async def _handle_track_finished(self) -> None:
|
async def _handle_track_finished(self) -> None:
|
||||||
print(f"Finished playing {self._state.track}")
|
print(f"Finished playing {self._state.track}")
|
||||||
await self._set_track(None)
|
await self._unload_track()
|
||||||
await self._set_playback_state(PlaybackState.Stopped)
|
await self._set_playback_state(PlaybackState.Stopped)
|
||||||
|
|
||||||
|
async def _unload_track(self) -> None:
|
||||||
|
pygame.mixer.music.unload()
|
||||||
|
# Delete file from disc
|
||||||
|
os.remove(self._state.track.filepath)
|
||||||
|
# Update state
|
||||||
|
await self._set_track(None)
|
||||||
|
|
||||||
async def _load_track(self, track: Track):
|
async def _load_track(self, track: Track):
|
||||||
await self._set_track(track)
|
await self._set_track(track)
|
||||||
pygame.mixer.music.unload()
|
pygame.mixer.music.unload()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue