From cabfbed25a62c27376c038f04389fd3051fea58d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 23 Jul 2022 01:20:12 +0200 Subject: [PATCH] auto create collage image for playlists --- .../controllers/metadata/__init__.py | 18 ++++- music_assistant/helpers/images.py | 71 ++++++++++++++----- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/music_assistant/controllers/metadata/__init__.py b/music_assistant/controllers/metadata/__init__.py index ae736c50..51647778 100755 --- a/music_assistant/controllers/metadata/__init__.py +++ b/music_assistant/controllers/metadata/__init__.py @@ -6,12 +6,13 @@ from time import time from typing import TYPE_CHECKING, Optional from music_assistant.helpers.database import TABLE_THUMBS -from music_assistant.helpers.images import create_thumbnail +from music_assistant.helpers.images import create_collage, create_thumbnail from music_assistant.models.enums import ImageType, MediaType from music_assistant.models.media_items import ( Album, Artist, ItemMapping, + MediaItemImage, MediaItemType, Playlist, Radio, @@ -104,9 +105,12 @@ class MetaDataController: # retrieve genres from tracks # TODO: retrieve style/mood ? playlist.metadata.genres = set() + images = set() for track in await self.mass.music.playlists.tracks( playlist.item_id, playlist.provider ): + if not playlist.image and track.image: + images.add(track.image) if track.media_type != MediaType.TRACK: # filter out radio items continue @@ -114,7 +118,17 @@ class MetaDataController: playlist.metadata.genres.update(track.metadata.genres) elif track.album and track.album.metadata.genres: playlist.metadata.genres.update(track.album.metadata.genres) - # TODO: create mosaic thumb/fanart from playlist tracks + # create collage thumb/fanart from playlist tracks + if images: + fake_path = f"playlist_collage.{playlist.provider.value}.{playlist.item_id}" + collage = await create_collage(self.mass, list(images)) + match = {"path": fake_path, "size": 0} + await self.mass.database.insert( + TABLE_THUMBS, {**match, "data": collage}, allow_replace=True + ) + playlist.metadata.images = [ + MediaItemImage(ImageType.THUMB, fake_path, True) + ] async def get_radio_metadata(self, radio: Radio) -> None: """Get/update rich metadata for a radio station.""" diff --git a/music_assistant/helpers/images.py b/music_assistant/helpers/images.py index a9208202..8034ba88 100644 --- a/music_assistant/helpers/images.py +++ b/music_assistant/helpers/images.py @@ -1,36 +1,48 @@ """Utilities for image manipulation and retrieval.""" from __future__ import annotations +import random from io import BytesIO -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional from PIL import Image +from music_assistant.helpers.database import TABLE_THUMBS from music_assistant.helpers.tags import get_embedded_image if TYPE_CHECKING: from music_assistant.mass import MusicAssistant -async def create_thumbnail( - mass: MusicAssistant, path: str, size: Optional[int] -) -> bytes: +async def get_image_data(mass: MusicAssistant, path: str) -> bytes: """Create thumbnail from image url.""" + # return from db if exists + match = {"path": path, "size": 0} + if result := await mass.database.get_row(TABLE_THUMBS, match): + return result["data"] # always try ffmpeg first to get the image because it supports # both online and offline image files as well as embedded images in media files img_data = await get_embedded_image(path) - if not img_data: - # assume file from file provider, we need to fetch it here... - for prov in mass.music.providers: - if not prov.type.is_file(): - continue - if not await prov.exists(path): - continue - path = await prov.resolve(path) - img_data = await get_embedded_image(path) - break - if not img_data: - raise FileNotFoundError(f"Image not found: {path}") + if img_data: + return img_data + # assume file from file provider, we need to fetch it here... + for prov in mass.music.providers: + if not prov.type.is_file(): + continue + if not await prov.exists(path): + continue + path = await prov.resolve(path) + img_data = await get_embedded_image(path) + if img_data: + return img_data + raise FileNotFoundError(f"Image not found: {path}") + + +async def create_thumbnail( + mass: MusicAssistant, path: str, size: Optional[int] +) -> bytes: + """Create thumbnail from image url.""" + img_data = await get_image_data(mass, path) def _create_image(): data = BytesIO(img_data) @@ -41,3 +53,30 @@ async def create_thumbnail( return data.getvalue() return await mass.loop.run_in_executor(None, _create_image) + + +async def create_collage(mass: MusicAssistant, images: List[str]) -> bytes: + """Create a basic collage image from multiple image urls.""" + + def _new_collage(): + return Image.new("RGBA", (1500, 1500), color=(255, 255, 255, 255)) + + collage = await mass.loop.run_in_executor(None, _new_collage) + + def _add_to_collage(img_data: bytes, coord_x: int, coord_y: int): + data = BytesIO(img_data) + photo = Image.open(data).convert("RGBA") + photo = photo.resize((500, 500)) + collage.paste(photo, (coord_x, coord_y)) + + for x_co in range(0, 1500, 500): + for y_co in range(0, 1500, 500): + img_data = await get_image_data(mass, random.choice(images)) + await mass.loop.run_in_executor(None, _add_to_collage, img_data, x_co, y_co) + + def _save_collage(): + final_data = BytesIO() + collage.convert("RGB").save(final_data, "PNG", optimize=True) + return final_data.getvalue() + + return await mass.loop.run_in_executor(None, _save_collage) -- 2.34.1