Skip to content

Commit

Permalink
Fix: Final round of fixes for multi value serializing mess
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Feb 18, 2025
1 parent 6dd24ed commit 2a66b0b
Show file tree
Hide file tree
Showing 52 changed files with 209 additions and 186 deletions.
59 changes: 31 additions & 28 deletions music_assistant/constants.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""All constants for Music Assistant."""

import pathlib
from typing import Final
from typing import Final, cast

from music_assistant_models.config_entries import (
MULTI_VALUE_SPLITTER,
ConfigEntry,
ConfigValueOption,
MultiValueConfigEntry,
)
from music_assistant_models.enums import ConfigEntryType, ContentType
from music_assistant_models.media_items import AudioFormat
Expand Down Expand Up @@ -408,28 +408,30 @@
{**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"}
)

CONF_ENTRY_SAMPLE_RATES = MultiValueConfigEntry(

CONF_ENTRY_SAMPLE_RATES = ConfigEntry(
key=CONF_SAMPLE_RATES,
type=ConfigEntryType.INTEGER_TUPLE,
type=ConfigEntryType.SPLITTED_STRING,
multi_value=True,
options=[
ConfigValueOption("44.1kHz / 16 bits", (44100, 16)),
ConfigValueOption("44.1kHz / 24 bits", (44100, 24)),
ConfigValueOption("48kHz / 16 bits", (48000, 16)),
ConfigValueOption("48kHz / 24 bits", (48000, 24)),
ConfigValueOption("88.2kHz / 16 bits", (88200, 16)),
ConfigValueOption("88.2kHz / 24 bits", (88200, 24)),
ConfigValueOption("96kHz / 16 bits", (96000, 16)),
ConfigValueOption("96kHz / 24 bits", (96000, 24)),
ConfigValueOption("176.4kHz / 16 bits", (176400, 16)),
ConfigValueOption("176.4kHz / 24 bits", (176400, 24)),
ConfigValueOption("192kHz / 16 bits", (192000, 16)),
ConfigValueOption("192kHz / 24 bits", (192000, 24)),
ConfigValueOption("352.8kHz / 16 bits", (352800, 16)),
ConfigValueOption("352.8kHz / 24 bits", (352800, 24)),
ConfigValueOption("384kHz / 16 bits", (384000, 16)),
ConfigValueOption("384kHz / 24 bits", (384000, 24)),
ConfigValueOption("44.1kHz / 16 bits", f"44100{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("44.1kHz / 24 bits", f"44100{MULTI_VALUE_SPLITTER}24"),
ConfigValueOption("48kHz / 16 bits", f"48000{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("48kHz / 24 bits", f"48000{MULTI_VALUE_SPLITTER}24"),
ConfigValueOption("88.2kHz / 16 bits", f"88200{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("88.2kHz / 24 bits", f"88200{MULTI_VALUE_SPLITTER}24"),
ConfigValueOption("96kHz / 16 bits", f"96000{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("96kHz / 24 bits", f"96000{MULTI_VALUE_SPLITTER}24"),
ConfigValueOption("176.4kHz / 16 bits", f"176400{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("176.4kHz / 24 bits", f"176400{MULTI_VALUE_SPLITTER}24"),
ConfigValueOption("192kHz / 16 bits", f"192000{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("192kHz / 24 bits", f"192000{MULTI_VALUE_SPLITTER}24"),
ConfigValueOption("352.8kHz / 16 bits", f"352800{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("352.8kHz / 24 bits", f"352800{MULTI_VALUE_SPLITTER}24"),
ConfigValueOption("384kHz / 16 bits", f"384000{MULTI_VALUE_SPLITTER}16"),
ConfigValueOption("384kHz / 24 bits", f"384000{MULTI_VALUE_SPLITTER}24"),
],
default_value=[(44100, 16), (48000, 16)],
default_value=[f"44100{MULTI_VALUE_SPLITTER}16", f"44100{MULTI_VALUE_SPLITTER}24"],
required=True,
label="Sample rates supported by this player",
category="advanced",
Expand Down Expand Up @@ -499,23 +501,24 @@ def create_sample_rates_config_entry(
safe_max_bit_depth: int = 16,
hidden: bool = False,
supported_sample_rates: list[int] | None = None,
) -> MultiValueConfigEntry:
) -> ConfigEntry:
"""Create sample rates config entry based on player specific helpers."""
assert CONF_ENTRY_SAMPLE_RATES.options
conf_entry = MultiValueConfigEntry.from_dict(CONF_ENTRY_SAMPLE_RATES.to_dict())
conf_entry = ConfigEntry.from_dict(CONF_ENTRY_SAMPLE_RATES.to_dict())
conf_entry.hidden = hidden
options: list[ConfigValueOption] = []
default_value: list[tuple[int, int]] = []
default_value: list[str] = []
for option in CONF_ENTRY_SAMPLE_RATES.options:
if not isinstance(option.value, tuple):
continue
sample_rate, bit_depth = option.value
option_value = cast(str, option.value)
sample_rate_str, bit_depth_str = option_value.split(MULTI_VALUE_SPLITTER, 1)
sample_rate = int(sample_rate_str)
bit_depth = int(bit_depth_str)
if supported_sample_rates and sample_rate not in supported_sample_rates:
continue
if sample_rate <= max_sample_rate and bit_depth <= max_bit_depth:
options.append(option)
if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth:
default_value.append(option.value)
default_value.append(option_value)
conf_entry.options = options
conf_entry.default_value = default_value
return conf_entry
Expand Down
4 changes: 2 additions & 2 deletions music_assistant/controllers/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from collections.abc import Callable, Iterator, MutableMapping
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar

from music_assistant_models.config_entries import ConfigEntry, ConfigValueTypes
from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
from music_assistant_models.enums import ConfigEntryType

from music_assistant.constants import DB_TABLE_CACHE, DB_TABLE_SETTINGS, MASS_LOGGER_NAME
Expand Down Expand Up @@ -46,7 +46,7 @@ def __init__(self, *args, **kwargs) -> None:
async def get_config_entries(
self,
action: str | None = None,
values: dict[str, ConfigValueTypes] | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""Return all Config Entries for this core module (if any)."""
if action == CONF_CLEAR_CACHE:
Expand Down
67 changes: 41 additions & 26 deletions music_assistant/controllers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
from cryptography.fernet import Fernet, InvalidToken
from music_assistant_models import config_entries
from music_assistant_models.config_entries import (
MULTI_VALUE_SPLITTER,
ConfigEntry,
ConfigValueTypes,
ConfigValueType,
CoreConfig,
PlayerConfig,
ProviderConfig,
Expand Down Expand Up @@ -77,7 +78,7 @@ def __init__(self, mass: MusicAssistant) -> None:
self._data: dict[str, Any] = {}
self.filename = os.path.join(self.mass.storage_path, "settings.json")
self._timer_handle: asyncio.TimerHandle | None = None
self._value_cache: dict[str, ConfigValueTypes] = {}
self._value_cache: dict[str, ConfigValueType] = {}

async def setup(self) -> None:
"""Async initialize of controller."""
Expand Down Expand Up @@ -208,7 +209,7 @@ async def get_provider_config(self, instance_id: str) -> ProviderConfig:
raise KeyError(msg)

@api_command("config/providers/get_value")
async def get_provider_config_value(self, instance_id: str, key: str) -> ConfigValueTypes:
async def get_provider_config_value(self, instance_id: str, key: str) -> ConfigValueType:
"""Return single configentry value for a provider."""
cache_key = f"prov_conf_value_{instance_id}.{key}"
if (cached_value := self._value_cache.get(cache_key)) is not None:
Expand All @@ -229,7 +230,7 @@ async def get_provider_config_entries(
provider_domain: str,
instance_id: str | None = None,
action: str | None = None,
values: dict[str, ConfigValueTypes] | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""
Return Config entries to setup/configure a provider.
Expand Down Expand Up @@ -261,7 +262,7 @@ async def get_provider_config_entries(
async def save_provider_config(
self,
provider_domain: str,
values: dict[str, ConfigValueTypes],
values: dict[str, ConfigValueType],
instance_id: str | None = None,
) -> ProviderConfig:
"""
Expand Down Expand Up @@ -356,18 +357,21 @@ async def get_player_config_value(
self,
player_id: str,
key: str,
) -> ConfigValueTypes:
unpack_splitted_values: bool = False,
) -> ConfigValueType:
"""Return single configentry value for a player."""
conf = await self.get_player_config(player_id)
if unpack_splitted_values:
return conf.values[key].get_splitted_values()
return (
conf.values[key].value
if conf.values[key].value is not None
else conf.values[key].default_value
)

def get_raw_player_config_value(
self, player_id: str, key: str, default: ConfigValueTypes = None
) -> ConfigValueTypes:
self, player_id: str, key: str, default: ConfigValueType = None
) -> ConfigValueType:
"""
Return (raw) single configentry value for a player.
Expand All @@ -380,7 +384,7 @@ def get_raw_player_config_value(

@api_command("config/players/save")
async def save_player_config(
self, player_id: str, values: dict[str, ConfigValueTypes]
self, player_id: str, values: dict[str, ConfigValueType]
) -> PlayerConfig:
"""Save/update PlayerConfig."""
config = await self.get_player_config(player_id)
Expand Down Expand Up @@ -513,7 +517,7 @@ def create_default_player_config(
provider: str,
name: str,
enabled: bool,
values: dict[str, ConfigValueTypes] | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> None:
"""
Create default/empty PlayerConfig.
Expand Down Expand Up @@ -600,7 +604,7 @@ async def get_core_config(self, domain: str) -> CoreConfig:
return CoreConfig.parse(config_entries, raw_conf)

@api_command("config/core/get_value")
async def get_core_config_value(self, domain: str, key: str) -> ConfigValueTypes:
async def get_core_config_value(self, domain: str, key: str) -> ConfigValueType:
"""Return single configentry value for a core controller."""
conf = await self.get_core_config(domain)
return (
Expand All @@ -614,7 +618,7 @@ async def get_core_config_entries(
self,
domain: str,
action: str | None = None,
values: dict[str, ConfigValueTypes] | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""
Return Config entries to configure a core controller.
Expand All @@ -635,7 +639,7 @@ async def get_core_config_entries(
async def save_core_config(
self,
domain: str,
values: dict[str, ConfigValueTypes],
values: dict[str, ConfigValueType],
) -> CoreConfig:
"""Save CoreController Config values."""
config = await self.get_core_config(domain)
Expand All @@ -656,8 +660,8 @@ async def save_core_config(
return await self.get_core_config(domain)

def get_raw_core_config_value(
self, core_module: str, key: str, default: ConfigValueTypes = None
) -> ConfigValueTypes:
self, core_module: str, key: str, default: ConfigValueType = None
) -> ConfigValueType:
"""
Return (raw) single configentry value for a core controller.
Expand All @@ -669,8 +673,8 @@ def get_raw_core_config_value(
)

def get_raw_provider_config_value(
self, provider_instance: str, key: str, default: ConfigValueTypes = None
) -> ConfigValueTypes:
self, provider_instance: str, key: str, default: ConfigValueType = None
) -> ConfigValueType:
"""
Return (raw) single config(entry) value for a provider.
Expand All @@ -685,7 +689,7 @@ def set_raw_provider_config_value(
self,
provider_instance: str,
key: str,
value: ConfigValueTypes,
value: ConfigValueType,
encrypted: bool = False,
) -> None:
"""
Expand All @@ -707,9 +711,7 @@ def set_raw_provider_config_value(
if prov := self.mass.get_provider(provider_instance, return_unavailable=True):
prov.config.values[key].value = value

def set_raw_core_config_value(
self, core_module: str, key: str, value: ConfigValueTypes
) -> None:
def set_raw_core_config_value(self, core_module: str, key: str, value: ConfigValueType) -> None:
"""
Set (raw) single config(entry) value for a core controller.
Expand All @@ -720,9 +722,7 @@ def set_raw_core_config_value(
self.set(f"{CONF_CORE}/{core_module}", CoreConfig({}, core_module).to_raw())
self.set(f"{CONF_CORE}/{core_module}/values/{key}", value)

def set_raw_player_config_value(
self, player_id: str, key: str, value: ConfigValueTypes
) -> None:
def set_raw_player_config_value(self, player_id: str, key: str, value: ConfigValueType) -> None:
"""
Set (raw) single config(entry) value for a player.
Expand Down Expand Up @@ -797,6 +797,21 @@ async def _migrate(self) -> None:
self._data[CONF_PROVIDERS].pop(instance_id, None)
LOGGER.warning("Removed corrupt provider configuration: %s", instance_id)
changed = True
# migrate sample_rates config entry
for player_id, player_config in list(self._data.get(CONF_PLAYERS, {}).items()):
if not (values := player_config.get("values")):
continue
if not (sample_rates := values.get("sample_rates")):
continue
if not isinstance(sample_rates, list):
del player_config["values"]["sample_rates"]
if not any(isinstance(x, list) for x in sample_rates):
continue
player_config["values"]["sample_rates"] = [
f"{x[0]}{MULTI_VALUE_SPLITTER}{x[1]}" if isinstance(x, list) else x
for x in sample_rates
]
changed = True

if changed:
await self._async_save()
Expand Down Expand Up @@ -825,7 +840,7 @@ async def _reload_provider(self, instance_id: str) -> None:
await self.mass.load_provider_config(config)

async def _update_provider_config(
self, instance_id: str, values: dict[str, ConfigValueTypes]
self, instance_id: str, values: dict[str, ConfigValueType]
) -> ProviderConfig:
"""Update ProviderConfig."""
config = await self.get_provider_config(instance_id)
Expand Down Expand Up @@ -865,7 +880,7 @@ async def _update_provider_config(
async def _add_provider_config(
self,
provider_domain: str,
values: dict[str, ConfigValueTypes],
values: dict[str, ConfigValueType],
) -> list[ConfigEntry] | ProviderConfig:
"""
Add new Provider (instance).
Expand Down
4 changes: 2 additions & 2 deletions music_assistant/controllers/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import aiofiles
from aiohttp import web
from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueTypes
from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType
from music_assistant_models.enums import (
AlbumType,
ConfigEntryType,
Expand Down Expand Up @@ -131,7 +131,7 @@ def __init__(self, *args, **kwargs) -> None:
async def get_config_entries(
self,
action: str | None = None,
values: dict[str, ConfigValueTypes] | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""Return all Config Entries for this core module (if any)."""
return (
Expand Down
6 changes: 4 additions & 2 deletions music_assistant/controllers/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from math import inf
from typing import TYPE_CHECKING, Final, cast

from music_assistant_models.config_entries import ConfigEntry, ConfigValueTypes
from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
from music_assistant_models.enums import (
CacheCategory,
ConfigEntryType,
Expand Down Expand Up @@ -113,7 +113,7 @@ def __init__(self, *args, **kwargs) -> None:
async def get_config_entries(
self,
action: str | None = None,
values: dict[str, ConfigValueTypes] | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""Return all Config Entries for this core module (if any)."""
entries = (
Expand Down Expand Up @@ -1048,6 +1048,8 @@ async def cleanup_provider(self, provider_instance: str) -> None:
if provider_instance.startswith(("filesystem", "jellyfin", "plex", "opensubsonic")):
# removal of a local provider can become messy very fast due to the relations
# such as images pointing at the files etc. so we just reset the whole db
# TODO: Handle this more gracefully in the future where we remove the provider
# and traverse the database to also remove all related items.
self.logger.warning(
"Removal of local provider detected, issuing full database reset..."
)
Expand Down
4 changes: 2 additions & 2 deletions music_assistant/controllers/player_queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from types import NoneType
from typing import TYPE_CHECKING, Any, TypedDict, cast

from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueTypes
from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType
from music_assistant_models.enums import (
CacheCategory,
ConfigEntryType,
Expand Down Expand Up @@ -139,7 +139,7 @@ async def close(self) -> None:
async def get_config_entries(
self,
action: str | None = None,
values: dict[str, ConfigValueTypes] | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""Return all Config Entries for this core module (if any)."""
enqueue_options = [ConfigValueOption(x.name, x.value) for x in QueueOption]
Expand Down
Loading

0 comments on commit 2a66b0b

Please sign in to comment.