hass_options = {}
log_level = hass_options.get("log_level", args.log_level).upper()
- dev_mode = bool(os.environ.get("PYTHONDEVMODE", "0"))
+ dev_mode = os.environ.get("PYTHONDEVMODE", "0") == "1"
# setup logger
logger = setup_logger(data_dir, log_level)
start_mass(),
use_uvloop=False,
shutdown_callback=on_shutdown,
- executor_workers=32,
+ executor_workers=64,
)
schema_version: int
min_supported_schema_version: int
base_url: str
+ homeassistant_addon: bool = False
MessageType = (
import pathlib
from typing import Final
-__version__: Final[str] = "2.0.0b41"
-
-VERSION: Final[int] = __version__
SCHEMA_VERSION: Final[int] = 22
MIN_SCHEMA_VERSION = 22
from typing import TYPE_CHECKING, Any
from music_assistant.common.helpers.json import json_dumps, json_loads
+from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
+from music_assistant.common.models.enums import ConfigEntryType
from music_assistant.constants import (
DB_TABLE_CACHE,
DB_TABLE_SETTINGS,
from music_assistant.server.models.core_controller import CoreController
if TYPE_CHECKING:
- pass
+ from music_assistant.common.models.config_entries import CoreConfig
LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.cache")
+CONF_CLEAR_CACHE = "clear_cache"
class CacheController(CoreController):
)
self.manifest.icon = "mdi-memory"
- async def setup(self) -> None:
+ async def get_config_entries(
+ self,
+ action: str | None = None, # noqa: ARG002
+ values: dict[str, ConfigValueType] | None = None, # noqa: ARG002
+ ) -> tuple[ConfigEntry, ...]:
+ """Return all Config Entries for this core module (if any)."""
+ if action == CONF_CLEAR_CACHE:
+ await self.clear()
+ return (
+ ConfigEntry(
+ key=CONF_CLEAR_CACHE,
+ type=ConfigEntryType.LABEL,
+ label="The cache has been cleared",
+ ),
+ )
+ return (
+ ConfigEntry(
+ key=CONF_CLEAR_CACHE,
+ type=ConfigEntryType.ACTION,
+ label="Clear cache",
+ description="Reset/clear all items in the cache. ",
+ ),
+ # ConfigEntry(
+ # key=CONF_BIND_IP,
+ # type=ConfigEntryType.STRING,
+ # default_value=default_ip,
+ # label="Bind to IP/interface",
+ # description="Start the streamserver on this specific interface. \n"
+ # "This IP address is communicated to players where to find this server. "
+ # "Override the default in advanced scenarios, such as multi NIC configurations. \n"
+ # "Make sure that this server can be reached "
+ # "on the given IP and TCP port by players on the local network. \n"
+ # "This is an advanced setting that should normally "
+ # "not be adjusted in regular setups.",
+ # advanced=True,
+ # ),
+ )
+
+ async def setup(self, config: CoreConfig) -> None: # noqa: ARG002
"""Async initialize of cache module."""
await self._setup_database()
self.__schedule_cleanup_task()
remove = wrap(os.remove)
rename = wrap(os.rename)
-CONFIGURABLE_CORE_CONTROLLERS = ("streams", "webserver", "players", "metadata", "cache")
+CONFIGURABLE_CORE_CONTROLLERS = (
+ "streams",
+ "webserver",
+ "players",
+ "metadata",
+ "cache",
+ "music",
+ "player_queues",
+)
class ConfigController:
return config
# try to load the provider first to catch errors before we save it.
controller: CoreController = getattr(self.mass, domain)
- await controller.reload()
+ await controller.reload(config)
# reload succeeded, save new config
config.last_error = None
conf_key = f"{CONF_CORE}/{domain}"
from music_assistant.server.models.core_controller import CoreController
if TYPE_CHECKING:
+ from music_assistant.common.models.config_entries import CoreConfig
from music_assistant.server.models.metadata_provider import MetadataProvider
LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.metadata")
)
self.manifest.icon = "mdi-book-information-variant"
- async def setup(self) -> None:
+ async def setup(self, config: CoreConfig) -> None: # noqa: ARG002
"""Async initialize of module."""
self.mass.streams.register_dynamic_route("/imageproxy", self._handle_imageproxy)
async def close(self) -> None:
"""Handle logic on server stop."""
+ self.mass.streams.unregister_dynamic_route("/imageproxy")
@property
def providers(self) -> list[MetadataProvider]:
from music_assistant.common.helpers.datetime import utc_timestamp
from music_assistant.common.helpers.uri import parse_uri
-from music_assistant.common.models.enums import EventType, MediaType, ProviderFeature, ProviderType
+from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
+from music_assistant.common.models.enums import (
+ ConfigEntryType,
+ EventType,
+ MediaType,
+ ProviderFeature,
+ ProviderType,
+)
from music_assistant.common.models.errors import MusicAssistantError
from music_assistant.common.models.media_items import BrowseFolder, MediaItemType, SearchResults
from music_assistant.common.models.provider import SyncTask
from .media.tracks import TracksController
if TYPE_CHECKING:
- pass
+ from music_assistant.common.models.config_entries import CoreConfig
LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.music")
-SYNC_INTERVAL = 3 * 3600
+DEFAULT_SYNC_INTERVAL = 3 * 60 # default sync interval in minutes
+CONF_SYNC_INTERVAL = "sync_interval"
class MusicController(CoreController):
"""Several helpers around the musicproviders."""
domain: str = "music"
-
database: DatabaseConnection | None = None
def __init__(self, *args, **kwargs) -> None:
"Music Assistant's core controller which manages all music from all providers."
)
self.manifest.icon = "mdi-archive-music"
+ self._sync_task: asyncio.Task | None = None
- async def setup(self):
+ async def get_config_entries(
+ self,
+ action: str | None = None, # noqa: ARG002
+ values: dict[str, ConfigValueType] | None = None, # noqa: ARG002
+ ) -> tuple[ConfigEntry, ...]:
+ """Return all Config Entries for this core module (if any)."""
+ return (
+ ConfigEntry(
+ key=CONF_SYNC_INTERVAL,
+ type=ConfigEntryType.INTEGER,
+ range=(5, 720),
+ default_value=DEFAULT_SYNC_INTERVAL,
+ label="Sync interval",
+ description="Interval (in minutes) that a (delta) sync "
+ "of all providers should be performed.",
+ ),
+ )
+
+ async def setup(self, config: CoreConfig) -> None:
"""Async initialize of module."""
# setup library database
await self._setup_database()
- self.mass.create_task(self.start_sync(reschedule=SYNC_INTERVAL))
+ sync_interval = config.get_value(CONF_SYNC_INTERVAL)
+ self.logger.info("Setting up the sync interval to %s minutes.", sync_interval)
+ self._sync_task = self.mass.create_task(self.start_sync(reschedule=sync_interval))
async def close(self) -> None:
"""Cleanup on exit."""
+ if self._sync_task and not self._sync_task.done():
+ self._sync_task.cancel()
await self.database.close()
@property
from music_assistant.constants import CONF_FLOW_MODE, FALLBACK_DURATION, ROOT_LOGGER_NAME
from music_assistant.server.helpers.api import api_command
from music_assistant.server.helpers.audio import get_stream_details
+from music_assistant.server.models.core_controller import CoreController
if TYPE_CHECKING:
from collections.abc import Iterator
from music_assistant.common.models.player import Player
- from .players import PlayerController
LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.players.queue")
-class PlayerQueuesController:
+class PlayerQueuesController(CoreController):
"""Controller holding all logic to enqueue music for players."""
- def __init__(self, players: PlayerController) -> None:
- """Initialize class."""
- self.players = players
- self.mass = players.mass
+ domain: str = "player_queues"
+
+ def __init__(self, *args, **kwargs) -> None:
+ """Initialize core controller."""
+ super().__init__(*args, **kwargs)
self._queues: dict[str, PlayerQueue] = {}
self._queue_items: dict[str, list[QueueItem]] = {}
self._prev_states: dict[str, dict] = {}
+ self.manifest.name = "Player Queues controller"
+ self.manifest.description = (
+ "Music Assistant's core controller which manages the queues for all players."
+ )
+ self.manifest.icon = "mdi-playlist-music"
async def close(self) -> None:
"""Cleanup on exit."""
LOGGER.warning("Ignore queue command for %s because an announcement is in progress.")
return
# simply forward the command to underlying player
- await self.players.cmd_stop(queue_id)
+ await self.mass.players.cmd_stop(queue_id)
@api_command("players/queue/play")
async def play(self, queue_id: str) -> None:
return
if self._queues[queue_id].state == PlayerState.PAUSED:
# simply forward the command to underlying player
- await self.players.cmd_play(queue_id)
+ await self.mass.players.cmd_play(queue_id)
else:
await self.resume(queue_id)
LOGGER.warning("Ignore queue command for %s because an announcement is in progress.")
return
# simply forward the command to underlying player
- await self.players.cmd_pause(queue_id)
+ await self.mass.players.cmd_pause(queue_id)
@api_command("players/queue/play_pause")
async def play_pause(self, queue_id: str) -> None:
# race condition
return
queue_id = player.player_id
- player = self.players.get(queue_id)
+ player = self.mass.players.get(queue_id)
queue = self._queues[queue_id]
# basic properties
import asyncio
import logging
from collections.abc import Iterator
-from typing import cast
+from typing import TYPE_CHECKING, cast
from music_assistant.common.helpers.util import get_changed_values
from music_assistant.common.models.enums import (
from music_assistant.server.models.core_controller import CoreController
from music_assistant.server.models.player_provider import PlayerProvider
-from .player_queues import PlayerQueuesController
+if TYPE_CHECKING:
+ from music_assistant.common.models.config_entries import CoreConfig
LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.players")
super().__init__(*args, **kwargs)
self._players: dict[str, Player] = {}
self._prev_states: dict[str, dict] = {}
- self.queues = PlayerQueuesController(self)
self.manifest.name = "Players controller"
self.manifest.description = (
"Music Assistant's core controller which manages all players from all providers."
)
self.manifest.icon = "mdi-speaker-multiple"
+ self._poll_task: asyncio.Task | None = None
- async def setup(self) -> None:
+ async def setup(self, config: CoreConfig) -> None: # noqa: ARG002
"""Async initialize of module."""
- self.mass.create_task(self._poll_players())
+ self._poll_task = self.mass.create_task(self._poll_players())
async def close(self) -> None:
"""Cleanup on exit."""
- await self.queues.close()
+ if self._poll_task and not self._poll_task.done():
+ self._poll_task.cancel()
@property
def providers(self) -> list[PlayerProvider]:
player.enabled = self.mass.config.get(f"{CONF_PLAYERS}/{player_id}/enabled", True)
# register playerqueue for this player
- self.mass.create_task(self.queues.on_player_register(player))
+ self.mass.create_task(self.mass.player_queues.on_player_register(player))
self._players[player_id] = player
if player is None:
return
LOGGER.info("Player removed: %s", player.name)
- self.queues.on_player_remove(player_id)
+ self.mass.player_queues.on_player_remove(player_id)
self.mass.config.remove(f"players/{player_id}")
self._prev_states.pop(player_id, None)
self.mass.signal_event(EventType.PLAYER_REMOVED, player_id)
return
# always signal update to the playerqueue
- self.queues.on_player_update(player, changed_values)
+ self.mass.player_queues.on_player_update(player, changed_values)
if len(changed_values) == 0 and not force_update:
return
from music_assistant.server.models.core_controller import CoreController
if TYPE_CHECKING:
+ from music_assistant.common.models.config_entries import CoreConfig
from music_assistant.common.models.player import Player
"""Initialize MultiClientStreamJob instance."""
self.stream_controller = stream_controller
self.queue_id = queue_id
- self.queue = self.stream_controller.mass.players.queues.get(queue_id)
+ self.queue = self.stream_controller.mass.player_queues.get(queue_id)
assert self.queue # just in case
self.pcm_format = pcm_format
self.start_queue_item = start_queue_item
"on the given IP and TCP port by players on the local network. \n"
"This is an advanced setting that should normally "
"not be adjusted in regular setups.",
+ advanced=True,
),
)
- async def setup(self) -> None:
+ async def setup(self, config: CoreConfig) -> None:
"""Async initialize of module."""
ffmpeg_present, libsoxr_support, version = await check_audio_support()
if not ffmpeg_present:
"with libsoxr support" if libsoxr_support else "",
)
# start the webserver
- self.publish_port = await self.mass.config.get_core_config_value(
- self.domain, CONF_BIND_PORT
- )
- self.publish_ip = await self.mass.config.get_core_config_value(self.domain, CONF_BIND_IP)
+ self.publish_port = config.get_value(CONF_BIND_PORT)
+ self.publish_ip = config.get_value(CONF_BIND_IP)
await self._server.setup(
bind_ip=self.publish_ip,
bind_port=self.publish_port,
"""Stream single queueitem audio to a player."""
self._log_request(request)
queue_id = request.match_info["queue_id"]
- queue = self.mass.players.queues.get(queue_id)
+ queue = self.mass.player_queues.get(queue_id)
if not queue:
raise web.HTTPNotFound(reason=f"Unknown Queue: {queue_id}")
queue_player = self.mass.players.get(queue_id)
queue_item_id = request.match_info["queue_item_id"]
- queue_item = self.mass.players.queues.get_item(queue_id, queue_item_id)
+ queue_item = self.mass.player_queues.get_item(queue_id, queue_item_id)
if not queue_item:
raise web.HTTPNotFound(reason=f"Unknown Queue item: {queue_item_id}")
try:
"""Stream Queue Flow audio to player."""
self._log_request(request)
queue_id = request.match_info["queue_id"]
- queue = self.mass.players.queues.get(queue_id)
+ queue = self.mass.player_queues.get(queue_id)
if not queue:
raise web.HTTPNotFound(reason=f"Unknown Queue: {queue_id}")
start_queue_item_id = request.match_info["queue_item_id"]
- start_queue_item = self.mass.players.queues.get_item(queue_id, start_queue_item_id)
+ start_queue_item = self.mass.player_queues.get_item(queue_id, start_queue_item_id)
if not start_queue_item:
raise web.HTTPNotFound(reason=f"Unknown Queue item: {start_queue_item_id}")
seek_position = int(request.query.get("seek_position", 0))
continue
# if icy metadata is enabled, send the icy metadata after the chunk
- current_item = self.mass.players.queues.get_item(
+ current_item = self.mass.player_queues.get_item(
queue.queue_id, queue.index_in_buffer
)
if (
_,
queue_track,
use_crossfade,
- ) = await self.mass.players.queues.preload_next_url(queue.queue_id)
+ ) = await self.mass.player_queues.preload_next_url(queue.queue_id)
except QueueEmpty:
break
from music_assistant.common.models.event import MassEvent
from music_assistant.constants import CONF_BIND_IP, CONF_BIND_PORT
from music_assistant.server.helpers.api import APICommandHandler, parse_arguments
-from music_assistant.server.helpers.util import get_ips, is_hass_supervisor
+from music_assistant.server.helpers.util import get_ips
from music_assistant.server.helpers.webserver import Webserver
from music_assistant.server.models.core_controller import CoreController
if TYPE_CHECKING:
- from music_assistant.common.models.config_entries import ConfigValueType
+ from music_assistant.common.models.config_entries import ConfigValueType, CoreConfig
CONF_BASE_URL = "base_url"
values: dict[str, ConfigValueType] | None = None, # noqa: ARG002
) -> tuple[ConfigEntry, ...]:
"""Return all Config Entries for this core module (if any)."""
- if await is_hass_supervisor():
+ if self.mass.running_as_hass_addon:
# if we're running on the HA supervisor the webserver is secured by HA ingress
# we only start the webserver on the internal docker network and ingress connects
# to that internally and exposes the webUI securely
"to enhance security and protect outside access to the webinterface and API. \n\n"
"This is an advanced setting that should normally "
"not be adjusted in regular setups.",
+ advanced=True,
),
)
- async def setup(self) -> None:
+ async def setup(self, config: CoreConfig) -> None:
"""Async initialize of module."""
# work out all routes
routes: list[tuple[str, str, Awaitable]] = []
routes.append(("GET", "/ws", self._handle_ws_client))
# start the webserver
await self._server.setup(
- bind_ip=await self.mass.config.get_core_config_value(self.domain, CONF_BIND_IP),
- bind_port=await self.mass.config.get_core_config_value(self.domain, CONF_BIND_PORT),
- base_url=await self.mass.config.get_core_config_value(self.domain, CONF_BASE_URL),
+ bind_ip=config.get_value(CONF_BIND_IP),
+ bind_port=config.get_value(CONF_BIND_PORT),
+ base_url=config.get_value(CONF_BASE_URL),
static_routes=routes,
# add assets subdir as static_content
static_content=("/assets", os.path.join(frontend_dir, "assets"), "assets"),
import urllib.request
from contextlib import suppress
from functools import lru_cache
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as pkg_version
from typing import TYPE_CHECKING
import memory_tempfile
raise RuntimeError(msg)
+async def get_package_version(pkg_name: str) -> str:
+ """
+ Return the version of an installed (python) package.
+
+ Will return `0.0.0` if the package is not found.
+ """
+ try:
+ installed_version = await asyncio.to_thread(pkg_version, pkg_name)
+ if installed_version is None:
+ return "0.0.0" # type: ignore[unreachable]
+ return installed_version
+ except PackageNotFoundError:
+ return "0.0.0"
+
+
async def get_ips(include_ipv6: bool = False) -> set[str]:
"""Return all IP-adresses of all network interfaces."""
async def is_hass_supervisor() -> bool:
"""Return if we're running inside the HA Supervisor (e.g. HAOS)."""
with suppress(urllib.error.URLError):
- res = await asyncio.to_thread(urllib.request.urlopen, "ws://supervisor/core/websocket")
+ res = await asyncio.to_thread(urllib.request.urlopen, "http://supervisor/core")
+ # this should return a 401 unauthorized if it exists
return res.code == 401
return False
from music_assistant.constants import CONF_LOG_LEVEL, ROOT_LOGGER_NAME
if TYPE_CHECKING:
- from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
+ from music_assistant.common.models.config_entries import (
+ ConfigEntry,
+ ConfigValueType,
+ CoreConfig,
+ )
from music_assistant.server import MusicAssistant
"""Return all Config Entries for this core module (if any)."""
return tuple()
- async def setup(self) -> None:
+ async def setup(self, config: CoreConfig) -> None:
"""Async initialize of module."""
async def close(self) -> None:
"""Handle logic on server stop."""
- async def reload(self) -> None:
+ async def reload(self, config: CoreConfig | None = None) -> None:
"""Reload this core controller."""
await self.close()
- log_level = self.mass.config.get_raw_core_config_value(
- self.domain, CONF_LOG_LEVEL, "GLOBAL"
- )
+ if config is None:
+ config = await self.mass.config.get_core_config(self.domain)
+ log_level = config.get_value(CONF_LOG_LEVEL)
if log_level == "GLOBAL":
log_level = logging.getLogger(ROOT_LOGGER_NAME).level
self.logger.setLevel(log_level)
- await self.setup()
+ await self.setup(config)
async def _enqueue_next_track(self, castplayer: CastPlayer) -> None:
"""Enqueue the next track of the MA queue on the CC queue."""
try:
- next_url, next_item, _ = await self.mass.players.queues.preload_next_url(
+ next_url, next_item, _ = await self.mass.player_queues.preload_next_url(
castplayer.player_id
)
except QueueEmpty:
next_url,
next_item,
_,
- ) = await self.mass.players.queues.preload_next_url(dlna_player.udn)
+ ) = await self.mass.player_queues.preload_next_url(dlna_player.udn)
except QueueEmpty:
return
SearchResults,
StreamDetails,
)
-from music_assistant.constants import __version__ as MASS_VERSION # noqa: N812
from music_assistant.server.helpers.audio import get_radio_stream
from music_assistant.server.models.music_provider import MusicProvider
async def handle_setup(self) -> None:
"""Handle async initialization of the provider."""
self.radios = RadioBrowser(
- session=self.mass.http_session, user_agent=f"MusicAssistant/{MASS_VERSION}"
+ session=self.mass.http_session, user_agent=f"MusicAssistant/{self.mass.version}"
)
try:
# Try to get some stats to check connection to RadioBrowser API
parent_player.group_childs.add(child_player.player_id)
child_player.synced_to = parent_player.player_id
# check if we should (re)start or join a stream session
- active_queue = self.mass.players.queues.get_active_queue(parent_player.player_id)
+ active_queue = self.mass.player_queues.get_active_queue(parent_player.player_id)
if (
ENABLE_EXPERIMENTAL_SYNC_JOIN
and (stream_job := self.mass.streams.multi_client_jobs.get(active_queue.queue_id))
await self._handle_play_url(client, url, None, auto_play=True)
elif parent_player.state == PlayerState.PLAYING:
# playback needs to be restarted to form a new multi client stream session
- await self.mass.players.queues.resume(active_queue.queue_id, fade_in=False)
+ await self.mass.player_queues.resume(active_queue.queue_id, fade_in=False)
else:
# make sure that the player manager gets an update
self.mass.players.update(child_player.player_id)
client.player_id, deque(maxlen=MIN_REQ_PLAYPOINTS)
)
- active_queue = self.mass.players.queues.get_active_queue(client.player_id)
+ active_queue = self.mass.player_queues.get_active_queue(client.player_id)
stream_job = self.mass.streams.multi_client_jobs.get(active_queue.queue_id)
if not stream_job:
# should not happen, but just in case
if player.active_source != player.player_id:
return
try:
- next_url, next_item, crossfade = await self.mass.players.queues.preload_next_url(
+ next_url, next_item, crossfade = await self.mass.player_queues.preload_next_url(
client.player_id
)
async with asyncio.TaskGroup() as tg:
def _get_corrected_elapsed_milliseconds(self, client: SlimClient) -> int:
"""Return corrected elapsed milliseconds."""
skipped_millis = 0
- active_queue = self.mass.players.queues.get_active_queue(client.player_id)
+ active_queue = self.mass.player_queues.get_active_queue(client.player_id)
if stream_job := self.mass.streams.multi_client_jobs.get(active_queue.queue_id):
skipped_millis = stream_job.client_seconds_skipped.get(client.player_id, 0) * 1000
sync_delay = self.mass.config.get_raw_player_config_value(
player = self.mass.players.get(player_id)
if player is None:
return None
- queue = self.mass.players.queues.get_active_queue(player_id)
+ queue = self.mass.player_queues.get_active_queue(player_id)
assert queue is not None
start_index = queue.current_index or 0 if offset == "-" else offset
queue_items: list[QueueItem] = []
index = 0
- async for item in self.mass.players.queues.items(queue.queue_id):
+ async for item in self.mass.player_queues.items(queue.queue_id):
if index >= start_index:
queue_items.append(item)
if len(queue_items) == limit:
# You may jump to a particular position in a song by specifying a number of seconds
# to seek to. You may also jump to a relative position within a song by putting an
# explicit "-" or "+" character before a number of seconds you would like to seek.
- player_queue = self.mass.players.queues.get_active_queue(player_id)
+ player_queue = self.mass.player_queues.get_active_queue(player_id)
assert player_queue is not None
if number == "?":
if isinstance(number, str) and ("+" in number or "-" in number):
jump = int(number.split("+")[1])
- self.mass.create_task(self.mass.players.queues.skip, player_queue.queue_id, jump)
+ self.mass.create_task(self.mass.player_queues.skip, player_queue.queue_id, jump)
else:
- self.mass.create_task(self.mass.players.queues.seek, player_queue.queue_id, number)
+ self.mass.create_task(self.mass.player_queues.seek, player_queue.queue_id, number)
def _handle_power(self, player_id: str, value: str | int, *args, **kwargs) -> int | None:
"""Handle player `time` command."""
) -> int | None:
"""Handle player `playlist` command."""
arg = args[0] if args else "?"
- queue = self.mass.players.queues.get_active_queue(player_id)
+ queue = self.mass.player_queues.get_active_queue(player_id)
assert queue is not None
# <playerid> playlist index <index|+index|-index|?> <fadeInSecs>
if subcommand == "index" and isinstance(arg, int):
- self.mass.create_task(self.mass.players.queues.play_index, player_id, arg)
+ self.mass.create_task(self.mass.player_queues.play_index, player_id, arg)
return
if subcommand == "index" and arg == "?":
return queue.current_index
if subcommand == "index" and "+" in arg:
next_index = (queue.current_index or 0) + int(arg.split("+")[1])
- self.mass.create_task(self.mass.players.queues.play_index, player_id, next_index)
+ self.mass.create_task(self.mass.player_queues.play_index, player_id, next_index)
return
if subcommand == "index" and "-" in arg:
next_index = (queue.current_index or 0) - int(arg.split("-")[1])
- self.mass.create_task(self.mass.players.queues.play_index, player_id, next_index)
+ self.mass.create_task(self.mass.player_queues.play_index, player_id, next_index)
return
if subcommand == "shuffle":
- self.mass.players.queues.set_shuffle(queue.queue_id, not queue.shuffle_enabled)
+ self.mass.player_queues.set_shuffle(queue.queue_id, not queue.shuffle_enabled)
return
if subcommand == "repeat":
if queue.repeat_mode == RepeatMode.ALL:
new_repeat_mode = RepeatMode.ONE
else:
new_repeat_mode = RepeatMode.ALL
- self.mass.players.queues.set_repeat(queue.queue_id, new_repeat_mode)
+ self.mass.player_queues.set_repeat(queue.queue_id, new_repeat_mode)
return
if subcommand == "crossfade":
- self.mass.players.queues.set_crossfade(queue.queue_id, not queue.crossfade_enabled)
+ self.mass.player_queues.set_crossfade(queue.queue_id, not queue.crossfade_enabled)
return
self.logger.warning("Unhandled command: playlist/%s", subcommand)
**kwargs,
) -> int | None:
"""Handle player `playlistcontrol` command."""
- queue = self.mass.players.queues.get_active_queue(player_id)
+ queue = self.mass.player_queues.get_active_queue(player_id)
if cmd == "play":
self.mass.create_task(
- self.mass.players.queues.play_media(queue.queue_id, uri, QueueOption.PLAY)
+ self.mass.player_queues.play_media(queue.queue_id, uri, QueueOption.PLAY)
)
return
if cmd == "load":
self.mass.create_task(
- self.mass.players.queues.play_media(queue.queue_id, uri, QueueOption.REPLACE)
+ self.mass.player_queues.play_media(queue.queue_id, uri, QueueOption.REPLACE)
)
return
if cmd == "add":
self.mass.create_task(
- self.mass.players.queues.play_media(queue.queue_id, uri, QueueOption.ADD)
+ self.mass.player_queues.play_media(queue.queue_id, uri, QueueOption.ADD)
)
return
if cmd == "insert":
self.mass.create_task(
- self.mass.players.queues.play_media(queue.queue_id, uri, QueueOption.IN)
+ self.mass.player_queues.play_media(queue.queue_id, uri, QueueOption.IN)
)
return
self.logger.warning("Unhandled command: playlistcontrol/%s", cmd)
**kwargs,
) -> int | None:
"""Handle player `play` command."""
- queue = self.mass.players.queues.get_active_queue(player_id)
+ queue = self.mass.player_queues.get_active_queue(player_id)
assert queue is not None
- self.mass.create_task(self.mass.players.queues.play, player_id)
+ self.mass.create_task(self.mass.player_queues.play, player_id)
def _handle_stop(
self,
**kwargs,
) -> int | None:
"""Handle player `stop` command."""
- queue = self.mass.players.queues.get_active_queue(player_id)
+ queue = self.mass.player_queues.get_active_queue(player_id)
assert queue is not None
- self.mass.create_task(self.mass.players.queues.stop, player_id)
+ self.mass.create_task(self.mass.player_queues.stop, player_id)
def _handle_pause(
self,
**kwargs,
) -> int | None:
"""Handle player `stop` command."""
- queue = self.mass.players.queues.get_active_queue(player_id)
+ queue = self.mass.player_queues.get_active_queue(player_id)
assert queue is not None
if force or queue.state == PlayerState.PLAYING:
- self.mass.create_task(self.mass.players.queues.pause, player_id)
+ self.mass.create_task(self.mass.player_queues.pause, player_id)
else:
- self.mass.create_task(self.mass.players.queues.play, player_id)
+ self.mass.create_task(self.mass.player_queues.play, player_id)
def _handle_mode(
self,
self.mass.create_task(self.mass.players.cmd_power, player_id, not player.powered)
return
# queue related button commands
- queue = self.mass.players.queues.get_active_queue(player_id)
+ queue = self.mass.player_queues.get_active_queue(player_id)
if subcommand == "jump_fwd":
- self.mass.create_task(self.mass.players.queues.next, queue.queue_id)
+ self.mass.create_task(self.mass.player_queues.next, queue.queue_id)
return
if subcommand == "jump_rew":
- self.mass.create_task(self.mass.players.queues.previous, queue.queue_id)
+ self.mass.create_task(self.mass.player_queues.previous, queue.queue_id)
return
if subcommand == "fwd":
- self.mass.create_task(self.mass.players.queues.skip, queue.queue_id, 10)
+ self.mass.create_task(self.mass.player_queues.skip, queue.queue_id, 10)
return
if subcommand == "rew":
- self.mass.create_task(self.mass.players.queues.skip, queue.queue_id, -10)
+ self.mass.create_task(self.mass.player_queues.skip, queue.queue_id, -10)
return
if subcommand == "shuffle":
- self.mass.players.queues.set_shuffle(queue.queue_id, not queue.shuffle_enabled)
+ self.mass.player_queues.set_shuffle(queue.queue_id, not queue.shuffle_enabled)
return
if subcommand == "repeat":
if queue.repeat_mode == RepeatMode.ALL:
new_repeat_mode = RepeatMode.ONE
else:
new_repeat_mode = RepeatMode.ALL
- self.mass.players.queues.set_repeat(queue.queue_id, new_repeat_mode)
+ self.mass.player_queues.set_repeat(queue.queue_id, new_repeat_mode)
return
if subcommand.startswith("preset_"):
preset_index = subcommand.split("preset_")[1].split(".")[0]
):
option = QueueOption.REPLACE if "playlist" in preset_uri else QueueOption.PLAY
self.mass.create_task(
- self.mass.players.queues.play_media, queue.queue_id, preset_uri, option
+ self.mass.player_queues.play_media, queue.queue_id, preset_uri, option
)
return
async def _enqueue_next_track(self, sonos_player: SonosPlayer) -> None:
"""Enqueue the next track of the MA queue on the CC queue."""
try:
- next_url, next_item, crossfade = await self.mass.players.queues.preload_next_url(
+ next_url, next_item, crossfade = await self.mass.player_queues.preload_next_url(
sonos_player.player_id
)
except QueueEmpty:
self.mass.players.cmd_sync, child_player.player_id, sync_leader
)
else:
- self.mass.create_task(self.mass.players.queues.resume, player_id)
+ self.mass.create_task(self.mass.player_queues.resume, player_id)
elif (
not child_player.powered
and group_player.extra_data["optimistic_state"] == PlayerState.PLAYING
):
# a sync master player turned OFF while the group player
# should still be playing - we need to resync/resume
- self.mass.create_task(self.mass.players.queues.resume, player_id)
+ self.mass.create_task(self.mass.player_queues.resume, player_id)
def _get_active_members(
self, player_id: str, only_powered: bool = False, skip_sync_childs: bool = True
MIN_SCHEMA_VERSION,
ROOT_LOGGER_NAME,
SCHEMA_VERSION,
- VERSION,
)
from music_assistant.server.controllers.cache import CacheController
from music_assistant.server.controllers.config import ConfigController
from music_assistant.server.controllers.metadata import MetaDataController
from music_assistant.server.controllers.music import MusicController
+from music_assistant.server.controllers.player_queues import PlayerQueuesController
from music_assistant.server.controllers.players import PlayerController
from music_assistant.server.controllers.streams import StreamsController
from music_assistant.server.controllers.webserver import WebserverController
from music_assistant.server.helpers.api import APICommandHandler, api_command
from music_assistant.server.helpers.images import get_icon_string
-from music_assistant.server.helpers.util import get_provider_module
+from music_assistant.server.helpers.util import (
+ get_package_version,
+ get_provider_module,
+ is_hass_supervisor,
+)
from .models import ProviderInstanceType
metadata: MetaDataController
music: MusicController
players: PlayerController
+ player_queues: PlayerQueuesController
streams: StreamsController
def __init__(self, storage_path: str) -> None:
self._providers: dict[str, ProviderInstanceType] = {}
self._tracked_tasks: dict[str, asyncio.Task] = {}
self.closing = False
+ self.running_as_hass_addon: bool = False
+ self.version: str = "0.0.0"
async def start(self) -> None:
"""Start running the Music Assistant server."""
self.loop = asyncio.get_running_loop()
+ self.running_as_hass_addon = await is_hass_supervisor()
+ self.version = await get_package_version("music_assistant")
# create shared zeroconf instance
self.zeroconf = Zeroconf(interfaces=InterfaceChoice.All)
# create shared aiohttp ClientSession
# setup config controller first and fetch important config values
self.config = ConfigController(self)
await self.config.setup()
- LOGGER.info(
- "Starting Music Assistant Server (%s)",
- self.server_id,
- )
+ LOGGER.info("Starting Music Assistant Server (%s) version %s", self.server_id, self.version)
# setup other core controllers
self.cache = CacheController(self)
self.webserver = WebserverController(self)
self.metadata = MetaDataController(self)
self.music = MusicController(self)
self.players = PlayerController(self)
+ self.player_queues = PlayerQueuesController(self)
self.streams = StreamsController(self)
+ await self.cache.setup(await self.config.get_core_config("cache"))
+ await self.webserver.setup(await self.config.get_core_config("webserver"))
+ await self.music.setup(await self.config.get_core_config("music"))
+ await self.metadata.setup(await self.config.get_core_config("metadata"))
+ await self.players.setup(await self.config.get_core_config("players"))
+ await self.player_queues.setup(await self.config.get_core_config("player_queues"))
+ await self.streams.setup(await self.config.get_core_config("streams"))
# register all api commands (methods with decorator)
self._register_api_commands()
- await self.cache.setup()
- await self.webserver.setup()
- await self.music.setup()
- await self.metadata.setup()
- await self.players.setup()
- await self.streams.setup()
# setup discovery
self.create_task(self._setup_discovery())
# load providers
await self.webserver.close()
await self.metadata.close()
await self.music.close()
+ await self.player_queues.close()
await self.players.close()
# cleanup cache and config
await self.config.close()
"""Return Info of this server."""
return ServerInfoMessage(
server_id=self.server_id,
- server_version=VERSION,
+ server_version=self.version,
schema_version=SCHEMA_VERSION,
min_supported_schema_version=MIN_SCHEMA_VERSION,
base_url=self.webserver.base_url,
+ homeassistant_addon=self.running_as_hass_addon,
)
@api_command("providers/available")
self.metadata,
self.music,
self.players,
- self.players.queues,
+ self.player_queues,
):
for attr_name in dir(cls):
if attr_name.startswith("__"):
[project]
name = "music_assistant"
-dynamic = ["version"]
+# The version is set by GH action on release
+version = "0.0.0"
license = {text = "Apache-2.0"}
description = "Music Assistant"
readme = "README.md"
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{name = "The Music Assistant Authors", email = "marcelveldt@users.noreply.github.com"}
]
[project.scripts]
mass = "music_assistant.__main__:main"
-[tool.setuptools.dynamic]
-version = {attr = "music_assistant.constants.__version__"}
-
[tool.black]
target-version = ['py311']
line-length = 100