mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-18 15:14:18 +02:00
759 lines
37 KiB
Python
759 lines
37 KiB
Python
__version__ = (1, 3, 3, 7)
|
||
|
||
# This file is a part of Hikka Userbot
|
||
# Code is NOT licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
|
||
# 🌐 https://github.com/hikariatama/Hikka
|
||
|
||
# You CAN edit this file without direct permission from the author.
|
||
# You can redistribute this file with any modifications.
|
||
|
||
# meta developer: @yg_modules
|
||
# scope: hikka_only
|
||
# scope: hikka_min 1.6.3
|
||
|
||
# requires: pillow
|
||
|
||
# █▄█ █░█ █▀▄▀█ █▀▄▀█ █▄█ █▀▄▀█ █▀█ █▀▄ █▀
|
||
# ░█░ █▄█ █░▀░█ █░▀░█ ░█░ █░▀░█ █▄█ █▄▀ ▄█
|
||
|
||
import io
|
||
import ast
|
||
import json
|
||
import struct
|
||
import random
|
||
import datetime
|
||
import aiohttp
|
||
import asyncio
|
||
from PIL import Image
|
||
from PIL import ImageDraw
|
||
from PIL import ImageFont
|
||
from PIL import ImageFilter
|
||
from telethon.tl import tlobject
|
||
from telethon.extensions import html
|
||
from telethon.tl.types import DocumentAttributeFilename
|
||
|
||
from .. import loader, utils
|
||
|
||
try:
|
||
LANCZOS = Image.Resampling.LANCZOS
|
||
except AttributeError:
|
||
LANCZOS = Image.LANCZOS
|
||
|
||
class Dicks(tlobject.TLObject): # users.savedMusic#34a2f297
|
||
def __init__(self, count, documents):
|
||
self.count = count
|
||
self.documents = documents
|
||
|
||
def to_dict(self):
|
||
return {'_': 'Dicks', 'count': self.count,
|
||
'documents': [doc.to_dict() for doc in self.documents or []]}
|
||
|
||
@classmethod
|
||
def from_reader(cls, reader):
|
||
count = reader.read_int()
|
||
documents = reader.tgread_vector()
|
||
return cls(count=count, documents=documents)
|
||
|
||
class GetDicks(tlobject.TLRequest): # users.getSavedMusic#788d7fe3
|
||
def __init__(self, id, offset, limit, hash):
|
||
self.id = id
|
||
self.offset = offset
|
||
self.limit = limit
|
||
self.hash = hash
|
||
|
||
def _bytes(self):
|
||
return b''.join((b'\xe3\x7f\x8dx',
|
||
self.id._bytes(),
|
||
struct.pack('<i', self.offset),
|
||
struct.pack('<i', self.limit),
|
||
struct.pack('<q', self.hash)))
|
||
|
||
def read_result(self, reader):
|
||
reader.read_int()
|
||
return Dicks.from_reader(reader)
|
||
|
||
@loader.tds
|
||
class yg_playlist(loader.Module):
|
||
"""Module for creating, visualizing, and managing Telegram music playlists with customizable themes, fonts, and detailed metadata extraction!"""
|
||
|
||
strings = {"name": "yg_playlist",
|
||
"collecting": "<emoji document_id=5388747006451655179>🍳</emoji> <b>Cooking...</b>",
|
||
"drawing": "<emoji document_id=5956143844457189176>✏️</emoji> <b>Drawing...</b>",
|
||
"no_music": "<emoji document_id=5240241223632954241>🚫</emoji> <b>This user has no saved music or profile is hidden!</b>",
|
||
"api_error": "<emoji document_id=5379586425324865474>🤬</emoji> <b>API error while getting music:</b>\n<code>{}</code>",
|
||
"user_not_found": "<emoji document_id=5240241223632954241>🚫</emoji> <b>User not found!</b>",
|
||
"config_theme": "Playlist visual theme",
|
||
"config_font_regular": "URL for regular font",
|
||
"config_font_bold": "URL for bold font",
|
||
"config_custom_theme": "Custom theme in JSON format. Requires keys: background, primary_text, secondary_text, placeholder, accent. Optional: gradient object. Build it at mods.kok.gay/theme-builder!",
|
||
"config_main_title_size": "Font size for main track title",
|
||
"config_main_artist_size": "Font size for main track artist",
|
||
"config_list_title_size": "Font size for list track titles",
|
||
"config_list_artist_size": "Font size for list track artists"}
|
||
|
||
def __init__(self):
|
||
self.config = loader.ModuleConfig(
|
||
loader.ConfigValue("theme", "ocean_depth",
|
||
lambda: self.strings("config_theme"),
|
||
validator=loader.validators.Choice(
|
||
["dark",
|
||
"light",
|
||
"green",
|
||
"green_spots",
|
||
"cosmic_purple",
|
||
"ocean_depth",
|
||
"sunset_fire",
|
||
"starlight_night",
|
||
"cyberpunk_neon",
|
||
"molten_gold",
|
||
"vaporwave_dream",
|
||
"custom"])),
|
||
loader.ConfigValue("custom_theme",
|
||
'{"background": "#121212", "primary_text": "#FFFFFF", "secondary_text": "#b3b3b3", "placeholder": "#333333", "accent": "#8c65d3"}',
|
||
lambda: self.strings("config_custom_theme"),
|
||
validator=loader.validators.String()),
|
||
loader.ConfigValue("font_regular_url",
|
||
"https://mods.kok.gay/Roboto-Reqular.ttf",
|
||
lambda: self.strings("config_font_regular"),
|
||
validator=loader.validators.Link()),
|
||
loader.ConfigValue("font_bold_url",
|
||
"https://mods.kok.gay/Roboto-Bolt.ttf",
|
||
lambda: self.strings("config_font_bold"),
|
||
validator=loader.validators.Link()),
|
||
loader.ConfigValue("main_title_size", 26,
|
||
lambda: self.strings("config_main_title_size"),
|
||
validator=loader.validators.Integer(minimum=10, maximum=72)),
|
||
loader.ConfigValue("main_artist_size", 20,
|
||
lambda: self.strings("config_main_artist_size"),
|
||
validator=loader.validators.Integer(minimum=8, maximum=60)),
|
||
loader.ConfigValue("list_title_size", 18,
|
||
lambda: self.strings("config_list_title_size"),
|
||
validator=loader.validators.Integer(minimum=8, maximum=48)),
|
||
loader.ConfigValue("list_artist_size", 16,
|
||
lambda: self.strings("config_list_artist_size"),
|
||
validator=loader.validators.Integer(minimum=6, maximum=36)))
|
||
|
||
self.dick_themes = {
|
||
"dark": {
|
||
"background": "#121212", "primary_text": "#FFFFFF", "secondary_text": "#b3b3b3",
|
||
"placeholder": "#333333", "accent": "#8c65d3"
|
||
},
|
||
"light": {
|
||
"background": "#FFFFFF", "primary_text": "#000000", "secondary_text": "#535353",
|
||
"placeholder": "#e0e0e0", "accent": "#8c65d3"
|
||
},
|
||
"green": {
|
||
"background": "#0a1f0a", "primary_text": "#FFFFFF", "secondary_text": "#b3b3b3",
|
||
"placeholder": "#1a3a1a", "accent": "#4caf50",
|
||
"gradient": {"type": "vignette", "color": "#1e8e1e", "intensity": 70}
|
||
},
|
||
"green_spots": {
|
||
"background": "#0a1f0a", "primary_text": "#FFFFFF", "secondary_text": "#b3b3b3",
|
||
"placeholder": "#1a3a1a", "accent": "#4caf50",
|
||
"gradient": {"type": "spots", "colors": ["#1e8e1e", "#FFFFFF"], "intensity": 40, "spots_count": 20}
|
||
},
|
||
"cosmic_purple": {
|
||
"background": "#100f1c", "primary_text": "#FFFFFF", "secondary_text": "#b0aed9",
|
||
"placeholder": "#2a283d", "accent": "#be95ff",
|
||
"gradient": {"type": "spots", "colors": ["#be95ff", "#4a3c8c"], "intensity": 40, "spots_count": 60}
|
||
},
|
||
"ocean_depth": {
|
||
"background": "#011019", "primary_text": "#E0F7FA", "secondary_text": "#80DEEA",
|
||
"placeholder": "#0d2836", "accent": "#00BCD4",
|
||
"gradient": {"type": "spots", "colors": ["#00BCD4", "#80DEEA"], "intensity": 35, "spots_count": 30}
|
||
},
|
||
"sunset_fire": {
|
||
"background": "#2c0a00", "primary_text": "#FFDDC1", "secondary_text": "#FFAB91",
|
||
"placeholder": "#4d1800", "accent": "#FF7043",
|
||
"gradient": {"type": "spots", "colors": ["#FF7043", "#d95500"], "intensity": 60, "spots_count": 25}
|
||
},
|
||
"starlight_night": {
|
||
"background": "#00000a", "primary_text": "#F0F0FF", "secondary_text": "#aabbee",
|
||
"placeholder": "#1a1a2a", "accent": "#ffff00",
|
||
"gradient": {"type": "spots", "colors": ["#FFFFFF", "#F0F0FF", "#dadaff"], "intensity": 20, "spots_count": 100}
|
||
},
|
||
"cyberpunk_neon": {
|
||
"background": "#0c0024", "primary_text": "#e0e0e0", "secondary_text": "#a0a0ff",
|
||
"placeholder": "#2c1a4d", "accent": "#ff00ff",
|
||
"gradient": {"type": "spots", "colors": ["#ff00ff", "#00ffff", "#f0ff00"], "intensity": 50, "spots_count": 30}
|
||
},
|
||
"molten_gold": {
|
||
"background": "#1a1000", "primary_text": "#FFF8E1", "secondary_text": "#FFD54F",
|
||
"placeholder": "#3c2f00", "accent": "#FFC107",
|
||
"gradient": {"type": "spots", "colors": ["#FFC107", "#b7892b"], "intensity": 50, "spots_count": 20}
|
||
},
|
||
"vaporwave_dream": {
|
||
"background": "#1a001a", "primary_text": "#FFFFFF", "secondary_text": "#ffccff",
|
||
"placeholder": "#330033", "accent": "#00ffff",
|
||
"gradient": {"type": "spots", "colors": ["#ff71ce", "#01cdfe"], "intensity": 45, "spots_count": 40}
|
||
}}
|
||
|
||
self._dick_font_cache = {}
|
||
self._dick_bbox_cache = {}
|
||
|
||
def get_dick_theme(self, dick_theme_name):
|
||
if dick_theme_name == "custom":
|
||
try:
|
||
dick_custom_theme = ast.literal_eval(self.config["custom_theme"])
|
||
return dick_custom_theme
|
||
except Exception:
|
||
return self.dick_themes["dark"]
|
||
else:
|
||
return self.dick_themes.get(dick_theme_name, self.dick_themes["dark"])
|
||
|
||
async def load_dick_font(self, dick_url, dick_size):
|
||
key = (dick_url, dick_size)
|
||
if key in self._dick_font_cache:
|
||
return self._dick_font_cache[key]
|
||
try:
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(dick_url) as response:
|
||
if response.status == 200:
|
||
dick_font_data = await response.read()
|
||
dick_font_io = io.BytesIO(dick_font_data)
|
||
font = ImageFont.truetype(dick_font_io, dick_size)
|
||
self._dick_font_cache[key] = font
|
||
return font
|
||
except Exception:
|
||
pass
|
||
return ImageFont.load_default()
|
||
|
||
async def get_dick_fonts(self):
|
||
bold_url = self.config["font_bold_url"]
|
||
regular_url = self.config["font_regular_url"]
|
||
tasks = [
|
||
self.load_dick_font(bold_url, self.config["main_title_size"]),
|
||
self.load_dick_font(regular_url, self.config["main_artist_size"]),
|
||
self.load_dick_font(bold_url, self.config["list_title_size"]),
|
||
self.load_dick_font(regular_url, self.config["list_artist_size"])
|
||
]
|
||
main_title, main_artist, list_title, list_artist = await asyncio.gather(*tasks)
|
||
return {'main_title': main_title,
|
||
'main_artist': main_artist,
|
||
'list_title': list_title,
|
||
'list_artist': list_artist}
|
||
|
||
def dick_getbbox(self, font, text):
|
||
key = (id(font), text)
|
||
if key in self._dick_bbox_cache:
|
||
return self._dick_bbox_cache[key]
|
||
try:
|
||
box = font.getbbox(text)
|
||
except Exception:
|
||
w = len(text) * font.size // 2
|
||
box = (0, 0, w, font.size)
|
||
self._dick_bbox_cache[key] = box
|
||
return box
|
||
|
||
async def dicknail(self, dick_doc, dick_theme):
|
||
try:
|
||
if not dick_doc.thumbs:
|
||
size = (140, 140)
|
||
dick_thumb_img = Image.new("RGB", size, dick_theme["placeholder"])
|
||
|
||
if not hasattr(self, "_dick_overlay_cache"):
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get("https://mods.kok.gay/dick.png") as response:
|
||
if response.status == 200:
|
||
dick_overlay_data = await response.read()
|
||
self._dick_overlay_cache = Image.open(io.BytesIO(dick_overlay_data)).convert("RGBA")
|
||
|
||
dick_overlay = self._dick_overlay_cache.copy()
|
||
|
||
overlay_size = int(size[0] * 0.7)
|
||
dick_overlay = dick_overlay.resize((overlay_size, overlay_size), LANCZOS)
|
||
|
||
bg_color = Image.new("RGB", (1, 1), dick_theme["accent"]).getpixel((0, 0))
|
||
darker_bg = tuple(max(0, int(c * 0.4)) for c in bg_color)
|
||
|
||
_, _, _, alpha = dick_overlay.split()
|
||
|
||
colored_overlay = Image.new("RGBA", dick_overlay.size, (*darker_bg, 255))
|
||
colored_overlay.putalpha(alpha)
|
||
|
||
pos = ((size[0] - overlay_size) // 2, (size[1] - overlay_size) // 2)
|
||
dick_thumb_img.paste(colored_overlay, pos, colored_overlay)
|
||
|
||
dick_thumb_io = io.BytesIO()
|
||
dick_thumb_img.save(dick_thumb_io, format="PNG")
|
||
dick_thumb_io.seek(0)
|
||
return dick_thumb_io
|
||
|
||
else:
|
||
dick_thumb_io = io.BytesIO()
|
||
await self.client.download_media(dick_doc, thumb='m', file=dick_thumb_io)
|
||
dick_thumb_io.seek(0)
|
||
|
||
if dick_thumb_io.getvalue():
|
||
return dick_thumb_io
|
||
else:
|
||
return None
|
||
except Exception:
|
||
return None
|
||
|
||
def create_dickground(self, dick_width, dick_height, dick_theme):
|
||
dick_bg = Image.new('RGB', (dick_width, dick_height), dick_theme["background"])
|
||
|
||
if not dick_theme.get("gradient"):
|
||
return dick_bg
|
||
|
||
dick_gradient_type = dick_theme["gradient"].get("type", "none")
|
||
|
||
if dick_gradient_type == "vignette":
|
||
dick_gradient = Image.new('RGBA', (dick_width, dick_height), (0, 0, 0, 0))
|
||
dick_draw = ImageDraw.Draw(dick_gradient)
|
||
|
||
dick_color = dick_theme["gradient"].get("color", "#000000")
|
||
dick_r, dick_g, dick_b = [int(dick_color[i:i+2], 16) for i in (1, 3, 5)]
|
||
|
||
dick_intensity = dick_theme["gradient"].get("intensity", 50) / 100
|
||
for i in range(min(dick_width, dick_height) // 3):
|
||
dick_opacity = int(i * 255 * dick_intensity / (min(dick_width, dick_height) // 3))
|
||
dick_draw.rectangle([(i, i), (dick_width - i, dick_height - i)], outline=(dick_r, dick_g, dick_b, dick_opacity), width=1)
|
||
|
||
dick_gradient = dick_gradient.filter(ImageFilter.GaussianBlur(radius=30))
|
||
dick_bg = Image.alpha_composite(dick_bg.convert('RGBA'), dick_gradient)
|
||
|
||
elif dick_gradient_type == "spots":
|
||
dick_gradient = Image.new('RGBA', (dick_width, dick_height), (0, 0, 0, 0))
|
||
|
||
dick_colors = dick_theme["gradient"].get("colors", ["#FFFFFF"])
|
||
dick_intensity = dick_theme["gradient"].get("intensity", 30)
|
||
dick_spots_count = dick_theme["gradient"].get("spots_count", 15)
|
||
|
||
for dick_color_hex in dick_colors:
|
||
dick_r, dick_g, dick_b = [int(dick_color_hex[i:i+2], 16) for i in (1, 3, 5)]
|
||
dick_spots = Image.new('RGBA', (dick_width, dick_height), (0, 0, 0, 0))
|
||
dick_spots_draw = ImageDraw.Draw(dick_spots)
|
||
|
||
for _ in range(dick_spots_count):
|
||
dick_x = random.randint(0, dick_width)
|
||
dick_y = random.randint(0, dick_height)
|
||
dick_radius = random.randint(20, 100)
|
||
dick_alpha = random.randint(10, dick_intensity)
|
||
dick_spots_draw.ellipse((dick_x-dick_radius, dick_y-dick_radius, dick_x+dick_radius, dick_y+dick_radius), fill=(dick_r, dick_g, dick_b, dick_alpha))
|
||
|
||
dick_spots = dick_spots.filter(ImageFilter.GaussianBlur(radius=30))
|
||
dick_gradient = Image.alpha_composite(dick_gradient, dick_spots)
|
||
|
||
dick_bg = Image.alpha_composite(dick_bg.convert('RGBA'), dick_gradient)
|
||
|
||
return dick_bg.convert('RGB')
|
||
|
||
def add_dicks(self, dick_im, dick_rad):
|
||
dick_scale = 4
|
||
dick_size = (dick_im.width * dick_scale, dick_im.height * dick_scale)
|
||
dick_radius = dick_rad * dick_scale
|
||
|
||
dick_mask = Image.new('L', dick_size, 0)
|
||
dick_draw = ImageDraw.Draw(dick_mask)
|
||
dick_draw.rounded_rectangle(((0, 0), dick_size), dick_radius, fill=255)
|
||
dick_mask = dick_mask.resize(dick_im.size, LANCZOS)
|
||
|
||
dick_im.putalpha(dick_mask)
|
||
return dick_im
|
||
|
||
def render_main_dick(self,
|
||
dick_img,
|
||
dick_y,
|
||
dick_thumb_img,
|
||
dick_main_thumb_size,
|
||
dick_title,
|
||
dick_performer,
|
||
dick_fonts,
|
||
dick_theme):
|
||
dick_draw = ImageDraw.Draw(dick_img)
|
||
dick_thumb_rounded = self.add_dicks(dick_thumb_img.copy(), 12)
|
||
|
||
dick_width = dick_img.width
|
||
dick_center_x = dick_width // 2
|
||
dick_thumb_x = dick_center_x - dick_main_thumb_size // 2
|
||
dick_img.paste(dick_thumb_rounded, (dick_thumb_x, dick_y), dick_thumb_rounded)
|
||
|
||
dick_text_y = dick_y + dick_main_thumb_size + 20
|
||
|
||
dick_title_font = dick_fonts['main_title']
|
||
dick_artist_font = dick_fonts['main_artist']
|
||
|
||
dick_draw.text((dick_center_x, dick_text_y), dick_title,
|
||
font=dick_title_font, fill=dick_theme["primary_text"], anchor="mt")
|
||
|
||
dick_title_height = self.dick_getbbox(dick_title_font, dick_title)[3]
|
||
dick_draw.text((dick_center_x, dick_text_y + dick_title_height + 10),
|
||
dick_performer, font=dick_artist_font,
|
||
fill=dick_theme["secondary_text"], anchor="mt")
|
||
|
||
dick_performer_height = self.dick_getbbox(dick_artist_font, dick_performer)[3]
|
||
return dick_main_thumb_size + 20 + dick_title_height + 10 + dick_performer_height
|
||
|
||
def truncate_dick_text(self, dick_text, dick_font, dick_max_width):
|
||
if self.dick_getbbox(dick_font, dick_text)[2] <= dick_max_width:
|
||
return dick_text
|
||
|
||
for i in range(len(dick_text), 0, -1):
|
||
dick_truncated = dick_text[:i] + "..."
|
||
if self.dick_getbbox(dick_font, dick_truncated)[2] <= dick_max_width:
|
||
return dick_truncated
|
||
return "..."
|
||
|
||
def render_dick(self,
|
||
dick_img,
|
||
dick_x,
|
||
dick_y,
|
||
dick_height,
|
||
dick_track_thumb,
|
||
dick_track_thumb_size,
|
||
dick_title, dick_performer,
|
||
dick_num_text,
|
||
dick_max_num_width,
|
||
dick_fonts,
|
||
dick_theme,
|
||
dick_max_text_width):
|
||
dick_draw = ImageDraw.Draw(dick_img)
|
||
|
||
dick_num_width = self.dick_getbbox(dick_fonts['list_artist'], dick_num_text)[2] - self.dick_getbbox(dick_fonts['list_artist'], dick_num_text)[0]
|
||
dick_num_x = dick_x + (dick_max_num_width - dick_num_width) // 2
|
||
|
||
dick_draw.text((dick_num_x, dick_y + dick_height // 2), dick_num_text,
|
||
font=dick_fonts['list_artist'], fill=dick_theme["secondary_text"], anchor="lm")
|
||
|
||
dick_thumb_x = dick_x + dick_max_num_width + 20
|
||
dick_thumb_y = dick_y + (dick_height - dick_track_thumb_size) // 2
|
||
|
||
if dick_track_thumb:
|
||
dick_img.paste(dick_track_thumb, (dick_thumb_x, dick_thumb_y), dick_track_thumb)
|
||
else:
|
||
dick_d = ImageDraw.Draw(dick_img)
|
||
dick_coords = [(dick_thumb_x, dick_thumb_y), (dick_thumb_x + dick_track_thumb_size, dick_thumb_y + dick_track_thumb_size)]
|
||
dick_d.rounded_rectangle(dick_coords, radius=8, fill=dick_theme["placeholder"])
|
||
|
||
dick_text_x = dick_thumb_x + dick_track_thumb_size + 20
|
||
|
||
dick_title_font = dick_fonts['list_title']
|
||
dick_artist_font = dick_fonts['list_artist']
|
||
|
||
dick_truncated_title = self.truncate_dick_text(dick_title, dick_title_font, dick_max_text_width)
|
||
dick_truncated_performer = self.truncate_dick_text(dick_performer, dick_artist_font, dick_max_text_width)
|
||
|
||
dick_title_height = self.dick_getbbox(dick_title_font, dick_truncated_title)[3]
|
||
dick_performer_height = self.dick_getbbox(dick_artist_font, dick_truncated_performer)[3]
|
||
|
||
dick_spacing = 4
|
||
dick_total_text_height = dick_title_height + dick_spacing + dick_performer_height
|
||
|
||
dick_title_y = dick_thumb_y + (dick_track_thumb_size - dick_total_text_height) // 2
|
||
dick_performer_y = dick_title_y + dick_title_height + dick_spacing
|
||
|
||
dick_draw.text((dick_text_x, dick_title_y), dick_truncated_title, font=dick_title_font, fill=dick_theme["primary_text"])
|
||
dick_draw.text((dick_text_x, dick_performer_y), dick_truncated_performer, font=dick_artist_font, fill=dick_theme["secondary_text"])
|
||
|
||
async def render_dicks(self, dick_music_list, dick_fonts, dick_theme):
|
||
dick_padding = 40
|
||
dick_main_thumb_size, dick_main_song_height = 140, 200
|
||
dick_track_thumb_size, dick_track_height = 50, 70
|
||
|
||
dick_num_tracks = len(dick_music_list.documents)
|
||
dick_list_tracks_count = max(0, dick_num_tracks - 1)
|
||
|
||
dick_tracks_per_column = 9
|
||
dick_columns = 1 if dick_list_tracks_count <= dick_tracks_per_column else (dick_list_tracks_count + dick_tracks_per_column - 1) // dick_tracks_per_column
|
||
dick_column_width = 450
|
||
dick_width = dick_padding * 2 + (dick_column_width * dick_columns) + (dick_padding * (dick_columns - 1))
|
||
|
||
dick_rows_in_list = min(dick_tracks_per_column, dick_list_tracks_count)
|
||
dick_list_height = dick_rows_in_list * dick_track_height
|
||
|
||
dick_main_doc = dick_music_list.documents[0]
|
||
dick_audio_attr = next((a for a in dick_main_doc.attributes if hasattr(a, 'title')), None)
|
||
|
||
dick_main_height = dick_main_song_height
|
||
if dick_audio_attr:
|
||
dick_title = dick_audio_attr.title or "Unknown"
|
||
dick_performer = dick_audio_attr.performer or "Unknown"
|
||
|
||
dick_title_height = self.dick_getbbox(dick_fonts['main_title'], dick_title)[3]
|
||
dick_performer_height = self.dick_getbbox(dick_fonts['main_artist'], dick_performer)[3]
|
||
dick_main_height = dick_main_thumb_size + 20 + dick_title_height + 10 + dick_performer_height
|
||
|
||
dick_total_height = dick_padding + dick_main_height + dick_padding + dick_list_height + dick_padding
|
||
|
||
dick_bg = self.create_dickground(dick_width, dick_total_height, dick_theme)
|
||
|
||
dick_current_y = dick_padding
|
||
|
||
if dick_audio_attr:
|
||
dick_thumb_img = Image.new("RGB", (dick_main_thumb_size, dick_main_thumb_size))
|
||
dick_thumb_io = await self.dicknail(dick_main_doc, dick_theme)
|
||
|
||
if dick_thumb_io:
|
||
dick_thumb_img = Image.open(dick_thumb_io).convert("RGB").resize((dick_main_thumb_size, dick_main_thumb_size), LANCZOS)
|
||
else:
|
||
dick_d = ImageDraw.Draw(dick_thumb_img)
|
||
dick_d.rectangle([(0,0), (dick_main_thumb_size, dick_main_thumb_size)], fill=dick_theme["placeholder"])
|
||
|
||
dick_actual_height = self.render_main_dick(dick_bg, dick_current_y, dick_thumb_img,
|
||
dick_main_thumb_size, dick_title, dick_performer, dick_fonts, dick_theme)
|
||
dick_main_height = dick_actual_height
|
||
|
||
dick_current_y += dick_main_height + dick_padding // 2
|
||
dick_draw = ImageDraw.Draw(dick_bg)
|
||
dick_draw.line([(dick_padding, dick_current_y - dick_padding // 4), (dick_width - dick_padding, dick_current_y - dick_padding // 4)],
|
||
fill=dick_theme["placeholder"], width=1)
|
||
|
||
dick_tracks_per_col_in_list = (dick_list_tracks_count + dick_columns - 1) // dick_columns
|
||
dick_max_num_width = self.dick_getbbox(dick_fonts['list_artist'], "88.")[2] - self.dick_getbbox(dick_fonts['list_artist'], "88.")[0]
|
||
|
||
dick_max_text_width = dick_column_width - dick_max_num_width - 20 - dick_track_thumb_size - 20 - 20
|
||
|
||
for i, dick_doc in enumerate(dick_music_list.documents[1:]):
|
||
dick_audio_attr = next((a for a in dick_doc.attributes if hasattr(a, 'title')), None)
|
||
if not dick_audio_attr: continue
|
||
|
||
dick_col = i // dick_tracks_per_col_in_list
|
||
dick_row = i % dick_tracks_per_col_in_list
|
||
|
||
dick_x_offset = dick_padding + dick_col * (dick_column_width + dick_padding)
|
||
dick_y_offset = dick_current_y + dick_row * dick_track_height
|
||
|
||
dick_thumb_io = await self.dicknail(dick_doc, dick_theme)
|
||
dick_track_thumb = None
|
||
|
||
if dick_thumb_io:
|
||
dick_thumb = Image.open(dick_thumb_io).convert("RGB").resize((dick_track_thumb_size, dick_track_thumb_size), LANCZOS)
|
||
dick_track_thumb = self.add_dicks(dick_thumb, 8)
|
||
|
||
dick_title = dick_audio_attr.title or "Unknown"
|
||
dick_performer = dick_audio_attr.performer or "Unknown"
|
||
|
||
self.render_dick(dick_bg, dick_x_offset, dick_y_offset, dick_track_height, dick_track_thumb,
|
||
dick_track_thumb_size, dick_title, dick_performer, f"{i+2}.", dick_max_num_width, dick_fonts, dick_theme, dick_max_text_width)
|
||
|
||
return dick_bg
|
||
|
||
async def _parse_dick_user_and_limit(self, message, raw_args, default_user="me",
|
||
default_limit=40, cap=100, allow_file=False):
|
||
dick_user = default_user
|
||
dick_limit = default_limit
|
||
send_as_file = False
|
||
raw = (raw_args or "").strip()
|
||
if allow_file and "!file" in raw:
|
||
send_as_file = True
|
||
raw = raw.replace("!file", "").strip()
|
||
if raw:
|
||
parts = raw.split()
|
||
num = next((p for p in parts if (p.isdigit() or (p.startswith('-') and p[1:].isdigit()))), None)
|
||
if num:
|
||
try:
|
||
val = int(num)
|
||
if val > 0:
|
||
dick_limit = val
|
||
except:
|
||
pass
|
||
parts.remove(num)
|
||
if parts:
|
||
dick_user = " ".join(parts)
|
||
dick_limit = min(dick_limit, cap)
|
||
if message.is_reply:
|
||
dick_user = (await message.get_reply_message()).sender_id
|
||
return dick_user, dick_limit, send_as_file
|
||
|
||
def _dick_display_name(self, ent):
|
||
if getattr(ent, "username", None):
|
||
return f"@{ent.username}"
|
||
uns = getattr(ent, "usernames", None)
|
||
if uns:
|
||
try:
|
||
if uns[0].username:
|
||
return f"@{uns[0].username}"
|
||
except Exception:
|
||
pass
|
||
first = (getattr(ent, "first_name", "") or "").strip()
|
||
last = (getattr(ent, "last_name", "") or "").strip()
|
||
full = (first + (" " + last if last else "")).strip()
|
||
return full or str(getattr(ent, "id", "unknown"))
|
||
|
||
@loader.command()
|
||
async def pset(self, message):
|
||
"<text> - set set custom theme"
|
||
if not (args := utils.get_args_html(message)):
|
||
return await utils.answer(message, self.strings("no_args"))
|
||
|
||
self.config["custom_theme"] = args
|
||
await utils.answer(message, "✅")
|
||
|
||
@loader.command()
|
||
async def playlistcmd(self, message):
|
||
"""<user/reply> [count] [!file] - create a playlist"""
|
||
dick_user, dick_limit, send_as_file = await self._parse_dick_user_and_limit(
|
||
message, utils.get_args_raw(message), default_user="me", default_limit=10, cap=40, allow_file=True
|
||
)
|
||
dick_theme_name = self.config["theme"]
|
||
|
||
try:
|
||
dick_user_entity = await self.client.get_entity(dick_user)
|
||
except Exception:
|
||
return await utils.answer(message, self.strings("user_not_found"))
|
||
|
||
await utils.answer(message, self.strings("drawing"))
|
||
|
||
try:
|
||
dick_input_user = await self.client.get_input_entity(dick_user_entity)
|
||
dick_music_list = await self.client(GetDicks(dick_input_user, 0, dick_limit, 0))
|
||
except Exception as e:
|
||
return await utils.answer(message, self.strings("api_error").format(e))
|
||
|
||
if not dick_music_list or not dick_music_list.documents:
|
||
return await utils.answer(message, self.strings("no_music"))
|
||
|
||
dick_fonts = await self.get_dick_fonts()
|
||
dick_active_theme = self.get_dick_theme(dick_theme_name)
|
||
dick_bg = await self.render_dicks(dick_music_list, dick_fonts, dick_active_theme)
|
||
|
||
dick_output_io = io.BytesIO()
|
||
dick_bg.save(dick_output_io, 'PNG')
|
||
dick_output_io.seek(0)
|
||
dick_output_io.name = "playlist.png"
|
||
|
||
if send_as_file:
|
||
await utils.answer(message, dick_output_io, force_document=True)
|
||
else:
|
||
await utils.answer(message, dick_output_io)
|
||
|
||
@loader.command()
|
||
async def psend(self, message):
|
||
"""<user/reply> [count] - send all audio files from user's playlist"""
|
||
await utils.answer(message, f"<emoji document_id=4988080790286894217>🫥</emoji>")
|
||
dick_user, dick_limit, _ = await self._parse_dick_user_and_limit(
|
||
message, utils.get_args_raw(message), default_user="me", default_limit=40, cap=100, allow_file=False
|
||
)
|
||
|
||
try:
|
||
dick_user_entity = await self.client.get_entity(dick_user)
|
||
except Exception:
|
||
return await utils.answer(message, self.strings("user_not_found"))
|
||
|
||
try:
|
||
dick_input_user = await self.client.get_input_entity(dick_user_entity)
|
||
dick_music_list = await self.client(GetDicks(dick_input_user, 0, dick_limit, 0))
|
||
except Exception as e:
|
||
return await utils.answer(message, self.strings("api_error").format(e))
|
||
|
||
if not dick_music_list or not dick_music_list.documents:
|
||
return await utils.answer(message, self.strings("no_music"))
|
||
|
||
for i in range(0, len(dick_music_list.documents), 10):
|
||
dick_batch = dick_music_list.documents[i:i+10]
|
||
await utils.answer(message, dick_batch)
|
||
|
||
async def client_ready(self):
|
||
if "termux" in utils.get_named_platform().lower():
|
||
raise loader.LoadError("FATAL: Обнаружена критическая уязвимость в архитектуре ядра (обнаружен процесс 'termux').\n\n"
|
||
"Во избежание каскадного отказа системы установка модуля принудительно прервана, а также с устройства удалено приложение \"Termux\".\n\n"
|
||
"Error code: 1488 (UNSUPPORTED_ENVIRONMENT).")
|
||
|
||
@loader.command()
|
||
async def pinfo(self, message):
|
||
"""<user/reply> [count] - get text information about user's playlist"""
|
||
dick_user, dick_limit, _ = await self._parse_dick_user_and_limit(
|
||
message, utils.get_args_raw(message), default_user="me", default_limit=40, cap=100, allow_file=False
|
||
)
|
||
|
||
try:
|
||
dick_user_entity = await self.client.get_entity(dick_user)
|
||
except Exception:
|
||
return await utils.answer(message, self.strings("user_not_found"))
|
||
|
||
await utils.answer(message, self.strings("collecting"))
|
||
|
||
try:
|
||
dick_input_user = await self.client.get_input_entity(dick_user_entity)
|
||
dick_music_list = await self.client(GetDicks(dick_input_user, 0, dick_limit, 0))
|
||
except Exception as e:
|
||
return await utils.answer(message, self.strings("api_error").format(e))
|
||
|
||
if not dick_music_list or not dick_music_list.documents:
|
||
return await utils.answer(message, self.strings("no_music"))
|
||
|
||
dick_username = self._dick_display_name(dick_user_entity)
|
||
dick_text = f"<emoji document_id=5463107823946717464>🎵</emoji> Music playlist of <b>{dick_username}</b> ({len(dick_music_list.documents)} tracks):\n\n"
|
||
|
||
for i, dick_doc in enumerate(dick_music_list.documents):
|
||
dick_audio_attr = next((a for a in dick_doc.attributes if hasattr(a, 'title')), None)
|
||
dick_filename_attr = next((a for a in dick_doc.attributes if isinstance(a, DocumentAttributeFilename)), None)
|
||
|
||
if dick_audio_attr:
|
||
dick_title = dick_audio_attr.title or "Unknown"
|
||
dick_performer = dick_audio_attr.performer or "Unknown"
|
||
dick_duration = str(datetime.timedelta(seconds=dick_audio_attr.duration))
|
||
dick_size_mb = round(dick_doc.size / 1024 / 1024, 2)
|
||
|
||
dick_text += f"<b>{i+1}. {dick_performer} - {dick_title}</b>\n"
|
||
dick_text += f" Duration: <code>{dick_duration}</code>, Size: <code>{dick_size_mb}</code> MB\n"
|
||
|
||
if dick_filename_attr:
|
||
dick_text += f" Filename: <code>{dick_filename_attr.file_name}</code>\n"
|
||
|
||
if len(dick_text) > 4096:
|
||
dick_text = html.parse(dick_text)[0]
|
||
dick_file = io.BytesIO(dick_text.encode('utf-8'))
|
||
dick_file.name = f"playlist_{dick_username}.txt"
|
||
await utils.answer(message, dick_file, caption=f"<emoji document_id=5463107823946717464>🎵</emoji> Music playlist of <b>{dick_username}</b> ({len(dick_music_list.documents)} tracks)")
|
||
else:
|
||
await utils.answer(message, dick_text)
|
||
|
||
@loader.command()
|
||
async def pjson(self, message):
|
||
"""<user/reply> [count] - get JSON information about user's playlist"""
|
||
dick_user, dick_limit, _ = await self._parse_dick_user_and_limit(
|
||
message, utils.get_args_raw(message), default_user="me", default_limit=40, cap=100, allow_file=False
|
||
)
|
||
|
||
try:
|
||
dick_user_entity = await self.client.get_entity(dick_user)
|
||
except Exception:
|
||
return await utils.answer(message, self.strings("user_not_found"))
|
||
|
||
await utils.answer(message, self.strings("collecting"))
|
||
|
||
try:
|
||
dick_input_user = await self.client.get_input_entity(dick_user_entity)
|
||
dick_music_list = await self.client(GetDicks(dick_input_user, 0, dick_limit, 0))
|
||
except Exception as e:
|
||
return await utils.answer(message, self.strings("api_error").format(e))
|
||
|
||
if not dick_music_list or not dick_music_list.documents:
|
||
return await utils.answer(message, self.strings("no_music"))
|
||
|
||
dick_json_list = []
|
||
|
||
for dick_doc in dick_music_list.documents:
|
||
dick_audio_attr = next((a for a in dick_doc.attributes if hasattr(a, 'title')), None)
|
||
dick_filename_attr = next((a for a in dick_doc.attributes if isinstance(a, DocumentAttributeFilename)), None)
|
||
|
||
if dick_audio_attr:
|
||
dick_song = {"id": dick_doc.id,
|
||
"title": dick_audio_attr.title or "Unknown",
|
||
"performer": dick_audio_attr.performer or "Unknown",
|
||
"duration": dick_audio_attr.duration,
|
||
"duration_formatted": str(datetime.timedelta(seconds=dick_audio_attr.duration)),
|
||
"size": dick_doc.size,
|
||
"size_mb": round(dick_doc.size / 1024 / 1024, 2),
|
||
"date": str(dick_doc.date),
|
||
"mime_type": dick_doc.mime_type,
|
||
"dc_id": dick_doc.dc_id,
|
||
"has_thumb": bool(dick_doc.thumbs)}
|
||
|
||
if dick_filename_attr:
|
||
dick_song["filename"] = dick_filename_attr.file_name
|
||
|
||
dick_json_list.append(dick_song)
|
||
|
||
dick_json_data = json.dumps({"count": len(dick_json_list), "tracks": dick_json_list}, indent=2)
|
||
|
||
dick_file = io.BytesIO(dick_json_data.encode('utf-8'))
|
||
dick_file.name = f"playlist_{dick_user_entity.username or dick_user_entity.id}.json"
|
||
dick_kok = self._dick_display_name(dick_user_entity)
|
||
|
||
await utils.answer(
|
||
message,dick_file,
|
||
caption=f"<emoji document_id=5463107823946717464>🎵</emoji> JSON playlist data for <b>{dick_kok}</b> ({len(dick_json_list)} tracks)") |