music-assistant: 2.6.3 -> 2.7.2 (#473061)

This commit is contained in:
Emily
2025-12-22 16:57:11 +00:00
committed by GitHub
10 changed files with 220 additions and 98 deletions
@@ -125,6 +125,9 @@ in
"AF_INET"
"AF_INET6"
"AF_NETLINK"
]
++ lib.optionals (lib.elem "snapcast" cfg.providers) [
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
@@ -132,6 +135,7 @@ in
SystemCallFilter = [
"@system-service"
"~@privileged @resources"
"mbind"
]
++ lib.optionals useYTMusic [
"@pkey"
@@ -1,8 +1,8 @@
diff --git a/music_assistant/providers/snapcast/__init__.py b/music_assistant/providers/snapcast/__init__.py
index 742d798183ec6fd1ac1fdbcad750912c222de5c3..8d0d95577d31f735977884280b08f384187774bd 100644
--- a/music_assistant/providers/snapcast/__init__.py
+++ b/music_assistant/providers/snapcast/__init__.py
@@ -87,7 +87,8 @@ DEFAULT_SNAPSTREAM_IDLE_THRESHOLD = 60000
diff --git a/music_assistant/providers/snapcast/constants.py b/music_assistant/providers/snapcast/constants.py
index eb5ecb1a..92f03212 100644
--- a/music_assistant/providers/snapcast/constants.py
+++ b/music_assistant/providers/snapcast/constants.py
@@ -43,7 +43,8 @@ CONTROL_SOCKET_PATH_TEMPLATE = "/tmp/ma-snapcast-{queue_id}.sock" # noqa: S108
MASS_STREAM_PREFIX = "Music Assistant - "
MASS_ANNOUNCEMENT_POSTFIX = " (announcement)"
SNAPWEB_DIR = pathlib.Path(__file__).parent.resolve().joinpath("snapweb")
@@ -12,7 +12,19 @@ index 742d798183ec6fd1ac1fdbcad750912c222de5c3..8d0d95577d31f735977884280b08f384
DEFAULT_SNAPCAST_FORMAT = AudioFormat(
content_type=ContentType.PCM_S16LE,
@@ -866,6 +867,10 @@ class SnapCastProvider(PlayerProvider):
diff --git a/music_assistant/providers/snapcast/provider.py b/music_assistant/providers/snapcast/provider.py
index 94c197fc..9a344f53 100644
--- a/music_assistant/providers/snapcast/provider.py
+++ b/music_assistant/providers/snapcast/provider.py
@@ -32,6 +32,7 @@ from music_assistant.providers.snapcast.constants import (
CONF_STREAM_IDLE_THRESHOLD,
CONF_USE_EXTERNAL_SERVER,
CONTROL_SCRIPT,
+ CONTROL_SCRIPT_DIR,
CONTROL_SOCKET_PATH_TEMPLATE,
DEFAULT_SNAPSERVER_PORT,
SNAPWEB_DIR,
@@ -215,6 +216,10 @@ class SnapCastProvider(PlayerProvider):
args = [
"snapserver",
@@ -23,7 +35,7 @@ index 742d798183ec6fd1ac1fdbcad750912c222de5c3..8d0d95577d31f735977884280b08f384
# config settings taken from
# https://raw.githubusercontent.com/badaix/snapcast/86cd4b2b63e750a72e0dfe6a46d47caf01426c8d/server/etc/snapserver.conf
f"--server.datadir={self.mass.storage_path}",
@@ -878,6 +883,7 @@ class SnapCastProvider(PlayerProvider):
@@ -227,6 +232,7 @@ class SnapCastProvider(PlayerProvider):
f"--stream.buffer={self._snapcast_server_buffer_size}",
f"--stream.chunk_ms={self._snapcast_server_chunk_ms}",
f"--stream.codec={self._snapcast_server_transport_codec}",
@@ -31,3 +43,14 @@ index 742d798183ec6fd1ac1fdbcad750912c222de5c3..8d0d95577d31f735977884280b08f384
f"--stream.send_to_muted={str(self._snapcast_server_send_to_muted).lower()}",
f"--streaming_client.initial_volume={self._snapcast_server_initial_volume}",
]
@@ -243,9 +249,7 @@ class SnapCastProvider(PlayerProvider):
# 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
- )
+ self._controlscript_available = True
def _get_ma_id(self, snap_client_id: str) -> str:
search_dict = self._ids_map.inverse
@@ -1,8 +1,8 @@
diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py
index 74540dd3..14f8f864 100644
index bc14912b..0527a831 100644
--- a/music_assistant/helpers/util.py
+++ b/music_assistant/helpers/util.py
@@ -434,30 +434,11 @@ async def load_provider_module(domain: str, requirements: list[str]) -> Provider
@@ -495,30 +495,11 @@ async def load_provider_module(domain: str, requirements: list[str]) -> Provider
"ProviderModuleType", importlib.import_module(f".{domain}", "music_assistant.providers")
)
@@ -35,10 +35,10 @@ index 74540dd3..14f8f864 100644
async def has_tmpfs_mount() -> bool:
"""Check if we have a tmpfs mount."""
diff --git a/music_assistant/providers/ytmusic/__init__.py b/music_assistant/providers/ytmusic/__init__.py
index 52a7544a..816d0425 100644
index 9cc5981a..dc424b0b 100644
--- a/music_assistant/providers/ytmusic/__init__.py
+++ b/music_assistant/providers/ytmusic/__init__.py
@@ -197,7 +197,6 @@ class YoutubeMusicProvider(MusicProvider):
@@ -200,7 +200,6 @@ class YoutubeMusicProvider(MusicProvider):
async def handle_async_init(self) -> None:
"""Set up the YTMusic provider."""
logging.getLogger("yt_dlp").setLevel(self.logger.level + 10)
@@ -46,3 +46,12 @@ index 52a7544a..816d0425 100644
self._cookie = self.config.get_value(CONF_COOKIE)
self._po_token_server_url = (
self.config.get_value(CONF_PO_TOKEN_SERVER_URL) or DEFAULT_PO_TOKEN_SERVER_URL
@@ -1091,8 +1090,6 @@ class YoutubeMusicProvider(MusicProvider):
# NOTE: Google breaks things quite often which requires us to update
# some packages very frequently. Installing them dynamically prevents
# us from having to update MA to ensure this provider works.
- for package_name in PACKAGES_TO_INSTALL:
- await install_package(package_name)
# verify if the yt_dlp package is usable
try:
await asyncio.to_thread(importlib.import_module, "yt_dlp")
+33 -20
View File
@@ -1,17 +1,21 @@
diff --git a/music_assistant/controllers/streams/smart_fades/fades.py b/music_assistant/controllers/streams/smart_fades/fades.py
index 03c11ef9..460d53a6 100644
--- a/music_assistant/controllers/streams/smart_fades/fades.py
+++ b/music_assistant/controllers/streams/smart_fades/fades.py
@@ -76,7 +76,7 @@ class SmartFade(ABC):
async with aiofiles.open(fadeout_filename, "wb") as outfile:
await outfile.write(fade_out_part)
args = [
- "ffmpeg",
+ "@ffmpeg@",
"-hide_banner",
"-loglevel",
"error",
diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py
index 482ab8e8..a02fc435 100644
index 0674b009..53158f4d 100644
--- a/music_assistant/helpers/audio.py
+++ b/music_assistant/helpers/audio.py
@@ -80,7 +80,7 @@ async def crossfade_pcm_parts(
await outfile.write(fade_out_part)
args = [
# generic args
- "ffmpeg",
+ "@ffmpeg@",
"-hide_banner",
"-loglevel",
"quiet",
@@ -141,7 +141,7 @@ async def strip_silence(
@@ -103,7 +103,7 @@ async def strip_silence(
reverse: bool = False,
) -> bytes:
"""Strip silence from begin or end of pcm audio using ffmpeg."""
@@ -20,7 +24,7 @@ index 482ab8e8..a02fc435 100644
args += [
"-acodec",
pcm_format.content_type.name.lower(),
@@ -912,7 +912,7 @@ async def get_silence(
@@ -1134,7 +1134,7 @@ async def get_silence(
return
# use ffmpeg for all other encodings
args = [
@@ -30,11 +34,11 @@ index 482ab8e8..a02fc435 100644
"-loglevel",
"quiet",
diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py
index 7c1c8d83..501a7370 100644
index 9ea5ea0c..51b1d62b 100644
--- a/music_assistant/helpers/ffmpeg.py
+++ b/music_assistant/helpers/ffmpeg.py
@@ -218,7 +218,7 @@ def get_ffmpeg_args(
extra_args = []
@@ -249,7 +249,7 @@ def get_ffmpeg_args( # noqa: PLR0915
extra_output_args = []
# generic args
generic_args = [
- "ffmpeg",
@@ -42,7 +46,7 @@ index 7c1c8d83..501a7370 100644
"-hide_banner",
"-loglevel",
loglevel,
@@ -370,7 +370,7 @@ async def check_ffmpeg_version() -> None:
@@ -416,7 +416,7 @@ async def check_ffmpeg_version() -> None:
"""Check if ffmpeg is present (with libsoxr support)."""
# check for FFmpeg presence
try:
@@ -52,10 +56,10 @@ index 7c1c8d83..501a7370 100644
raise AudioError(
"FFmpeg binary is missing from system. "
diff --git a/music_assistant/helpers/tags.py b/music_assistant/helpers/tags.py
index 06c8bcb5..a703aacd 100644
index fe83301c..d35760b6 100644
--- a/music_assistant/helpers/tags.py
+++ b/music_assistant/helpers/tags.py
@@ -438,7 +438,7 @@ def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags:
@@ -484,7 +484,7 @@ def parse_tags(
Input_file may be a (local) filename or URL accessible by ffmpeg.
"""
args = (
@@ -64,11 +68,20 @@ index 06c8bcb5..a703aacd 100644
"-hide_banner",
"-loglevel",
"fatal",
@@ -553,7 +553,7 @@ async def get_embedded_image(input_file: str) -> bytes | None:
Input_file may be a (local) filename or URL accessible by ffmpeg.
@@ -551,7 +551,7 @@ def get_file_duration(input_file: str) -> float:
NOT Async friendly.
"""
args = (
- "ffmpeg",
+ "@ffmpeg@",
"-hide_banner",
"-loglevel",
"info",
@@ -707,7 +707,7 @@ async def get_embedded_image(input_file: str) -> bytes | None:
# Use FFmpeg for all other cases (URLs, ID3 tags, Vorbis comments, etc.)
args = [
- "ffmpeg",
+ "@ffmpeg@",
"-hide_banner",
"-loglevel",
@@ -0,0 +1,13 @@
diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py
index bc14912b..d207b855 100644
--- a/music_assistant/helpers/util.py
+++ b/music_assistant/helpers/util.py
@@ -274,7 +274,7 @@ async def get_ip_addresses(include_ipv6: bool = False) -> tuple[str, ...]:
if ip.is_IPv6 and not include_ipv6:
continue
ip_str = str(ip.ip)
- if ip_str.startswith(("127", "169.254")):
+ if ip_str.startswith(("127", "169.254")) and "PYTEST_VERSION" not in os.environ:
# filter out IPv4 loopback/APIPA address
continue
if ip_str.startswith(("::1", "::ffff:", "fe80")):
+2 -2
View File
@@ -7,13 +7,13 @@
buildPythonPackage rec {
pname = "music-assistant-frontend";
version = "2.15.3";
version = "2.17.55";
pyproject = true;
src = fetchPypi {
pname = "music_assistant_frontend";
inherit version;
hash = "sha256-atwFGd6KplVPw4e6rHrNlXmMCsozL56lCVYVWCg9RPs=";
hash = "sha256-fRn2hLg0y+G2atyxpzMq86mJJA41nfSS5UweNZL+0ew=";
};
build-system = [ setuptools ];
@@ -1,16 +1,16 @@
diff --git a/music_assistant/providers/spotify/helpers.py b/music_assistant/providers/spotify/helpers.py
index 6f87b44f46b64805ef6799a54d596fd1684017f1..23de7fc2145619dd4f02551e81cb383f2b79fa15 100644
index fe7af648..3f5121de 100644
--- a/music_assistant/providers/spotify/helpers.py
+++ b/music_assistant/providers/spotify/helpers.py
@@ -4,6 +4,7 @@ from __future__ import annotations
@@ -6,6 +6,7 @@ import asyncio
import os
import platform
import time
+from shutil import which
from typing import TYPE_CHECKING, Any
from urllib.parse import urlencode
from music_assistant.helpers.process import check_output
@@ -20,12 +21,8 @@ async def get_librespot_binary() -> str:
@@ -35,12 +36,8 @@ async def get_librespot_binary() -> str:
except OSError:
return None
+71 -49
View File
@@ -15,13 +15,13 @@ let
music-assistant-frontend = self.callPackage ./frontend.nix { };
music-assistant-models = super.music-assistant-models.overridePythonAttrs (oldAttrs: rec {
version = "1.1.47";
version = "1.1.86";
src = fetchFromGitHub {
owner = "music-assistant";
repo = "models";
tag = version;
hash = "sha256-NNKF61CRBe+N9kY+JUa77ClHSJ9RhpsiheMg7Ytyq2M=";
hash = "sha256-dQwFsuelp/3s2CO/5jxNrZcmWxE9xYhrpx0O37Tq/TQ=";
};
postPatch = ''
@@ -47,14 +47,14 @@ assert
python.pkgs.buildPythonApplication rec {
pname = "music-assistant";
version = "2.6.3";
version = "2.7.2";
pyproject = true;
src = fetchFromGitHub {
owner = "music-assistant";
repo = "server";
tag = version;
hash = "sha256-vvhynBor5tj5n53Dm3K4ZOkFZ5LM7bFevOCdZjJsbbM=";
hash = "sha256-JOS7t4vXkxHXvcI0tCAw3KfcrohUD4NV4w6oYIncQv4=";
};
patches = [
@@ -71,12 +71,26 @@ python.pkgs.buildPythonApplication rec {
# Fix running the built-in snapcast server
./builtin-snapcast-server.patch
# Fix running the webserver pytests in our nix sandbox, which only has a loopback interface,
# by not skipping over its loopback IPv4 address:
#
# """Return all Config Entries for this core module (if any)."""
# ip_addresses = await get_ip_addresses()
# > default_publish_ip = ip_addresses[0]
# ^^^^^^^^^^^^^^^
# E IndexError: tuple index out of range
./fix-webserver-tests-in-sandbox.patch
];
postPatch = ''
substituteInPlace pyproject.toml \
--replace-fail "0.0.0" "${version}"
# get-mac is a deprecated alias of getmac since 2018
substituteInPlace pyproject.toml \
--replace-fail "get-mac" "getmac"
rm -rv music_assistant/providers/spotify/bin
'';
@@ -87,13 +101,16 @@ python.pkgs.buildPythonApplication rec {
pythonRelaxDeps = [
"aiohttp"
"aiosqlite"
"aiovban" # PyPi and GitHub versioning is out of sync
"certifi"
"colorlog"
"cryptography"
"getmac"
"mashumaro"
"orjson"
"pillow"
"podcastparser"
"pycares"
"xmltodict"
"zeroconf"
];
@@ -103,68 +120,73 @@ python.pkgs.buildPythonApplication rec {
"uv"
];
dependencies =
with python.pkgs;
[
aiohttp
chardet
mashumaro
orjson
]
++ optional-dependencies.server;
dependencies = with python.pkgs; [
# Only packages required in pyproject.toml
aiodns
aiofiles
aiohttp
aiohttp-asyncmdnsresolver
aiohttp-fast-zlib
aiortc
aiorun
aiosqlite
aiovban
awesomeversion
brotli
certifi
chardet
colorlog
cryptography
getmac
gql
ifaddr
librosa
mashumaro
music-assistant-frontend
music-assistant-models
mutagen
orjson
pillow
podcastparser
propcache
python-slugify
shortuuid
unidecode
xmltodict
zeroconf
# Used in music_assistant/controllers/webserver/helpers/auth_providers.py
# but somehow not part of pyproject.toml
hass-client
];
optional-dependencies = with python.pkgs; {
server = [
aiodns
aiofiles
aiohttp
aiorun
aiosqlite
asyncio-throttle
brotli
certifi
colorlog
cryptography
eyed3
faust-cchardet
ifaddr
mashumaro
memory-tempfile
music-assistant-frontend
music-assistant-models
mutagen
orjson
pillow
podcastparser
python-slugify
shortuuid
unidecode
xmltodict
zeroconf
# Required subset of optional-dependencies in pyproject.toml
test = [
pytest-aiohttp
pytest-cov-stub
syrupy
];
};
nativeCheckInputs =
with python.pkgs;
[
pytest-aiohttp
pytest-cov-stub
pytest-timeout
pytestCheckHook
syrupy
pytest-timeout
]
++ lib.concatAttrValues optional-dependencies
++ (providerPackages.jellyfin python.pkgs)
++ (providerPackages.opensubsonic python.pkgs);
++ (providerPackages.opensubsonic python.pkgs)
++ (providerPackages.tidal python.pkgs);
disabledTestPaths = [
# blocks in poll()
# no multicast support in build sandbox:
# "OSError: [Errno 19] No such device"
"tests/providers/jellyfin/test_init.py::test_initial_sync"
"tests/core/test_server_base.py::test_start_and_stop_server"
"tests/core/test_server_base.py::test_events"
# not compatible with the required py-subsonic version
"tests/providers/opensubsonic/test_parsers.py"
# provider is missing dependencies
"tests/providers/nicovideo"
];
pythonImportsCheck = [ "music_assistant" ];
+45 -9
View File
@@ -1,16 +1,22 @@
# Do not edit manually, run ./update-providers.py
{
version = "2.6.3";
version = "2.7.2";
providers = {
airplay = ps: [
];
airplay_receiver = ps: [
];
alexa =
ps: with ps; [
alexapy
];
apple_music = ps: [
]; # missing pywidevine
ard_audiothek =
ps: with ps; [
gql
];
audible =
ps: with ps; [
audible
@@ -19,14 +25,16 @@
ps: with ps; [
aioaudiobookshelf
];
bbc_sounds =
ps: with ps; [
pytz
]; # missing auntie-sounds
bluesound =
ps: with ps; [
pyblu
];
builtin = ps: [
];
builtin_player = ps: [
];
chromecast =
ps: with ps; [
pychromecast
@@ -36,6 +44,8 @@
deezer-python-async
pycryptodome
];
digitally_incorporated = ps: [
];
dlna =
ps: with ps; [
async-upnp-client
@@ -50,6 +60,8 @@
ps: with ps; [
python-fullykiosk
];
genius_lyrics = ps: [
]; # missing lyricsgenius
gpodder = ps: [
];
hass =
@@ -60,6 +72,8 @@
];
ibroadcast = ps: [
]; # missing ibroadcastaio
internet_archive = ps: [
];
itunes_podcasts = ps: [
];
jellyfin =
@@ -70,8 +84,10 @@
ps: with ps; [
pylast
];
listenbrainz_scrobble = ps: [
]; # missing liblistenbrainz
listenbrainz_scrobble =
ps: with ps; [
liblistenbrainz
];
lrclib = ps: [
];
musicbrainz = ps: [
@@ -79,20 +95,27 @@
musiccast =
ps: with ps; [
aiomusiccast
setuptools
];
nicovideo = ps: [
]; # missing niconico.py-ma
nugs = ps: [
];
opensubsonic =
ps: with ps; [
py-opensonic
];
player_group = ps: [
phishin = ps: [
];
plex =
ps: with ps; [
plexapi
];
plex_connect =
ps: with ps; [
plexapi
];
podcast_index = ps: [
];
podcastfeed = ps: [
];
qobuz = ps: [
@@ -101,6 +124,17 @@
ps: with ps; [
radios
];
radioparadise = ps: [
];
roku_media_assistant =
ps: with ps; [
async-upnp-client
rokuecp
];
sendspin =
ps: with ps; [
aiosendspin
];
siriusxm = ps: [
]; # missing sxm
snapcast =
@@ -132,8 +166,6 @@
];
subsonic_scrobble = ps: [
];
template_player_provider = ps: [
];
test = ps: [
];
theaudiodb = ps: [
@@ -144,6 +176,10 @@
];
tunein = ps: [
];
universal_group = ps: [
];
vban_receiver = ps: [
];
ytmusic =
ps: with ps; [
bgutil-ytdlp-pot-provider
@@ -123,7 +123,9 @@ async def get_provider_manifests(version: str = "master") -> List:
basedir = Path(os.path.join(tmp, f"server-{version}"))
sys.path.append(str(basedir))
for fn in basedir.glob("**/manifest.json"):
for fn in basedir.glob("**/providers/*/manifest.json"):
if "_demo_" in str(fn):
continue
try:
manifests.append(await ProviderManifest.parse(str(fn)))
except MissingField as ex: