/
/
1"""Backend contract for the Spotify Connect provider."""
2
3from __future__ import annotations
4
5from abc import ABC, abstractmethod
6from typing import TYPE_CHECKING
7
8if TYPE_CHECKING:
9 from music_assistant_models.media_items import AudioFormat
10
11 from music_assistant.providers.spotify_connect.models import AudioChunkReader
12
13
14class SpotifyConnectBackend(ABC):
15 """
16 Contract between the SpotifyConnectProvider and a Spotify Connect implementation.
17
18 A backend owns everything specific to one way of talking to Spotify
19 (daemon lifecycle, credentials, wire protocol, audio delivery) and reports
20 state changes as normalized ``BackendEvent``s (see ``models.py``) through
21 the single async callback supplied at construction time. The provider
22 drives the backend exclusively through the methods below, so it never
23 needs to know which backend it is talking to.
24 """
25
26 @property
27 @abstractmethod
28 def audio_format(self) -> AudioFormat:
29 """Return the source audio format (advertised to clients for display)."""
30
31 @property
32 @abstractmethod
33 def decoded_audio_format(self) -> AudioFormat:
34 """Return the decoded PCM format the audio reader actually delivers."""
35
36 @abstractmethod
37 async def start(self) -> None:
38 """Start the backend and its supervised Spotify Connect implementation."""
39
40 @abstractmethod
41 async def stop(self) -> None:
42 """Stop the backend and release all its resources."""
43
44 @abstractmethod
45 def get_audio_reader(self) -> AudioChunkReader | None:
46 """
47 Return a PCM chunk reader bound to the currently live audio pipe.
48
49 The reader yields raw PCM in ``decoded_audio_format`` and returns an
50 empty bytes object once that pipe closes (it does not follow a backend
51 restart). None is returned when no audio pipe is available.
52 """
53
54 @abstractmethod
55 async def play(self, uri: str, *, skip_to_uri: str | None = None) -> None:
56 """
57 Start playing a Spotify URI/context, making this device the active one.
58
59 :param uri: Spotify URI (track, album, playlist, ...) â typically a context.
60 :param skip_to_uri: Optional track URI within the context to start at.
61 """
62
63 @abstractmethod
64 async def resume(self) -> None:
65 """Resume playback on the active session."""
66
67 @abstractmethod
68 async def pause(self) -> None:
69 """Pause playback on the active session."""
70
71 @abstractmethod
72 async def next(self) -> None:
73 """Skip to the next track."""
74
75 @abstractmethod
76 async def previous(self) -> None:
77 """Skip to the previous track (or rewind the current one)."""
78
79 @abstractmethod
80 async def seek(self, position_ms: int) -> None:
81 """
82 Seek to an absolute position in the current track.
83
84 :param position_ms: Target position in milliseconds.
85 """
86
87 @abstractmethod
88 async def set_volume(self, volume: int) -> None:
89 """
90 Set the Spotify-side playback volume.
91
92 :param volume: Absolute volume as a 0-100 percentage.
93 """
94