From: Marcel van der Veldt Date: Sat, 27 Jan 2024 10:35:36 +0000 (+0100) Subject: A collection of small fixes and enhancements (#1030) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=e03d9d4450baa5bc2a17078b353729f4e8254b80;p=music-assistant-server.git A collection of small fixes and enhancements (#1030) --- diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py index 1d822c3c..b6e52596 100644 --- a/music_assistant/common/models/config_entries.py +++ b/music_assistant/common/models/config_entries.py @@ -288,7 +288,6 @@ CONF_ENTRY_LOG_LEVEL = ConfigEntry( ConfigValueOption("debug", "DEBUG"), ), default_value="GLOBAL", - description="Set the log verbosity for this provider", advanced=True, ) @@ -302,9 +301,6 @@ CONF_ENTRY_FLOW_MODE = ConfigEntry( type=ConfigEntryType.BOOLEAN, label="Enable queue flow mode", default_value=False, - description='Enable "flow" mode where all queue tracks are sent as a continuous ' - "audio stream. \nUse for players that do not natively support gapless and/or " - "crossfading or if the player has trouble transitioning between tracks.", advanced=False, ) @@ -329,18 +325,15 @@ CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry( ], default_value="stereo", label="Output Channel Mode", - description="You can configure this player to play only the left or right channel, " - "for example to a create a stereo pair with 2 players.", advanced=True, ) CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry( key=CONF_VOLUME_NORMALIZATION, type=ConfigEntryType.BOOLEAN, - label="Enable volume normalization (EBU-R128 based)", + label="Enable volume normalization", default_value=True, - description="Enable volume normalization based on the EBU-R128 " - "standard without affecting dynamic range", + description="Enable volume normalization (EBU-R128 based)", ) CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry( @@ -349,8 +342,7 @@ CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry( range=(-30, 0), default_value=-17, label="Target level for volume normalization", - description="Adjust average (perceived) loudness to this target level, " - "default is -17 LUFS \n\n WARNING: Setting levels higher than this may result in clipping", + description="Adjust average (perceived) loudness to this target level", depends_on=CONF_VOLUME_NORMALIZATION, advanced=True, ) @@ -411,8 +403,5 @@ CONF_ENTRY_HIDE_PLAYER = ConfigEntry( type=ConfigEntryType.BOOLEAN, label="Hide this player in the user interface", default_value=False, - description="Hide this player in the user interface. \n\n" - "Note that it can still be controlled and it will also show up in any " - "sync groups the player belongs to.", advanced=True, ) diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index a4a66030..8e3a983d 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -311,7 +311,7 @@ class ConfigController: @api_command("config/players") async def get_player_configs(self, provider: str | None = None) -> list[PlayerConfig]: """Return all known player configurations, optionally filtered by provider domain.""" - available_providers = {x.domain for x in self.mass.providers} + available_providers = {x.instance_id for x in self.mass.providers} return [ await self.get_player_config(player_id) for player_id, raw_conf in self.get(CONF_PLAYERS, {}).items() @@ -469,12 +469,13 @@ class ConfigController: else: raise KeyError(f"Unknown provider domain: {provider_domain}") config_entries = await self.get_provider_config_entries(provider_domain) + instance_id = f"{manifest.domain}--{shortuuid.random(8)}" default_config: ProviderConfig = ProviderConfig.parse( config_entries, { "type": manifest.type.value, "domain": manifest.domain, - "instance_id": manifest.domain, + "instance_id": instance_id, "name": manifest.name, # note: this will only work for providers that do # not have any required config entries or provide defaults @@ -716,13 +717,7 @@ class ConfigController: # determine instance id based on previous configs if existing and not manifest.multi_instance: raise ValueError(f"Provider {manifest.name} does not support multiple instances") - if len(existing) == 0: - instance_id = provider_domain - name = manifest.name - else: - random_id = shortuuid.random(6) - instance_id = f"{provider_domain}_{random_id}" - name = f"{manifest.name} {random_id}" + instance_id = f"{manifest.domain}--{shortuuid.random(8)}" # all checks passed, create config object config_entries = await self.get_provider_config_entries( provider_domain=provider_domain, instance_id=instance_id, values=values @@ -733,7 +728,7 @@ class ConfigController: "type": manifest.type.value, "domain": manifest.domain, "instance_id": instance_id, - "name": name, + "name": manifest.name, "values": values, }, ) diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index e0884f29..08b3a6ee 100755 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -631,14 +631,15 @@ class PlayerQueuesController(CoreController): self._prev_states[queue_id] = new_state return # handle player was playing and is now stopped - # if player finished playing a track for 90%, mark current item as finished + # if player finished playing a track for 85%, mark current item as finished if ( prev_state.get("state") == "playing" and queue.state == PlayerState.IDLE and ( queue.current_item and queue.current_item.duration - and queue.elapsed_time > (queue.current_item.duration * 0.8) + and prev_state.get("elapsed_time", queue.elapsed_time) + > (queue.current_item.duration * 0.85) ) ): queue.current_index += 1 diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index 607cf6b4..e1b76fa2 100755 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -840,7 +840,7 @@ class PlayerController(CoreController): # - every 30 seconds if the player is powered # - every 10 seconds if the player is playing if ( - player.available + (player.available or count == 360) and ( (player.powered and count % 30 == 0) or (player_playing and count % 10 == 0) diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index f75b6c82..1ad7e084 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -62,7 +62,7 @@ DEFAULT_STREAM_HEADERS = { "contentFeatures.dlna.org": "DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000", # noqa: E501 "Cache-Control": "no-cache", "Connection": "close", - # "Accept-Ranges": "none", + "Accept-Ranges": "none", "icy-name": "Music Assistant", "icy-pub": "0", } @@ -487,8 +487,8 @@ class StreamsController(CoreController): ) await resp.prepare(request) - # return early if this is only a HEAD request - if request.method == "HEAD": + # return early if this is not a GET request + if request.method != "GET": return resp # all checks passed, start streaming! @@ -578,8 +578,8 @@ class StreamsController(CoreController): ) await resp.prepare(request) - # return early if this is only a HEAD request - if request.method == "HEAD": + # return early if this is not a GET request + if request.method != "GET": return resp # all checks passed, start streaming! @@ -691,8 +691,8 @@ class StreamsController(CoreController): ) await resp.prepare(request) - # return early if this is only a HEAD request - if request.method == "HEAD": + # return early if this is not a GET request + if request.method != "GET": return resp # some players (e.g. dlna, sonos) misbehave and do multiple GET requests diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py index 2fd3273f..c0205029 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/server/providers/chromecast/__init__.py @@ -20,6 +20,7 @@ from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_S from music_assistant.common.models.config_entries import ( CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_FLOW_MODE, ConfigEntry, ConfigValueType, ) @@ -34,7 +35,13 @@ from music_assistant.common.models.enums import ( from music_assistant.common.models.errors import PlayerUnavailableError from music_assistant.common.models.player import DeviceInfo, Player from music_assistant.common.models.queue_item import QueueItem -from music_assistant.constants import CONF_CROSSFADE, CONF_LOG_LEVEL, CONF_PLAYERS, MASS_LOGO_ONLINE +from music_assistant.constants import ( + CONF_CROSSFADE, + CONF_FLOW_MODE, + CONF_LOG_LEVEL, + CONF_PLAYERS, + MASS_LOGO_ONLINE, +) from music_assistant.server.models.player_provider import PlayerProvider from .helpers import CastStatusListener, ChromecastInfo @@ -57,11 +64,13 @@ PLAYER_CONFIG_ENTRIES = ( type=ConfigEntryType.BOOLEAN, label="Enable crossfade", default_value=False, - description="Enable a crossfade transition between (queue) tracks. \n" - "Note that Chromecast does not natively support crossfading so Music Assistant " - "uses a 'flow mode' workaround for this at the cost of on-player metadata.", + description="Enable a crossfade transition between (queue) tracks. \n\n" + "Note that Cast does not natively support crossfading so you need to enable " + "the 'flow mode' workaround to use crossfading with Cast players.", advanced=False, + depends_on=CONF_FLOW_MODE, ), + CONF_ENTRY_FLOW_MODE, CONF_ENTRY_CROSSFADE_DURATION, ) @@ -75,6 +84,7 @@ def _patched_process_media_status(self, data): _patched_process_media_status_org(self, data) for status_msg in data.get("status", []): if items := status_msg.get("items"): + self.status.current_item_id = status_msg.get("currentItemId", 0) self.status.items = items @@ -236,8 +246,9 @@ class ChromecastProvider(PlayerProvider): - fade_in: Optionally fade in the item at playback start. """ castplayer = self.castplayers[player_id] - # Google cast does not support crossfading so we use flow mode to provide this feature - use_flow_mode = await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE) + use_flow_mode = await self.mass.config.get_player_config_value( + player_id, CONF_FLOW_MODE + ) or await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE) url = await self.mass.streams.resolve_stream_url( queue_item=queue_item, output_codec=ContentType.FLAC, @@ -298,12 +309,21 @@ class ChromecastProvider(PlayerProvider): output_codec=ContentType.FLAC, ) next_item_id = None - if (cast_queue_items := getattr(castplayer.cc.media_controller.status, "items")) and len( - cast_queue_items - ) > 1: - next_item_id = cast_queue_items[-1]["itemId"] + status = castplayer.cc.media_controller.status + # lookup position of current track in cast queue + cast_current_item_id = getattr(status, "current_item_id", 0) + cast_queue_items = getattr(status, "items", []) + cur_item_found = False + for item in cast_queue_items: + if item["itemId"] == cast_current_item_id: + cur_item_found = True + continue + elif not cur_item_found: + continue + next_item_id = item["itemId"] + # check if the next queue item isn't already queued if ( - cast_queue_items[-1].get("media", {}).get("customData", {}).get("queue_item_id") + item.get("media", {}).get("customData", {}).get("queue_item_id") == queue_item.queue_item_id ): return @@ -315,7 +335,7 @@ class ChromecastProvider(PlayerProvider): media_controller = castplayer.cc.media_controller queuedata["mediaSessionId"] = media_controller.status.media_session_id self.mass.create_task(media_controller.send_message, queuedata, inc_session_id=True) - self.logger.info( + self.logger.debug( "Enqued next track (%s) to player %s", queue_item.name if queue_item else url, castplayer.player.display_name, @@ -490,15 +510,20 @@ class ChromecastProvider(PlayerProvider): """Handle updated MediaStatus.""" castplayer.logger.debug("Received media status update: %s", status.player_state) # player state + castplayer.player.elapsed_time_last_updated = time.time() if status.player_is_playing: castplayer.player.state = PlayerState.PLAYING + castplayer.player.current_item_id = status.content_id elif status.player_is_paused: castplayer.player.state = PlayerState.PAUSED + castplayer.player.current_item_id = status.content_id else: castplayer.player.state = PlayerState.IDLE + castplayer.player.current_item_id = None # elapsed time castplayer.player.elapsed_time_last_updated = time.time() + castplayer.player.elapsed_time = status.adjusted_current_time if status.player_is_playing: castplayer.player.elapsed_time = status.adjusted_current_time else: @@ -513,18 +538,8 @@ class ChromecastProvider(PlayerProvider): castplayer.player.active_source = castplayer.cc.app_display_name # current media - castplayer.player.current_item_id = status.content_id self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) - # handle end of MA queue - reset current_item_id - if ( - castplayer.player.state == PlayerState.IDLE - and castplayer.player.current_item_id - and (queue := self.mass.player_queues.get(castplayer.player_id)) - and queue.next_item is None - ): - castplayer.player.current_item_id = None - def on_new_connection_status(self, castplayer: CastPlayer, status: ConnectionStatus) -> None: """Handle updated ConnectionStatus.""" castplayer.logger.debug("Received connection status update - status: %s", status.status) diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py index f973dc87..9ce9c7c9 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/server/providers/dlna/__init__.py @@ -344,7 +344,6 @@ class DLNAPlayerProvider(PlayerProvider): - seek_position: Optional seek to this position. - fade_in: Optionally fade in the item at playback start. """ - # DLNA players do not support crossfading so we enforce flow mode to provide this feature use_flow_mode = await self.mass.config.get_player_config_value( player_id, CONF_FLOW_MODE ) or await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE) @@ -419,7 +418,7 @@ class DLNAPlayerProvider(PlayerProvider): didl_metadata = create_didl_metadata(self.mass, url, queue_item) title = queue_item.name await dlna_player.device.async_set_next_transport_uri(url, title, didl_metadata) - self.logger.info( + self.logger.debug( "Enqued next track (%s) to player %s", title, dlna_player.player.display_name, diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index 9248f0db..5ade6051 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -758,7 +758,7 @@ class SonosPlayerProvider(PlayerProvider): [("InstanceID", 0), ("NextURI", url), ("NextURIMetaData", metadata)], timeout=60, ) - self.logger.info( + self.logger.debug( "Enqued next track (%s) to player %s", queue_item.name if queue_item else url, sonos_player.soco_device.player_name,