/
/
1"""Normalized models shared between the Spotify Connect provider and its backends."""
2
3from __future__ import annotations
4
5from collections.abc import Awaitable, Callable
6from dataclasses import dataclass, field
7from enum import StrEnum
8from typing import TYPE_CHECKING
9
10from music_assistant_models.enums import RepeatMode
11
12if TYPE_CHECKING:
13 from music_assistant_models.enums import StreamType
14
15
16class BackendEventType(StrEnum):
17 """Type discriminator for the normalized events a backend emits to the provider."""
18
19 # session lifecycle: this device became / stopped being the active Spotify device
20 SESSION_ACTIVE = "session_active"
21 SESSION_INACTIVE = "session_inactive"
22 # playback state reported by the backend (BUFFERING is informational: reserved
23 # for backends that report it, the provider does not act on it)
24 PLAYING = "playing"
25 PAUSED = "paused"
26 STOPPED = "stopped"
27 BUFFERING = "buffering"
28 # track metadata and playback position updates
29 METADATA = "metadata"
30 POSITION = "position"
31 # Spotify-side volume change (normalized to a 0-100 percentage)
32 VOLUME = "volume"
33 # queue listing / playback options reported by the session (only emitted
34 # by backends with ``supports_queue_control``)
35 QUEUE_CHANGED = "queue_changed"
36 OPTIONS_CHANGED = "options_changed"
37 # the backend lost its Spotify connection (e.g. daemon exit) and will recover
38 # on its own; any session/playback state is gone until a new SESSION_ACTIVE
39 CONNECTION_LOST = "connection_lost"
40 # a non-fatal backend error worth surfacing (message in the ``error`` field)
41 ERROR = "error"
42 # the backend failed permanently and the provider must unload with an error
43 FATAL_ERROR = "fatal_error"
44 # any other backend activity; carries at most refreshed context/track uris
45 OTHER = "other"
46
47
48@dataclass(slots=True, frozen=True)
49class BackendStreamSource:
50 """
51 How a backend delivers its audio to the streams controller.
52
53 ``path`` is only set for path-based stream types (e.g. NAMED_PIPE); CUSTOM
54 sources deliver their audio through the backend's audio reader instead.
55 ``extra_input_args`` are passed to ffmpeg for the audio input.
56 """
57
58 stream_type: StreamType
59 path: str | None = None
60 extra_input_args: list[str] = field(default_factory=list)
61
62
63@dataclass(slots=True)
64class BackendTrackMetadata:
65 """
66 Normalized track metadata carried by a METADATA event.
67
68 ``duration`` and ``position`` are in seconds. A None ``title`` means the
69 backend did not report one (the provider keeps the previous title).
70 """
71
72 track_uri: str | None = None
73 title: str | None = None
74 artist: str | None = None
75 album: str | None = None
76 image_url: str | None = None
77 duration: int | None = None
78 position: int = 0
79
80
81class QueueEntrySource(StrEnum):
82 """Where an entry in the session's queue listing comes from."""
83
84 # part of the playing context (album/playlist/artist)
85 CONTEXT = "context"
86 # explicitly queued by the user
87 QUEUE = "queue"
88 # session autoplay continuation
89 AUTOPLAY = "autoplay"
90 UNKNOWN = "unknown"
91
92 @classmethod
93 def _missing_(cls, value: object) -> QueueEntrySource: # noqa: ARG003
94 """Return UNKNOWN if an unknown value is provided."""
95 return cls.UNKNOWN
96
97
98@dataclass(slots=True)
99class BackendQueueEntry:
100 """
101 One entry in the session's queue listing.
102
103 ``uid`` is the session's stable handle for this entry (two occurrences of
104 the same track carry distinct uids). ``name`` is the display title when
105 the backend reported one.
106 """
107
108 uid: str
109 uri: str
110 source: QueueEntrySource
111 name: str | None = None
112
113
114@dataclass(slots=True)
115class BackendQueueState:
116 """The session's queue view: recently played and upcoming entries, in play order."""
117
118 previous: list[BackendQueueEntry] = field(default_factory=list)
119 upcoming: list[BackendQueueEntry] = field(default_factory=list)
120
121
122@dataclass(slots=True)
123class BackendPlaybackOptions:
124 """Playback options of the session."""
125
126 shuffle: bool = False
127 repeat: RepeatMode = RepeatMode.OFF
128
129
130@dataclass(slots=True)
131class BackendEvent:
132 """
133 A single normalized event emitted by a backend to the provider.
134
135 ``context_uri`` / ``track_uri`` piggyback on every event type: they carry
136 the latest context/track seen by the backend so the provider can take
137 playback back after the user moved the active device away. ``position`` is
138 the elapsed time in seconds (POSITION events), ``volume`` a 0-100
139 percentage (VOLUME events), ``error`` the failure description (ERROR and
140 FATAL_ERROR events), ``queue`` the session's queue view (QUEUE_CHANGED
141 events) and ``options`` the session's playback options (OPTIONS_CHANGED
142 events).
143 """
144
145 type: BackendEventType
146 context_uri: str | None = None
147 track_uri: str | None = None
148 metadata: BackendTrackMetadata | None = None
149 position: int | None = None
150 volume: int | None = None
151 error: str | None = None
152 queue: BackendQueueState | None = None
153 options: BackendPlaybackOptions | None = None
154
155
156# Awaited by the backend for every normalized event, in emit order.
157BackendEventCallback = Callable[[BackendEvent], Awaitable[None]]
158
159# Reads the next chunk of decoded PCM; returns b"" once the audio pipe closes.
160AudioChunkReader = Callable[[], Awaitable[bytes]]
161