Complete player metadata
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 26 Oct 2025 23:25:02 +0000 (00:25 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 26 Oct 2025 23:25:02 +0000 (00:25 +0100)
music_assistant/helpers/images.py
music_assistant/models/player.py
music_assistant/providers/airplay/raop.py
music_assistant/providers/chromecast/player.py
music_assistant/providers/roku_media_assistant/player.py
music_assistant/providers/sonos/player.py
music_assistant/providers/squeezelite/player.py

index da323fa80c5298bfe3fc36e32ebcb39a426d860b..1900bb51a96e4af5819f8144330b3600191ecad1 100644 (file)
@@ -80,10 +80,18 @@ async def get_image_thumb(
         except UnidentifiedImageError:
             raise FileNotFoundError(f"Invalid image: {path_or_url}")
         if size:
+            # Use LANCZOS for high quality downsampling
             img.thumbnail((size, size), Image.Resampling.LANCZOS)
 
         mode = "RGBA" if image_format == "PNG" else "RGB"
-        img.convert(mode).save(data, image_format, optimize=True)
+
+        # Save with high quality settings
+        if image_format == "JPEG":
+            # For JPEG, use quality=95 for better quality
+            img.convert(mode).save(data, image_format, quality=95, optimize=False)
+        else:
+            # For PNG, disable optimize to preserve quality
+            img.convert(mode).save(data, image_format, optimize=False)
         return data.getvalue()
 
     image_format = image_format.upper()
index fdda11908e88e20227cd640c8c0eda6b46f7403e..c6aa1135ab0e4d245a1c24d4d14510784f1caf78 100644 (file)
@@ -931,6 +931,7 @@ class Player(ABC):
         ):
             return PlayerMedia(
                 uri=source.metadata.uri or source.id,
+                media_type=MediaType.PLUGIN_SOURCE,
                 title=source.metadata.title,
                 artist=source.metadata.artist,
                 album=source.metadata.album,
@@ -949,7 +950,10 @@ class Player(ABC):
 
         if active_queue and (current_item := active_queue.current_item):
             item_image_url = (
-                self.mass.metadata.get_image_url(current_item.image) if current_item.image else None
+                # the image format needs to be 500x500 jpeg for maximum compatibility with players
+                self.mass.metadata.get_image_url(current_item.image, size=500, image_format="png")
+                if current_item.image
+                else None
             )
             if current_item.streamdetails and (
                 stream_metadata := current_item.streamdetails.stream_metadata
@@ -957,6 +961,7 @@ class Player(ABC):
                 # handle stream metadata in streamdetails (e.g. for radio stream)
                 return PlayerMedia(
                     uri=current_item.uri,
+                    media_type=current_item.media_type,
                     title=stream_metadata.title or current_item.name,
                     artist=stream_metadata.artist,
                     album=stream_metadata.album or current_item.name,
@@ -972,10 +977,14 @@ class Player(ABC):
                 # normal media item
                 return PlayerMedia(
                     uri=str(media_item.uri),
+                    media_type=media_item.media_type,
                     title=media_item.name,
                     artist=getattr(media_item, "artist_str", None),
                     album=album.name if (album := getattr(media_item, "album", None)) else None,
-                    image_url=self.mass.metadata.get_image_url(current_item.media_item.image)
+                    # the image format needs to be 500x500 jpeg for maximum player compatibility
+                    image_url=self.mass.metadata.get_image_url(
+                        current_item.media_item.image, size=500, image_format="jpeg"
+                    )
                     or item_image_url
                     if current_item.media_item.image
                     else item_image_url,
@@ -989,6 +998,7 @@ class Player(ABC):
             # fallback to basic current item details
             return PlayerMedia(
                 uri=current_item.uri,
+                media_type=current_item.media_type,
                 title=current_item.name,
                 image_url=item_image_url,
                 duration=current_item.duration,
@@ -998,7 +1008,24 @@ class Player(ABC):
                 elapsed_time_last_updated=active_queue.elapsed_time_last_updated,
             )
         # return native current media if no group/queue is active
-        return self._current_media
+        if self._current_media:
+            return PlayerMedia(
+                uri=self._current_media.uri,
+                media_type=self._current_media.media_type,
+                title=self._current_media.title,
+                artist=self._current_media.artist,
+                album=self._current_media.album,
+                image_url=self._current_media.image_url,
+                duration=self._current_media.duration,
+                source_id=self._current_media.source_id or self._active_source,
+                queue_item_id=self._current_media.queue_item_id,
+                elapsed_time=self._current_media.elapsed_time or int(self.elapsed_time)
+                if self.elapsed_time
+                else None,
+                elapsed_time_last_updated=self._current_media.elapsed_time_last_updated
+                or self.elapsed_time_last_updated,
+            )
+        return None
 
     @cached_property
     @final
index dd72d012e7e21da47dd23243777cfe273656c327..50ae76f4d166118879d927ddf5c55b59df6084a5 100644 (file)
@@ -29,7 +29,6 @@ from .constants import (
 
 if TYPE_CHECKING:
     from music_assistant_models.media_items import AudioFormat
-    from music_assistant_models.player_queue import PlayerQueue
 
     from .player import AirPlayPlayer
     from .provider import AirPlayProvider
@@ -401,7 +400,6 @@ class RaopStream:
     async def _stderr_reader(self) -> None:
         """Monitor stderr for the running CLIRaop process."""
         player = self.player
-        queue = self.mass.players.get_active_queue(player)
         logger = player.logger
         lost_packets = 0
         prev_metadata_checksum: str = ""
@@ -417,24 +415,16 @@ class RaopStream:
                 # send metadata to player(s) if needed
                 # NOTE: this must all be done in separate tasks to not disturb audio
                 now = time.time()
-                if (
-                    (player.elapsed_time or 0) > 2
-                    and queue
-                    and queue.current_item
-                    and queue.current_item.streamdetails
-                ):
-                    metadata_checksum = (
-                        queue.current_item.streamdetails.stream_title
-                        or queue.current_item.queue_item_id
-                    )
+                if (player.elapsed_time or 0) > 2 and player.current_media:
+                    metadata_checksum = f"{player.current_media.uri}.{player.current_media.title}.{player.current_media.image_url}"  # noqa: E501
                     if prev_metadata_checksum != metadata_checksum:
                         prev_metadata_checksum = metadata_checksum
                         prev_progress_report = now
-                        self.mass.create_task(self._send_metadata(queue))
+                        self.mass.create_task(self._send_metadata())
                     # send the progress report every 5 seconds
                     elif now - prev_progress_report >= 5:
                         prev_progress_report = now
-                        self.mass.create_task(self._send_progress(queue))
+                        self.mass.create_task(self._send_progress())
             if "set pause" in line or "Pause at" in line:
                 player.set_state_from_raop(state=PlaybackState.PAUSED)
             if "Restarted at" in line or "restarting w/ pause" in line:
@@ -444,9 +434,9 @@ class RaopStream:
                 player.set_state_from_raop(state=PlaybackState.PLAYING, elapsed_time=0)
             if "lost packet out of backlog" in line:
                 lost_packets += 1
-                if lost_packets == 100 and queue:
+                if lost_packets == 100:
                     logger.error("High packet loss detected, restarting playback...")
-                    self.mass.create_task(self.mass.player_queues.resume(queue.queue_id, False))
+                    self.mass.create_task(self.mass.players.cmd_resume(self.player.player_id))
                 else:
                     logger.warning("Packet loss detected!")
             if "end of stream reached" in line:
@@ -457,48 +447,27 @@ class RaopStream:
         # ensure we're cleaned up afterwards (this also logs the returncode)
         await self.stop()
 
-    async def _send_metadata(self, queue: PlayerQueue) -> None:
+    async def _send_metadata(self) -> None:
         """Send metadata to player (and connected sync childs)."""
-        if not queue or not queue.current_item or self._stopped:
+        if not self.player or not self.player.current_media or self._stopped:
             return
-        duration = min(queue.current_item.duration or 0, 3600)
-        title = queue.current_item.name
-        artist = ""
-        album = ""
-        if queue.current_item.streamdetails and queue.current_item.streamdetails.stream_title:
-            # stream title/metadata from radio/live stream
-            if " - " in queue.current_item.streamdetails.stream_title:
-                artist, title = queue.current_item.streamdetails.stream_title.split(" - ", 1)
-            else:
-                title = queue.current_item.streamdetails.stream_title
-                artist = ""
-            # set album to radio station name
-            album = queue.current_item.name
-        elif media_item := queue.current_item.media_item:
-            title = media_item.name
-            if artist_str := getattr(media_item, "artist_str", None):
-                artist = artist_str
-            if _album := getattr(media_item, "album", None):
-                album = _album.name
-
-        cmd = f"TITLE={title or 'Music Assistant'}\nARTIST={artist}\nALBUM={album}\n"
+        duration = min(self.player.current_media.duration or 0, 3600)
+        title = self.player.current_media.title or ""
+        artist = self.player.current_media.artist or ""
+        album = self.player.current_media.album or ""
+        cmd = f"TITLE={title}\nARTIST={artist}\nALBUM={album}\n"
         cmd += f"DURATION={duration}\nPROGRESS=0\nACTION=SENDMETA\n"
 
         await self.send_cli_command(cmd)
 
         # get image
-        if not queue.current_item.image or self._stopped:
+        if not self.player.current_media.image_url or self._stopped:
             return
+        await self.send_cli_command(f"ARTWORK={self.player.current_media.image_url}\n")
 
-        # the image format needs to be 500x500 jpeg for maximum compatibility with players
-        image_url = self.mass.metadata.get_image_url(
-            queue.current_item.image, size=500, prefer_proxy=True, image_format="jpeg"
-        )
-        await self.send_cli_command(f"ARTWORK={image_url}\n")
-
-    async def _send_progress(self, queue: PlayerQueue) -> None:
+    async def _send_progress(self) -> None:
         """Send progress report to player (and connected sync childs)."""
-        if not queue or not queue.current_item or self._stopped:
+        if not self.player.current_media or self._stopped:
             return
-        progress = int(queue.corrected_elapsed_time)
+        progress = int(self.player.corrected_elapsed_time or 0)
         await self.send_cli_command(f"PROGRESS={progress}\n")
index 5d7e19e83efd810e490be7716946523e9c508465..ec41633e7f9ac2ffb5c4a69ad0e40003f9f7efbb 100644 (file)
@@ -245,41 +245,29 @@ class ChromecastPlayer(Player):
             return
         if self.extra_attributes.get(ATTR_ANNOUNCEMENT_IN_PROGRESS):
             return
-        if not (queue := self.mass.player_queues.get_active_queue(self.player_id)):
+        if not (current_media := self.current_media):
             return
-        if not (current_item := queue.current_item):
-            return
-        if not (queue.flow_mode or current_item.media_type == MediaType.RADIO):
+        if not (
+            "/flow/" in self._attr_current_media.uri
+            or self.current_media.media_type
+            in (
+                MediaType.RADIO,
+                MediaType.PLUGIN_SOURCE,
+            )
+        ):
+            # only update metadata for streams without known duration
             return
         self._attr_poll_interval = 2
         media_controller = self.cc.media_controller
         # update metadata of current item chromecast
-        image_url = ""
-        if (streamdetails := current_item.streamdetails) and streamdetails.stream_metadata:
-            album = current_item.media_item.name if current_item.media_item else ""
-            artist = streamdetails.stream_metadata.artist or ""
-            title = streamdetails.stream_metadata.title or ""
-            if streamdetails.stream_metadata.album:
-                album = streamdetails.stream_metadata.album
-            if streamdetails.stream_metadata.image_url:
-                image_url = streamdetails.stream_metadata.image_url
-        elif media_item := current_item.media_item:
-            album = _album.name if (_album := getattr(media_item, "album", None)) else ""
-            artist = getattr(media_item, "artist_str", "")
-            title = media_item.name
-        else:
-            album = ""
-            artist = ""
-            title = current_item.name
-        flow_meta_checksum = f"{current_item.queue_item_id}-{album}-{artist}-{title}-{image_url}"
+        title = current_media.title or "Music Assistant"
+        artist = current_media.artist or ""
+        album = current_media.album or ""
+        image_url = current_media.image_url or MASS_LOGO_ONLINE
+        flow_meta_checksum = f"{current_media.uri}-{album}-{artist}-{title}-{image_url}"
         if self.flow_meta_checksum != flow_meta_checksum:
             # only update if something changed
             self.flow_meta_checksum = flow_meta_checksum
-            image_url = image_url or (
-                self.mass.metadata.get_image_url(current_item.image, size=512)
-                if current_item.image
-                else MASS_LOGO_ONLINE
-            )
             queuedata = {
                 "type": "PLAY",
                 "mediaSessionId": media_controller.status.media_session_id,
@@ -302,7 +290,7 @@ class ChromecastPlayer(Player):
             # In flow mode, all queue tracks are sent to the player as continuous stream.
             # add a special 'command' item to the queue
             # this allows for on-player next buttons/commands to still work
-            cmd_next_url = self.mass.streams.get_command_url(queue.queue_id, "next")
+            cmd_next_url = self.mass.streams.get_command_url(self.player_id, "next")
             msg = {
                 "type": "QUEUE_INSERT",
                 "mediaSessionId": media_controller.status.media_session_id,
index 168179b4673f8d6eee7921a712dd352803bb67ac..fab76d71562b07b6e792df3577521cfe27b40aa5 100644 (file)
@@ -298,67 +298,26 @@ class MediaAssistantPlayer(Player):
                             "Playback Position received from %s Was Invalid", self.name
                         )
 
-                if self.current_media and self.current_media.source_id:
-                    if not (
-                        queue := self.mass.player_queues.get_active_queue(
-                            self.current_media.source_id
-                        )
-                    ):
-                        return
-                else:
+                if not self.current_media or self._attr_playback_state != PlaybackState.PLAYING:
                     return
 
-                if (
-                    self._attr_playback_state == PlaybackState.PLAYING
-                    and queue.next_item
-                    and queue.current_item
-                    and queue.current_item.duration
-                ):
-                    if queue.elapsed_time >= queue.current_item.duration:
-                        self._attr_current_media = self.queued
-
-                if (
-                    self._attr_playback_state == PlaybackState.PLAYING
-                    and queue.current_item
-                    and queue.flow_mode
-                ):
-                    current_item = queue.current_item
-
-                    image_url = (
-                        self.mass.metadata.get_image_url(current_item.image, size=512)
-                        if current_item.image
-                        else ""
-                    )
+                image_url = self.current_media.image_url or ""
 
-                    album_name = ""
-                    song_name = ""
-                    artist_name = ""
-
-                    if current_item.media_item is not None:
-                        media_item = current_item.media_item
-
-                        song_name = media_item.name if media_item is not None else ""
-
-                        if hasattr(media_item, "album"):
-                            album_name = (
-                                media_item.album.name if media_item.album is not None else ""
-                            )
-
-                        if hasattr(media_item, "artist_str"):
-                            artist_name = media_item.artist_str
-
-                    if app_running:
-                        await self.roku_input(
-                            {
-                                "u": "",
-                                "t": "m",
-                                "albumName": album_name,
-                                "songName": song_name,
-                                "artistName": artist_name,
-                                "albumArt": image_url,
-                                "isLive": "true",
-                            },
-                        )
+                album_name = self.current_media.album or ""
+                song_name = self.current_media.title or ""
+                artist_name = self.current_media.artist or ""
+                if app_running:
+                    await self.roku_input(
+                        {
+                            "u": "",
+                            "t": "m",
+                            "albumName": album_name,
+                            "songName": song_name,
+                            "artistName": artist_name,
+                            "albumArt": image_url,
+                            "isLive": "true",
+                        },
+                    )
             except Exception:
                 self.logger.warning("Failed to update media state for: %s", self.name)
 
index d3526971ffd0938ff828b4e7cf7cf5fbfbc73764..1e56f9a589d538df6d443d0df69bb0786defd363 100644 (file)
@@ -707,6 +707,7 @@ class SonosPlayer(Player):
             track_duration_millis = track.get("durationMillis")
             current_media = PlayerMedia(
                 uri=track.get("id", {}).get("objectId") or track.get("mediaUrl"),
+                media_type=MediaType.TRACK,
                 title=track["name"],
                 artist=track.get("artist", {}).get("name"),
                 album=track.get("album", {}).get("name"),
@@ -722,6 +723,7 @@ class SonosPlayer(Player):
             image_url = images[0].get("url") if images else None
             current_media = PlayerMedia(
                 uri=container.get("id", {}).get("objectId"),
+                media_type=MediaType.RADIO,
                 title=active_group.playback_metadata["streamInfo"],
                 album=container["name"],
                 image_url=image_url,
@@ -729,7 +731,9 @@ class SonosPlayer(Player):
         # generic info from container (also when MA is playing!)
         if container and container.get("name") and container.get("id"):
             if not current_media:
-                current_media = PlayerMedia(container["id"]["objectId"])
+                current_media = PlayerMedia(
+                    uri=container["id"]["objectId"], media_type=MediaType.UNKNOWN
+                )
             if not current_media.image_url:
                 images = container.get("images", [])
                 current_media.image_url = images[0].get("url") if images else None
index 8dfadfc33611bd0b9ef1506535d84a83b38a1883..e0edf2aceb47c43d759c4e04af4b290d05fe054e 100644 (file)
@@ -638,7 +638,11 @@ class SqueezelitePlayer(Player):
                         SlimPreset(
                             uri=media_item.uri,
                             text=media_item.name,
-                            icon=self.mass.metadata.get_image_url(media_item.image),
+                            icon=(
+                                self.mass.metadata.get_image_url(media_item.image)
+                                if media_item.image
+                                else ""
+                            ),
                         )
                     )
                 except MusicAssistantError: