| 1 | |
| 2 | |
| 3 | |
| 4 | import os |
| 5 | import re |
| 6 | import textwrap |
| 7 | import numpy as np |
| 8 | |
| 9 | import aiofiles |
| 10 | import aiohttp |
| 11 | from PIL import ( |
| 12 | Image, |
| 13 | ImageDraw, |
| 14 | ImageEnhance, |
| 15 | ImageFilter, |
| 16 | ImageFont, |
| 17 | ) |
| 18 | |
| 19 | from youtubesearchpython.__future__ import VideosSearch |
| 20 | from config import YOUTUBE_IMG_URL |
| 21 | |
| 22 | |
| 23 | def 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 |
| 33 | image = image.resize((newWidth, newHeight), resample) |
| 34 | return image |
| 35 | |
| 36 | |
| 37 | def get_dominant_color(image): |
| 38 | """Extract the dominant color from the image""" |
| 39 | |
| 40 | image = image.convert('RGB') |
| 41 | |
| 42 | |
| 43 | image = image.resize((50, 50)) |
| 44 | |
| 45 | |
| 46 | pixels = np.array(image) |
| 47 | |
| 48 | |
| 49 | pixel_list = pixels.reshape(-1, 3) |
| 50 | |
| 51 | |
| 52 | avg_color = tuple(pixel_list.mean(axis=0).astype(int)) |
| 53 | |
| 54 | |
| 55 | |
| 56 | if sum(avg_color) < 200: |
| 57 | brightened = tuple(min(255, int(c * 1.5)) for c in avg_color) |
| 58 | return brightened |
| 59 | |
| 60 | return avg_color |
| 61 | |
| 62 | |
| 63 | def get_contrasting_color(bg_color): |
| 64 | """Get a contrasting color for better visibility""" |
| 65 | |
| 66 | luminance = (0.299 * bg_color[0] + 0.587 * bg_color[1] + 0.114 * bg_color[2]) |
| 67 | |
| 68 | |
| 69 | return (255, 255, 255) if luminance < 128 else (50, 50, 50) |
| 70 | |
| 71 | |
| 72 | async 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 | |
| 92 | os.makedirs("cache", exist_ok=True) |
| 93 | |
| 94 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 124 | image2 = image1.convert("RGBA") |
| 125 | background = image2.filter(ImageFilter.BoxBlur(18)) |
| 126 | background = ImageEnhance.Brightness(background).enhance(0.8) |
| 127 | |
| 128 | |
| 129 | thumb_pos = (170, 90) |
| 130 | center_thumb_rgba = center_thumb.convert("RGBA") |
| 131 | background.paste(center_thumb_rgba, thumb_pos, mask) |
| 132 | |
| 133 | |
| 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 | |
| 145 | draw = ImageDraw.Draw(background) |
| 146 | |
| 147 | |
| 148 | draw.text((50, 565), f"{channel} | {views[:23]}", fill="white", font=arial) |
| 149 | |
| 150 | |
| 151 | title = textwrap.shorten(title, width=50, placeholder="...") |
| 152 | draw.text((50, 600), title, fill="white", font=font, stroke_fill="white") |
| 153 | |
| 154 | |
| 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 | |
| 159 | draw.line((150, 660, 1130, 660), width=6, fill=bar_color) |
| 160 | |
| 161 | |
| 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 | |
| 172 | try: |
| 173 | os.remove(thumb_path) |
| 174 | except: |
| 175 | pass |
| 176 | |
| 177 | |
| 178 | background.save(final_path, format="PNG") |
| 179 | return final_path |
| 180 | |
| 181 | except Exception: |
| 182 | return YOUTUBE_IMG_URL |
| 183 | |