Some small bugfixes and tweaks (#1642)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 3 Sep 2024 19:29:48 +0000 (21:29 +0200)
committerGitHub <noreply@github.com>
Tue, 3 Sep 2024 19:29:48 +0000 (21:29 +0200)
* Require extra duration match for 'soft' external id matches

* Fix race condition in spotify stream when token expired

* Add a bit of extra logging

* fix playlist matching

* limit radiobrowser results

* update airplay mute state on volume changes

music_assistant/server/helpers/audio.py
music_assistant/server/helpers/compare.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/radiobrowser/__init__.py
music_assistant/server/providers/spotify/__init__.py

index 13b4417cb6ebcff7e663a3db1db1d441ebe636c7..da4c342198f9d5abafb94a75e63de497b278d2a1 100644 (file)
@@ -254,7 +254,7 @@ async def crossfade_pcm_parts(
     _returncode, crossfaded_audio, _stderr = await communicate(args, fade_in_part)
     if crossfaded_audio:
         LOGGER.log(
-            5,
+            VERBOSE_LOG_LEVEL,
             "crossfaded 2 pcm chunks. fade_in_part: %s - "
             "fade_out_part: %s - fade_length: %s seconds",
             len(fade_in_part),
@@ -310,12 +310,12 @@ async def strip_silence(
 
     # return stripped audio
     bytes_stripped = len(audio_data) - len(stripped_data)
-    if LOGGER.isEnabledFor(5):
+    if LOGGER.isEnabledFor(VERBOSE_LOG_LEVEL):
         pcm_sample_size = int(sample_rate * (bit_depth / 8) * 2)
         seconds_stripped = round(bytes_stripped / pcm_sample_size, 2)
         location = "end" if reverse else "begin"
         LOGGER.log(
-            5,
+            VERBOSE_LOG_LEVEL,
             "stripped %s seconds of silence from %s of pcm audio. bytes stripped: %s",
             seconds_stripped,
             location,
@@ -336,6 +336,8 @@ async def get_stream_details(
     Do not try to request streamdetails in advance as this is expiring data.
         param media_item: The QueueItem for which to request the streamdetails for.
     """
+    time_start = time.time()
+    LOGGER.debug("Getting streamdetails for %s", queue_item.uri)
     if seek_position and (queue_item.media_type == MediaType.RADIO or not queue_item.duration):
         LOGGER.warning("seeking is not possible on duration-less streams!")
         seek_position = 0
@@ -406,7 +408,8 @@ async def get_stream_details(
         streamdetails.target_loudness = None
     else:
         streamdetails.target_loudness = player_settings.get_value(CONF_VOLUME_NORMALIZATION_TARGET)
-
+    process_time = int((time.time() - time_start) * 1000)
+    LOGGER.debug("retrieved streamdetails for %s in %s milliseconds", queue_item.uri, process_time)
     return streamdetails
 
 
index 93b120270b9d7fb6ec23789b4ea8d5f3e90f9709..808b319f3326c013649d8a1eaeefbfd71bbfcc17 100644 (file)
@@ -128,22 +128,32 @@ def compare_track(
     # return early on exact item_id match
     if compare_item_ids(base_item, compare_item):
         return True
-    # return early on (un)matched external id
+    # return early on (un)matched primary/unique external id
     for ext_id in (
         ExternalID.MB_RECORDING,
-        ExternalID.DISCOGS,
+        ExternalID.MB_TRACK,
         ExternalID.ACOUSTID,
+    ):
+        external_id_match = compare_external_ids(
+            base_item.external_ids, compare_item.external_ids, ext_id
+        )
+        if external_id_match is not None:
+            return external_id_match
+    # check secondary external id matches
+    for ext_id in (
+        ExternalID.DISCOGS,
         ExternalID.TADB,
-        # make sure to check musicbrainz before isrc
-        # https://github.com/music-assistant/hass-music-assistant/issues/2316
         ExternalID.ISRC,
         ExternalID.ASIN,
     ):
         external_id_match = compare_external_ids(
             base_item.external_ids, compare_item.external_ids, ext_id
         )
-        if external_id_match is not None:
-            return external_id_match
+        if external_id_match is True:
+            # we got a 'soft-match' on a secondary external id (like ISRC)
+            # but we do a double check on duration
+            if abs(base_item.duration - compare_item.duration) <= 2:
+                return True
 
     # compare name
     if not compare_strings(base_item.name, compare_item.name, strict=True):
@@ -227,18 +237,15 @@ def compare_playlist(
     """Compare two Playlist items and return True if they match."""
     if base_item is None or compare_item is None:
         return False
-    # return early on exact item_id match
-    if compare_item_ids(base_item, compare_item):
-        return True
-    # compare owner (if not ItemMapping)
+    # require (exact) name match
+    if not compare_strings(base_item.name, compare_item.name, strict=strict):
+        return False
+    # require exact owner match (if not ItemMapping)
     if isinstance(base_item, Playlist) and isinstance(compare_item, Playlist):
         if not compare_strings(base_item.owner, compare_item.owner):
             return False
-    # compare version
-    if not compare_version(base_item.version, compare_item.version):
-        return False
-    # finally comparing on (exact) name match
-    return compare_strings(base_item.name, compare_item.name, strict=strict)
+    # a playlist is always unique - so do a strict compare on item id(s)
+    return compare_item_ids(base_item, compare_item)
 
 
 def compare_radio(
index cc96b275b267ac1146ce87602dcfd30af4c37c25..08e9293544aa7c1bd23a9dfb71c1373c8e7907e2 100644 (file)
@@ -749,6 +749,7 @@ class AirplayProvider(PlayerProvider):
             await airplay_player.raop_stream.send_cli_command(f"VOLUME={volume_level}\n")
         mass_player = self.mass.players.get(player_id)
         mass_player.volume_level = volume_level
+        mass_player.volume_muted = volume_level == 0
         self.mass.players.update(player_id)
         # store last state in cache
         await self.mass.cache.set(player_id, volume_level, base_key=CACHE_KEY_PREV_VOLUME)
index dde24e19ea5674b99e9cbb4951d5c419491db6f8..5675f4c6d1d0832b6cd06dcfdd3fb7080b7d5d65 100644 (file)
@@ -245,7 +245,7 @@ class RadioBrowserProvider(MusicProvider):
     async def get_country_folders(self, base_path: str) -> list[BrowseFolder]:
         """Get a list of country names as BrowseFolder."""
         items: list[BrowseFolder] = []
-        for country in await self.radios.countries(order=Order.NAME, hide_broken=True):
+        for country in await self.radios.countries(order=Order.NAME, hide_broken=True, limit=1000):
             folder = BrowseFolder(
                 item_id=country.code.lower(),
                 provider=self.domain,
@@ -270,7 +270,7 @@ class RadioBrowserProvider(MusicProvider):
         """Get radio stations by popularity."""
         stations = await self.radios.stations(
             hide_broken=True,
-            limit=5000,
+            limit=1000,
             order=Order.CLICK_COUNT,
             reverse=True,
         )
@@ -287,7 +287,8 @@ class RadioBrowserProvider(MusicProvider):
             filter_by=FilterBy.TAG_EXACT,
             filter_term=tag,
             hide_broken=True,
-            order=Order.NAME,
+            limit=1000,
+            order=Order.CLICK_COUNT,
             reverse=False,
         )
         for station in stations:
@@ -302,7 +303,8 @@ class RadioBrowserProvider(MusicProvider):
             filter_by=FilterBy.COUNTRY_CODE_EXACT,
             filter_term=country_code,
             hide_broken=True,
-            order=Order.NAME,
+            limit=1000,
+            order=Order.CLICK_COUNT,
             reverse=False,
         )
         for station in stations:
index 13388f09914aaf6e14e4a31b89f39ea9893f0db0..8b0914a12e340eec91803678b3b3ccc82543e1e7 100644 (file)
@@ -563,28 +563,29 @@ class SpotifyProvider(MusicProvider):
         auth_info = await self.login()
         librespot = await self.get_librespot_binary()
         spotify_uri = f"spotify://track:{streamdetails.item_id}"
-        args = [
-            librespot,
-            "-c",
-            CACHE_DIR,
-            "-M",
-            "256M",
-            "--passthrough",
-            "-b",
-            "320",
-            "--backend",
-            "pipe",
-            "--single-track",
-            spotify_uri,
-            "--token",
-            auth_info["access_token"],
-        ]
-        if seek_position:
-            args += ["--start-position", str(int(seek_position))]
-        chunk_size = get_chunksize(streamdetails.audio_format)
-        stderr = None if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else False
-        self.logger.log(VERBOSE_LOG_LEVEL, f"Start streaming {spotify_uri} using librespot")
         for retry in (True, False):
+            args = [
+                librespot,
+                "-c",
+                CACHE_DIR,
+                "-M",
+                "256M",
+                "--passthrough",
+                "-b",
+                "320",
+                "--backend",
+                "pipe",
+                "--single-track",
+                spotify_uri,
+                "--token",
+                auth_info["access_token"],
+            ]
+            if seek_position:
+                args += ["--start-position", str(int(seek_position))]
+            chunk_size = get_chunksize(streamdetails.audio_format)
+            stderr = None if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else False
+            self.logger.log(VERBOSE_LOG_LEVEL, f"Start streaming {spotify_uri} using librespot")
+
             async with AsyncProcess(
                 args,
                 stdout=True,
@@ -600,7 +601,7 @@ class SpotifyProvider(MusicProvider):
                     raise AudioError(
                         f"Failed to stream {spotify_uri} - error: {librespot_proc.returncode}"
                     )
-                # do one retry attempt
+                # do one retry attempt - accounting for the fact that the token might have expired
                 auth_info = await self.login(force_refresh=True)
 
     def _parse_artist(self, artist_obj):