/
/
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
10if TYPE_CHECKING:
11 from music_assistant_models.enums import StreamType
12
13
14class BackendEventType(StrEnum):
15 """Type discriminator for the normalized events a backend emits to the provider."""
16
17 # session lifecycle: this device became / stopped being the active Spotify device
18 SESSION_ACTIVE = "session_active"
19 SESSION_INACTIVE = "session_inactive"
20 # playback state reported by the backend (BUFFERING is informational: reserved
21 # for backends that report it, the provider does not act on it)
22 PLAYING = "playing"
23 PAUSED = "paused"
24 STOPPED = "stopped"
25 BUFFERING = "buffering"
26 # track metadata and playback position updates
27 METADATA = "metadata"
28 POSITION = "position"
29 # Spotify-side volume change (normalized to a 0-100 percentage)
30 VOLUME = "volume"
31 # the backend lost its Spotify connection (e.g. daemon exit) and will recover
32 # on its own; any session/playback state is gone until a new SESSION_ACTIVE
33 CONNECTION_LOST = "connection_lost"
34 # a non-fatal backend error worth surfacing (message in the ``error`` field)
35 ERROR = "error"
36 # the backend lost its Spotify authentication and needs the user to log in again
37 AUTH_REQUIRED = "auth_required"
38 # the backend failed permanently and the provider must unload with an error
39 FATAL_ERROR = "fatal_error"
40 # any other backend activity; carries at most refreshed context/track uris
41 OTHER = "other"
42
43
44@dataclass(slots=True, frozen=True)
45class BackendStreamSource:
46 """
47 How a backend delivers its audio to the streams controller.
48
49 ``path`` is only set for path-based stream types (e.g. NAMED_PIPE); CUSTOM
50 sources deliver their audio through the backend's audio reader instead.
51 ``extra_input_args`` are passed to ffmpeg for the audio input.
52 """
53
54 stream_type: StreamType
55 path: str | None = None
56 extra_input_args: list[str] = field(default_factory=list)
57
58
59@dataclass(slots=True)
60class BackendTrackMetadata:
61 """
62 Normalized track metadata carried by a METADATA event.
63
64 ``duration`` and ``position`` are in seconds. A None ``title`` means the
65 backend did not report one (the provider keeps the previous title).
66 """
67
68 track_uri: str | None = None
69 title: str | None = None
70 artist: str | None = None
71 album: str | None = None
72 image_url: str | None = None
73 duration: int | None = None
74 position: int = 0
75
76
77@dataclass(slots=True)
78class BackendEvent:
79 """
80 A single normalized event emitted by a backend to the provider.
81
82 ``context_uri`` / ``track_uri`` piggyback on every event type: they carry
83 the latest context/track seen by the backend so the provider can take
84 playback back after the user moved the active device away. ``position`` is
85 the elapsed time in seconds (POSITION events), ``volume`` a 0-100
86 percentage (VOLUME events) and ``error`` the failure description
87 (ERROR and FATAL_ERROR events).
88 """
89
90 type: BackendEventType
91 context_uri: str | None = None
92 track_uri: str | None = None
93 metadata: BackendTrackMetadata | None = None
94 position: int | None = None
95 volume: int | None = None
96 error: str | None = None
97
98
99# Awaited by the backend for every normalized event, in emit order.
100BackendEventCallback = Callable[[BackendEvent], Awaitable[None]]
101
102# Reads the next chunk of decoded PCM; returns b"" once the audio pipe closes.
103AudioChunkReader = Callable[[], Awaitable[bytes]]
104