From: Marcel van der Veldt Date: Sun, 26 Oct 2025 23:25:02 +0000 (+0100) Subject: Complete player metadata X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=596c35e95a1cd60d8ba462b21d36e8002df8eda2;p=music-assistant-server.git Complete player metadata --- diff --git a/music_assistant/helpers/images.py b/music_assistant/helpers/images.py index da323fa8..1900bb51 100644 --- a/music_assistant/helpers/images.py +++ b/music_assistant/helpers/images.py @@ -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() diff --git a/music_assistant/models/player.py b/music_assistant/models/player.py index fdda1190..c6aa1135 100644 --- a/music_assistant/models/player.py +++ b/music_assistant/models/player.py @@ -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 diff --git a/music_assistant/providers/airplay/raop.py b/music_assistant/providers/airplay/raop.py index dd72d012..50ae76f4 100644 --- a/music_assistant/providers/airplay/raop.py +++ b/music_assistant/providers/airplay/raop.py @@ -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") diff --git a/music_assistant/providers/chromecast/player.py b/music_assistant/providers/chromecast/player.py index 5d7e19e8..ec41633e 100644 --- a/music_assistant/providers/chromecast/player.py +++ b/music_assistant/providers/chromecast/player.py @@ -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, diff --git a/music_assistant/providers/roku_media_assistant/player.py b/music_assistant/providers/roku_media_assistant/player.py index 168179b4..fab76d71 100644 --- a/music_assistant/providers/roku_media_assistant/player.py +++ b/music_assistant/providers/roku_media_assistant/player.py @@ -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) diff --git a/music_assistant/providers/sonos/player.py b/music_assistant/providers/sonos/player.py index d3526971..1e56f9a5 100644 --- a/music_assistant/providers/sonos/player.py +++ b/music_assistant/providers/sonos/player.py @@ -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 diff --git a/music_assistant/providers/squeezelite/player.py b/music_assistant/providers/squeezelite/player.py index 8dfadfc3..e0edf2ac 100644 --- a/music_assistant/providers/squeezelite/player.py +++ b/music_assistant/providers/squeezelite/player.py @@ -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: