from music_assistant.models.player import Player
from music_assistant.providers.snapcast.constants import (
CONF_ENTRY_SAMPLE_RATES_SNAPCAST,
- CONTROL_SCRIPT,
DEFAULT_SNAPCAST_FORMAT,
MASS_ANNOUNCEMENT_POSTFIX,
MASS_STREAM_PREFIX,
# prefer to reuse existing stream if possible
if stream := self._get_snapstream(stream_name):
return stream
-
# The control script is used only for music streams in the builtin server
# (queue_id is None only for announcement streams).
- if self.provider._use_builtin_server and queue_id:
+ if (
+ self.provider._use_builtin_server
+ and queue_id
+ and self.provider._controlscript_available
+ ):
extra_args = (
- f"&controlscript={urllib.parse.quote_plus(str(CONTROL_SCRIPT))}"
+ f"&controlscript={urllib.parse.quote_plus('control.py')}"
f"&controlscriptparams=--queueid={urllib.parse.quote_plus(queue_id)}%20"
f"--api-port={self.mass.webserver.publish_port}%20"
f"--streamserver-ip={self.mass.streams.publish_ip}%20"
import asyncio
import logging
import re
+import shutil
import socket
+from pathlib import Path
from typing import cast
from bidict import bidict
CONF_SERVER_TRANSPORT_CODEC,
CONF_STREAM_IDLE_THRESHOLD,
CONF_USE_EXTERNAL_SERVER,
+ CONTROL_SCRIPT,
DEFAULT_SNAPSERVER_PORT,
SNAPWEB_DIR,
)
_ids_map: bidict[str, str] # ma_id / snapclient_id
_use_builtin_server: bool
_stop_called: bool
+ _controlscript_available: bool
async def handle_async_init(self) -> None:
"""Handle async initialization of the provider."""
logging.getLogger("snapcast").setLevel(self.logger.level)
self._use_builtin_server = not self.config.get_value(CONF_USE_EXTERNAL_SERVER)
self._stop_called = False
+ self._controlscript_available = False
if self._use_builtin_server:
self._snapcast_server_host = "127.0.0.1"
self._snapcast_server_control_port = DEFAULT_SNAPSERVER_PORT
if self._snapserver_started is not None:
self._snapserver_started.clear()
+ def _setup_controlscript(self) -> bool:
+ """Copy control script to plugin directory (blocking I/O).
+
+ :return: True if successful, False otherwise.
+ """
+ plugin_dir = Path("/usr/share/snapserver/plug-ins")
+ control_dest = plugin_dir / "control.py"
+ logger = self.logger.getChild("snapserver")
+ try:
+ plugin_dir.mkdir(parents=True, exist_ok=True)
+ # Clean up existing file
+ control_dest.unlink(missing_ok=True)
+ if not CONTROL_SCRIPT.exists():
+ logger.warning("Control script does not exist: %s", CONTROL_SCRIPT)
+ return False
+ # Copy the control script to the plugin directory
+ shutil.copy2(CONTROL_SCRIPT, control_dest)
+ # Ensure it's executable
+ control_dest.chmod(0o755)
+ logger.debug("Copied controlscript to: %s", control_dest)
+ return True
+ except (OSError, PermissionError) as err:
+ logger.warning(
+ "Could not copy controlscript (metadata/control disabled): %s",
+ err,
+ )
+ return False
+
async def _builtin_server_runner(self) -> None:
"""Start running the builtin snapserver."""
assert self._snapserver_started is not None # for type checking
# delay init a small bit to prevent race conditions
# where we try to connect too soon
self.mass.loop.call_later(2, self._snapserver_started.set)
+ # Copy control script after snapserver starts
+ # (run in executor to avoid blocking)
+ loop = asyncio.get_running_loop()
+ self._controlscript_available = await loop.run_in_executor(
+ None, self._setup_controlscript
+ )
def _get_ma_id(self, snap_client_id: str) -> str:
search_dict = self._ids_map.inverse