From 7e1b9c911a12061faa86a13a75d57baf83079849 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 19 Apr 2024 12:49:07 +0200 Subject: [PATCH] fix png thumbs with transparency --- music_assistant/server/helpers/images.py | 39 +++++++++++++------ .../server/models/metadata_provider.py | 3 +- .../server/models/music_provider.py | 2 +- .../server/providers/filesystem_local/base.py | 6 ++- .../server/providers/plex/__init__.py | 2 +- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/music_assistant/server/helpers/images.py b/music_assistant/server/helpers/images.py index 9713e7f1..9894bb0d 100644 --- a/music_assistant/server/helpers/images.py +++ b/music_assistant/server/helpers/images.py @@ -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)) diff --git a/music_assistant/server/models/metadata_provider.py b/music_assistant/server/models/metadata_provider.py index c4670fbc..f19432fe 100644 --- a/music_assistant/server/models/metadata_provider.py +++ b/music_assistant/server/models/metadata_provider.py @@ -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. diff --git a/music_assistant/server/models/music_provider.py b/music_assistant/server/models/music_provider.py index 14fb2d9a..3791aa9d 100644 --- a/music_assistant/server/models/music_provider.py +++ b/music_assistant/server/models/music_provider.py @@ -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. diff --git a/music_assistant/server/providers/filesystem_local/base.py b/music_assistant/server/providers/filesystem_local/base.py index d2671624..524eebf9 100644 --- a/music_assistant/server/providers/filesystem_local/base.py +++ b/music_assistant/server/providers/filesystem_local/base.py @@ -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.""" diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/server/providers/plex/__init__.py index 3a1b7697..e45abf66 100644 --- a/music_assistant/server/providers/plex/__init__.py +++ b/music_assistant/server/providers/plex/__init__.py @@ -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) -- 2.34.1