| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- # -*- 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})"
- )
|