Merge pull request #311927 from mweinelt/music-assistant

music-assistant: init at 2.0.7

Changed files
+781
nixos
doc
manual
release-notes
modules
tests
pkgs
by-name
development
python-modules
memory-tempfile
py-opensonic
servers
home-assistant
build-custom-component
custom-components
top-level
+2
nixos/doc/manual/release-notes/rl-2411.section.md
···
- [Renovate](https://github.com/renovatebot/renovate), a dependency updating tool for various git forges and language ecosystems. Available as [services.renovate](#opt-services.renovate.enable).
+
- [Music Assistant](https://music-assistant.io/), a music library manager for your offline and online music sources which can easily stream your favourite music to a wide range of supported players. Available as [services.music-assistant](#opt-services.music-assistant.enable).
+
- [wg-access-server](https://github.com/freifunkMUC/wg-access-server/), an all-in-one WireGuard VPN solution with a web ui for connecting devices. Available at [services.wg-access-server](#opt-services.wg-access-server.enable).
- [Envision](https://gitlab.com/gabmus/envision), a UI for building, configuring and running Monado, the open source OpenXR runtime. Available as [programs.envision](#opt-programs.envision.enable).
+1
nixos/modules/module-list.nix
···
./services/audio/mopidy.nix
./services/audio/mpd.nix
./services/audio/mpdscribble.nix
+
./services/audio/music-assistant.nix
./services/audio/mympd.nix
./services/audio/navidrome.nix
./services/audio/networkaudiod.nix
+113
nixos/modules/services/audio/music-assistant.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
utils,
+
...
+
}:
+
+
let
+
inherit (lib)
+
mkIf
+
mkEnableOption
+
mkOption
+
mkPackageOption
+
types
+
;
+
+
inherit (types)
+
listOf
+
enum
+
str
+
;
+
+
cfg = config.services.music-assistant;
+
+
finalPackage = cfg.package.override {
+
inherit (cfg) providers;
+
};
+
in
+
+
{
+
meta.buildDocsInSandbox = false;
+
+
options.services.music-assistant = {
+
enable = mkEnableOption "Music Assistant";
+
+
package = mkPackageOption pkgs "music-assistant" { };
+
+
extraOptions = mkOption {
+
type = listOf str;
+
default = [ "--config" "/var/lib/music-assistant" ];
+
example = [
+
"--log-level"
+
"DEBUG"
+
];
+
description = ''
+
List of extra options to pass to the music-assistant executable.
+
'';
+
};
+
+
providers = mkOption {
+
type = listOf (enum cfg.package.providerNames);
+
default = [];
+
example = [
+
"opensubsonic"
+
"snapcast"
+
];
+
description = ''
+
List of provider names for which dependencies will be installed.
+
'';
+
};
+
};
+
+
config = mkIf cfg.enable {
+
systemd.services.music-assistant = {
+
description = "Music Assistant";
+
documentation = [ "https://music-assistant.io" ];
+
+
wantedBy = [ "multi-user.target" ];
+
+
environment = {
+
HOME = "/var/lib/music-assistant";
+
PYTHONPATH = finalPackage.pythonPath;
+
};
+
+
serviceConfig = {
+
ExecStart = utils.escapeSystemdExecArgs ([
+
(lib.getExe cfg.package)
+
] ++ cfg.extraOptions);
+
DynamicUser = true;
+
StateDirectory = "music-assistant";
+
AmbientCapabilities = "";
+
CapabilityBoundingSet = [ "" ];
+
DevicePolicy = "closed";
+
LockPersonality = true;
+
MemoryDenyWriteExecute = true;
+
ProcSubset = "pid";
+
ProtectClock = true;
+
ProtectControlGroups = true;
+
ProtectHome = true;
+
ProtectHostname = true;
+
ProtectKernelLogs = true;
+
ProtectKernelModules = true;
+
ProtectKernelTunables = true;
+
ProtectProc = "invisible";
+
RestrictAddressFamilies = [
+
"AF_INET"
+
"AF_INET6"
+
"AF_NETLINK"
+
];
+
RestrictNamespaces = true;
+
RestrictRealtime = true;
+
SystemCallArchitectures = "native";
+
SystemCallFilter = [
+
"@system-service"
+
"~@privileged @resources"
+
];
+
RestrictSUIDSGID = true;
+
UMask = "0077";
+
};
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
# Fails on aarch64-linux at the PDF creation step - need to debug this on an
# aarch64 machine..
musescore = handleTestOn ["x86_64-linux"] ./musescore.nix {};
+
music-assistant = runTest ./music-assistant.nix;
munin = handleTest ./munin.nix {};
mutableUsers = handleTest ./mutable-users.nix {};
mycelium = handleTest ./mycelium {};
+21
nixos/tests/music-assistant.nix
···
+
{
+
lib,
+
...
+
}:
+
+
{
+
name = "music-assistant";
+
meta.maintainers = with lib.maintainers; [ hexa ];
+
+
nodes.machine = {
+
services.music-assistant = {
+
enable = true;
+
};
+
};
+
+
testScript = ''
+
machine.wait_for_unit("music-assistant.service")
+
machine.wait_until_succeeds("curl --fail http://localhost:8095")
+
machine.log(machine.succeed("systemd-analyze security music-assistant.service | grep -v ✓"))
+
'';
+
}
+71
pkgs/by-name/mu/music-assistant/ffmpeg.patch
···
+
diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py
+
index 42011923..1e5dc112 100644
+
--- a/music_assistant/server/helpers/audio.py
+
+++ b/music_assistant/server/helpers/audio.py
+
@@ -218,7 +218,7 @@ async def crossfade_pcm_parts(
+
await outfile.write(fade_out_part)
+
args = [
+
# generic args
+
- "ffmpeg",
+
+ "@ffmpeg@",
+
"-hide_banner",
+
"-loglevel",
+
"quiet",
+
@@ -281,7 +281,7 @@ async def strip_silence(
+
) -> bytes:
+
"""Strip silence from begin or end of pcm audio using ffmpeg."""
+
fmt = ContentType.from_bit_depth(bit_depth)
+
- args = ["ffmpeg", "-hide_banner", "-loglevel", "quiet"]
+
+ args = ["@ffmpeg@", "-hide_banner", "-loglevel", "quiet"]
+
args += [
+
"-acodec",
+
fmt.name.lower(),
+
@@ -823,7 +823,7 @@ async def get_ffmpeg_stream(
+
async def check_audio_support() -> tuple[bool, bool, str]:
+
"""Check if ffmpeg is present (with/without libsoxr support)."""
+
# check for FFmpeg presence
+
- returncode, output = await check_output("ffmpeg -version")
+
+ returncode, output = await check_output("@ffmpeg@ -version")
+
ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode()
+
+
# use globals as in-memory cache
+
@@ -877,7 +877,7 @@ async def get_silence(
+
return
+
# use ffmpeg for all other encodings
+
args = [
+
- "ffmpeg",
+
+ "@ffmpeg@",
+
"-hide_banner",
+
"-loglevel",
+
"quiet",
+
@@ -971,7 +971,7 @@ def get_ffmpeg_args(
+
+
# generic args
+
generic_args = [
+
- "ffmpeg",
+
+ "@ffmpeg@",
+
"-hide_banner",
+
"-loglevel",
+
loglevel,
+
diff --git a/music_assistant/server/helpers/tags.py b/music_assistant/server/helpers/tags.py
+
index dc38e4c0..f4f3e2fe 100644
+
--- a/music_assistant/server/helpers/tags.py
+
+++ b/music_assistant/server/helpers/tags.py
+
@@ -368,7 +368,7 @@ async def parse_tags(
+
file_path = input_file if isinstance(input_file, str) else "-"
+
+
args = (
+
- "ffprobe",
+
+ "@ffprobe@",
+
"-hide_banner",
+
"-loglevel",
+
"fatal",
+
@@ -440,7 +440,7 @@ async def get_embedded_image(input_file: str | AsyncGenerator[bytes, None]) -> b
+
"""
+
file_path = input_file if isinstance(input_file, str) else "-"
+
args = (
+
- "ffmpeg",
+
+ "@ffmpeg@",
+
"-hide_banner",
+
"-loglevel",
+
"error",
+35
pkgs/by-name/mu/music-assistant/frontend.nix
···
+
{ lib
+
, buildPythonPackage
+
, fetchPypi
+
, setuptools
+
}:
+
+
buildPythonPackage rec {
+
pname = "music-assistant-frontend";
+
version = "2.5.15";
+
pyproject = true;
+
+
src = fetchPypi {
+
inherit pname version;
+
hash = "sha256-D8VFdXgaVXSxk7c24kvb9TflFztS1zLwW4qGqV32nLo=";
+
};
+
+
postPatch = ''
+
substituteInPlace pyproject.toml \
+
--replace-fail "~=" ">="
+
'';
+
+
build-system = [ setuptools ];
+
+
doCheck = false;
+
+
pythonImportsCheck = [ "music_assistant_frontend" ];
+
+
meta = with lib; {
+
changelog = "https://github.com/music-assistant/frontend/releases/tag/${version}";
+
description = "The Music Assistant frontend";
+
homepage = "https://github.com/music-assistant/frontend";
+
license = licenses.asl20;
+
maintainers = with maintainers; [ hexa ];
+
};
+
}
+119
pkgs/by-name/mu/music-assistant/package.nix
···
+
{ lib
+
, python3
+
, fetchFromGitHub
+
, ffmpeg-headless
+
, nixosTests
+
, substituteAll
+
, providers ? [ ]
+
}:
+
+
let
+
python = python3.override {
+
packageOverrides = self: super: {
+
music-assistant-frontend = self.callPackage ./frontend.nix { };
+
};
+
};
+
+
providerPackages = (import ./providers.nix).providers;
+
providerNames = lib.attrNames providerPackages;
+
providerDependencies = lib.concatMap (provider: (providerPackages.${provider} python.pkgs)) providers;
+
+
pythonPath = python.pkgs.makePythonPath providerDependencies;
+
in
+
+
python.pkgs.buildPythonApplication rec {
+
pname = "music-assistant";
+
version = "2.0.7";
+
pyproject = true;
+
+
src = fetchFromGitHub {
+
owner = "music-assistant";
+
repo = "server";
+
rev = version;
+
hash = "sha256-JtdlZ3hH4fRU5TjmMUlrdSSCnLrIGCuSwSSrnLgjYEs=";
+
};
+
+
patches = [
+
(substituteAll {
+
src = ./ffmpeg.patch;
+
ffmpeg = "${lib.getBin ffmpeg-headless}/bin/ffmpeg";
+
ffprobe = "${lib.getBin ffmpeg-headless}/bin/ffprobe";
+
})
+
];
+
+
postPatch = ''
+
sed -i "/--cov/d" pyproject.toml
+
+
substituteInPlace pyproject.toml \
+
--replace-fail "0.0.0" "${version}"
+
'';
+
+
build-system = with python.pkgs; [
+
setuptools
+
];
+
+
dependencies = with python.pkgs; [
+
aiohttp
+
mashumaro
+
orjson
+
] ++ optional-dependencies.server;
+
+
optional-dependencies = with python.pkgs; {
+
server = [
+
aiodns
+
aiofiles
+
aiohttp
+
aiorun
+
aiosqlite
+
asyncio-throttle
+
brotli
+
certifi
+
colorlog
+
cryptography
+
faust-cchardet
+
ifaddr
+
mashumaro
+
memory-tempfile
+
music-assistant-frontend
+
orjson
+
pillow
+
python-slugify
+
shortuuid
+
unidecode
+
xmltodict
+
zeroconf
+
];
+
};
+
+
nativeCheckInputs = with python.pkgs; [
+
ffmpeg-headless
+
pytest-aiohttp
+
pytestCheckHook
+
] ++ lib.flatten (lib.attrValues optional-dependencies);
+
+
pythonImportsCheck = [ "music_assistant" ];
+
+
passthru = {
+
inherit
+
python
+
pythonPath
+
providerPackages
+
providerNames
+
;
+
tests = nixosTests.music-assistant;
+
};
+
+
meta = with lib; {
+
changelog = "https://github.com/music-assistant/server/releases/tag/${version}";
+
description = "Music Assistant is a music library manager for various music sources which can easily stream to a wide range of supported players";
+
longDescription = ''
+
Music Assistant is a free, opensource Media library manager that connects to your streaming services and a wide
+
range of connected speakers. The server is the beating heart, the core of Music Assistant and must run on an
+
always-on device like a Raspberry Pi, a NAS or an Intel NUC or alike.
+
'';
+
homepage = "https://github.com/music-assistant/server";
+
license = licenses.asl20;
+
maintainers = with maintainers; [ hexa ];
+
mainProgram = "mass";
+
};
+
}
+78
pkgs/by-name/mu/music-assistant/providers.nix
···
+
# Do not edit manually, run ./update-providers.py
+
+
{
+
version = "2.0.7";
+
providers = {
+
airplay = [
+
];
+
builtin = [
+
];
+
chromecast = ps: with ps; [
+
pychromecast
+
];
+
deezer = ps: with ps; [
+
pycryptodome
+
]; # missing deezer-python-async
+
dlna = ps: with ps; [
+
async-upnp-client
+
];
+
fanarttv = [
+
];
+
filesystem_local = [
+
];
+
filesystem_smb = [
+
];
+
fully_kiosk = ps: with ps; [
+
python-fullykiosk
+
];
+
hass = [
+
]; # missing hass-client
+
hass_players = [
+
];
+
jellyfin = [
+
]; # missing jellyfin_apiclient_python
+
musicbrainz = [
+
];
+
opensubsonic = ps: with ps; [
+
py-opensonic
+
];
+
plex = ps: with ps; [
+
plexapi
+
];
+
qobuz = [
+
];
+
radiobrowser = ps: with ps; [
+
radios
+
];
+
slimproto = ps: with ps; [
+
aioslimproto
+
];
+
snapcast = ps: with ps; [
+
snapcast
+
];
+
sonos = ps: with ps; [
+
defusedxml
+
soco
+
sonos-websocket
+
];
+
soundcloud = [
+
]; # missing soundcloudpy
+
spotify = [
+
];
+
test = [
+
];
+
theaudiodb = [
+
];
+
tidal = ps: with ps; [
+
tidalapi
+
];
+
tunein = [
+
];
+
ugp = [
+
];
+
ytmusic = ps: with ps; [
+
pytube
+
ytmusicapi
+
];
+
};
+
}
+218
pkgs/by-name/mu/music-assistant/update-providers.py
···
+
#!/usr/bin/env nix-shell
+
#!nix-shell -i python3 -p "python3.withPackages (ps: with ps; [ jinja2 mashumaro orjson aiofiles packaging ])" -p pyright ruff isort
+
import asyncio
+
import json
+
import os.path
+
import re
+
import sys
+
import tarfile
+
import tempfile
+
from dataclasses import dataclass, field
+
from functools import cache
+
from io import BytesIO
+
from pathlib import Path
+
from subprocess import check_output, run
+
from typing import Dict, Final, List, Optional, Set, Union, cast
+
from urllib.request import urlopen
+
+
from jinja2 import Environment
+
from packaging.requirements import Requirement
+
+
TEMPLATE = """# Do not edit manually, run ./update-providers.py
+
+
{
+
version = "{{ version }}";
+
providers = {
+
{%- for provider in providers | sort(attribute='domain') %}
+
{{ provider.domain }} = {% if provider.available %}ps: with ps; {% endif %}[
+
{%- for requirement in provider.available | sort %}
+
{{ requirement }}
+
{%- endfor %}
+
];{% if provider.missing %} # missing {{ ", ".join(provider.missing) }}{% endif %}
+
{%- endfor %}
+
};
+
}
+
+
"""
+
+
+
ROOT: Final = (
+
check_output(
+
[
+
"git",
+
"rev-parse",
+
"--show-toplevel",
+
]
+
)
+
.decode()
+
.strip()
+
)
+
+
PACKAGE_MAP = {
+
"git+https://github.com/MarvinSchenkel/pytube.git": "pytube",
+
}
+
+
+
def run_sync(cmd: List[str]) -> None:
+
print(f"$ {' '.join(cmd)}")
+
process = run(cmd)
+
+
if process.returncode != 0:
+
sys.exit(1)
+
+
+
async def check_async(cmd: List[str]) -> str:
+
print(f"$ {' '.join(cmd)}")
+
process = await asyncio.create_subprocess_exec(
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
+
)
+
stdout, stderr = await process.communicate()
+
+
if process.returncode != 0:
+
error = stderr.decode()
+
raise RuntimeError(f"{cmd[0]} failed: {error}")
+
+
return stdout.decode().strip()
+
+
+
class Nix:
+
base_cmd: Final = [
+
"nix",
+
"--show-trace",
+
"--extra-experimental-features",
+
"nix-command",
+
]
+
+
@classmethod
+
async def _run(cls, args: List[str]) -> Optional[str]:
+
return await check_async(cls.base_cmd + args)
+
+
@classmethod
+
async def eval(cls, expr: str) -> Union[List, Dict, int, float, str, bool]:
+
response = await cls._run(["eval", "-f", f"{ROOT}/default.nix", "--json", expr])
+
if response is None:
+
raise RuntimeError("Nix eval expression returned no response")
+
try:
+
return json.loads(response)
+
except (TypeError, ValueError):
+
raise RuntimeError("Nix eval response could not be parsed from JSON")
+
+
+
async def get_provider_manifests(version: str = "master") -> List:
+
manifests = []
+
with tempfile.TemporaryDirectory() as tmp:
+
with urlopen(
+
f"https://github.com/music-assistant/music-assistant/archive/{version}.tar.gz"
+
) as response:
+
tarfile.open(fileobj=BytesIO(response.read())).extractall(
+
tmp, filter="data"
+
)
+
+
basedir = Path(os.path.join(tmp, f"server-{version}"))
+
sys.path.append(str(basedir))
+
from music_assistant.common.models.provider import ProviderManifest # type: ignore
+
+
for fn in basedir.glob("**/manifest.json"):
+
manifests.append(await ProviderManifest.parse(fn))
+
+
return manifests
+
+
+
@cache
+
def packageset_attributes():
+
output = check_output(
+
[
+
"nix-env",
+
"-f",
+
ROOT,
+
"-qa",
+
"-A",
+
"music-assistant.python.pkgs",
+
"--arg",
+
"config",
+
"{ allowAliases = false; }",
+
"--json",
+
]
+
)
+
return json.loads(output)
+
+
+
class TooManyMatches(Exception):
+
pass
+
+
+
class NoMatch(Exception):
+
pass
+
+
+
def resolve_package_attribute(package: str) -> str:
+
pattern = re.compile(rf"^music-assistant\.python\.pkgs\.{package}$", re.I)
+
packages = packageset_attributes()
+
matches = []
+
for attr in packages.keys():
+
if pattern.match(attr):
+
matches.append(attr.split(".")[-1])
+
+
if len(matches) > 1:
+
raise TooManyMatches(
+
f"Too many matching attributes for {package}: {' '.join(matches)}"
+
)
+
if not matches:
+
raise NoMatch(f"No matching attribute for {package}")
+
+
return matches.pop()
+
+
+
@dataclass
+
class Provider:
+
domain: str
+
available: list[str] = field(default_factory=list)
+
missing: list[str] = field(default_factory=list)
+
+
def __eq__(self, other):
+
return self.domain == other.domain
+
+
def __hash__(self):
+
return hash(self.domain)
+
+
+
def resolve_providers(manifests) -> Set:
+
providers = set()
+
for manifest in manifests:
+
provider = Provider(manifest.domain)
+
for requirement in manifest.requirements:
+
# allow substituting requirement specifications that packaging cannot parse
+
if requirement in PACKAGE_MAP:
+
requirement = PACKAGE_MAP[requirement]
+
requirement = Requirement(requirement)
+
try:
+
provider.available.append(resolve_package_attribute(requirement.name))
+
except TooManyMatches as ex:
+
print(ex, file=sys.stderr)
+
provider.missing.append(requirement.name)
+
except NoMatch:
+
provider.missing.append(requirement.name)
+
providers.add(provider)
+
return providers
+
+
+
def render(version: str, providers: Set):
+
path = os.path.join(ROOT, "pkgs/by-name/mu/music-assistant/providers.nix")
+
env = Environment()
+
template = env.from_string(TEMPLATE)
+
template.stream(version=version, providers=providers).dump(path)
+
+
+
async def main():
+
version: str = cast(str, await Nix.eval("music-assistant.version"))
+
manifests = await get_provider_manifests(version)
+
providers = resolve_providers(manifests)
+
render(version, providers)
+
+
+
if __name__ == "__main__":
+
run_sync(["pyright", __file__])
+
run_sync(["ruff", "check", "--ignore=E501", __file__])
+
run_sync(["isort", __file__])
+
run_sync(["ruff", "format", __file__])
+
asyncio.run(main())
+42
pkgs/development/python-modules/memory-tempfile/default.nix
···
+
{ lib
+
, buildPythonPackage
+
, fetchFromGitHub
+
, fetchpatch2
+
, poetry-core
+
}:
+
+
buildPythonPackage rec {
+
pname = "memory-tempfile";
+
version = "2.2.3";
+
pyproject = true;
+
+
src = fetchFromGitHub {
+
owner = "mbello";
+
repo = "memory-tempfile";
+
rev = "v${version}";
+
hash = "sha256-4fz2CLkZdy2e1GwGw/afG54LkUVJ4cza70jcbX3rVlQ=";
+
};
+
+
patches = [
+
(fetchpatch2 {
+
# Migrate to poetry-core build backend
+
# https://github.com/mbello/memory-tempfile/pull/13
+
name = "poetry-core.patch";
+
url = "https://github.com/mbello/memory-tempfile/commit/938a3a3abf01756b1629eca6c69e970021bbc7c0.patch";
+
hash = "sha256-q3027MwKXtX09MH7T2UrX19BImK1FJo+YxADfxcdTME=";
+
})
+
];
+
+
build-system = [ poetry-core ];
+
+
doCheck = false; # constrained selection of memory backed filesystems due to build sandbox
+
+
pythonImportsCheck = [ "memory_tempfile" ];
+
+
meta = with lib; {
+
description = "Create temporary files and temporary dirs in memory-based filesystems on Linux";
+
homepage = "https://github.com/mbello/memory-tempfile";
+
license = licenses.mit;
+
maintainers = with maintainers; [ hexa ];
+
};
+
}
+37
pkgs/development/python-modules/py-opensonic/default.nix
···
+
{ lib
+
, buildPythonPackage
+
, fetchFromGitHub
+
, setuptools
+
, requests
+
}:
+
+
buildPythonPackage rec {
+
pname = "py-opensonic";
+
version = "5.1.1";
+
pyproject = true;
+
+
src = fetchFromGitHub {
+
owner = "khers";
+
repo = "py-opensonic";
+
rev = "v${version}";
+
hash = "sha256-wXTXuX+iIMEoALxsciopucmvBxAyEeiOgjJPrbD63gM=";
+
};
+
+
build-system = [ setuptools ];
+
+
dependencies = [ requests ];
+
+
doCheck = false; # no tests
+
+
pythonImportsCheck = [
+
"libopensonic"
+
];
+
+
meta = with lib; {
+
description = "Python library to wrap the Open Subsonic REST API";
+
homepage = "https://github.com/khers/py-opensonic";
+
changelog = "https://github.com/khers/py-opensonic/blob/${src.rev}/CHANGELOG.md";
+
license = licenses.gpl3Only;
+
maintainers = with maintainers; [ hexa ];
+
};
+
}
+3
pkgs/servers/home-assistant/build-custom-component/default.nix
···
mkdir $out
cp -r ./custom_components/ $out/
+
# optionally copy sentences, if they exist
+
cp -r ./custom_sentences/ $out/ || true
+
runHook postInstall
'';
+2
pkgs/servers/home-assistant/custom-components/default.nix
···
localtuya = callPackage ./localtuya {};
+
mass = callPackage ./mass { };
+
midea_ac_lan = callPackage ./midea_ac_lan {};
midea-air-appliances-lan = callPackage ./midea-air-appliances-lan {};
+34
pkgs/servers/home-assistant/custom-components/mass/default.nix
···
+
{ lib
+
, buildHomeAssistantComponent
+
, fetchFromGitHub
+
, toPythonModule
+
, async-timeout
+
, music-assistant
+
}:
+
+
buildHomeAssistantComponent rec {
+
owner = "music-assistant";
+
domain = "mass";
+
version = "2024.6.2";
+
+
src = fetchFromGitHub {
+
owner = "music-assistant";
+
repo = "hass-music-assistant";
+
rev = version;
+
hash = "sha256-Wvc+vUYkUJmS4U34Sh/sDCVXmQA0AtEqIT8MNXd++3M=";
+
};
+
+
dependencies = [
+
async-timeout
+
(toPythonModule music-assistant)
+
];
+
+
dontCheckManifest = true; # expects music-assistant 2.0.6, we have 2.0.7
+
+
meta = with lib; {
+
description = "Turn your Home Assistant instance into a jukebox, hassle free streaming of your favorite media to Home Assistant media players";
+
homepage = "https://github.com/music-assistant/hass-music-assistant";
+
license = licenses.asl20;
+
maintainers = with maintainers; [ hexa ];
+
};
+
}
+4
pkgs/top-level/python-packages.nix
···
memory-profiler = callPackage ../development/python-modules/memory-profiler { };
+
memory-tempfile = callPackage ../development/python-modules/memory-tempfile { };
+
meraki = callPackage ../development/python-modules/meraki { };
mercadopago = callPackage ../development/python-modules/mercadopago { };
···
py-eth-sig-utils = callPackage ../development/python-modules/py-eth-sig-utils { };
py-expression-eval = callPackage ../development/python-modules/py-expression-eval { };
+
+
py-opensonic = callPackage ../development/python-modules/py-opensonic { };
py-radix-sr = callPackage ../development/python-modules/py-radix-sr { };