<BatBin/>

1# a part of Opus Music Project 2025 ©
2# this code is & will be our property as it is or even after modified
3# must give credits if used this code anywhere
4import os
5import re
6import textwrap
7import numpy as np
8
9import aiofiles
10import aiohttp
11from PIL import (
12 Image,
13 ImageDraw,
14 ImageEnhance,
15 ImageFilter,
16 ImageFont,
17)
18
19from youtubesearchpython.__future__ import VideosSearch
20from config import YOUTUBE_IMG_URL
21
22
23def changeImageSize(maxWidth, maxHeight, image):
24 widthRatio = maxWidth / image.size[0]
25 heightRatio = maxHeight / image.size[1]
26 ratio = min(widthRatio, heightRatio)
27 newWidth = int(image.size[0] * ratio)
28 newHeight = int(image.size[1] * ratio)
29 try:
30 resample = Image.Resampling.LANCZOS
31 except AttributeError:
32 resample = Image.ANTIALIAS # For Pillow<10
33 image = image.resize((newWidth, newHeight), resample)
34 return image
35
36
37def get_dominant_color(image):
38 """Extract the dominant color from the image"""
39 # Convert to RGB if not already
40 image = image.convert('RGB')
41
42 # Resize to speed up processing
43 image = image.resize((50, 50))
44
45 # Get all pixels
46 pixels = np.array(image)
47
48 # Reshape to get list of RGB values
49 pixel_list = pixels.reshape(-1, 3)
50
51 # Calculate average color
52 avg_color = tuple(pixel_list.mean(axis=0).astype(int))
53
54 # Ensure color is bright enough for visibility
55 # If too dark, brighten it
56 if sum(avg_color) < 200: # If color is too dark
57 brightened = tuple(min(255, int(c * 1.5)) for c in avg_color)
58 return brightened
59
60 return avg_color
61
62
63def get_contrasting_color(bg_color):
64 """Get a contrasting color for better visibility"""
65 # Calculate luminance
66 luminance = (0.299 * bg_color[0] + 0.587 * bg_color[1] + 0.114 * bg_color[2])
67
68 # Return white for dark backgrounds, dark for light backgrounds
69 return (255, 255, 255) if luminance < 128 else (50, 50, 50)
70
71
72async def get_thumb(videoid):
73 final_path = f"cache/{videoid}.png"
74 if os.path.isfile(final_path):
75 return final_path
76
77 url = f"https://www.youtube.com/watch?v={videoid}"
78 try:
79 results = VideosSearch(url, limit=1)
80 result_data = await results.next()
81 if not result_data.get("result"):
82 return YOUTUBE_IMG_URL
83
84 result = result_data["result"][0]
85 title = re.sub(r"\W+", " ", result.get("title", "Unknown Title")).title()
86 duration = result.get("duration", "Unknown Duration")
87 thumbnail = result["thumbnails"][0]["url"].split("?")[0]
88 views = result.get("viewCount", {}).get("short", "Unknown Views")
89 channel = result.get("channel", {}).get("name", "Unknown Channel")
90
91 # Ensure cache directory exists
92 os.makedirs("cache", exist_ok=True)
93
94 # Download thumbnail
95 async with aiohttp.ClientSession() as session:
96 async with session.get(thumbnail) as resp:
97 thumb_path = f"cache/thumb{videoid}.png"
98 async with aiofiles.open(thumb_path, mode="wb") as f:
99 await f.write(await resp.read())
100
101 # Verify downloaded file is a valid image
102 try:
103 youtube = Image.open(thumb_path)
104 except:
105 os.remove(thumb_path) if os.path.exists(thumb_path) else None
106 return YOUTUBE_IMG_URL
107
108 # Extract dominant color from thumbnail for duration bar
109 bar_color = get_dominant_color(youtube)
110
111 image1 = changeImageSize(1280, 720, youtube.copy())
112 center_thumb = changeImageSize(940, 420, youtube.copy())
113
114 # Rounded center image mask
115 mask = Image.new("L", center_thumb.size, 0)
116 draw_mask = ImageDraw.Draw(mask)
117 draw_mask.rounded_rectangle(
118 [0, 0, center_thumb.size[0], center_thumb.size[1]],
119 radius=40,
120 fill=255
121 )
122
123 # Background blur (softer)
124 image2 = image1.convert("RGBA")
125 background = image2.filter(ImageFilter.BoxBlur(18))
126 background = ImageEnhance.Brightness(background).enhance(0.8)
127
128 # Paste rounded thumbnail
129 thumb_pos = (170, 90)
130 center_thumb_rgba = center_thumb.convert("RGBA")
131 background.paste(center_thumb_rgba, thumb_pos, mask)
132
133 # Load fonts safely
134 def safe_font(path, size):
135 try:
136 return ImageFont.truetype(path, size)
137 except:
138 return ImageFont.load_default()
139
140 font = safe_font("OpusV/resources/font.ttf", 30)
141 font2 = safe_font("OpusV/resources/font.ttf", 30)
142 arial = safe_font("OpusV/resources/font2.ttf", 30)
143
144 # Draw text
145 draw = ImageDraw.Draw(background)
146
147 # Channel | Views
148 draw.text((50, 565), f"{channel} | {views[:23]}", fill="white", font=arial)
149
150 # Title
151 title = textwrap.shorten(title, width=50, placeholder="...")
152 draw.text((50, 600), title, fill="white", font=font, stroke_fill="white")
153
154 # Start and End Time
155 draw.text((50, 640), "00:25", fill="white", font=font2, stroke_width=1, stroke_fill="grey")
156 draw.text((1150, 640), duration[:23], fill="white", font=font2, stroke_width=1, stroke_fill="white")
157
158 # Duration bar with auto color from thumbnail
159 draw.line((150, 660, 1130, 660), width=6, fill=bar_color)
160
161 # Recreation Music text at right side of center thumbnail
162 rec_font = safe_font("OpusV/resources/font.ttf", 40)
163 rec_text = "Recreation Music"
164 bbox = draw.textbbox((0, 0), rec_text, font=rec_font)
165 rec_text_w = bbox[2] - bbox[0]
166 rec_text_h = bbox[3] - bbox[1]
167 rec_x = thumb_pos[0] + center_thumb.width + 25
168 rec_y = thumb_pos[1] + (center_thumb.height // 2) - (rec_text_h // 2)
169 draw.text((rec_x, rec_y), rec_text, fill="white", font=rec_font)
170
171 # Clean up temporary file
172 try:
173 os.remove(thumb_path)
174 except:
175 pass
176
177 # Save final image
178 background.save(final_path, format="PNG")
179 return final_path
180
181 except Exception:
182 return YOUTUBE_IMG_URL
183