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()
):
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,
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
# 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,
# 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,
# 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,
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
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
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 = ""
# 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:
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:
# 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")
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,
# 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,
"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)
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"),
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,
# 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
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: