queue.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. # -*- coding: utf-8 -*-
  2. """
  3. Play-Queue.
  4. Verwaltet die aktuelle Wiedergabe-Warteschlange mit "Play Next" Funktion.
  5. """
  6. import uuid
  7. from dataclasses import dataclass, field
  8. from datetime import datetime
  9. from enum import Enum
  10. from typing import Any, Iterator
  11. from trixy_core.music.track import Track
  12. class QueueState(Enum):
  13. """Zustand der Queue."""
  14. IDLE = "idle" # Keine Wiedergabe
  15. PLAYING = "playing" # Aktive Wiedergabe
  16. PAUSED = "paused" # Pausiert
  17. LOADING = "loading" # Track wird geladen
  18. BUFFERING = "buffering" # Puffert
  19. ERROR = "error" # Fehler
  20. @dataclass
  21. class QueueItem:
  22. """
  23. Ein Eintrag in der Wiedergabe-Queue.
  24. Zusätzlich zur Playlist-Wiedergabe können Tracks zur Queue hinzugefügt werden.
  25. """
  26. track: Track
  27. id: str = ""
  28. added_at: datetime = field(default_factory=datetime.now)
  29. added_by: str = "" # User/Source
  30. priority: bool = False # True = "Play Next"
  31. # Metadaten
  32. metadata: dict[str, Any] = field(default_factory=dict)
  33. def __post_init__(self) -> None:
  34. if not self.id:
  35. self.id = f"queue_{uuid.uuid4().hex[:8]}"
  36. @dataclass
  37. class PlayQueue:
  38. """
  39. Wiedergabe-Warteschlange.
  40. Verwaltet die aktuelle Wiedergabeliste mit Unterstützung für:
  41. - "Play Next" (Prioritäts-Einreihung)
  42. - Queue-History (bereits gespielte Tracks)
  43. - Shuffle innerhalb der Queue
  44. """
  45. # Queue-Items
  46. items: list[QueueItem] = field(default_factory=list)
  47. # History (bereits abgespielt)
  48. history: list[QueueItem] = field(default_factory=list)
  49. # Aktueller Zustand
  50. state: QueueState = QueueState.IDLE
  51. current_item: QueueItem | None = None
  52. # Einstellungen
  53. max_history: int = 100 # Maximale History-Größe
  54. # ID für Identifikation
  55. id: str = ""
  56. def __post_init__(self) -> None:
  57. if not self.id:
  58. self.id = f"playqueue_{uuid.uuid4().hex[:8]}"
  59. # ==========================================================================
  60. # Properties
  61. # ==========================================================================
  62. @property
  63. def count(self) -> int:
  64. """Anzahl der wartenden Tracks."""
  65. return len(self.items)
  66. @property
  67. def is_empty(self) -> bool:
  68. """Prüft ob Queue leer ist."""
  69. return len(self.items) == 0
  70. @property
  71. def has_next(self) -> bool:
  72. """Prüft ob es einen nächsten Track gibt."""
  73. return len(self.items) > 0
  74. @property
  75. def has_previous(self) -> bool:
  76. """Prüft ob es einen vorherigen Track (History) gibt."""
  77. return len(self.history) > 0
  78. @property
  79. def total_duration_ms(self) -> int:
  80. """Gesamtdauer der Queue in Millisekunden."""
  81. return sum(item.track.duration_ms for item in self.items)
  82. @property
  83. def current_track(self) -> Track | None:
  84. """Aktuell spielender Track."""
  85. return self.current_item.track if self.current_item else None
  86. # ==========================================================================
  87. # Queue-Management
  88. # ==========================================================================
  89. def add(
  90. self,
  91. track: Track,
  92. added_by: str = "",
  93. play_next: bool = False,
  94. ) -> QueueItem:
  95. """
  96. Fügt einen Track zur Queue hinzu.
  97. Args:
  98. track: Der hinzuzufügende Track
  99. added_by: Wer hat den Track hinzugefügt
  100. play_next: True = Als nächstes abspielen
  101. Returns:
  102. Das erstellte QueueItem
  103. """
  104. item = QueueItem(
  105. track=track,
  106. added_by=added_by,
  107. priority=play_next,
  108. )
  109. if play_next:
  110. # Am Anfang der Queue einfügen
  111. self.items.insert(0, item)
  112. else:
  113. # Am Ende anhängen
  114. self.items.append(item)
  115. return item
  116. def add_tracks(
  117. self,
  118. tracks: list[Track],
  119. added_by: str = "",
  120. ) -> list[QueueItem]:
  121. """
  122. Fügt mehrere Tracks zur Queue hinzu.
  123. Args:
  124. tracks: Liste von Tracks
  125. added_by: Wer hat die Tracks hinzugefügt
  126. Returns:
  127. Liste der erstellten QueueItems
  128. """
  129. items = []
  130. for track in tracks:
  131. item = self.add(track, added_by=added_by)
  132. items.append(item)
  133. return items
  134. def remove(self, index: int) -> QueueItem | None:
  135. """
  136. Entfernt Item an Position.
  137. Args:
  138. index: Position des zu entfernenden Items
  139. Returns:
  140. Entferntes Item oder None
  141. """
  142. if 0 <= index < len(self.items):
  143. return self.items.pop(index)
  144. return None
  145. def remove_by_id(self, item_id: str) -> QueueItem | None:
  146. """
  147. Entfernt Item nach ID.
  148. Args:
  149. item_id: ID des zu entfernenden Items
  150. Returns:
  151. Entferntes Item oder None
  152. """
  153. for i, item in enumerate(self.items):
  154. if item.id == item_id:
  155. return self.items.pop(i)
  156. return None
  157. def clear(self) -> int:
  158. """
  159. Leert die Queue (behält Current).
  160. Returns:
  161. Anzahl der entfernten Items
  162. """
  163. count = len(self.items)
  164. self.items.clear()
  165. return count
  166. def clear_all(self) -> int:
  167. """
  168. Leert Queue und History komplett.
  169. Returns:
  170. Anzahl der entfernten Items
  171. """
  172. count = len(self.items) + len(self.history)
  173. self.items.clear()
  174. self.history.clear()
  175. self.current_item = None
  176. self.state = QueueState.IDLE
  177. return count
  178. def move(self, from_index: int, to_index: int) -> bool:
  179. """
  180. Verschiebt Item an neue Position.
  181. Args:
  182. from_index: Aktuelle Position
  183. to_index: Ziel-Position
  184. Returns:
  185. True wenn erfolgreich
  186. """
  187. if not (0 <= from_index < len(self.items) and 0 <= to_index < len(self.items)):
  188. return False
  189. item = self.items.pop(from_index)
  190. self.items.insert(to_index, item)
  191. return True
  192. # ==========================================================================
  193. # Navigation
  194. # ==========================================================================
  195. def next(self) -> QueueItem | None:
  196. """
  197. Holt nächsten Track aus der Queue.
  198. Verschiebt aktuellen Track in History.
  199. Returns:
  200. Nächstes Item oder None
  201. """
  202. # Aktuellen zur History hinzufügen
  203. if self.current_item:
  204. self.history.append(self.current_item)
  205. # History begrenzen
  206. while len(self.history) > self.max_history:
  207. self.history.pop(0)
  208. # Nächsten aus Queue holen
  209. if self.items:
  210. self.current_item = self.items.pop(0)
  211. return self.current_item
  212. self.current_item = None
  213. self.state = QueueState.IDLE
  214. return None
  215. def previous(self) -> QueueItem | None:
  216. """
  217. Geht zum vorherigen Track (aus History).
  218. Verschiebt aktuellen Track zurück in Queue.
  219. Returns:
  220. Vorheriges Item oder None
  221. """
  222. if not self.history:
  223. return None
  224. # Aktuellen zurück an Anfang der Queue
  225. if self.current_item:
  226. self.items.insert(0, self.current_item)
  227. # Letzten aus History holen
  228. self.current_item = self.history.pop()
  229. return self.current_item
  230. def skip_to(self, index: int) -> QueueItem | None:
  231. """
  232. Springt zu einem bestimmten Item in der Queue.
  233. Alles davor wird übersprungen (nicht in History).
  234. Args:
  235. index: Ziel-Position
  236. Returns:
  237. Item an der Position oder None
  238. """
  239. if not (0 <= index < len(self.items)):
  240. return None
  241. # Vorherige Items verwerfen
  242. for _ in range(index):
  243. self.items.pop(0)
  244. return self.next()
  245. def peek(self, count: int = 1) -> list[QueueItem]:
  246. """
  247. Zeigt die nächsten Items ohne sie zu entfernen.
  248. Args:
  249. count: Anzahl der Items
  250. Returns:
  251. Liste der nächsten Items
  252. """
  253. return self.items[:count]
  254. def peek_next(self) -> QueueItem | None:
  255. """
  256. Zeigt das nächste Item ohne es zu entfernen.
  257. Returns:
  258. Nächstes Item oder None wenn Queue leer
  259. """
  260. return self.items[0] if self.items else None
  261. # ==========================================================================
  262. # State Management
  263. # ==========================================================================
  264. def set_playing(self) -> None:
  265. """Setzt Zustand auf PLAYING."""
  266. if self.current_item:
  267. self.state = QueueState.PLAYING
  268. def set_paused(self) -> None:
  269. """Setzt Zustand auf PAUSED."""
  270. if self.current_item:
  271. self.state = QueueState.PAUSED
  272. def set_loading(self) -> None:
  273. """Setzt Zustand auf LOADING."""
  274. self.state = QueueState.LOADING
  275. def set_error(self) -> None:
  276. """Setzt Zustand auf ERROR."""
  277. self.state = QueueState.ERROR
  278. # ==========================================================================
  279. # Hilfsmethoden
  280. # ==========================================================================
  281. def find_track(self, track_id: str) -> tuple[int, QueueItem] | None:
  282. """
  283. Findet Item nach Track-ID.
  284. Args:
  285. track_id: Track-ID
  286. Returns:
  287. Tuple (index, item) oder None
  288. """
  289. for i, item in enumerate(self.items):
  290. if item.track.id == track_id:
  291. return (i, item)
  292. return None
  293. def contains_track(self, track_id: str) -> bool:
  294. """Prüft ob Track in Queue ist."""
  295. return any(item.track.id == track_id for item in self.items)
  296. def to_dict(self) -> dict[str, Any]:
  297. """Konvertiert zu Dictionary."""
  298. return {
  299. "id": self.id,
  300. "state": self.state.value,
  301. "count": self.count,
  302. "total_duration_ms": self.total_duration_ms,
  303. "current": self.current_item.track.to_dict() if self.current_item else None,
  304. "items": [
  305. {
  306. "id": item.id,
  307. "track": item.track.to_dict(),
  308. "added_at": item.added_at.isoformat(),
  309. "added_by": item.added_by,
  310. "priority": item.priority,
  311. }
  312. for item in self.items
  313. ],
  314. "history_count": len(self.history),
  315. }
  316. def __len__(self) -> int:
  317. return len(self.items)
  318. def __iter__(self) -> Iterator[QueueItem]:
  319. return iter(self.items)
  320. def __getitem__(self, index: int) -> QueueItem:
  321. return self.items[index]
  322. def __repr__(self) -> str:
  323. return (
  324. f"PlayQueue(id={self.id!r}, "
  325. f"state={self.state.value!r}, "
  326. f"count={self.count})"
  327. )