from __future__ import annotations
import contextlib
import logging
from collections.abc import Generator, Iterable
from typing import TYPE_CHECKING, Any
from pykka.typing import proxy_method
from mopidy import exceptions
from mopidy.internal import validation
from mopidy.internal.models import MixerState
from mopidy.types import Percentage
if TYPE_CHECKING:
from mopidy.mixer import MixerProxy
logger = logging.getLogger(__name__)
@contextlib.contextmanager
def _mixer_error_handling(mixer: MixerProxy) -> Generator[None, Any, None]:
try:
yield
except exceptions.ValidationError as e:
logger.error(
"%s mixer returned bad data: %s",
mixer.actor_ref.actor_class.__name__,
e,
)
except Exception:
logger.exception(
"%s mixer caused an exception.",
mixer.actor_ref.actor_class.__name__,
)
class MixerController:
def __init__(self, mixer: MixerProxy | None) -> None:
self._mixer = mixer
[docs]
def get_volume(self) -> Percentage | None:
"""Get the volume.
Integer in range [0..100] or :class:`None` if unknown.
The volume scale is linear.
"""
if self._mixer is None:
return None
with _mixer_error_handling(self._mixer):
volume = self._mixer.get_volume().get()
if volume is not None:
validation.check_integer(volume, min=0, max=100)
return volume
return None
[docs]
def set_volume(self, volume: Percentage) -> bool:
"""Set the volume.
The volume is defined as an integer in range [0..100].
The volume scale is linear.
Returns :class:`True` if call is successful, otherwise :class:`False`.
"""
validation.check_integer(volume, min=0, max=100)
if self._mixer is None:
return False # TODO: 2.0 return None
with _mixer_error_handling(self._mixer):
result = self._mixer.set_volume(volume).get()
validation.check_instance(result, bool)
return result
return False
[docs]
def get_mute(self) -> bool | None:
"""Get mute state.
:class:`True` if muted, :class:`False` unmuted, :class:`None` if
unknown.
"""
if self._mixer is None:
return None
with _mixer_error_handling(self._mixer):
mute = self._mixer.get_mute().get()
if mute is not None:
validation.check_instance(mute, bool)
return mute
return None
[docs]
def set_mute(self, mute: bool) -> bool:
"""Set mute state.
:class:`True` to mute, :class:`False` to unmute.
Returns :class:`True` if call is successful, otherwise :class:`False`.
"""
validation.check_boolean(mute)
if self._mixer is None:
return False # TODO: 2.0 return None
with _mixer_error_handling(self._mixer):
result = self._mixer.set_mute(bool(mute)).get()
validation.check_instance(result, bool)
return result
return False
def _save_state(self) -> MixerState:
return MixerState(volume=self.get_volume(), mute=self.get_mute())
def _load_state(self, state: MixerState, coverage: Iterable[str]) -> None:
if state and "mixer" in coverage:
self.set_mute(state.mute)
if state.volume:
self.set_volume(Percentage(state.volume))
class MixerControllerProxy:
get_volume = proxy_method(MixerController.get_volume)
set_volume = proxy_method(MixerController.set_volume)
get_mute = proxy_method(MixerController.get_mute)
set_mute = proxy_method(MixerController.set_mute)