mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-01-22 12:53:54 +00:00
music-assistant: init at 2.0.7
This commit is contained in:
parent
622256abb5
commit
241fe52147
71
pkgs/by-name/mu/music-assistant/ffmpeg.patch
Normal file
71
pkgs/by-name/mu/music-assistant/ffmpeg.patch
Normal file
@ -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",
|
35
pkgs/by-name/mu/music-assistant/frontend.nix
Normal file
35
pkgs/by-name/mu/music-assistant/frontend.nix
Normal file
@ -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 ];
|
||||
};
|
||||
}
|
117
pkgs/by-name/mu/music-assistant/package.nix
Normal file
117
pkgs/by-name/mu/music-assistant/package.nix
Normal file
@ -0,0 +1,117 @@
|
||||
{ lib
|
||||
, python3
|
||||
, fetchFromGitHub
|
||||
, ffmpeg-headless
|
||||
, 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
|
||||
;
|
||||
};
|
||||
|
||||
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
Normal file
78
pkgs/by-name/mu/music-assistant/providers.nix
Normal file
@ -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
|
||||
];
|
||||
};
|
||||
}
|
218
pkgs/by-name/mu/music-assistant/update-providers.py
Executable file
218
pkgs/by-name/mu/music-assistant/update-providers.py
Executable file
@ -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())
|
Loading…
Reference in New Issue
Block a user