# -*- coding: utf-8 -*- """ Play-Queue. Verwaltet die aktuelle Wiedergabe-Warteschlange mit "Play Next" Funktion. """ import uuid from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import Any, Iterator from trixy_core.music.track import Track class QueueState(Enum): """Zustand der Queue.""" IDLE = "idle" # Keine Wiedergabe PLAYING = "playing" # Aktive Wiedergabe PAUSED = "paused" # Pausiert LOADING = "loading" # Track wird geladen BUFFERING = "buffering" # Puffert ERROR = "error" # Fehler @dataclass class QueueItem: """ Ein Eintrag in der Wiedergabe-Queue. Zusätzlich zur Playlist-Wiedergabe können Tracks zur Queue hinzugefügt werden. """ track: Track id: str = "" added_at: datetime = field(default_factory=datetime.now) added_by: str = "" # User/Source priority: bool = False # True = "Play Next" # Metadaten metadata: dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: if not self.id: self.id = f"queue_{uuid.uuid4().hex[:8]}" @dataclass class PlayQueue: """ Wiedergabe-Warteschlange. Verwaltet die aktuelle Wiedergabeliste mit Unterstützung für: - "Play Next" (Prioritäts-Einreihung) - Queue-History (bereits gespielte Tracks) - Shuffle innerhalb der Queue """ # Queue-Items items: list[QueueItem] = field(default_factory=list) # History (bereits abgespielt) history: list[QueueItem] = field(default_factory=list) # Aktueller Zustand state: QueueState = QueueState.IDLE current_item: QueueItem | None = None # Einstellungen max_history: int = 100 # Maximale History-Größe # ID für Identifikation id: str = "" def __post_init__(self) -> None: if not self.id: self.id = f"playqueue_{uuid.uuid4().hex[:8]}" # ========================================================================== # Properties # ========================================================================== @property def count(self) -> int: """Anzahl der wartenden Tracks.""" return len(self.items) @property def is_empty(self) -> bool: """Prüft ob Queue leer ist.""" return len(self.items) == 0 @property def has_next(self) -> bool: """Prüft ob es einen nächsten Track gibt.""" return len(self.items) > 0 @property def has_previous(self) -> bool: """Prüft ob es einen vorherigen Track (History) gibt.""" return len(self.history) > 0 @property def total_duration_ms(self) -> int: """Gesamtdauer der Queue in Millisekunden.""" return sum(item.track.duration_ms for item in self.items) @property def current_track(self) -> Track | None: """Aktuell spielender Track.""" return self.current_item.track if self.current_item else None # ========================================================================== # Queue-Management # ========================================================================== def add( self, track: Track, added_by: str = "", play_next: bool = False, ) -> QueueItem: """ Fügt einen Track zur Queue hinzu. Args: track: Der hinzuzufügende Track added_by: Wer hat den Track hinzugefügt play_next: True = Als nächstes abspielen Returns: Das erstellte QueueItem """ item = QueueItem( track=track, added_by=added_by, priority=play_next, ) if play_next: # Am Anfang der Queue einfügen self.items.insert(0, item) else: # Am Ende anhängen self.items.append(item) return item def add_tracks( self, tracks: list[Track], added_by: str = "", ) -> list[QueueItem]: """ Fügt mehrere Tracks zur Queue hinzu. Args: tracks: Liste von Tracks added_by: Wer hat die Tracks hinzugefügt Returns: Liste der erstellten QueueItems """ items = [] for track in tracks: item = self.add(track, added_by=added_by) items.append(item) return items def remove(self, index: int) -> QueueItem | None: """ Entfernt Item an Position. Args: index: Position des zu entfernenden Items Returns: Entferntes Item oder None """ if 0 <= index < len(self.items): return self.items.pop(index) return None def remove_by_id(self, item_id: str) -> QueueItem | None: """ Entfernt Item nach ID. Args: item_id: ID des zu entfernenden Items Returns: Entferntes Item oder None """ for i, item in enumerate(self.items): if item.id == item_id: return self.items.pop(i) return None def clear(self) -> int: """ Leert die Queue (behält Current). Returns: Anzahl der entfernten Items """ count = len(self.items) self.items.clear() return count def clear_all(self) -> int: """ Leert Queue und History komplett. Returns: Anzahl der entfernten Items """ count = len(self.items) + len(self.history) self.items.clear() self.history.clear() self.current_item = None self.state = QueueState.IDLE return count def move(self, from_index: int, to_index: int) -> bool: """ Verschiebt Item an neue Position. Args: from_index: Aktuelle Position to_index: Ziel-Position Returns: True wenn erfolgreich """ if not (0 <= from_index < len(self.items) and 0 <= to_index < len(self.items)): return False item = self.items.pop(from_index) self.items.insert(to_index, item) return True # ========================================================================== # Navigation # ========================================================================== def next(self) -> QueueItem | None: """ Holt nächsten Track aus der Queue. Verschiebt aktuellen Track in History. Returns: Nächstes Item oder None """ # Aktuellen zur History hinzufügen if self.current_item: self.history.append(self.current_item) # History begrenzen while len(self.history) > self.max_history: self.history.pop(0) # Nächsten aus Queue holen if self.items: self.current_item = self.items.pop(0) return self.current_item self.current_item = None self.state = QueueState.IDLE return None def previous(self) -> QueueItem | None: """ Geht zum vorherigen Track (aus History). Verschiebt aktuellen Track zurück in Queue. Returns: Vorheriges Item oder None """ if not self.history: return None # Aktuellen zurück an Anfang der Queue if self.current_item: self.items.insert(0, self.current_item) # Letzten aus History holen self.current_item = self.history.pop() return self.current_item def skip_to(self, index: int) -> QueueItem | None: """ Springt zu einem bestimmten Item in der Queue. Alles davor wird übersprungen (nicht in History). Args: index: Ziel-Position Returns: Item an der Position oder None """ if not (0 <= index < len(self.items)): return None # Vorherige Items verwerfen for _ in range(index): self.items.pop(0) return self.next() def peek(self, count: int = 1) -> list[QueueItem]: """ Zeigt die nächsten Items ohne sie zu entfernen. Args: count: Anzahl der Items Returns: Liste der nächsten Items """ return self.items[:count] def peek_next(self) -> QueueItem | None: """ Zeigt das nächste Item ohne es zu entfernen. Returns: Nächstes Item oder None wenn Queue leer """ return self.items[0] if self.items else None # ========================================================================== # State Management # ========================================================================== def set_playing(self) -> None: """Setzt Zustand auf PLAYING.""" if self.current_item: self.state = QueueState.PLAYING def set_paused(self) -> None: """Setzt Zustand auf PAUSED.""" if self.current_item: self.state = QueueState.PAUSED def set_loading(self) -> None: """Setzt Zustand auf LOADING.""" self.state = QueueState.LOADING def set_error(self) -> None: """Setzt Zustand auf ERROR.""" self.state = QueueState.ERROR # ========================================================================== # Hilfsmethoden # ========================================================================== def find_track(self, track_id: str) -> tuple[int, QueueItem] | None: """ Findet Item nach Track-ID. Args: track_id: Track-ID Returns: Tuple (index, item) oder None """ for i, item in enumerate(self.items): if item.track.id == track_id: return (i, item) return None def contains_track(self, track_id: str) -> bool: """Prüft ob Track in Queue ist.""" return any(item.track.id == track_id for item in self.items) def to_dict(self) -> dict[str, Any]: """Konvertiert zu Dictionary.""" return { "id": self.id, "state": self.state.value, "count": self.count, "total_duration_ms": self.total_duration_ms, "current": self.current_item.track.to_dict() if self.current_item else None, "items": [ { "id": item.id, "track": item.track.to_dict(), "added_at": item.added_at.isoformat(), "added_by": item.added_by, "priority": item.priority, } for item in self.items ], "history_count": len(self.history), } def __len__(self) -> int: return len(self.items) def __iter__(self) -> Iterator[QueueItem]: return iter(self.items) def __getitem__(self, index: int) -> QueueItem: return self.items[index] def __repr__(self) -> str: return ( f"PlayQueue(id={self.id!r}, " f"state={self.state.value!r}, " f"count={self.count})" )