__version__ = (1, 1, 0)
# █▄▀ ▄▀█ █▀▄▀█ █▀▀ █▄▀ █ █ █▀█ █▀█
# █ █ █▀█ █ ▀ █ ██▄ █ █ ▀▄▄▀ █▀▄ █▄█ ▄
# © Copyright 2025
# ✈ https://t.me/kamekuro
# 🔒 Licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# 🌐 https://creativecommons.org/licenses/by-nc-nd/4.0
# + attribution
# + non-commercial
# + no-derivatives
# You CANNOT edit, distribute or redistribute this file without direct permission from the author.
# meta banner: https://raw.githubusercontent.com/kamekuro/hikka-mods/main/banners/yamusic.png
# meta pic: https://raw.githubusercontent.com/kamekuro/hikka-mods/main/icons/yamusic.png
# meta developer: @kamekuro_hmods
# packurl: https://raw.githubusercontent.com/kamekuro/hikka-mods/main/langpacks/yamusic.yml
# scope: hikka_only
# scope: hikka_min 1.6.3
# requires: aiohttp asyncio requests pillow==11.2.1 git+https://github.com/MarshalX/yandex-music-api
import aiohttp
import asyncio
import io
import json
import logging
import random
import re
import requests
import string
import yandex_music
import telethon
import textwrap
from PIL import (
Image, ImageDraw, ImageEnhance,
ImageFilter, ImageFont
)
from .. import loader, utils
logger = logging.getLogger(__name__)
class YandexMusic():
token: str
client: yandex_music.ClientAsync
def __init__(self, token: str):
self.client = yandex_music.ClientAsync(token)
self.token = token
async def init(self):
self.client = await self.client.init()
return self
# Original code: https://raw.githubusercontent.com/MIPOHBOPOHIH/YMMBFA/main/main.py
async def _create_ynison_ws(self, ws_proto: dict) -> dict:
async with aiohttp.ClientSession() as session:
async with session.ws_connect(
"wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison",
headers={
"Sec-WebSocket-Protocol": f"Bearer, v2, {json.dumps(ws_proto)}",
"Origin": "http://music.yandex.ru",
"Authorization": f"OAuth {self.token}",
},
) as ws:
response = await ws.receive()
return json.loads(response.data)
# Original code: https://raw.githubusercontent.com/MIPOHBOPOHIH/YMMBFA/main/main.py
async def _get_ynison(self):
device_id = ''.join(random.choices(string.ascii_lowercase, k=16))
ws_proto = {
"Ynison-Device-Id": device_id,
"Ynison-Device-Info": json.dumps({"app_name": "Chrome", "type": 1}),
}
data = await self._create_ynison_ws(ws_proto)
ws_proto["Ynison-Redirect-Ticket"] = data["redirect_ticket"]
payload = {
"update_full_state": {
"player_state": {
"player_queue": {
"current_playable_index": -1,
"entity_id": "",
"entity_type": "VARIOUS",
"playable_list": [],
"options": {"repeat_mode": "NONE"},
"entity_context": "BASED_ON_ENTITY_BY_DEFAULT",
"version": {"device_id": device_id, "version": 9021243204784341000, "timestamp_ms": 0},
"from_optional": "",
},
"status": {
"duration_ms": 0,
"paused": True,
"playback_speed": 1,
"progress_ms": 0,
"version": {"device_id": device_id, "version": 8321822175199937000, "timestamp_ms": 0},
},
},
"device": {
"capabilities": {"can_be_player": True, "can_be_remote_controller": False, "volume_granularity": 16},
"info": {
"device_id": device_id,
"type": "WEB",
"title": "Chrome Browser",
"app_name": "Chrome",
},
"volume_info": {"volume": 0},
"is_shadow": True,
},
"is_currently_active": False,
},
"rid": "ac281c26-a047-4419-ad00-e4fbfda1cba3",
"player_action_timestamp_ms": 0,
"activity_interception_type": "DO_NOT_INTERCEPT_BY_DEFAULT",
}
async with aiohttp.ClientSession() as session:
async with session.ws_connect(
f"wss://{data['host']}/ynison_state.YnisonStateService/PutYnisonState",
headers={
"Sec-WebSocket-Protocol": f"Bearer, v2, {json.dumps(ws_proto)}",
"Origin": "http://music.yandex.ru",
"Authorization": f"OAuth {self.token}",
}
) as ws:
await ws.send_str(json.dumps(payload))
response = await ws.receive()
ynison: dict = json.loads(response.data)
return ynison
async def get_lyrics(self, track_id: int, with_timecodes: bool = False):
t = (await self.client.tracks(track_id))[0]
if with_timecodes:
if t.lyrics_info.has_available_sync_lyrics:
lyrics = await self.client.tracks_lyrics(track_id, "LRC")
return {
"text": requests.get(lyrics.download_url).text,
"writers": lyrics.writers
}
else:
if t.lyrics_info.has_available_text_lyrics:
lyrics = await self.client.tracks_lyrics(track_id, "TEXT")
return {
"text": requests.get(lyrics.download_url).text,
"writers": lyrics.writers
}
return None
async def get_now_playing(self):
ynison = await self._get_ynison()
if len(ynison.get("player_state", {}).get("player_queue", {}).get("playable_list", [])) == 0:
return {}
raw_track = ynison["player_state"]["player_queue"]["playable_list"][
ynison["player_state"]["player_queue"]["current_playable_index"]
]
return {
"paused": ynison["player_state"]["status"]["paused"],
"duration_ms": int(ynison["player_state"]["status"]["duration_ms"]),
"progress_ms": int(ynison["player_state"]["status"]["progress_ms"]),
"entity_id": ynison["player_state"]["player_queue"]["entity_id"],
"entity_type": ynison["player_state"]["player_queue"]["entity_type"],
"playable_id": raw_track["playable_id"],
"device": [
x for x in ynison['devices']
if x['info']['device_id'] == ynison.get('active_device_id_optional', "")
],
"track": (await self.client.tracks(raw_track["playable_id"]))[0]
} if raw_track['playable_type'] != "LOCAL_TRACK" else {}
@loader.tds
class YaMusicMod(loader.Module):
"""The module for Yandex.Music streaming service"""
strings = {"name": "YaMusic"}
strings_ru = {"_cls_doc": "Модуль для стримингового сервиса Яндекс.Музыка"}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"token",
None,
lambda: self.strings["_cfg"]["token"],
validator=loader.validators.Hidden()
),
loader.ConfigValue(
"now_playing_text",
"🎧 {performer} — {title}\n\n" \
"⌨️ Now is listening on " \
"{device} (🔊 " \
"{volume}%)\n🗂 Playing from: " \
"{playing_from}\n\n🎵 {link} | " \
"song.link",
lambda: self.strings["_cfg"]["now_playing_text"],
validator=loader.validators.String()
),
loader.ConfigValue(
"autobio",
"🎧 {artist} - {title}",
lambda: self.strings["_cfg"]["autobio"],
validator=loader.validators.String()
),
loader.ConfigValue(
"no_playing_bio",
"Hello!",
lambda: self.strings["_cfg"]["no_playing_bio"],
validator=loader.validators.String()
),
loader.ConfigValue(
"banner_version",
"new",
"Version of track banner (old/new)",
validator=loader.validators.Choice(["new", "old"])
)
)
async def on_dlmod(self):
if not self.get("guide_send", False):
await self.inline.bot.send_message(self._tg_id, self.strings("iguide"))
self.set("guide_send", True)
async def client_ready(self, client, db):
self._client = client
self._db = db
me = await self._client.get_me()
self._premium = me.premium if hasattr(me, "premium") else False
self.premium_check.start()
if self.get("autobio", False):
self.autobio.start()
@loader.loop(1800)
async def premium_check(self):
me = await self._client.get_me()
self._premium = me.premium if hasattr(me, "premium") else False
@loader.loop(30)
async def autobio(self):
if not self.config['token']:
self.autobio.stop(); self.set("autobio", False)
return
ym = await YandexMusic(self.config['token']).init()
now = await ym.get_now_playing()
if now and (not now['paused']):
out = self.config['autobio'].format(
title=now['track'].title,
artist=", ".join([x.name for x in now['track'].artists])
)[:(140 if self._premium else 70)]
try:
await self._client(
telethon.functions.account.UpdateProfileRequest(about=out)
)
except telethon.errors.rpcerrorlist.FloodWaitError as e:
logger.info(f"Sleeping {max(e.seconds, 60)} because of floodwait")
await asyncio.sleep(max(e.seconds, 60))
@loader.command(
ru_doc="👉 Гайд по получению токена Яндекс.Музыки",
alias="yg"
)
async def yguidecmd(self, message: telethon.types.Message):
"""👉 Guide for obtaining a Yandex.Music token"""
await utils.answer(message, self.strings("guide"))
@loader.command(
ru_doc="👉 Включить/выключить автобио",
alias="yb"
)
async def ybiocmd(self, message: telethon.types.Message):
"""👉 Enable/disable autobio"""
if (not self.config['token']) and self.get("autobio", False):
return await utils.answer(message, self.strings("no_token"))
bio = not self.get("autobio", False)
self.set("autobio", bio)
if bio: self.autobio.start()
else:
self.autobio.stop()
try:
await self._client(
telethon.functions.account.UpdateProfileRequest(
about=self.config['no_playing_bio'][:(140 if self._premium else 70)]
)
)
except: pass
await utils.answer(
message,
self.strings("autobio")['e' if bio else 'd']
)
@loader.command(
ru_doc="👉 Получить трек, который играет сейчас (с файлом трека)",
alias="ynt"
)
async def ynowtcmd(self, message: telethon.types.Message):
"""👉 Get now playing track (with track file)"""
if not self.config['token']:
return await utils.answer(message, self.strings("no_token"))
ym = await YandexMusic(self.config['token']).init()
now = await ym.get_now_playing()
if not now:
return await utils.answer(message, self.strings("there_is_no_playing"))
if now['entity_type'] not in self.strings("queue_types").keys():
now['entity_type'] = "VARIOUS"
playlist_name = ""
if now['entity_type'] == "PLAYLIST":
playlist = (await ym.client.playlists_list(now['entity_id']))[0]
playlist_name = f"{playlist.title}"
if now['entity_type'] == "ALBUM":
album = (await ym.client.albums(now['entity_id']))[0]
playlist_name = f"{album.title}"
if now['entity_type'] == "ARTIST":
artist = (await ym.client.artists(now['entity_id']))[0]
playlist_name = f"{artist.name}"
device, volume = "Unknown Device", "❓"
if now['device']:
device=now['device'][0]['info']['title']
volume=round(now['device'][0]['volume']*100, 2)
out = self.config['now_playing_text'].format(
title=now['track'].title,
performer=", ".join([x.name for x in now['track'].artists]),
device=device, volume=volume,
playing_from=self.strings("queue_types").get(now['entity_type']).format(playlist_name),
track_id=now['track'].id,
album_id=now['track'].albums[0].id,
link=f"Яндекс.Музыка"
)
await utils.answer(
message, out+self.strings("downloading")
)
audio = io.BytesIO((await utils.run_sync(requests.get, (await ym.client.tracks_download_info(now['track'].id, get_direct_links=True))[0].direct_link)).content)
audio.name = "audio.mp3"
await utils.answer(
message=message, response=out,
file=audio,
attributes=([
telethon.types.DocumentAttributeAudio(
duration=now['track'].duration_ms // 1000,
title=now['track'].title,
performer=", ".join([x.name for x in now['track'].artists])
)
])
)
@loader.command(
ru_doc="👉 Получить баннер трека, который играет сейчас",
alias="yn"
)
async def ynowcmd(self, message: telethon.types.Message):
"""👉 Get now playing track's banner"""
if not self.config['token']:
return await utils.answer(message, self.strings("no_token"))
ym = await YandexMusic(self.config['token']).init()
now = await ym.get_now_playing()
if not now:
return await utils.answer(message, self.strings("there_is_no_playing"))
if now['entity_type'] not in self.strings("queue_types").keys():
now['entity_type'] = "VARIOUS"
playlist_name = ""
if now['entity_type'] == "PLAYLIST":
playlist = (await ym.client.playlists_list(now['entity_id']))[0]
playlist_name = f"{playlist.title}"
if now['entity_type'] == "ALBUM":
album = (await ym.client.albums(now['entity_id']))[0]
playlist_name = f"{album.title}"
if now['entity_type'] == "ARTIST":
artist = (await ym.client.artists(now['entity_id']))[0]
playlist_name = f"{artist.name}"
device, volume = "Unknown Device", "❓"
if now['device']:
device=now['device'][0]['info']['title']
volume=round(now['device'][0]['volume']*100, 2)
out = self.config['now_playing_text'].format(
title=now['track'].title,
performer=", ".join([x.name for x in now['track'].artists]),
device=device, volume=volume,
playing_from=self.strings("queue_types").get(now['entity_type']).format(playlist_name),
track_id=now['track'].id,
album_id=now['track'].albums[0].id,
link=f"Яндекс.Музыка"
)
await utils.answer(
message, out+self.strings("uploading_banner")
)
lyrics = await ym.get_lyrics(now['track'].id, True)
func = self.__create_banner if self.config['banner_version'] == "new" else self.__create_banner_old
file = func(
now['track'].title, [x.name for x in now['track'].artists],
now['duration_ms'], now['progress_ms'],
requests.get(f"https://{now['track'].cover_uri[:-2]}1000x1000").content,
lyrics['text'] if lyrics else None
)
await utils.answer(
message=message, response=out, file=file
)
@loader.command(
ru_doc="👉 Лайкнуть играющий сейчас трек"
)
async def ylikecmd(self, message: telethon.types.Message):
"""👉 Like now playing track's banner"""
if not self.config['token']:
return await utils.answer(message, self.strings("no_token"))
ym = await YandexMusic(self.config['token']).init()
now = await ym.get_now_playing()
if not now:
return await utils.answer(message, self.strings("there_is_no_playing"))
await ym.client.users_likes_tracks_add(now['track'].id)
await utils.answer(
message, self.strings("likes")['liked'].format(
track_id=now['track'].id, album_id=now['track'].albums[0].id,
track=f"{', '.join([x.name for x in now['track'].artists])} — {now['track'].title}"
)
)
@loader.command(
ru_doc="👉 Убрать лайк с играющего сейчас трека"
)
async def yunlikecmd(self, message: telethon.types.Message):
"""👉 Unlike now playing track"""
if not self.config['token']:
return await utils.answer(message, self.strings("no_token"))
ym = await YandexMusic(self.config['token']).init()
now = await ym.get_now_playing()
if not now:
return await utils.answer(message, self.strings("there_is_no_playing"))
await ym.client.users_likes_tracks_remove(now['track'].id)
await utils.answer(
message, self.strings("likes")['unliked'].format(
track_id=now['track'].id, album_id=now['track'].albums[0].id,
track=f"{', '.join([x.name for x in now['track'].artists])} — {now['track'].title}"
)
)
@loader.command(
ru_doc="👉 Дизлайкнуть играющий сейчас трек",
alias="ydis"
)
async def ydislikecmd(self, message: telethon.types.Message):
"""👉 Dislike now playing track"""
if not self.config['token']:
return await utils.answer(message, self.strings("no_token"))
ym = await YandexMusic(self.config['token']).init()
now = await ym.get_now_playing()
if not now:
return await utils.answer(message, self.strings("there_is_no_playing"))
await ym.client.users_dislikes_tracks_add(now['track'].id)
await utils.answer(
message, self.strings("likes")['disliked'].format(
track_id=now['track'].id, album_id=now['track'].albums[0].id,
track=f"{', '.join([x.name for x in now['track'].artists])} — {now['track'].title}"
)
)
@loader.command(
ru_doc="👉 Получить текст играющего сейчас трека"
)
async def ylyricscmd(self, message: telethon.types.Message):
"""👉 Get lyrics of the now playing track"""
if not self.config['token']:
return await utils.answer(message, self.strings("no_token"))
ym = await YandexMusic(self.config['token']).init()
now = await ym.get_now_playing()
if not now:
return await utils.answer(message, self.strings("there_is_no_playing"))
lyrics = await ym.get_lyrics(now['playable_id'])
if lyrics:
await utils.answer(
message, self.strings("lyrics").format(
track_id=now['track'].id, album_id=now['track'].albums[0].id,
track=f"{', '.join([x.name for x in now['track'].artists])} — {now['track'].title}",
text=lyrics['text'],
writers=", ".join(lyrics['writers'])
)
)
else:
await utils.answer(
message, self.strings("no_lyrics").format(
track_id=now['track'].id, album_id=now['track'].albums[0].id,
track=f"{', '.join([x.name for x in now['track'].artists])} — {now['track'].title}"
)
)
@loader.command(
ru_doc="<запрос> 👉 Поиск трека в Яндекс.Музыке",
alias="yq"
)
async def ysearchcmd(self, message: telethon.types.Message):
""" 👉 Search track in Yandex.Music"""
if not self.config['token']:
return await utils.answer(message, self.strings("no_token"))
ym = await YandexMusic(self.config['token']).init()
query = utils.get_args_raw(message)
if not query:
await utils.answer(message, self.strings("args"))
return
message = await utils.answer(message, self.strings("searching"))
search = await ym.client.search(query, type_="track")
if (not search.tracks) or (len(search.tracks.results) == 0):
return await utils.answer(message, self.strings("404"))
out = self.strings("search").format(
title=search.tracks.results[0].title + (
f" ({search.tracks.results[0].version})" if search.tracks.results[0].version else ""
),
performer=", ".join([x.name for x in search.tracks.results[0].artists]),
album_id=search.tracks.results[0].albums[0].id, track_id=search.tracks.results[0].id
)
message = await utils.answer(message, out+self.strings("downloading"))
info = await ym.client.tracks_download_info(search.tracks.results[0].id, True)
link = info[0].direct_link
audio = None
audio = io.BytesIO((await utils.run_sync(requests.get, link)).content)
audio.name = "audio.mp3"
await utils.answer(
message=message, response=out,
file=audio,
attributes=([
telethon.types.DocumentAttributeAudio(
duration=int(search.tracks.results[0].duration_ms / 1000),
title=search.tracks.results[0].title,
performer=", ".join([x.name for x in search.tracks.results[0].artists])
)
])
)
def __create_banner(
self,
title: str, artists: list,
duration: int, progress: int,
track_cover: bytes, lyrics: str,
):
# ——————————————— CONSTS ———————————————
W, H = 1920, 768
title_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf"
).content), 55)
artist_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf"
).content), 46)
time_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf"
).content), 36)
lyrics_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/YSMusic-HeadlineBold.ttf"
).content), 75)
nlyrics_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/YSMusic-HeadlineBold.ttf"
).content), 60)
def measure(t: str, f: ImageFont.FreeTypeFont, d: ImageDraw.ImageDraw):
bb = d.textbbox((0, 0), t, font=f)
return bb[2] - bb[0], bb[3] - bb[1]
# ——————————————— BACKGROUND ———————————————
track_cov = Image.open(io.BytesIO(track_cover)).convert("RGBA")
banner = (
track_cov.resize((W, W))
.crop((0, (W-H) // 2, W, ((W-H) // 2) + H))
.filter(ImageFilter.GaussianBlur(radius=14))
)
banner = ImageEnhance.Brightness(banner).enhance(0.3)
draw = ImageDraw.Draw(banner)
# ——————————————— TRACK COVER ———————————————
track_cov = track_cov.resize((H-350, H-350))
mask = Image.new("L", track_cov.size, 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, track_cov.size[0], track_cov.size[1]), radius=35, fill=255
)
track_cov.putalpha(mask)
track_cov = track_cov.crop(track_cov.getbbox())
banner.paste(track_cov, (175, 175), mask)
# ——————————————— ARTIST & TITLE ———————————————
text_width, _ = measure(f"{', '.join(artists)} — {title}", title_font, draw)
if text_width > 1680:
lines = [f"{title}", f"{', '.join(artists)}"]
lsizes = [measure(lines[0], title_font, draw), measure(lines[1], artist_font, draw)]
else:
lines = [f"{', '.join(artists)} — {title}"]
lsizes = [measure(lines[0], title_font, draw)]
text_h = sum(th for _, th in lsizes) + (len(lines) - 1)
text_y = (150 - text_h) / 2
for i, (l, (lw, lh)) in enumerate(zip(lines, lsizes)):
if len(lines) == 2 and i == 1:
ftu = artist_font
else:
ftu = title_font
if lw > 1680:
while lw > 1680 and len(l) > 3:
l = l[:-4] + "…"
lw, _ = measure(l, ftu, draw)
tx = (W - lw) / 2
draw.text((tx, text_y), l, font=ftu, fill="#A0A0A0")
text_y += lh + 5
# ——————————————— LYRICS ———————————————
if lyrics:
lyrics_lines = []
for match in re.finditer(r"\[(\d{2}):(\d{2}\.\d{2})\] (.+)", lyrics):
minutes = int(match.group(1))
seconds = float(match.group(2))
text = match.group(3)
time_ms = int((minutes * 60 + seconds) * 1000)
lyrics_lines.append((time_ms, text))
llast, lnext = "", ""
for i, (time_ms, text) in enumerate(lyrics_lines):
if time_ms <= progress:
llast = text
if i+1 < len(lyrics_lines):
lnext = lyrics_lines[i+1][1]
else:
break
y_start = None
if llast:
lines = textwrap.wrap(llast, width=23)
if len(lines) > 3:
lines = lines[:3]
lines[-1] += "…"
lines_sizes = [draw.textbbox((0, 0), l, font=lyrics_font) for l in lines]
line_heights = [bb[3] - bb[1] for bb in lines_sizes]
total_text_height = sum(line_heights) + (len(lines) - 1) * 10
y_start = (150 + (track_cov.size[0]-total_text_height)) / 2
for i, line in enumerate(lines):
lw = lines_sizes[i][2] - lines_sizes[i][0]
tx = (track_cov.size[0]+325 + ((W-track_cov.size[0]+285) - lw)) / 2
draw.text((tx, y_start), line, font=lyrics_font, fill="#FFFFFF")
y_start += line_heights[i] + 10
if lnext:
next_lines = textwrap.wrap(lnext, width=23)
if len(next_lines) > 2:
next_lines = next_lines[:2]
next_lines[-1] += "…"
next_sizes = [draw.textbbox((0, 0), l, font=nlyrics_font) for l in next_lines]
next_heights = [bb[3] - bb[1] for bb in next_sizes]
total_text_height = sum(next_heights) + (len(next_lines) - 1) * 10
if not y_start:
y_start = (150 + (track_cov.size[0] - total_text_height))/2 + 150
for j, line in enumerate(next_lines):
lw = next_sizes[j][2] - next_sizes[j][0]
tx = (track_cov.size[0] + 325 + ((W - track_cov.size[0] + 285) - lw)) / 2
draw.text((tx, y_start + 40), line, font=nlyrics_font, fill="#A0A0A0")
y_start += next_heights[j] + 10
# ——————————————— STATUS BAR ———————————————
draw.rounded_rectangle([75, 700, 768 + 1072, 700 + 15], radius=15 // 2, fill="#A0A0A0")
draw.rounded_rectangle([75, 700, 768 + int(1072 * (progress / duration)), 700 + 15], radius=15 // 2, fill="#FFFFFF")
draw.text((75, 650), f"{(progress//1000//60):02}:{(progress//1000%60):02}", font=time_font, fill="#FFFFFF")
draw.text((1745, 650), f"{(duration//1000//60):02}:{(duration//1000%60):02}", font=time_font, fill="#FFFFFF")
# ——————————————— SAVE ———————————————
by = io.BytesIO()
banner.save(by, format="PNG"); by.seek(0)
by.name = "banner.png"
return by
def __create_banner_old(
self,
title: str, artists: list,
duration: int, progress: int,
track_cover: bytes,
*args, **kwargs
):
w, h = 1920, 768
title_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf"
).content), 80)
art_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Regular.ttf"
).content), 55)
time_font = ImageFont.truetype(io.BytesIO(requests.get(
"https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf"
).content), 36)
# Gen banner (bg)
track_cov = Image.open(io.BytesIO(track_cover)).convert("RGBA")
banner = track_cov.resize((w, w)).crop(
(0, (w-h)//2, w, ((w-h)//2)+h)
).filter(ImageFilter.GaussianBlur(radius=14))
banner = ImageEnhance.Brightness(banner).enhance(0.3)
# Gen track cover and put to bg
track_cov = track_cov.resize((banner.size[1]-150, banner.size[1]-150))
mask = Image.new("L", track_cov.size, 0)
ImageDraw.Draw(mask).rounded_rectangle((0, 0, track_cov.size[0], track_cov.size[1]), radius=35, fill=255)
track_cov.putalpha(mask)
track_cov = track_cov.crop(track_cov.getbbox())
banner.paste(track_cov, (75, 75), mask)
# Editing text
title_lines = textwrap.wrap(title, 23)
if len(title_lines) > 1:
title_lines[1] = title_lines[1] + "..." if len(title_lines) > 2 else title_lines[1]
title_lines = title_lines[:2]
artists_lines = textwrap.wrap(" • ".join(artists), width=40)
if len(artists_lines) > 1:
for index, art in enumerate(artists_lines):
if "•" in art[-2:]:
artists_lines[index] = art[:art.rfind("•") - 1]
# Put title and artists to banner
draw = ImageDraw.Draw(banner)
x, y = 150+track_cov.size[0], 110
for index, line in enumerate(title_lines):
draw.text((x, y), line, font=title_font, fill="#FFFFFF")
if index != len(title_lines)-1:
y += 70
x, y = 150+track_cov.size[0], 110*2
if len(title_lines) > 1: y += 70
for index, line in enumerate(artists_lines):
draw.text((x, y), line, font=art_font, fill="#A0A0A0")
if index != len(artists_lines)-1:
y += 50
# Drawing status bar
draw.rounded_rectangle([768, 650, 768+1072, 650+15], radius=15//2, fill="#A0A0A0")
draw.rounded_rectangle([768, 650, 768+int(1072*(progress/duration)), 650+15], radius=15//2, fill="#FFFFFF")
draw.text((768, 600), f"{(progress//1000//60):02}:{(progress//1000%60):02}", font=time_font, fill="#FFFFFF")
draw.text((1745, 600), f"{(duration//1000//60):02}:{(duration//1000%60):02}", font=time_font, fill="#FFFFFF")
by = io.BytesIO()
banner.save(by, format="PNG"); by.seek(0)
by.name = "banner.png"
return by