diff --git a/download_service.py b/download_service.py
deleted file mode 100644
index 9f1793d..0000000
--- a/download_service.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import asyncio
-import sys
-
-import yt_dlp
-
-from music_player import Track
-
-ydl_opts = {
- "format": "mp3/bestaudio/best",
- # ℹ️ See help(yt_dlp.postprocessor) for a list of available Postprocessors and their arguments
- "postprocessors": [
- {
- "key": "FFmpegExtractAudio",
- "preferredcodec": "mp3",
- }
- ],
- "paths": {"home": "queue"},
- "outtmpl": {"default": "%(artist)s - %(track)s [%(id)s].%(ext)s"},
-}
-
-
-class DownloadService:
- def __init__(self) -> None:
- self.ydl = yt_dlp.YoutubeDL(ydl_opts)
-
- async def download(self, url: str) -> Track:
- def extract():
- return self.ydl.extract_info(url, download=True)
-
- info = await asyncio.to_thread(extract)
-
- try:
- filepath = info["requested_downloads"][-1]["filepath"] # type: ignore
- except KeyError:
- raise ValueError("Could not ")
- track = Track(
- artist=info.get("artist", None), # type: ignore
- title=info.get("title", None), # type: ignore
- duration=info.get("duration", None), # type: ignore
- filepath=filepath, # type: ignore
- )
-
- print(f"Finished processing {track}")
-
- return track
-
-
-def main():
- if len(sys.argv) < 2:
- sys.exit(1)
-
- qser = DownloadService()
- qser.download(sys.argv[1])
-
-
-if __name__ == "__main__":
- main()
diff --git a/index.html b/index.html
index 0e4e05b..fe5ade4 100644
--- a/index.html
+++ b/index.html
@@ -42,6 +42,7 @@
Artist |
Title |
Duration |
+ Actions |
@@ -56,6 +57,12 @@ const s = Math.floor(seconds % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
+const removeFromQueue = async (trackId) => {
+ await api(`/queue/${trackId}`, { method: "DELETE" });
+ updateQueue(); // Refresh queue after deletion
+};
+
+
function updateProgress(position, duration) {
const progressBar = document.getElementById("progressBar");
const elapsedTime = document.getElementById("elapsedTime");
@@ -117,22 +124,25 @@ if (track) {
};
queueSocket.onmessage = (event) => {
-const queue = JSON.parse(event.data);
-const queueBody = document.getElementById("queueBody");
-queueBody.innerHTML = "";
+ const queue = JSON.parse(event.data);
+ const queueBody = document.getElementById("queueBody");
+ queueBody.innerHTML = "";
-(queue.items || queue).forEach((track, index) => {
- const row = document.createElement("tr");
+ (queue.items || queue).forEach((track, index) => {
+ const row = document.createElement("tr");
- row.innerHTML = `
- ${index + 1} |
- ${track.artist} |
- ${track.title} |
- ${formatTime(track.duration)} |
- `;
+ row.innerHTML = `
+ ${index + 1} |
+ ${track.artist} |
+ ${track.title} |
+ ${formatTime(track.duration)} |
+
+
+ |
+ `;
- queueBody.appendChild(row);
-});
+ queueBody.appendChild(row);
+ });
};
playerSocket.onerror = queueSocket.onerror = console.error;
diff --git a/main.py b/main.py
index bcd6acf..c54ef6d 100644
--- a/main.py
+++ b/main.py
@@ -1,62 +1,10 @@
import asyncio
-from enum import Enum
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
-from pydantic import BaseModel
-
-from download_service import DownloadService
-from music_player import MusicPlayer, PlayerState
-
-
-class ChangePlayerState(Enum):
- play = "play"
- pause = "pause"
- resume = "resume"
- stop = "stop"
-
-
-class WSConnectionType(Enum):
- state = "state"
- queue = "queue"
-
-
-class ConnectionManager:
- def __init__(self) -> None:
- self.active_connections: dict[str, set[WebSocket]] = {
- WSConnectionType.state.value: set(),
- WSConnectionType.queue.value: set(),
- }
-
- async def connect(self, websocket: WebSocket, type: WSConnectionType):
- await websocket.accept()
- self.active_connections[type.value].add(websocket)
-
- async def send(self, ws: WebSocket, message: BaseModel):
- try:
- await ws.send_json(message.model_dump())
- except Exception:
- self.disconnect(ws)
-
- async def broadcast(self, ws_type: WSConnectionType, message: BaseModel):
- broken = set()
- conn_list = list(self.active_connections[ws_type.value])
- for idx in range(len(conn_list)):
- ws = conn_list[idx]
- try:
- await ws.send_json(message.model_dump())
- except Exception:
- broken.add(ws)
- for ws in broken:
- self.disconnect(ws)
-
- def disconnect(self, websocket: WebSocket):
- if websocket in self.active_connections[WSConnectionType.state.value]:
- self.active_connections[WSConnectionType.state.value].remove(websocket)
-
- if websocket in self.active_connections[WSConnectionType.queue.value]:
- self.active_connections[WSConnectionType.queue.value].remove(websocket)
+from models import MusicPlayer, PlayerState, WSConnectionType
+from services import ConnectionManager, DownloadService
# Setup
tags_metadata = [
@@ -132,6 +80,11 @@ async def post_to_queue(url: str):
await player.add_to_queue(track)
+@app.delete("/queue/{track_id}", tags=["queue"])
+def delete_from_queue(track_id: str):
+ player.remove_from_queue_by_id(track_id)
+
+
@app.post("/player/play", tags=["player"])
async def player_play():
await player.play()
diff --git a/music_player.py b/models.py
similarity index 90%
rename from music_player.py
rename to models.py
index 2376b90..bf7a68e 100644
--- a/music_player.py
+++ b/models.py
@@ -3,18 +3,25 @@ import os
import threading
import time
from enum import Enum
+from uuid import uuid4
import pygame
from pydantic import BaseModel
class Track(BaseModel):
+ id: str = str(uuid4())
artist: str | None
title: str | None
duration: int | None
filepath: str
+class WSConnectionType(Enum):
+ state = "state"
+ queue = "queue"
+
+
class PlaybackState(str, Enum):
Playing = "Playing"
Paused = "Paused"
@@ -40,6 +47,14 @@ class Queue(BaseModel):
def add(self, track: Track) -> None:
self.items.append(track)
+ def remove_by_id(self, track_id: str) -> None:
+ track = self.get_by_id(track_id)
+ if track:
+ self.items.remove(track)
+
+ def get_by_id(self, track_id: str) -> Track | None:
+ return next((x for x in self.items if x.id == track_id), None)
+
def len(self) -> int:
return len(self.items)
@@ -111,7 +126,8 @@ class MusicPlayer:
async def _unload_track(self) -> None:
pygame.mixer.music.unload()
# Delete file from disc
- os.remove(self._state.track.filepath)
+ if self._state.track:
+ os.remove(self._state.track.filepath)
# Update state
await self._set_track(None)
@@ -162,6 +178,11 @@ class MusicPlayer:
if que_len == 0 and not self._state.track:
await self._play_next_track()
+ def remove_from_queue_by_id(self, track_id: str):
+ with self.lock:
+ self._queue.remove_by_id(track_id)
+ self._queue_event.set()
+
async def play(self):
with self.lock:
match self._state.playback_state:
diff --git a/services.py b/services.py
new file mode 100644
index 0000000..84dce68
--- /dev/null
+++ b/services.py
@@ -0,0 +1,83 @@
+import asyncio
+
+import yt_dlp
+from fastapi import WebSocket
+from pydantic import BaseModel
+
+from models import Track, WSConnectionType
+
+
+class ConnectionManager:
+ def __init__(self) -> None:
+ self.active_connections: dict[str, set[WebSocket]] = {
+ WSConnectionType.state.value: set(),
+ WSConnectionType.queue.value: set(),
+ }
+
+ async def connect(self, websocket: WebSocket, type: WSConnectionType):
+ await websocket.accept()
+ self.active_connections[type.value].add(websocket)
+
+ async def send(self, ws: WebSocket, message: BaseModel):
+ try:
+ await ws.send_json(message.model_dump())
+ except Exception:
+ self.disconnect(ws)
+
+ async def broadcast(self, ws_type: WSConnectionType, message: BaseModel):
+ broken = set()
+ conn_list = list(self.active_connections[ws_type.value])
+ for idx in range(len(conn_list)):
+ ws = conn_list[idx]
+ try:
+ await ws.send_json(message.model_dump())
+ except Exception:
+ broken.add(ws)
+ for ws in broken:
+ self.disconnect(ws)
+
+ def disconnect(self, websocket: WebSocket):
+ if websocket in self.active_connections[WSConnectionType.state.value]:
+ self.active_connections[WSConnectionType.state.value].remove(websocket)
+
+ if websocket in self.active_connections[WSConnectionType.queue.value]:
+ self.active_connections[WSConnectionType.queue.value].remove(websocket)
+
+
+class DownloadService:
+ ydl_opts = {
+ "format": "mp3/bestaudio/best",
+ # ℹ️ See help(yt_dlp.postprocessor) for a list of available Postprocessors and their arguments
+ "postprocessors": [
+ {
+ "key": "FFmpegExtractAudio",
+ "preferredcodec": "mp3",
+ }
+ ],
+ "paths": {"home": "queue"},
+ "outtmpl": {"default": "%(artist)s - %(track)s [%(id)s].%(ext)s"},
+ }
+
+ def __init__(self) -> None:
+ self.ydl = yt_dlp.YoutubeDL(self.ydl_opts)
+
+ async def download(self, url: str) -> Track:
+ def extract():
+ return self.ydl.extract_info(url, download=True)
+
+ info = await asyncio.to_thread(extract)
+
+ try:
+ filepath = info["requested_downloads"][-1]["filepath"] # type: ignore
+ except KeyError:
+ raise ValueError("Could not ")
+ track = Track(
+ artist=info.get("artist", None), # type: ignore
+ title=info.get("title", None), # type: ignore
+ duration=info.get("duration", None), # type: ignore
+ filepath=filepath, # type: ignore
+ )
+
+ print(f"Finished processing {track}")
+
+ return track