auto create collage image for playlists
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 22 Jul 2022 23:20:12 +0000 (01:20 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 22 Jul 2022 23:20:12 +0000 (01:20 +0200)
music_assistant/controllers/metadata/__init__.py
music_assistant/helpers/images.py

index ae736c500ee11a6b8778713e0e232c3000ac3fae..516477784b9a830410ad44d5979cb25cff78ea0a 100755 (executable)
@@ -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."""
index a92082029ed84f285da9dc3e0500c6576d1e045b..8034ba88ecbe145823dd4cb586eb2ac0272e93bd 100644 (file)
@@ -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)