Fix image of queue item if source is file based (#541)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 17 Mar 2023 09:33:59 +0000 (10:33 +0100)
committerGitHub <noreply@github.com>
Fri, 17 Mar 2023 09:33:59 +0000 (10:33 +0100)
Resolve queue item image url (use image proxy) so players can also
display embedded covers

music_assistant/common/models/queue_item.py
music_assistant/server/controllers/metadata.py
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/streams.py
music_assistant/server/helpers/didl_lite.py
music_assistant/server/providers/chromecast/__init__.py
music_assistant/server/providers/dlna/__init__.py
music_assistant/server/providers/json_rpc/models.py
music_assistant/server/providers/slimproto/__init__.py
music_assistant/server/providers/sonos/__init__.py

index 6b96d9f9687bddbecc81e94522ee5667083ddf16..ce89d4e318ee1c614f6ff3d25d708a440785289b 100644 (file)
@@ -2,13 +2,16 @@
 from __future__ import annotations
 
 from dataclasses import dataclass
-from typing import Any
+from typing import TYPE_CHECKING, Any
 from uuid import uuid4
 
 from mashumaro import DataClassDictMixin
 
 from .enums import MediaType
-from .media_items import ItemMapping, MediaItemImage, Radio, StreamDetails, Track
+from .media_items import ItemMapping, Radio, StreamDetails, Track
+
+if TYPE_CHECKING:
+    from music_assistant.server import MusicAssistant
 
 
 @dataclass
@@ -23,7 +26,7 @@ class QueueItem(DataClassDictMixin):
     sort_index: int = 0
     streamdetails: StreamDetails | None = None
     media_item: Track | Radio | None = None
-    image: MediaItemImage | None = None
+    image_url: str | None = None
 
     def __post_init__(self):
         """Set default values."""
@@ -71,5 +74,14 @@ class QueueItem(DataClassDictMixin):
             name=name,
             duration=media_item.duration,
             media_item=media_item,
-            image=media_item.image,
+        )
+
+    async def resolve_image_url(self, mass: MusicAssistant) -> None:
+        """Resolve Image URL for the MediaItem."""
+        if self.image_url:
+            return
+        if not self.media_item:
+            return
+        self.image_url = await mass.metadata.get_image_url_for_item(
+            self.media_item, resolve_local=True
         )
index 61f90e5708ae92af3da568fe299450adc982ffeb..570a48a886d436a976d0f72080f080a0e7d87139 100755 (executable)
@@ -249,8 +249,6 @@ class MetaDataController:
         img_path = await self.get_image_url_for_item(
             media_item=media_item,
             img_type=img_type,
-            allow_local=True,
-            local_as_base64=False,
         )
         if not img_path:
             return None
@@ -260,8 +258,8 @@ class MetaDataController:
         self,
         media_item: MediaItemType,
         img_type: ImageType = ImageType.THUMB,
+        resolve_local: bool = True,
         allow_local: bool = True,
-        local_as_base64: bool = False,
     ) -> str | None:
         """Get url to image for given media media_item."""
         if not media_item:
@@ -274,29 +272,25 @@ class MetaDataController:
                     continue
                 if img.is_file and not allow_local:
                     continue
-                if img.is_file and local_as_base64:
-                    # return base64 string of the image (compatible with browsers)
-                    return await self.get_thumbnail(img.url, base64=True)
+                if img.is_file and resolve_local:
+                    # return imageproxy url for local filesystem items
+                    # the original path is double encoded
+                    encoded_url = urllib.parse.quote(urllib.parse.quote(img.url))
+                    return f"{self.mass.base_url}/imageproxy?path={encoded_url}"
                 return img.url
 
         # retry with track's album
         if media_item.media_type == MediaType.TRACK and media_item.album:
-            return await self.get_image_url_for_item(
-                media_item.album, img_type, allow_local, local_as_base64
-            )
+            return await self.get_image_url_for_item(media_item.album, img_type, resolve_local)
 
         # try artist instead for albums
         if media_item.media_type == MediaType.ALBUM and media_item.artist:
-            return await self.get_image_url_for_item(
-                media_item.artist, img_type, allow_local, local_as_base64
-            )
+            return await self.get_image_url_for_item(media_item.artist, img_type, resolve_local)
 
         # last resort: track artist(s)
         if media_item.media_type == MediaType.TRACK and media_item.artists:
             for artist in media_item.artists:
-                return await self.get_image_url_for_item(
-                    artist, img_type, allow_local, local_as_base64
-                )
+                return await self.get_image_url_for_item(artist, img_type, resolve_local)
 
         return None
 
index 8b7e26300e8df4e1b14a58fd5a4d068c03137e92..b00c23ce9d7c156c05675e9adc0590f9356d0bec 100755 (executable)
@@ -15,19 +15,11 @@ from music_assistant.common.models.enums import (
     QueueOption,
     RepeatMode,
 )
-from music_assistant.common.models.errors import (
-    MediaNotFoundError,
-    MusicAssistantError,
-    QueueEmpty,
-)
+from music_assistant.common.models.errors import MediaNotFoundError, MusicAssistantError, QueueEmpty
 from music_assistant.common.models.media_items import MediaItemType, media_from_dict
 from music_assistant.common.models.player_queue import PlayerQueue
 from music_assistant.common.models.queue_item import QueueItem
-from music_assistant.constants import (
-    CONF_FLOW_MODE,
-    FALLBACK_DURATION,
-    ROOT_LOGGER_NAME,
-)
+from music_assistant.constants import CONF_FLOW_MODE, FALLBACK_DURATION, ROOT_LOGGER_NAME
 from music_assistant.server.helpers.api import api_command
 
 if TYPE_CHECKING:
@@ -469,6 +461,8 @@ class PlayerQueuesController:
         player_prov = self.mass.players.get_player_provider(queue_id)
         flow_mode = self.mass.config.get_player_config_value(queue.queue_id, CONF_FLOW_MODE)
         queue.flow_mode = flow_mode.value
+        # make sure that the queue item image is resolved
+        await queue_item.resolve_image_url(self.mass)
         await player_prov.cmd_play_media(
             queue_id,
             queue_item=queue_item,
@@ -587,7 +581,7 @@ class PlayerQueuesController:
         self._queues.pop(player_id, None)
         self._queue_items.pop(player_id, None)
 
-    def player_ready_for_next_track(
+    async def player_ready_for_next_track(
         self, queue_or_player_id: str, current_item_id: str | None = None
     ) -> tuple[QueueItem, bool]:
         """Call when a player is ready to load the next track into the buffer.
@@ -609,6 +603,8 @@ class PlayerQueuesController:
         next_item = self.get_item(queue.queue_id, next_index)
         if not next_item:
             raise QueueEmpty("No more tracks left in the queue.")
+        # make sure that the queue item image is resolved
+        await next_item.resolve_image_url(self.mass)
         queue.index_in_buffer = next_index
         # work out crossfade
         crossfade = queue.crossfade_enabled
index 8f0c559fa5a009bd344fed257e4272440441b4b9..716ae3f92cc534bd3bc439862ab718099697d802 100644 (file)
@@ -477,7 +477,7 @@ class StreamsController:
                     (
                         queue_track,
                         use_crossfade,
-                    ) = self.mass.players.queues.player_ready_for_next_track(
+                    ) = await self.mass.players.queues.player_ready_for_next_track(
                         queue_id, queue_track.queue_item_id
                     )
                 except QueueEmpty:
index ece34b6927261bceea97b376cf0152d1011eeb84..7c1f5149a1a2375eb9bfefdc939e0cc51a1d44dd 100644 (file)
@@ -31,14 +31,15 @@ def create_didl_metadata(url: str, queue_item: QueueItem, flow_mode: bool = Fals
             "</item>"
             "</DIDL-Lite>"
         )
+
     if is_radio:
         # radio or other non-track item
-        image = queue_item.image.url if queue_item.image else ""
+        image = queue_item.image_url if queue_item.image else ""
         return (
             '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">'
             f'<item id="{queue_item.queue_item_id}" parentID="0" restricted="1">'
             f"<dc:title>{_escape_str(queue_item.name)}</dc:title>"
-            f"<upnp:albumArtURI>{queue_item.image.url}</upnp:albumArtURI>"
+            f"<upnp:albumArtURI>{queue_item.image_url}</upnp:albumArtURI>"
             f"<dc:queueItemId>{image}</dc:queueItemId>"
             "<upnp:class>object.item.audioItem.audioBroadcast</upnp:class>"
             f"<upnp:mimeType>audio/{ext}</upnp:mimeType>"
@@ -55,7 +56,6 @@ def create_didl_metadata(url: str, queue_item: QueueItem, flow_mode: bool = Fals
         album = _escape_str(queue_item.media_item.album.name)
     else:
         album = ""
-    image = queue_item.image.url if queue_item.image else ""
     item_class = "object.item.audioItem.musicTrack"
     duration_str = str(datetime.timedelta(seconds=queue_item.duration))
     return (
@@ -68,7 +68,7 @@ def create_didl_metadata(url: str, queue_item: QueueItem, flow_mode: bool = Fals
         f"<upnp:duration>{queue_item.duration}</upnp:duration>"
         "<upnp:playlistTitle>Music Assistant</upnp:playlistTitle>"
         f"<dc:queueItemId>{queue_item.queue_item_id}</dc:queueItemId>"
-        f"<upnp:albumArtURI>{image}</upnp:albumArtURI>"
+        f"<upnp:albumArtURI>{queue_item.image_url}</upnp:albumArtURI>"
         f"<upnp:class>{item_class}</upnp:class>"
         f"<upnp:mimeType>audio/{ext}</upnp:mimeType>"
         f'<res duration="{duration_str}" protocolInfo="http-get:*:audio/{ext}:DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000">{url}</res>'
index d9523937107ac166b87ccaf1fe4d4d7cd2c360ed..6bc85ffcde5e0a5890a1aa20ab45ca01947092c6 100644 (file)
@@ -408,7 +408,7 @@ class ChromecastProvider(PlayerProvider):
         if not current_queue_item_id:
             return  # guard
         try:
-            next_item, crossfade = self.mass.players.queues.player_ready_for_next_track(
+            next_item, crossfade = await self.mass.players.queues.player_ready_for_next_track(
                 castplayer.player_id, current_queue_item_id
             )
         except QueueEmpty:
@@ -483,14 +483,14 @@ class ChromecastProvider(PlayerProvider):
                 "songName": queue_item.media_item.name,
                 "artist": queue_item.media_item.artist.name if queue_item.media_item.artist else "",
                 "title": queue_item.name,
-                "images": [{"url": queue_item.image.url}] if queue_item.image else None,
+                "images": [{"url": queue_item.image_url}] if queue_item.image_url else None,
             }
         else:
             stream_type = STREAM_TYPE_LIVE
             metadata = {
                 "metadataType": 0,
                 "title": queue_item.name,
-                "images": [{"url": queue_item.image.url}] if queue_item.image else None,
+                "images": [{"url": queue_item.image_url}] if queue_item.image_url else None,
             }
         return {
             "autoplay": True,
index 789464f4beb482f39d9ac988bbe793a2a84fe97b..60500a310f6e08863c56a26982ea0529e86ab2aa 100644 (file)
@@ -504,7 +504,7 @@ class DLNAPlayerProvider(PlayerProvider):
         if not self.mass.players.queues.get_item(dlna_player.udn, current_queue_item_id):
             return  # guard
         try:
-            next_item, crossfade = self.mass.players.queues.player_ready_for_next_track(
+            next_item, crossfade = await self.mass.players.queues.player_ready_for_next_track(
                 dlna_player.udn, current_queue_item_id
             )
         except QueueEmpty:
index 19fb8c8db8c5fb5a836f047fc67623badc43f79e..8bfe882e2de4da58a25c9a15afd73e72c7d164cb 100644 (file)
@@ -147,7 +147,7 @@ def playlist_item_from_mass(queue_item: QueueItem, index: int = 0) -> PlaylistIt
         "genre": "",
         "remote": 0,
         "remote_title": queue_item.streamdetails.stream_title if queue_item.streamdetails else "",
-        "artwork_url": queue_item.image.url if queue_item.image else "",
+        "artwork_url": queue_item.image_url or "",
         "bitrate": "",
         "duration": queue_item.duration or 0,
         "coverid": "-94099753136392",
index 67585d3ac9e165c483641584fc793c06b1b404a1..09ae5698ebeb9dc1f10270dc3dedfc2b8d7f97bc 100644 (file)
@@ -474,7 +474,7 @@ class SlimprotoProvider(PlayerProvider):
         if not client.current_metadata:
             return
         try:
-            next_item, crossfade = self.mass.players.queues.player_ready_for_next_track(
+            next_item, crossfade = await self.mass.players.queues.player_ready_for_next_track(
                 client.player_id, client.current_metadata["item_id"]
             )
             await self._handle_play_media(client, next_item, send_flush=False, crossfade=crossfade)
index 0950113a3fe0e7739933c8341bdf370d1152b948..73dcf60e089f51d99a66229fe52a66d3184e67b5 100644 (file)
@@ -504,7 +504,7 @@ class SonosPlayerProvider(PlayerProvider):
         if not self.mass.players.queues.get_item(sonos_player.player_id, current_queue_item_id):
             return  # guard
         try:
-            next_item, crossfade = self.mass.players.queues.player_ready_for_next_track(
+            next_item, crossfade = await self.mass.players.queues.player_ready_for_next_track(
                 sonos_player.player_id, current_queue_item_id
             )
         except QueueEmpty: