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,
# 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
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."""
"""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)
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)