feat: split frontend into separate folder
This commit is contained in:
parent
4bd6ad85e3
commit
d265b92f1d
3 changed files with 79 additions and 80 deletions
33
main.py
33
main.py
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from models import MusicPlayer, PlayerState, WSConnectionType
|
from models import MusicPlayer, PlayerState, WSConnectionType
|
||||||
from services import ConnectionManager, DownloadService
|
from services import ConnectionManager, DownloadService
|
||||||
|
@ -12,17 +12,16 @@ tags_metadata = [
|
||||||
{"name": "experimental"},
|
{"name": "experimental"},
|
||||||
{"name": "queue"},
|
{"name": "queue"},
|
||||||
]
|
]
|
||||||
app = FastAPI(openapi_tags=tags_metadata)
|
api_app = FastAPI(openapi_tags=tags_metadata, title="Dashdio API")
|
||||||
player = MusicPlayer()
|
player = MusicPlayer()
|
||||||
dl_service = DownloadService()
|
dl_service = DownloadService()
|
||||||
ws_manager = ConnectionManager()
|
ws_manager = ConnectionManager()
|
||||||
|
|
||||||
|
app = FastAPI(title="Dashdio App")
|
||||||
|
|
||||||
# Interface
|
# Interface
|
||||||
@app.get("/", response_class=HTMLResponse)
|
app.mount("/api", api_app)
|
||||||
async def root():
|
app.mount("/", StaticFiles(directory="ui", html=True), name="ui")
|
||||||
with open("index.html") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
@ -46,7 +45,7 @@ async def queue_broadcast_loop():
|
||||||
|
|
||||||
|
|
||||||
# Status updates
|
# Status updates
|
||||||
@app.websocket("/player")
|
@api_app.websocket("/player")
|
||||||
async def websocket_player(websocket: WebSocket):
|
async def websocket_player(websocket: WebSocket):
|
||||||
await ws_manager.connect(websocket, WSConnectionType.state)
|
await ws_manager.connect(websocket, WSConnectionType.state)
|
||||||
try:
|
try:
|
||||||
|
@ -58,7 +57,7 @@ async def websocket_player(websocket: WebSocket):
|
||||||
|
|
||||||
|
|
||||||
# Queue updates
|
# Queue updates
|
||||||
@app.websocket("/queue")
|
@api_app.websocket("/queue")
|
||||||
async def websocket_queue(websocket: WebSocket):
|
async def websocket_queue(websocket: WebSocket):
|
||||||
await ws_manager.connect(websocket, WSConnectionType.queue)
|
await ws_manager.connect(websocket, WSConnectionType.queue)
|
||||||
try:
|
try:
|
||||||
|
@ -69,48 +68,48 @@ async def websocket_queue(websocket: WebSocket):
|
||||||
ws_manager.disconnect(websocket)
|
ws_manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/queue", tags=["queue"])
|
@api_app.get("/queue", tags=["queue"])
|
||||||
def get_queue():
|
def get_queue():
|
||||||
return player.get_queue()
|
return player.get_queue()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/queue", tags=["queue"])
|
@api_app.post("/queue", tags=["queue"])
|
||||||
async def post_to_queue(url: str):
|
async def post_to_queue(url: str):
|
||||||
track = await dl_service.download(url)
|
track = await dl_service.download(url)
|
||||||
await player.add_to_queue(track)
|
await player.add_to_queue(track)
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/queue/{track_id}", tags=["queue"])
|
@api_app.delete("/queue/{track_id}", tags=["queue"])
|
||||||
def delete_from_queue(track_id: str):
|
def delete_from_queue(track_id: str):
|
||||||
player.remove_from_queue_by_id(track_id)
|
player.remove_from_queue_by_id(track_id)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/player/play", tags=["player"])
|
@api_app.post("/player/play", tags=["player"])
|
||||||
async def player_play():
|
async def player_play():
|
||||||
await player.play()
|
await player.play()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/player/stop", tags=["player"])
|
@api_app.post("/player/stop", tags=["player"])
|
||||||
async def player_stop():
|
async def player_stop():
|
||||||
await player.stop()
|
await player.stop()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/player/skip", tags=["player"])
|
@api_app.post("/player/skip", tags=["player"])
|
||||||
async def player_skip():
|
async def player_skip():
|
||||||
await player.next()
|
await player.next()
|
||||||
|
|
||||||
|
|
||||||
# Player
|
# Player
|
||||||
@app.put("/player/volume", tags=["player"])
|
@api_app.put("/player/volume", tags=["player"])
|
||||||
async def set_volume(volume: float):
|
async def set_volume(volume: float):
|
||||||
await player.set_volume(volume)
|
await player.set_volume(volume)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/player/volume", tags=["player"])
|
@api_app.get("/player/volume", tags=["player"])
|
||||||
def get_volume():
|
def get_volume():
|
||||||
return {"volume": player.get_volume()}
|
return {"volume": player.get_volume()}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/player", tags=["player"])
|
@api_app.get("/player", tags=["player"])
|
||||||
def get_player_state() -> PlayerState:
|
def get_player_state() -> PlayerState:
|
||||||
return player.get_state()
|
return player.get_state()
|
||||||
|
|
56
ui/index.html
Normal file
56
ui/index.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<!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>
|
||||||
|
<th style="text-align: center; padding: 8px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="queueBody">
|
||||||
|
<!-- Filled by JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1,56 +1,4 @@
|
||||||
<!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>
|
|
||||||
<th style="text-align: center; padding: 8px;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="queueBody">
|
|
||||||
<!-- Filled by JS -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
function formatTime(seconds) {
|
function formatTime(seconds) {
|
||||||
const m = Math.floor(seconds / 60);
|
const m = Math.floor(seconds / 60);
|
||||||
const s = Math.floor(seconds % 60).toString().padStart(2, '0');
|
const s = Math.floor(seconds % 60).toString().padStart(2, '0');
|
||||||
|
@ -58,7 +6,7 @@ return `${m}:${s}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFromQueue = async (trackId) => {
|
const removeFromQueue = async (trackId) => {
|
||||||
await api(`/queue/${trackId}`, { method: "DELETE" });
|
await api(`/api/queue/${trackId}`, { method: "DELETE" });
|
||||||
updateQueue(); // Refresh queue after deletion
|
updateQueue(); // Refresh queue after deletion
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,26 +27,26 @@ const api = (endpoint, options = {}) =>
|
||||||
fetch(endpoint, options).then(res => res.json()).catch(console.error);
|
fetch(endpoint, options).then(res => res.json()).catch(console.error);
|
||||||
|
|
||||||
const setVolume = async (val) => {
|
const setVolume = async (val) => {
|
||||||
await api(`/player/volume?volume=${val}`, { method: "PUT" });
|
await api(`/api/player/volume?volume=${val}`, { method: "PUT" });
|
||||||
document.getElementById("volumeValue").textContent = val;
|
document.getElementById("volumeValue").textContent = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToQueue = async () => {
|
const addToQueue = async () => {
|
||||||
const url = document.getElementById("trackUrl").value;
|
const url = document.getElementById("trackUrl").value;
|
||||||
await api(`/queue?url=${encodeURIComponent(url)}`, { method: "POST" });
|
await api(`/api/queue?url=${encodeURIComponent(url)}`, { method: "POST" });
|
||||||
updateQueue();
|
updateQueue();
|
||||||
};
|
};
|
||||||
|
|
||||||
const play = async () => { await api("/player/play", { method: "POST" });};
|
const play = async () => { await api("/api/player/play", { method: "POST" });};
|
||||||
const stop = async () => { await api("/player/stop", { method: "POST" });};
|
const stop = async () => { await api("/api/player/stop", { method: "POST" });};
|
||||||
const skip = async () => { await api("/player/skip", { method: "POST" });};
|
const skip = async () => { await api("/api/player/skip", { method: "POST" });};
|
||||||
|
|
||||||
// WebSocket connections
|
// WebSocket connections
|
||||||
let playerSocket, queueSocket;
|
let playerSocket, queueSocket;
|
||||||
|
|
||||||
function connectWebSockets() {
|
function connectWebSockets() {
|
||||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||||
const base = `${proto}://${location.host}`;
|
const base = `${proto}://${location.host}/api`;
|
||||||
|
|
||||||
playerSocket = new WebSocket(`${base}/player`);
|
playerSocket = new WebSocket(`${base}/player`);
|
||||||
queueSocket = new WebSocket(`${base}/queue`);
|
queueSocket = new WebSocket(`${base}/queue`);
|
||||||
|
@ -152,7 +100,3 @@ queueSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
||||||
|
|
||||||
connectWebSockets();
|
connectWebSockets();
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue