| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- # -*- coding: utf-8 -*-
- """
- Source-Manager.
- Verwaltet mehrere Musikquellen und bietet einheitlichen Zugriff.
- """
- import asyncio
- import logging
- from typing import Any, AsyncIterator
- from trixy_core.music.sources.base import MusicSource, SourceType, SourceState, SourceInfo
- from trixy_core.music.track import Track, TrackMetadata
- class SourceManager:
- """
- Verwaltet mehrere Musikquellen.
- Bietet einheitlichen Zugriff auf alle registrierten Quellen:
- - Lokale Dateien
- - USB-Speicher
- - URL-Streams
- - (zukünftig) Streaming-Dienste
- """
- def __init__(self) -> None:
- """Initialisiert den SourceManager."""
- self._sources: dict[str, MusicSource] = {}
- self._default_source: str = ""
- self.logger = logging.getLogger(__name__)
- # ==========================================================================
- # Properties
- # ==========================================================================
- @property
- def sources(self) -> list[MusicSource]:
- """Alle registrierten Quellen."""
- return list(self._sources.values())
- @property
- def source_count(self) -> int:
- """Anzahl der Quellen."""
- return len(self._sources)
- @property
- def total_track_count(self) -> int:
- """Gesamtzahl der Tracks über alle Quellen."""
- return sum(source.track_count for source in self._sources.values())
- @property
- def default_source(self) -> MusicSource | None:
- """Standard-Quelle."""
- return self._sources.get(self._default_source)
- # ==========================================================================
- # Quellen-Management
- # ==========================================================================
- def register(
- self,
- source: MusicSource,
- name: str = "",
- set_default: bool = False,
- ) -> str:
- """
- Registriert eine Musikquelle.
- Args:
- source: Musikquelle
- name: Optionaler eindeutiger Name
- set_default: Als Standard-Quelle setzen
- Returns:
- Registrierter Name
- """
- # Name generieren falls nicht angegeben
- if not name:
- name = f"{source.source_type.value}_{len(self._sources)}"
- # Eindeutigkeit prüfen
- if name in self._sources:
- i = 1
- while f"{name}_{i}" in self._sources:
- i += 1
- name = f"{name}_{i}"
- self._sources[name] = source
- self.logger.debug(f"Quelle registriert: {name} ({source.source_type.value})")
- if set_default or not self._default_source:
- self._default_source = name
- return name
- def unregister(self, name: str) -> bool:
- """
- Entfernt eine Musikquelle.
- Args:
- name: Quellen-Name
- Returns:
- True wenn entfernt
- """
- if name in self._sources:
- del self._sources[name]
- if self._default_source == name:
- self._default_source = next(iter(self._sources), "")
- self.logger.debug(f"Quelle entfernt: {name}")
- return True
- return False
- def get(self, name: str) -> MusicSource | None:
- """
- Holt Quelle nach Name.
- Args:
- name: Quellen-Name
- Returns:
- Quelle oder None
- """
- return self._sources.get(name)
- def get_by_type(self, source_type: SourceType) -> list[MusicSource]:
- """
- Holt alle Quellen eines Typs.
- Args:
- source_type: Quellen-Typ
- Returns:
- Liste der Quellen
- """
- return [s for s in self._sources.values() if s.source_type == source_type]
- def set_default(self, name: str) -> bool:
- """
- Setzt Standard-Quelle.
- Args:
- name: Quellen-Name
- Returns:
- True wenn gesetzt
- """
- if name in self._sources:
- self._default_source = name
- return True
- return False
- # ==========================================================================
- # Lifecycle
- # ==========================================================================
- async def connect_all(self) -> dict[str, bool]:
- """
- Verbindet alle Quellen.
- Returns:
- Dict Name → Erfolg
- """
- results = {}
- tasks = {
- name: source.connect()
- for name, source in self._sources.items()
- }
- for name, task in tasks.items():
- try:
- results[name] = await task
- except Exception as e:
- self.logger.error(f"Verbindung zu {name} fehlgeschlagen: {e}")
- results[name] = False
- self.logger.info(f"Quellen verbunden: {sum(results.values())}/{len(results)}")
- return results
- async def disconnect_all(self) -> None:
- """Trennt alle Quellen."""
- for source in self._sources.values():
- try:
- await source.disconnect()
- except Exception as e:
- self.logger.error(f"Trennen fehlgeschlagen: {e}")
- async def scan_all(self) -> dict[str, int]:
- """
- Scannt alle Quellen.
- Returns:
- Dict Name → Anzahl Tracks
- """
- results = {}
- # Parallel scannen
- async def scan_source(name: str, source: MusicSource) -> tuple[str, int]:
- try:
- count = await source.scan()
- return (name, count)
- except Exception as e:
- self.logger.error(f"Scan von {name} fehlgeschlagen: {e}")
- return (name, 0)
- tasks = [
- scan_source(name, source)
- for name, source in self._sources.items()
- ]
- for coro in asyncio.as_completed(tasks):
- name, count = await coro
- results[name] = count
- total = sum(results.values())
- self.logger.info(f"Scan abgeschlossen: {total} Tracks gesamt")
- return results
- # ==========================================================================
- # Track-Zugriff
- # ==========================================================================
- async def get_track(self, track_id: str) -> Track | None:
- """
- Sucht Track in allen Quellen.
- Args:
- track_id: Track-ID
- Returns:
- Track oder None
- """
- for source in self._sources.values():
- track = await source.get_track(track_id)
- if track:
- return track
- return None
- async def get_track_from_source(
- self,
- source_name: str,
- track_id: str,
- ) -> Track | None:
- """
- Holt Track von bestimmter Quelle.
- Args:
- source_name: Quellen-Name
- track_id: Track-ID
- Returns:
- Track oder None
- """
- source = self._sources.get(source_name)
- if source:
- return await source.get_track(track_id)
- return None
- def get_all_tracks(self) -> list[Track]:
- """
- Holt alle Tracks aus allen Quellen.
- Returns:
- Liste aller Tracks
- """
- all_tracks = []
- for source in self._sources.values():
- all_tracks.extend(source.tracks)
- return all_tracks
- # ==========================================================================
- # Streaming
- # ==========================================================================
- async def get_audio_stream(self, track: Track) -> AsyncIterator[bytes]:
- """
- Liefert Audio-Stream für Track.
- Findet automatisch die richtige Quelle.
- Args:
- track: Track zum Streamen
- Yields:
- Audio-Daten in Chunks
- """
- # Quelle anhand source_type finden
- for source in self._sources.values():
- if source.source_type.value == track.source_type:
- async for chunk in source.get_audio_stream(track):
- yield chunk
- return
- # Fallback: Alle Quellen durchsuchen
- for source in self._sources.values():
- found = await source.get_track(track.id)
- if found:
- async for chunk in source.get_audio_stream(track):
- yield chunk
- return
- self.logger.warning(f"Keine Quelle für Track: {track.id}")
- # ==========================================================================
- # Suche
- # ==========================================================================
- async def search(
- self,
- query: str,
- limit: int = 50,
- source_types: list[SourceType] | None = None,
- ) -> list[Track]:
- """
- Sucht in allen (oder ausgewählten) Quellen.
- Args:
- query: Suchstring
- limit: Maximale Ergebnisse
- source_types: Optionale Einschränkung auf Quellen-Typen
- Returns:
- Liste der gefundenen Tracks
- """
- results = []
- for source in self._sources.values():
- # Typ-Filter
- if source_types and source.source_type not in source_types:
- continue
- # Suchen
- try:
- tracks = await source.search(query, limit=limit - len(results))
- results.extend(tracks)
- if len(results) >= limit:
- break
- except Exception as e:
- self.logger.error(f"Suche in {source.name} fehlgeschlagen: {e}")
- return results[:limit]
- # ==========================================================================
- # Metadaten
- # ==========================================================================
- async def get_metadata(self, track: Track) -> TrackMetadata | None:
- """
- Lädt Metadaten für Track.
- Args:
- track: Track
- Returns:
- Metadaten oder None
- """
- for source in self._sources.values():
- if source.source_type.value == track.source_type:
- return await source.get_metadata(track)
- return track.metadata
- async def get_artwork(self, track: Track) -> bytes | None:
- """
- Lädt Artwork für Track.
- Args:
- track: Track
- Returns:
- Artwork-Daten oder None
- """
- for source in self._sources.values():
- if source.source_type.value == track.source_type:
- return await source.get_artwork(track)
- return None
- # ==========================================================================
- # Status
- # ==========================================================================
- def get_status(self) -> dict[str, Any]:
- """
- Liefert Status aller Quellen.
- Returns:
- Status-Dictionary
- """
- return {
- "source_count": self.source_count,
- "total_tracks": self.total_track_count,
- "default_source": self._default_source,
- "sources": {
- name: {
- "type": source.source_type.value,
- "state": source.state.value,
- "track_count": source.track_count,
- "is_available": source.is_available,
- }
- for name, source in self._sources.items()
- },
- }
- def get_source_infos(self) -> list[SourceInfo]:
- """
- Liefert Informationen über alle Quellen.
- Returns:
- Liste von SourceInfo
- """
- return [source.get_info() for source in self._sources.values()]
- def __contains__(self, name: str) -> bool:
- return name in self._sources
- def __getitem__(self, name: str) -> MusicSource:
- return self._sources[name]
- def __iter__(self):
- return iter(self._sources.values())
- def __len__(self) -> int:
- return len(self._sources)
- def __repr__(self) -> str:
- return (
- f"SourceManager(sources={self.source_count}, "
- f"tracks={self.total_track_count})"
- )
|