/
/
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 (
12 AudioChunkReader,
13 BackendStreamSource,
14 )
15
16
17class SpotifyConnectBackend(ABC):
18 """
19 Contract between the SpotifyConnectProvider and a Spotify Connect implementation.
20
21 A backend owns everything specific to one way of talking to Spotify
22 (daemon lifecycle, credentials, wire protocol, audio delivery) and reports
23 state changes as normalized ``BackendEvent``s (see ``models.py``) through
24 the single async callback supplied at construction time. The provider
25 drives the backend exclusively through the methods below, so it never
26 needs to know which backend it is talking to.
27 """
28
29 @property
30 @abstractmethod
31 def audio_format(self) -> AudioFormat:
32 """Return the source audio format (advertised to clients for display)."""
33
34 @property
35 @abstractmethod
36 def decoded_audio_format(self) -> AudioFormat:
37 """Return the decoded PCM format the audio reader actually delivers."""
38
39 @property
40 def stream_ends_on_pause(self) -> bool:
41 """
42 Whether the audio stream reaches a clean end when playback pauses.
43
44 Pipe-fed backends deliver silence on pause instead; the provider then
45 stops the player actively on the paused state event.
46 """
47 return True
48
49 @abstractmethod
50 async def start(self) -> None:
51 """Start the backend and its supervised Spotify Connect implementation."""
52
53 @abstractmethod
54 async def stop(self) -> None:
55 """Stop the backend and release all its resources."""
56
57 @abstractmethod
58 async def get_stream_source(self) -> BackendStreamSource:
59 """
60 Return how the streams controller should consume this backend's audio.
61
62 Called on every stream request â including queue preload, so this must
63 be side-effect-free. The result describes the live audio delivery
64 (stream type, optional pipe path and extra ffmpeg input arguments).
65 The delivered PCM is in ``decoded_audio_format``.
66 """
67
68 @abstractmethod
69 def get_audio_reader(self) -> AudioChunkReader | None:
70 """
71 Return a PCM chunk reader bound to the currently live audio pipe.
72
73 The reader yields raw PCM in ``decoded_audio_format`` and returns an
74 empty bytes object once that pipe closes (it does not follow a backend
75 restart). None is returned when no audio pipe is available.
76 """
77
78 @abstractmethod
79 async def play(self, uri: str, *, skip_to_uri: str | None = None) -> None:
80 """
81 Start playing a Spotify URI/context, making this device the active one.
82
83 :param uri: Spotify URI (track, album, playlist, ...) â typically a context.
84 :param skip_to_uri: Optional track URI within the context to start at.
85 """
86
87 @abstractmethod
88 async def resume(self) -> None:
89 """Resume playback on the active session."""
90
91 @abstractmethod
92 async def pause(self) -> None:
93 """Pause playback on the active session."""
94
95 @abstractmethod
96 async def deactivate(self) -> None:
97 """
98 Release this device as the active Spotify Connect device.
99
100 Ends the current session so the Spotify apps drop the device as their
101 playback target; the device stays available for reselection.
102 """
103
104 @abstractmethod
105 async def next(self) -> None:
106 """Skip to the next track."""
107
108 @abstractmethod
109 async def previous(self) -> None:
110 """Skip to the previous track (or rewind the current one)."""
111
112 @abstractmethod
113 async def seek(self, position_ms: int) -> None:
114 """
115 Seek to an absolute position in the current track.
116
117 :param position_ms: Target position in milliseconds.
118 """
119
120 @abstractmethod
121 async def set_volume(self, volume: int) -> None:
122 """
123 Set the Spotify-side playback volume.
124
125 :param volume: Absolute volume as a 0-100 percentage.
126 """
127