- package-ecosystem: "github-actions"
directory: "/"
schedule:
- interval: weekly
+ interval: daily
- package-ecosystem: "pip"
directory: "/"
schedule:
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
- if [[ $VERSION =~ ^\d+\.\d+\.\d+[[:space:]]?(b[[:space:]]?\d+)? ]]; then
+ if [[ $VERSION =~ [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
python-version: "3.11"
- name: Install dependencies
run: |
- sudo apt-get update
- sudo apt-get install -y ffmpeg
- python -m pip install --upgrade pip
- pip install -e .[server] -r requirements-test.txt
+ python -m pip install --upgrade pip build setuptools
+ pip install .[test]
- name: Lint/test with pre-commit
run: pre-commit run --all-files
- - name: Flake8
- run: flake8 scripts/ music_assistant/
- - name: Black
- run: black --check scripts/ music_assistant/
- - name: isort
- run: isort --check scripts/ music_assistant/
- - name: pylint
- run: pylint music_assistant/
- # - name: mypy
- # run: mypy music_assistant/
test:
runs-on: ubuntu-latest
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
- sudo apt-get update
- sudo apt-get install -y libgirepository1.0-dev
- python -m pip install --upgrade pip
- pip install -e .[server] -r requirements-test.txt
+ python -m pip install --upgrade pip build setuptools
+ pip install .[test]
- name: Pytest
- run: pytest --durations 10 --cov-report term-missing --cov=music_assistant --cov-report=xml tests/server/
+ run: pytest --durations 10 --cov-report term-missing --cov=music_assistant --cov-report=xml tests/
# listed in module rope.base.oi.type_hinting.providers.interfaces
# For example, you can add you own providers for Django Models, or disable
# the search type-hinting in a class hierarchy, etc.
- prefs[
- "type_hinting_factory"
- ] = "rope.base.oi.type_hinting.factory.default_type_hinting_factory"
+ prefs["type_hinting_factory"] = "rope.base.oi.type_hinting.factory.default_type_hinting_factory"
def project_opened(project):
]
}
]
-}
\ No newline at end of file
+}
cls,
entry: ConfigEntry,
value: ConfigValueType,
- allow_none: bool = False,
+ allow_none: bool = True,
) -> ConfigEntryValue:
"""Parse ConfigEntryValue from the config entry and plain value."""
result = ConfigEntryValue.from_dict(entry.to_dict())
result.value = result.label
if not isinstance(result.value, expected_type):
if result.value is None and allow_none:
- # In some cases we allow this (e.g. create default config), hence the allow_none
+ # In some cases we allow this (e.g. create default config)
return result
# handle common conversions/mistakes
if expected_type == float and isinstance(result.value, int):
cls,
config_entries: Iterable[ConfigEntry],
raw: dict[str, Any],
- allow_none: bool = False,
) -> Config:
"""Parse Config from the raw values (as stored in persistent storage)."""
values = {
- x.key: ConfigEntryValue.parse(x, raw.get("values", {}).get(x.key), allow_none).to_dict()
+ x.key: ConfigEntryValue.parse(x, raw.get("values", {}).get(x.key)).to_dict()
for x in config_entries
}
conf = cls.from_dict({**raw, "values": values})
for prov in self.mass.get_available_providers():
if prov.domain != raw_conf["domain"]:
continue
- return ProviderConfig.parse(prov.config_entries, raw_conf, allow_none=True)
+ return ProviderConfig.parse(prov.config_entries, raw_conf)
raise KeyError(f"No config found for provider id {instance_id}")
@api_command("config/providers/update")
self.mass.create_task(self.mass.load_provider(updated_config))
@api_command("config/providers/create")
- def create_provider_config(self, provider_domain: str) -> ProviderConfig:
+ def create_provider_config(
+ self, provider_domain: str, default_enabled: bool = False
+ ) -> ProviderConfig:
"""Create default/empty ProviderConfig.
This is intended to be used as helper method to add a new provider,
"domain": manifest.domain,
"instance_id": instance_id,
"name": name,
+ "enabled": default_enabled,
"values": {},
},
- allow_none=True,
)
# config provided and checks passed, storeconfig
self._serve_queue_stream,
)
- ffmpeg_present, libsoxr_support = await check_audio_support(True)
+ ffmpeg_present, libsoxr_support, version = await check_audio_support()
if not ffmpeg_present:
LOGGER.error("FFmpeg binary not found on your system, playback will NOT work!.")
elif not libsoxr_support:
"highest quality audio not available. "
)
await self._cleanup_stale()
- LOGGER.info("Started stream controller")
+ LOGGER.info(
+ "Started stream controller (using ffmpeg version %s %s)",
+ version,
+ "with libsoxr support" if libsoxr_support else "",
+ )
async def close(self) -> None:
"""Cleanup on exit."""
yield data
-async def check_audio_support(try_install: bool = False) -> tuple[bool, bool]:
+async def check_audio_support() -> tuple[bool, bool, str]:
"""Check if ffmpeg is present (with/without libsoxr support)."""
cache_key = "audio_support_cache"
if cache := globals().get(cache_key):
# check for FFmpeg presence
returncode, output = await check_output("ffmpeg -version")
ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode()
- if not ffmpeg_present and try_install:
- # try a few common ways to install ffmpeg
- # this all assumes we have enough rights and running on a linux based platform (or docker)
- await check_output("apt-get update && apt-get install ffmpeg")
- await check_output("apk add ffmpeg")
- # test again
- returncode, output = await check_output("ffmpeg -version")
- ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode()
# use globals as in-memory cache
+ version = output.decode().split("ffmpeg version ")[1].split(" ")[0].split("-")[0]
libsoxr_support = "enable-libsoxr" in output.decode()
- result = (ffmpeg_present, libsoxr_support)
+ result = (ffmpeg_present, libsoxr_support, version)
globals()[cache_key] = result
return result
"""Collect all args to send to the ffmpeg process."""
input_format = streamdetails.content_type
- ffmpeg_present, libsoxr_support = await check_audio_support()
+ ffmpeg_present, libsoxr_support, version = await check_audio_support()
if not ffmpeg_present:
raise AudioError(
"FFmpeg binary is missing from system."
"Please install ffmpeg on your OS to enable playback.",
)
+
+ major_version = int(version.split(".")[0])
+
# generic args
generic_args = [
"ffmpeg",
"1",
"-reconnect_streamed",
"1",
- "-reconnect_on_network_error",
- "1",
- "-reconnect_on_http_error",
- "5xx",
"-reconnect_delay_max",
"10",
]
+ if major_version > 4:
+ # these options are only supported in ffmpeg > 5
+ input_args += [
+ "-reconnect_on_network_error",
+ "1",
+ "-reconnect_on_http_error",
+ "5xx",
+ ]
+
if seek_position:
input_args += ["-ss", str(seek_position)]
input_args += ["-i", streamdetails.direct]
sql_query += f' VALUES ({",".join((f":{x}" for x in keys))})'
await self.execute(sql_query, values)
# return inserted/replaced item
- lookup_vals = {
- key: value for key, value in values.items() if value is not None and value != ""
- }
+ lookup_vals = {key: value for key, value in values.items() if value not in (None, "")}
return await self.get_row(table, lookup_vals)
async def insert_or_replace(self, table: str, values: dict[str, Any]) -> Mapping:
result: list[str] = []
for key, value in source.items():
- if value is None or value == "":
+ if value in (None, ""):
continue
if isinstance(value, list):
for subval in value:
existing = any(x for x in provider_configs if x.domain == prov_manifest.domain)
if existing:
continue
- self.config.create_provider_config(prov_manifest.domain)
+ self.config.create_provider_config(prov_manifest.domain, True)
# load all configured (and enabled) providers
for allow_depends_on in (False, True):
coloredlogs==15.0.1
cryptography==39.0.2
databases==0.7.0
-getmac==0.9.2
+getmac==0.8.2
mashumaro==3.5.0
memory-tempfile==2.2.3
music-assistant-frontend==20230313.0