import threading import time from enum import Enum import pygame from pydantic import BaseModel, Field class PlayerState(Enum): Playing = "Playing" Paused = "Paused" Stopped = "Stopped" class Track(BaseModel): artist: str title: str duration: int filepath: str = Field(hidden=True) # don't show it in API responses class Queue: queue: list[Track] def __init__(self) -> None: self._queue: list[Track] = [] def next(self) -> Track | None: if len(self._queue) > 0: return self._queue.pop(0) else: return None def add(self, track: Track) -> None: self._queue.append(track) def len(self) -> int: return len(self._queue) class MusicPlayer: def __init__(self): pygame.init() pygame.mixer.init() pygame.mixer.music.set_endevent(pygame.USEREVENT) # 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._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._play_next_track() time.sleep(0.1) def _load_track(self, track: Track): self.current_track = track pygame.mixer.music.unload() pygame.mixer.music.load(self.current_track.filepath) def _play_next_track(self): next_track = self.queue.next() if next_track: self._load_track(next_track) pygame.mixer.music.play() self._state = PlayerState.Playing def add_to_queue(self, track: Track): with self.lock: 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: self._play_next_track() def play(self): with self.lock: if self.current_track: pygame.mixer.music.play() self._state = PlayerState.Playing else: self._play_next_track() def pause(self): with self.lock: pygame.mixer.music.pause() self._state = PlayerState.Paused def resume(self): with self.lock: pygame.mixer.music.unpause() self._state = PlayerState.Playing def stop(self): with self.lock: pygame.mixer.music.stop() self._state = PlayerState.Stopped self.current_track = None def shutdown(self): with self.lock: self._running = False self.thread.join() pygame.mixer.quit() def get_queue(self) -> Queue: return self.queue._queue def set_volume(self, volume: float): with self.lock: pygame.mixer.music.set_volume(volume) def get_volume(self): return pygame.mixer.music.get_volume() def get_state(self) -> PlayerState: return self._state def get_current_track(self) -> Track | None: return self.current_track