A collection of small fixes and enhancements (#1030)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 27 Jan 2024 10:35:36 +0000 (11:35 +0100)
committerGitHub <noreply@github.com>
Sat, 27 Jan 2024 10:35:36 +0000 (11:35 +0100)
music_assistant/common/models/config_entries.py
music_assistant/server/controllers/config.py
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/players.py
music_assistant/server/controllers/streams.py
music_assistant/server/providers/chromecast/__init__.py
music_assistant/server/providers/dlna/__init__.py
music_assistant/server/providers/sonos/__init__.py

index 1d822c3c774dcf6bc3d52a9ae9a4db53db6b01ee..b6e5259626de5e6068181a5fdb9e80e3523010a6 100644 (file)
@@ -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,
 )
index a4a660309a8d60d9c339e37e324bc5fdf10c5289..8e3a983dbbf7bf811171051a6bc4fb01e8d09b32 100644 (file)
@@ -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,
             },
         )
index e0884f299497cf21596f0d1c242c990c8543d22b..08b3a6ee56aa7b72a52295676c02b8f1ac3b9ab5 100755 (executable)
@@ -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
index 607cf6b4f8d036e23e3c99b1dc4ae25f47527e74..e1b76fa21ed6acce7b3038eb04fc04dc262113d2 100755 (executable)
@@ -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)
index f75b6c8279f1ff649b6ebd80c38eefcd5b05b887..1ad7e084d85b2c916cda6477d973e5cdc0f9655c 100644 (file)
@@ -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
index 2fd3273f7d139fe7259e6cb9c8d7652b2874b236..c0205029b312e6221334b78f160115d22d30e936 100644 (file)
@@ -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)
index f973dc87e82f75dadba32e3afe19eb1d443a37b9..9ce9c7c9a439013f935eb21c46208750c4e06389 100644 (file)
@@ -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,
index 9248f0db99eed9688f08ada374cae3ad53070671..5ade60512b06882c52bc7a567de1e7266c967e24 100644 (file)
@@ -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,