Skip to content

Commit

Permalink
Multi-lang spotify command
Browse files Browse the repository at this point in the history
  • Loading branch information
trumully committed Sep 14, 2024
1 parent c89d855 commit df309e5
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 50 deletions.
Binary file removed assets/fonts/Roboto-Bold.ttf
Binary file not shown.
Binary file removed assets/fonts/Roboto-Regular.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSans-Bold.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSans-Regular.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSansJP-Bold.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSansJP-Regular.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSansKR-Bold.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSansKR-Regular.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSansTC-Bold.ttf
Binary file not shown.
Binary file added assets/fonts/static/NotoSansTC-Regular.ttf
Binary file not shown.
File renamed without changes
1 change: 0 additions & 1 deletion dynamo/extensions/cogs/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ async def cache(self, ctx: commands.Context) -> None:
async def shutdown(self, ctx: commands.Context) -> None:
"""Shutdown the bot"""
await ctx.send("Shutting down...")
log.debug("Shutting down...")
await self.bot.close()


Expand Down
1 change: 1 addition & 0 deletions dynamo/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def stop_when_done(fut: asyncio.Future[None]) -> None:
except KeyboardInterrupt:
log.info("Shutdown via keyboard interrupt")
finally:
log.info("Shutting down")
fut.remove_done_callback(stop_when_done)
if not bot.is_closed():
_close_task = loop.create_task(bot.close()) # noqa: RUF006
Expand Down
38 changes: 38 additions & 0 deletions dynamo/utils/format.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import re
from dataclasses import dataclass
from enum import StrEnum, auto
from typing import Sequence


Expand Down Expand Up @@ -114,3 +116,39 @@ def human_join(seq: Sequence[str], sep: str = ", ", conjunction: str = "or", *,
return f"{seq[0]} {conjunction} {seq[1]}"

return f"{sep.join(seq[:-1])}{sep if oxford_comma else " "}{conjunction} {seq[-1]}"


class CJK(StrEnum):
CHINESE = auto()
JAPANESE = auto()
KOREAN = auto()
NONE = auto()


def is_cjk(text: str) -> CJK:
"""
Check if a string contains any CJK characters.
Parameters
----------
text : str
The string to check.
Returns
-------
CJK
The CJK language of the string.
"""
# Chinese characters (including traditional and simplified)
if re.search(r"[\u4e00-\u9fff\u3400-\u4dbf]", text):
return CJK.CHINESE

# Hiragana and Katakana (Japanese-specific characters)
if re.search(r"[\u3040-\u309f\u30a0-\u30ff]", text):
return CJK.JAPANESE

# Hangul (Korean characters)
if re.search(r"[\uac00-\ud7af\u1100-\u11ff]", text):
return CJK.KOREAN

return CJK.NONE
175 changes: 126 additions & 49 deletions dynamo/utils/spotify.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime
import logging
import re
import textwrap
from dataclasses import dataclass
from io import BytesIO
from pathlib import Path
Expand All @@ -11,14 +10,46 @@
from PIL import Image, ImageDraw, ImageFont

from dynamo.utils.cache import async_lru_cache
from dynamo.utils.format import CJK, is_cjk
from dynamo.utils.helper import ROOT, resolve_path_with_links

log = logging.getLogger(__name__)

FONT_PATH = resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "Roboto-Regular.ttf"))
BOLD_FONT_PATH = resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "Roboto-Bold.ttf"))
SPOTIFY_LOGO_PATH = resolve_path_with_links(Path(ROOT / "assets" / "img" / "spotify.png"))

@dataclass(slots=True, frozen=True)
class FontFamily:
regular: Path
bold: Path


latin: FontFamily = FontFamily(
regular=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSans-Regular.ttf")),
bold=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSans-Bold.ttf")),
)

chinese: FontFamily = FontFamily(
regular=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSansTC-Regular.ttf")),
bold=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSansTC-Bold.ttf")),
)

japanese: FontFamily = FontFamily(
regular=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSansJP-Regular.ttf")),
bold=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSansJP-Bold.ttf")),
)

korean: FontFamily = FontFamily(
regular=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSansKR-Regular.ttf")),
bold=resolve_path_with_links(Path(ROOT / "assets" / "fonts" / "static" / "NotoSansKR-Bold.ttf")),
)

SPOTIFY_LOGO_PATH = resolve_path_with_links(Path(ROOT / "assets" / "images" / "spotify.png"))

FONTS: dict[CJK, FontFamily] = {
CJK.NONE: latin,
CJK.CHINESE: chinese,
CJK.JAPANESE: japanese,
CJK.KOREAN: korean,
}

# Dark blue
BACKGROUND_COLOR: tuple[int, int, int] = (5, 5, 25)
Expand All @@ -32,12 +63,42 @@

@dataclass(frozen=True)
class SpotifyCard:
album_size: ClassVar[tuple[int, int]] = (160, 160)
width: ClassVar[int] = 500
height: ClassVar[int] = 170
padding: ClassVar[int] = 5
max_size: ClassVar[int] = 20
percentage: ClassVar[float] = 0.75
# Card dimensions
width: ClassVar[int] = 490
height: ClassVar[int] = 160
padding: ClassVar[int] = 10
border: ClassVar[int] = 5

# Album cover
album_size: ClassVar[int] = height - (border * 2) # Fits exactly within the blue box

# Font settings
title_font_size: ClassVar[int] = 20
artist_font_size: ClassVar[int] = 16
progress_font_size: ClassVar[int] = 14

# Spotify logo
logo_size: ClassVar[int] = 32
logo_x: ClassVar[int] = width - logo_size - padding - border
logo_y: ClassVar[int] = padding + border

# Layout
content_start_x: ClassVar[int] = album_size + border * 2
content_width: ClassVar[int] = width - content_start_x - padding - border
title_start_y: ClassVar[int] = padding

# Progress bar
progress_bar_start_x: ClassVar[int] = content_start_x
progress_bar_width: ClassVar[int] = width - content_start_x - padding - border - 50 # Account for Spotify logo
progress_bar_height: ClassVar[int] = 4
progress_bar_y: ClassVar[int] = height - padding - border - progress_bar_height - 20
progress_text_y: ClassVar[int] = height - padding - border - 16

@staticmethod
def get_font(text: str, bold: bool = False, size: int = 22) -> ImageFont.FreeTypeFont:
font_family = FONTS[is_cjk(text)]
font_path = font_family.bold if bold else font_family.regular
return ImageFont.truetype(font_path, size)

@staticmethod
def track_duration(seconds: int) -> str:
Expand All @@ -61,62 +122,78 @@ def draw(
duration: datetime.timedelta | None = None,
end: datetime.datetime | None = None,
) -> BytesIO:
# Create base image with the green border
# Create base image with the colored border
base = Image.new("RGBA", (self.width, self.height), color)
base_draw = ImageDraw.Draw(base)

# Draw the background, leaving a 5px border
base_draw.rectangle(
(self.padding, self.padding, self.width - self.padding, self.height - self.padding), fill=BACKGROUND_COLOR
(self.border, self.border, self.width - self.border, self.height - self.border),
fill=BACKGROUND_COLOR,
)

# Resize and paste the album cover
album_size = self.height - 2 * self.padding
album_bytes = Image.open(album).resize((album_size, album_size))
base.paste(album_bytes, (self.padding, self.padding))

font_size = min(self.max_size, int(self.width * self.percentage))
font = ImageFont.truetype(FONT_PATH, int(font_size * 0.8))
bold = ImageFont.truetype(BOLD_FONT_PATH, font_size)

# Title
max_title_width = 437 - (album_size + 2 * self.padding)
title_lines = textwrap.wrap(name, width=int(max_title_width / (font_size * 0.6)))
title_height = 0
for i, line in enumerate(title_lines[:2]): # Limit to 2 lines
base_draw.text(
(album_size + 2 * self.padding, self.max_size + i * (font_size + 2)),
text=line,
fill=TEXT_COLOR,
font=bold,
)
title_height += font_size + 2
album_bytes = Image.open(album).resize((self.album_size, self.album_size))
album_position = (self.border, self.border)
base.paste(album_bytes, album_position)

# Title and artist
title_font = self.get_font(name, bold=True, size=self.title_font_size)
artist_font = self.get_font(", ".join(artists), bold=False, size=self.artist_font_size)

base_draw.text(
(self.content_start_x, self.title_start_y),
text=name,
fill=TEXT_COLOR,
font=title_font,
)

# Artists
max_artists_width = 437 - (album_size + 2 * self.padding)
artists_text = ", ".join(artists)
artists_lines = textwrap.wrap(artists_text, width=int(max_artists_width / (font_size * 0.5)))
for i, line in enumerate(artists_lines[:2]): # Limit to 2 lines
base_draw.text(
(album_size + 2 * self.padding, self.max_size + title_height + 5 + i * (int(font_size * 0.8) + 2)),
text=line,
fill=TEXT_COLOR,
font=font,
)
base_draw.text(
(self.content_start_x, self.title_start_y + self.title_font_size + 5),
text=", ".join(artists),
fill=TEXT_COLOR,
font=artist_font,
)

# Progress bar
# Progress bar and duration
if duration and end:
progress = self.get_progress(end, duration)
base_draw.rectangle((175, 135, 375, 140), fill=LENGTH_BAR_COLOR)
base_draw.rectangle((175, 135, 175 + int(200 * progress), 140), fill=PROGRESS_BAR_COLOR)

base_draw.rectangle(
(
self.progress_bar_start_x,
self.progress_bar_y,
self.progress_bar_start_x + self.progress_bar_width,
self.progress_bar_y + self.progress_bar_height,
),
fill=LENGTH_BAR_COLOR,
)

base_draw.rectangle(
(
self.progress_bar_start_x,
self.progress_bar_y,
self.progress_bar_start_x + int(self.progress_bar_width * progress),
self.progress_bar_y + self.progress_bar_height,
),
fill=PROGRESS_BAR_COLOR,
)

played = self.track_duration(int(duration.total_seconds() * progress))
track_duration = self.track_duration(int(duration.total_seconds()))
progress_text = f"{played} / {track_duration}"
base_draw.text((175, 145), text=progress_text, fill=TEXT_COLOR, font=font)
progress_font = self.get_font(progress_text, bold=False, size=self.progress_font_size)

base_draw.text(
(self.progress_bar_start_x, self.progress_text_y),
text=progress_text,
fill=TEXT_COLOR,
font=progress_font,
)

spotify_logo = Image.open(SPOTIFY_LOGO_PATH).resize((48, 48))
base.paste(spotify_logo, (437, 15), spotify_logo)
# Spotify logo
spotify_logo = Image.open(SPOTIFY_LOGO_PATH).resize((self.logo_size, self.logo_size))
base.paste(spotify_logo, (self.logo_x, self.logo_y), spotify_logo)

buffer = BytesIO()
base.save(buffer, format="PNG")
Expand Down

0 comments on commit df309e5

Please sign in to comment.