diff --git a/download_service.py b/download_service.py
index dc7b96f..0636905 100644
--- a/download_service.py
+++ b/download_service.py
@@ -25,11 +25,15 @@ class DownloadService:
def download(self, url: str) -> Track:
info = self.ydl.extract_info(url, download=True)
+ try:
+ filepath = info["requested_downloads"][-1]["filepath"] # type: ignore
+ except KeyError:
+ raise ValueError("Could not ")
track = Track(
- artist=info["artists"][0], # type: ignore
- title=info["title"], # type: ignore
- duration=info["duration"], # type: ignore
- filepath=info["requested_downloads"][-1]["filepath"], # type: ignore
+ 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}")
diff --git a/interface.html b/interface.html
new file mode 100644
index 0000000..10b51a6
--- /dev/null
+++ b/interface.html
@@ -0,0 +1,88 @@
+
+
+
+
+ Music Player
+
+
+
+ Music Player
+
+
+
Player Controls
+
+
+
+
+
+
+
+
+
Volume
+
+ 0
+
+
+
+
Queue
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main.py b/main.py
index e54970b..dba7306 100644
--- a/main.py
+++ b/main.py
@@ -1,9 +1,10 @@
from enum import Enum
from fastapi import FastAPI
+from fastapi.responses import HTMLResponse
from download_service import DownloadService
-from music_player import MusicPlayer
+from music_player import MusicPlayer, PlayerState
class ChangePlayerState(Enum):
@@ -25,9 +26,16 @@ player = MusicPlayer()
dl_service = DownloadService()
+# Interface
+@app.get("/", response_class=HTMLResponse)
+async def root():
+ with open("interface.html") as f:
+ return f.read()
+
+
# Experimental
@app.get("/queue", tags=["queue"])
-def play_music():
+def get_queue():
return player.get_queue()
@@ -57,6 +65,11 @@ def player_stop():
player.stop()
+@app.post("/player/skip", tags=["player"])
+def player_skip():
+ player.next()
+
+
# Player
@app.put("/player/volume", tags=["player"])
def set_volume(volume: float):
@@ -68,6 +81,6 @@ def get_volume():
return {"volume": player.get_volume()}
-@app.get("/player/state", tags=["player"])
-def get_state():
- return {"state": player.get_state(), "current_track": player.get_current_track()}
+@app.get("/player", tags=["player"])
+def get_player_state() -> PlayerState:
+ return player.get_state()
diff --git a/music_player.py b/music_player.py
index 94aac96..737fbf8 100644
--- a/music_player.py
+++ b/music_player.py
@@ -3,20 +3,26 @@ import time
from enum import Enum
import pygame
-from pydantic import BaseModel, Field
-
-
-class PlayerState(Enum):
- Playing = "Playing"
- Paused = "Paused"
- Stopped = "Stopped"
+from pydantic import BaseModel
class Track(BaseModel):
artist: str
title: str
duration: int
- filepath: str = Field(hidden=True) # don't show it in API responses
+ filepath: str
+
+
+class PlaybackState(Enum):
+ Playing = "Playing"
+ Paused = "Paused"
+ Stopped = "Stopped"
+
+
+class PlayerState(BaseModel):
+ playback_state: PlaybackState = PlaybackState.Stopped
+ track: Track | None = None
+ volume: float
class Queue:
@@ -40,94 +46,141 @@ class Queue:
class MusicPlayer:
def __init__(self):
+ # Player State
+ self._state = PlayerState(volume=1)
+ self._state_changed: bool = False
+
+ # Sound initialization
pygame.init()
pygame.mixer.init()
pygame.mixer.music.set_endevent(pygame.USEREVENT)
+ pygame.mixer.music.set_volume(self._state.volume)
+
# Threading
self.lock = threading.Lock()
self._running = True
- self.thread = threading.Thread(target=self._loop, daemon=True)
- self.thread.start()
- # Music Player
- self.queue: Queue = Queue()
- self.current_track: Track | None = None
- self._state: PlayerState = PlayerState.Stopped
- # State change flags
+ self._thread = threading.Thread(target=self._loop, daemon=True)
+ self._thread.start()
+
+ # Queue
+ self._queue: Queue = Queue()
self._queue_changed: bool = False
- self._track_changed: bool = False
def _loop(self):
while self._running:
for event in pygame.event.get():
if event.type == pygame.USEREVENT: # a song just ended
- if self.current_track:
- print(f"Track {self.current_track.title} ended")
+ self._handle_track_finished()
self._play_next_track()
time.sleep(0.1)
+ def _set_playback_state(self, value: PlaybackState):
+ self._state.playback_state = value
+ self._state_changed = True
+
+ def _set_track(self, track: Track | None):
+ self._state.track = track
+ self._state_changed = True
+
+ def _handle_track_finished(self) -> None:
+ print(f"Finished playing {self._state.track}")
+ self._set_track(None)
+ self._set_playback_state(PlaybackState.Stopped)
+
def _load_track(self, track: Track):
- self.current_track = track
+ self._set_track(track)
pygame.mixer.music.unload()
- pygame.mixer.music.load(self.current_track.filepath)
+ pygame.mixer.music.load(track.filepath)
+
+ def _queue_add(self, track):
+ self._queue.add(track)
+ self._queue_changed = True
+
+ def _queue_next(self) -> Track | None:
+ next_track = self._queue.next()
+ self._queue_changed = True
+ return next_track
+
+ def _start_playback(self):
+ pygame.mixer.music.play()
+ self._set_playback_state(PlaybackState.Playing)
+
+ def _pause_playback(self):
+ pygame.mixer.music.pause()
+ self._set_playback_state(PlaybackState.Paused)
+
+ def _resume_playback(self):
+ pygame.mixer.music.unpause()
+ self._set_playback_state(PlaybackState.Playing)
+
+ def _stop_playback(self):
+ pygame.mixer.music.stop()
+ self._set_playback_state(PlaybackState.Stopped)
+ self._set_track(None)
def _play_next_track(self):
- next_track = self.queue.next()
+ next_track = self._queue_next()
if next_track:
self._load_track(next_track)
- pygame.mixer.music.play()
- self._state = PlayerState.Playing
+ self._start_playback()
def add_to_queue(self, track: Track):
with self.lock:
- que_len = self.queue.len()
- self.queue.add(track)
+ que_len = self._queue.len()
+ self._queue_add(track)
# If queue is empty and no corrent track, start playing
- if que_len == 0 and not self.current_track:
+ if que_len == 0 and not self._state.track:
self._play_next_track()
def play(self):
with self.lock:
- if self.current_track:
- pygame.mixer.music.play()
- self._state = PlayerState.Playing
+ if self._state.playback_state == PlaybackState.Playing:
+ return
+ if self._state.track:
+ self._start_playback()
else:
self._play_next_track()
def pause(self):
with self.lock:
- pygame.mixer.music.pause()
- self._state = PlayerState.Paused
+ self._pause_playback()
def resume(self):
with self.lock:
- pygame.mixer.music.unpause()
- self._state = PlayerState.Playing
+ self._resume_playback()
def stop(self):
with self.lock:
- pygame.mixer.music.stop()
- self._state = PlayerState.Stopped
- self.current_track = None
+ self._stop_playback()
+
+ def next(self):
+ with self.lock:
+ self._play_next_track()
def shutdown(self):
with self.lock:
self._running = False
- self.thread.join()
+ self._thread.join()
pygame.mixer.quit()
- def get_queue(self) -> Queue:
- return self.queue._queue
+ def get_queue(self) -> list[Track]:
+ return self._queue._queue
- def set_volume(self, volume: float):
+ def _set_volume(self, volume: float):
+ self._state.volume = volume
+ pygame.mixer.music.set_volume(volume)
+ self._state_changed = True
+
+ def set_volume(self, volume: float) -> None:
with self.lock:
- pygame.mixer.music.set_volume(volume)
+ self._set_volume(volume)
- def get_volume(self):
- return pygame.mixer.music.get_volume()
+ def get_volume(self) -> float:
+ return self._state.volume
def get_state(self) -> PlayerState:
return self._state
def get_current_track(self) -> Track | None:
- return self.current_track
+ return self._state.track