self._on_unload_callbacks: list[Callable[..., None]] = []
self._runner_error_count = 0
self._spotify_device_id: str | None = None
+ self._last_session_connected_time: float = 0
async def handle_async_init(self) -> None:
"""Handle async initialization of the provider."""
self._librespot_bin = await get_librespot_binary()
- self.player = self.mass.players.get(self.mass_player_id)
- if self.player:
+ if self.mass.players.get(self.mass_player_id):
self._setup_player_daemon()
# Subscribe to events
if self._connected_spotify_username:
self.mass.create_task(self._check_spotify_provider_match())
+ def _process_librespot_stderr_line(self, line: str) -> None:
+ """
+ Process a single line from librespot stderr output.
+
+ :param line: A line from librespot's stderr output.
+ """
+ if (
+ not self._librespot_started.is_set()
+ and "Using StdoutSink (pipe) with format: S16" in line
+ ):
+ self._librespot_started.set()
+ if "error sending packet Os" in line:
+ return
+ if "dropping truncated packet" in line:
+ return
+ if "couldn't parse packet from " in line:
+ return
+ if "Authenticated as '" in line:
+ # Extract username from librespot authentication message
+ # Format: "Authenticated as 'username'"
+ try:
+ parts = line.split("Authenticated as '")
+ if len(parts) > 1:
+ username_part = parts[1].split("'")
+ if len(username_part) > 0 and username_part[0]:
+ username = username_part[0]
+ self._connected_spotify_username = username
+ self.logger.debug("Authenticated to Spotify as: %s", username)
+ # Check for provider match now that we have the username
+ self.mass.create_task(self._check_spotify_provider_match())
+ else:
+ self.logger.warning("Could not parse Spotify username from line: %s", line)
+ else:
+ self.logger.warning("Could not parse Spotify username from line: %s", line)
+ except Exception as err:
+ self.logger.warning("Error parsing Spotify username from line: %s - %s", line, err)
+ return
+ self.logger.debug(line)
+
async def _librespot_runner(self) -> None:
"""Run the spotify connect daemon in a background task."""
assert self._librespot_bin
await check_output("mkfifo", self.named_pipe)
await asyncio.sleep(0.1)
try:
+ # Get initial volume from player, or use 20 as fallback
+ initial_volume = 20
+ _player = self.mass.players.get(self.mass_player_id)
+ if _player and _player.volume_level:
+ initial_volume = _player.volume_level
+
args: list[str] = [
self._librespot_bin,
"--name",
"--volume-ctrl",
"fixed",
"--initial-volume",
- f"{self.player.volume_level if self.player and self.player.volume_level else 100}",
+ str(initial_volume),
"--enable-volume-normalisation",
# forward events to the events script
"--onevent",
# keep reading logging from stderr until exit
async for line in librespot.iter_stderr():
- if (
- not self._librespot_started.is_set()
- and "Using StdoutSink (pipe) with format: S16" in line
- ):
- self._librespot_started.set()
- if "error sending packet Os" in line:
- continue
- if "dropping truncated packet" in line:
- continue
- if "couldn't parse packet from " in line:
- continue
- if "Authenticated as '" in line:
- # Extract username from librespot authentication message
- # Format: "Authenticated as 'username'"
- try:
- parts = line.split("Authenticated as '")
- if len(parts) > 1:
- username_part = parts[1].split("'")
- if len(username_part) > 0 and username_part[0]:
- username = username_part[0]
- self._connected_spotify_username = username
- self.logger.debug("Authenticated to Spotify as: %s", username)
- # Check for provider match now that we have the username
- self.mass.create_task(self._check_spotify_provider_match())
- else:
- self.logger.warning(
- "Could not parse Spotify username from line: %s", line
- )
- else:
- self.logger.warning(
- "Could not parse Spotify username from line: %s", line
- )
- except Exception as err:
- self.logger.warning(
- "Error parsing Spotify username from line: %s - %s", line, err
- )
- continue
- self.logger.debug(line)
+ self._process_librespot_stderr_line(line)
finally:
await librespot.close()
self.logger.info("Spotify Connect background daemon stopped for %s", self.name)
self._runner_task = self.mass.create_task(self._librespot_runner())
def _on_mass_player_event(self, event: MassEvent) -> None:
- """Handle incoming event from linked airplay player."""
+ """Handle incoming event from linked player."""
if event.object_id != self.mass_player_id:
return
if event.event == EventType.PLAYER_REMOVED:
# handle session connected event
# extract the connected username and check for matching Spotify provider
if event_name == "session_connected":
+ # Track when session connected for volume event filtering
+ self._last_session_connected_time = time.time()
username = json_data.get("user_name")
self.logger.debug(
"Session connected event - username from event: %s, current username: %s",
self._source_details.metadata.elapsed_time_last_updated = int(time.time())
if event_name == "volume_changed" and (volume := json_data.get("volume")):
- # Spotify Connect volume is 0-65535
- volume = int(int(volume) / 65535 * 100)
- try:
- await self.mass.players.cmd_volume_set(self.mass_player_id, volume)
- except UnsupportedFeaturedException:
- self.logger.debug(f"Player {self.mass_player_id} does not support volume control")
+ # Ignore volume_changed events that fire immediately after session_connect
+ # We want to use the volume from MA in that case
+ time_since_connect = time.time() - self._last_session_connected_time
+ if time_since_connect < 2.0:
+ self.logger.debug(
+ "Ignoring initial volume_changed event (%.2fs after session_connect)",
+ time_since_connect,
+ )
+ else:
+ # Spotify Connect volume is 0-65535
+ volume = int(int(volume) / 65535 * 100)
+ try:
+ await self.mass.players.cmd_volume_set(self.mass_player_id, volume)
+ except UnsupportedFeaturedException:
+ self.logger.debug(
+ "Player %s does not support volume control", self.mass_player_id
+ )
# signal update to connected player
if self._source_details.in_use_by: