/
/
/
1"""Model/base for a Core controller within Music Assistant."""
2
3from __future__ import annotations
4
5import logging
6from typing import TYPE_CHECKING
7
8from music_assistant_models.enums import ProviderStage, ProviderType
9from music_assistant_models.provider import ProviderManifest
10
11from music_assistant.constants import CONF_LOG_LEVEL, MASS_LOGGER_NAME
12
13if TYPE_CHECKING:
14 from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, CoreConfig
15
16 from music_assistant.mass import MusicAssistant
17
18
19class CoreController:
20 """Base representation of a Core controller within Music Assistant."""
21
22 domain: str # used as identifier (=name of the module)
23 manifest: ProviderManifest # some info for the UI only
24
25 def __init__(self, mass: MusicAssistant) -> None:
26 """Initialize MusicProvider."""
27 self.mass = mass
28 self._set_logger()
29 self.manifest = ProviderManifest(
30 type=ProviderType.CORE,
31 domain=self.domain,
32 name=f"{self.domain.title()} Core controller",
33 description=f"{self.domain.title()} Core controller",
34 codeowners=["@music-assistant"],
35 stage=ProviderStage.STABLE,
36 icon="puzzle-outline",
37 builtin=True,
38 allow_disable=False,
39 )
40
41 async def get_config_entries(
42 self,
43 action: str | None = None,
44 values: dict[str, ConfigValueType] | None = None,
45 ) -> tuple[ConfigEntry, ...]:
46 """Return all Config Entries for this core module (if any)."""
47 return ()
48
49 async def setup(self, config: CoreConfig) -> None:
50 """Async initialize of module."""
51
52 async def close(self) -> None:
53 """Handle logic on server stop."""
54
55 async def reload(self, config: CoreConfig | None = None) -> None:
56 """Reload this core controller."""
57 await self.close()
58 if config is None:
59 config = await self.mass.config.get_core_config(self.domain)
60 log_level = str(config.get_value(CONF_LOG_LEVEL))
61 self._set_logger(log_level)
62 await self.setup(config)
63
64 async def update_config(self, config: CoreConfig, changed_keys: set[str]) -> None:
65 """Handle logic when the config is updated."""
66 # always update the stored config so dynamic reads pick up new values
67 self.config = config
68
69 # apply log level change dynamically (doesn't require reload)
70 if f"values/{CONF_LOG_LEVEL}" in changed_keys:
71 log_value = str(config.get_value(CONF_LOG_LEVEL))
72 self._set_logger(log_value)
73
74 # reload if any changed value entry has requires_reload set to True
75 needs_reload = any(
76 (entry := config.values.get(key.removeprefix("values/"))) is not None
77 and entry.requires_reload is True
78 for key in changed_keys
79 if key.startswith("values/")
80 )
81 if needs_reload:
82 self.logger.info(
83 "Config updated, reloading %s core controller",
84 self.manifest.name,
85 )
86 task_id = f"core_reload_{self.domain}"
87 self.mass.call_later(1, self.reload, config, task_id=task_id)
88
89 def _set_logger(self, log_level: str | None = None) -> None:
90 """Set the logger settings."""
91 mass_logger = logging.getLogger(MASS_LOGGER_NAME)
92 self.logger = mass_logger.getChild(self.domain)
93 if log_level is None:
94 log_level = str(
95 self.mass.config.get_raw_core_config_value(self.domain, CONF_LOG_LEVEL, "GLOBAL")
96 )
97 if log_level == "GLOBAL":
98 self.logger.setLevel(mass_logger.level)
99 else:
100 self.logger.setLevel(log_level)
101 if logging.getLogger().level > self.logger.level:
102 # if the root logger's level is higher, we need to adjust that too
103 logging.getLogger().setLevel(self.logger.level)
104