diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 2db19200de4d..c045c07673e5 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -33,6 +33,8 @@ - [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). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 25a930f4a8bb..9dddfa311a00 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -376,6 +376,7 @@ ./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 diff --git a/nixos/modules/services/audio/music-assistant.nix b/nixos/modules/services/audio/music-assistant.nix new file mode 100644 index 000000000000..90c0b41fc587 --- /dev/null +++ b/nixos/modules/services/audio/music-assistant.nix @@ -0,0 +1,113 @@ +{ + 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"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 52450760d991..0e0f4c7306a3 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -598,6 +598,7 @@ in { # 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 {}; diff --git a/nixos/tests/music-assistant.nix b/nixos/tests/music-assistant.nix new file mode 100644 index 000000000000..ac667ee95303 --- /dev/null +++ b/nixos/tests/music-assistant.nix @@ -0,0 +1,21 @@ +{ + 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 ✓")) + ''; +} diff --git a/pkgs/by-name/mu/music-assistant/ffmpeg.patch b/pkgs/by-name/mu/music-assistant/ffmpeg.patch new file mode 100644 index 000000000000..d14e16e37ea1 --- /dev/null +++ b/pkgs/by-name/mu/music-assistant/ffmpeg.patch @@ -0,0 +1,71 @@ +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", diff --git a/pkgs/by-name/mu/music-assistant/frontend.nix b/pkgs/by-name/mu/music-assistant/frontend.nix new file mode 100644 index 000000000000..1db07591408b --- /dev/null +++ b/pkgs/by-name/mu/music-assistant/frontend.nix @@ -0,0 +1,35 @@ +{ 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 ]; + }; +} diff --git a/pkgs/by-name/mu/music-assistant/package.nix b/pkgs/by-name/mu/music-assistant/package.nix new file mode 100644 index 000000000000..ba7d00824652 --- /dev/null +++ b/pkgs/by-name/mu/music-assistant/package.nix @@ -0,0 +1,119 @@ +{ 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"; + }; +} diff --git a/pkgs/by-name/mu/music-assistant/providers.nix b/pkgs/by-name/mu/music-assistant/providers.nix new file mode 100644 index 000000000000..945d570dca30 --- /dev/null +++ b/pkgs/by-name/mu/music-assistant/providers.nix @@ -0,0 +1,78 @@ +# 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 + ]; + }; +} diff --git a/pkgs/by-name/mu/music-assistant/update-providers.py b/pkgs/by-name/mu/music-assistant/update-providers.py new file mode 100755 index 000000000000..301a5041a2e7 --- /dev/null +++ b/pkgs/by-name/mu/music-assistant/update-providers.py @@ -0,0 +1,218 @@ +#!/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()) diff --git a/pkgs/development/python-modules/memory-tempfile/default.nix b/pkgs/development/python-modules/memory-tempfile/default.nix new file mode 100644 index 000000000000..7255e35f18ef --- /dev/null +++ b/pkgs/development/python-modules/memory-tempfile/default.nix @@ -0,0 +1,42 @@ +{ 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 ]; + }; +} diff --git a/pkgs/development/python-modules/py-opensonic/default.nix b/pkgs/development/python-modules/py-opensonic/default.nix new file mode 100644 index 000000000000..22f3ebe7ecc8 --- /dev/null +++ b/pkgs/development/python-modules/py-opensonic/default.nix @@ -0,0 +1,37 @@ +{ 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 ]; + }; +} diff --git a/pkgs/servers/home-assistant/build-custom-component/default.nix b/pkgs/servers/home-assistant/build-custom-component/default.nix index 2948d15bb814..17677e99d125 100644 --- a/pkgs/servers/home-assistant/build-custom-component/default.nix +++ b/pkgs/servers/home-assistant/build-custom-component/default.nix @@ -27,6 +27,9 @@ home-assistant.python.pkgs.buildPythonPackage ( mkdir $out cp -r ./custom_components/ $out/ + # optionally copy sentences, if they exist + cp -r ./custom_sentences/ $out/ || true + runHook postInstall ''; diff --git a/pkgs/servers/home-assistant/custom-components/default.nix b/pkgs/servers/home-assistant/custom-components/default.nix index 1acebef48217..3687e3ac7cf0 100644 --- a/pkgs/servers/home-assistant/custom-components/default.nix +++ b/pkgs/servers/home-assistant/custom-components/default.nix @@ -32,6 +32,8 @@ localtuya = callPackage ./localtuya {}; + mass = callPackage ./mass { }; + midea_ac_lan = callPackage ./midea_ac_lan {}; midea-air-appliances-lan = callPackage ./midea-air-appliances-lan {}; diff --git a/pkgs/servers/home-assistant/custom-components/mass/default.nix b/pkgs/servers/home-assistant/custom-components/mass/default.nix new file mode 100644 index 000000000000..39059b4c70ff --- /dev/null +++ b/pkgs/servers/home-assistant/custom-components/mass/default.nix @@ -0,0 +1,34 @@ +{ 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 ]; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index 6ccbd3cc079d..61f2bfd290c3 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -7524,6 +7524,8 @@ self: super: with self; { 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 { }; @@ -9195,6 +9197,8 @@ self: super: with self; { 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 { }; nwdiag = callPackage ../development/python-modules/nwdiag { };