main.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. # -*- coding: utf-8 -*-
  2. """
  3. Spotify-Plugin (Beispiel).
  4. Demonstriert wie ein Plugin neue Musikquellen über das Extension-System
  5. hinzufügen kann. Dies ist ein Beispiel - für echte Spotify-Integration
  6. wäre die Spotipy-Bibliothek und OAuth2-Authentifizierung nötig.
  7. """
  8. import asyncio
  9. import logging
  10. from pathlib import Path
  11. from typing import Any, AsyncIterator
  12. from trixy_core.plugins import (
  13. TrixyPlugin,
  14. MusicSourceExtension,
  15. get_global_registry,
  16. MUSIC_SOURCE_POINT,
  17. )
  18. from trixy_core.music.sources.base import MusicSource, SourceType, SourceState
  19. from trixy_core.music.track import Track, TrackMetadata, TrackState
  20. from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
  21. class SpotifySource(MusicSource):
  22. """
  23. Spotify als Musikquelle.
  24. Hinweis: Dies ist eine Beispiel-Implementierung.
  25. Für echte Nutzung wäre spotipy und OAuth2 nötig.
  26. """
  27. def __init__(
  28. self,
  29. client_id: str = "",
  30. client_secret: str = "",
  31. redirect_uri: str = "http://localhost:8888/callback",
  32. ) -> None:
  33. super().__init__(name="Spotify", source_type=SourceType.SPOTIFY)
  34. self.client_id = client_id
  35. self.client_secret = client_secret
  36. self.redirect_uri = redirect_uri
  37. self.logger = logging.getLogger(__name__)
  38. self._authenticated = False
  39. self._access_token = ""
  40. async def connect(self) -> bool:
  41. """
  42. Verbindet zu Spotify (OAuth2).
  43. In einer echten Implementierung würde hier OAuth2 stattfinden.
  44. """
  45. if not self.client_id or not self.client_secret:
  46. pwarn("Spotify: Keine Credentials konfiguriert")
  47. self._state = SourceState.UNAVAILABLE
  48. return False
  49. # Hier würde OAuth2-Flow stattfinden
  50. pinfo("Spotify: Verbindung wird hergestellt...")
  51. # Simuliert erfolgreiche Verbindung
  52. self._authenticated = True
  53. self._state = SourceState.AVAILABLE
  54. pinfo("Spotify: Verbunden (Beispiel-Modus)")
  55. return True
  56. async def disconnect(self) -> None:
  57. """Trennt die Verbindung zu Spotify."""
  58. self._authenticated = False
  59. self._access_token = ""
  60. self._state = SourceState.UNAVAILABLE
  61. pinfo("Spotify: Verbindung getrennt")
  62. async def scan(self) -> int:
  63. """
  64. Scannt ist bei Spotify nicht sinnvoll.
  65. Stattdessen wird bei Bedarf gesucht.
  66. """
  67. return 0
  68. async def search(self, query: str, limit: int = 50) -> list[Track]:
  69. """
  70. Sucht auf Spotify.
  71. In echter Implementierung: Spotify Web API aufrufen.
  72. """
  73. if not self._authenticated:
  74. return []
  75. self.logger.debug(f"Spotify-Suche: {query}")
  76. # Beispiel-Ergebnisse (würde von API kommen)
  77. tracks = []
  78. # Simulierte Ergebnisse für Demo
  79. for i in range(min(5, limit)):
  80. track = Track(
  81. id=f"spotify_track_{i}",
  82. uri=f"spotify:track:example{i}",
  83. source_type="spotify",
  84. source_id=f"example{i}",
  85. metadata=TrackMetadata(
  86. title=f"Beispiel-Track {i + 1} für '{query}'",
  87. artist="Demo Artist",
  88. album="Demo Album",
  89. duration_ms=180000 + i * 30000,
  90. ),
  91. state=TrackState.AVAILABLE,
  92. )
  93. tracks.append(track)
  94. return tracks
  95. async def get_track(self, track_id: str) -> Track | None:
  96. """Holt Track-Details von Spotify."""
  97. return self._tracks.get(track_id)
  98. async def get_audio_stream(self, track: Track) -> AsyncIterator[bytes]:
  99. """
  100. Liefert Audio-Stream.
  101. Hinweis: Spotify erlaubt kein direktes Streaming über die API.
  102. Optionen:
  103. - Spotify Connect verwenden
  104. - Librespot für lokales Streaming
  105. """
  106. pwarn(
  107. "Spotify: Direktes Streaming nicht verfügbar. "
  108. "Verwende Spotify Connect oder Librespot."
  109. )
  110. # Leerer Generator - keine Audio-Daten
  111. return
  112. yield # macht dies zu einem Generator
  113. async def get_metadata(self, track: Track) -> TrackMetadata | None:
  114. """Lädt Metadaten von Spotify."""
  115. # Würde API aufrufen
  116. return track.metadata
  117. class SpotifySourceExtension(MusicSourceExtension):
  118. """
  119. Extension die SpotifySource bereitstellt.
  120. """
  121. def __init__(self, plugin_name: str) -> None:
  122. super().__init__(
  123. extension_id="spotify_source",
  124. name="Spotify",
  125. plugin_name=plugin_name,
  126. source_type="spotify",
  127. description="Spotify-Integration für Musiksuche und Wiedergabe",
  128. )
  129. self._info.priority = 100 # Hohe Priorität
  130. async def create_instance(self, config: dict[str, Any]) -> SpotifySource:
  131. """Erstellt SpotifySource mit Konfiguration."""
  132. return SpotifySource(
  133. client_id=config.get("client_id", ""),
  134. client_secret=config.get("client_secret", ""),
  135. redirect_uri=config.get("redirect_uri", "http://localhost:8888/callback"),
  136. )
  137. def get_default_config(self) -> dict[str, Any]:
  138. """Standard-Konfiguration."""
  139. return {
  140. "client_id": "",
  141. "client_secret": "",
  142. "redirect_uri": "http://localhost:8888/callback",
  143. }
  144. def get_config_schema(self) -> dict[str, Any]:
  145. """JSON-Schema für Konfiguration."""
  146. return {
  147. "type": "object",
  148. "properties": {
  149. "client_id": {
  150. "type": "string",
  151. "description": "Spotify Client ID",
  152. },
  153. "client_secret": {
  154. "type": "string",
  155. "description": "Spotify Client Secret",
  156. },
  157. "redirect_uri": {
  158. "type": "string",
  159. "description": "OAuth Redirect URI",
  160. },
  161. },
  162. "required": ["client_id", "client_secret"],
  163. }
  164. class SpotifyPlugin(TrixyPlugin):
  165. """
  166. Spotify-Plugin.
  167. Registriert SpotifySource als Extension am music.source Extension Point.
  168. """
  169. NAME = "spotify"
  170. VERSION = "1.0.0"
  171. DESCRIPTION = "Spotify-Integration für Trixy"
  172. AUTHOR = "Trixy Team"
  173. def __init__(self, application, plugin_path: Path, config: dict | None = None) -> None:
  174. super().__init__(application, plugin_path, config)
  175. self._extension: SpotifySourceExtension | None = None
  176. async def on_load(self) -> None:
  177. """Plugin wird geladen - Extension registrieren."""
  178. pinfo("Spotify-Plugin: Lade...")
  179. # Extension erstellen
  180. self._extension = SpotifySourceExtension(self.NAME)
  181. # Am Extension Point registrieren
  182. registry = get_global_registry()
  183. point = registry.get_point(MUSIC_SOURCE_POINT)
  184. if point is not None:
  185. # Konfiguration aus Plugin-Config
  186. config = {
  187. "client_id": self.get_config_value("spotify.client_id", ""),
  188. "client_secret": self.get_config_value("spotify.client_secret", ""),
  189. "redirect_uri": self.get_config_value(
  190. "spotify.redirect_uri",
  191. "http://localhost:8888/callback"
  192. ),
  193. }
  194. success = await point.register(self._extension, config)
  195. if success:
  196. pinfo("Spotify-Plugin: Extension registriert")
  197. else:
  198. perror("Spotify-Plugin: Extension-Registrierung fehlgeschlagen")
  199. else:
  200. pwarn("Spotify-Plugin: music.source Extension Point nicht gefunden")
  201. async def on_unload(self) -> None:
  202. """Plugin wird entladen - Extension entfernen."""
  203. pinfo("Spotify-Plugin: Entlade...")
  204. if self._extension is not None:
  205. registry = get_global_registry()
  206. point = registry.get_point(MUSIC_SOURCE_POINT)
  207. if point is not None:
  208. await point.unregister(self._extension.id)
  209. self._extension = None
  210. async def on_enable(self) -> None:
  211. """Plugin wird aktiviert."""
  212. pinfo("Spotify-Plugin: Aktiviert")
  213. # Spotify-Source verbinden
  214. if self._extension and self._extension.instance:
  215. await self._extension.instance.connect()
  216. async def on_disable(self) -> None:
  217. """Plugin wird deaktiviert."""
  218. pinfo("Spotify-Plugin: Deaktiviert")
  219. # Spotify-Source trennen
  220. if self._extension and self._extension.instance:
  221. await self._extension.instance.disconnect()
  222. async def on_config_change(self, old_config: dict) -> None:
  223. """Konfiguration hat sich geändert."""
  224. pinfo("Spotify-Plugin: Konfiguration aktualisiert")
  225. # Bei Credential-Änderung neu verbinden
  226. if self._extension and self._extension.instance:
  227. new_client_id = self.get_config_value("spotify.client_id", "")
  228. old_client_id = old_config.get("spotify", {}).get("client_id", "")
  229. if new_client_id != old_client_id:
  230. pinfo("Spotify: Credentials geändert, verbinde neu...")
  231. await self._extension.instance.disconnect()
  232. self._extension.instance.client_id = new_client_id
  233. self._extension.instance.client_secret = self.get_config_value(
  234. "spotify.client_secret", ""
  235. )
  236. await self._extension.instance.connect()