fix png thumbs with transparency
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 19 Apr 2024 10:49:07 +0000 (12:49 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 19 Apr 2024 10:49:07 +0000 (12:49 +0200)
music_assistant/server/helpers/images.py
music_assistant/server/models/metadata_provider.py
music_assistant/server/models/music_provider.py
music_assistant/server/providers/filesystem_local/base.py
music_assistant/server/providers/plex/__init__.py

index 9713e7f1c3e0fddb5cf98b91aa827900e1ba476f..9894bb0dfade86ead096db24d3dfc29e5821457e 100644 (file)
@@ -4,6 +4,7 @@ from __future__ import annotations
 
 import asyncio
 import itertools
+import os
 import random
 from collections.abc import Iterable
 from io import BytesIO
@@ -21,16 +22,26 @@ if TYPE_CHECKING:
     from music_assistant.server.models.music_provider import MusicProvider
 
 
-async def get_image_data(mass: MusicAssistant, path_or_url: str, provider: str = "url") -> bytes:
+async def get_image_data(mass: MusicAssistant, path_or_url: str, provider: str) -> bytes:
     """Create thumbnail from image url."""
+    # TODO: add local cache here !
     if prov := mass.get_provider(provider):
         prov: MusicProvider | MetadataProvider
-        if resolved_data := await prov.resolve_image(path_or_url):
-            if isinstance(resolved_data, bytes):
-                return resolved_data
-            return await get_embedded_image(resolved_data)
-    # always use ffmpeg to get the image because it supports
-    # both online and offline image files as well as embedded images in media files
+        if resolved_image := await prov.resolve_image(path_or_url):
+            if isinstance(resolved_image, bytes):
+                return resolved_image
+            if isinstance(resolved_image, str):
+                path_or_url = resolved_image
+    # handle HTTP location
+    if path_or_url.startswith("http"):
+        async with mass.http_session.get(path_or_url) as resp:
+            return await resp.read()
+    # handle FILE location (of type image)
+    if path_or_url.endswith(("jpg", "JPG", "png", "PNG", "jpeg")):
+        if await asyncio.to_thread(os.path.isfile, path_or_url):
+            async with aiofiles.open(path_or_url, "rb") as _file:
+                return await _file.read()
+    # use ffmpeg for embedded images
     if img_data := await get_embedded_image(path_or_url):
         return img_data
     msg = f"Image not found: {path_or_url}"
@@ -41,20 +52,24 @@ async def get_image_thumb(
     mass: MusicAssistant,
     path_or_url: str,
     size: int | None,
-    provider: str = "url",
+    provider: str,
     image_format: str = "PNG",
 ) -> bytes:
     """Get (optimized) PNG thumbnail from image url."""
     img_data = await get_image_data(mass, path_or_url, provider)
-    if not img_data:
+    if not img_data or not isinstance(img_data, bytes):
         raise FileNotFoundError(f"Image not found: {path_or_url}")
 
+    if not size and image_format.encode() in img_data:
+        return img_data
+
     def _create_image():
         data = BytesIO()
         img = Image.open(BytesIO(img_data))
         if size:
             img.thumbnail((size, size), Image.LANCZOS)  # pylint: disable=no-member
-        img.convert("RGB").save(data, image_format, optimize=True)
+        mode = "RGBA" if image_format == "PNG" else "RGB"
+        img.convert(mode).save(data, image_format, optimize=True)
         return data.getvalue()
 
     return await asyncio.to_thread(_create_image)
@@ -67,13 +82,13 @@ async def create_collage(
     image_size = 250
 
     def _new_collage():
-        return Image.new("RGBA", (dimensions[0], dimensions[1]), color=(255, 255, 255, 255))
+        return Image.new("RGB", (dimensions[0], dimensions[1]), color=(255, 255, 255, 255))
 
     collage = await asyncio.to_thread(_new_collage)
 
     def _add_to_collage(img_data: bytes, coord_x: int, coord_y: int) -> None:
         data = BytesIO(img_data)
-        photo = Image.open(data).convert("RGBA")
+        photo = Image.open(data).convert("RGB")
         photo = photo.resize((image_size, image_size))
         collage.paste(photo, (coord_x, coord_y))
 
index c4670fbce4928ca87868de4424c60752075a6073..f19432feb57f4146c6829f50657d23dd78545fdc 100644 (file)
@@ -2,7 +2,6 @@
 
 from __future__ import annotations
 
-from collections.abc import AsyncGenerator
 from typing import TYPE_CHECKING
 
 from music_assistant.common.models.enums import ProviderFeature
@@ -47,7 +46,7 @@ class MetadataProvider(Provider):
         if ProviderFeature.TRACK_METADATA in self.supported_features:
             raise NotImplementedError
 
-    async def resolve_image(self, path: str) -> str | bytes | AsyncGenerator[bytes, None]:
+    async def resolve_image(self, path: str) -> str | bytes:
         """
         Resolve an image from an image path.
 
index 14fb2d9a27daac321b9b09566947349b73d5634b..3791aa9dbc7f09312cc0fef9c5dec0ddf8b726ed 100644 (file)
@@ -264,7 +264,7 @@ class MusicProvider(Provider):
     async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None:
         """Handle callback when an item completed streaming."""
 
-    async def resolve_image(self, path: str) -> str | bytes | AsyncGenerator[bytes, None]:
+    async def resolve_image(self, path: str) -> str | bytes:
         """
         Resolve an image from an image path.
 
index d2671624978efc42c2431601657ef3d5f173b735..524eebf9bad5503ea82ae78b2ef8adec5f0c781d 100644 (file)
@@ -649,7 +649,7 @@ class FileSystemProviderBase(MusicProvider):
         async for chunk in self.read_file_content(streamdetails.item_id, seek_bytes):
             yield chunk
 
-    async def resolve_image(self, path: str) -> str | bytes | AsyncGenerator[bytes, None]:
+    async def resolve_image(self, path: str) -> str | bytes:
         """
         Resolve an image from an image path.
 
@@ -657,7 +657,9 @@ class FileSystemProviderBase(MusicProvider):
         a string with an http(s) URL or local path that is accessible from the server.
         """
         file_item = await self.resolve(path)
-        return file_item.local_path or self.read_file_content(file_item.absolute_path)
+        if file_item.local_path:
+            return file_item.local_path
+        return file_item.absolute_path
 
     async def _parse_track(self, file_item: FileSystemItem) -> Track:
         """Get full track details by id."""
index 3a1b7697781a77c2b984388593b0ea90405957d7..e45abf662b403fba6184643ac815679c9588587d 100644 (file)
@@ -300,7 +300,7 @@ class PlexProvider(MusicProvider):
         """
         return False
 
-    async def resolve_image(self, path: str) -> str | bytes | AsyncGenerator[bytes, None]:
+    async def resolve_image(self, path: str) -> str | bytes:
         """Return the full image URL including the auth token."""
         return self._plex_server.url(path, True)