Created
April 5, 2026 15:21
-
-
Save sovetski/f7ae3ece35902782db7c2e063872fc32 to your computer and use it in GitHub Desktop.
YouTube auto thumbnail and title update depending on the current views
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import time | |
| import pickle | |
| import datetime | |
| from PIL import Image, ImageDraw, ImageFont | |
| from googleapiclient.discovery import build | |
| from googleapiclient.http import MediaFileUpload | |
| # Source and destination video IDs | |
| SOURCE_VIDEO_ID = "XXX" | |
| DEST_VIDEO_ID = "XXX" | |
| # Thumbnail text position and info | |
| X_COORD = 730 | |
| Y_COORD = 250 | |
| FONT_SIZE = 120 | |
| WAIT_MINUTES = 8 # Title updated every... | |
| last_thumbnail_text = "" | |
| def get_authenticated_service(): | |
| now = datetime.datetime.now() | |
| hour = now.hour | |
| # 12h/12h rotation | |
| if 8 <= hour < 20: | |
| token_file = "token_2.pickle" | |
| label = "PROJECT #2 (DAY)" | |
| else: | |
| token_file = "token.pickle" | |
| label = "PROJECT #1 (NIGHT)" | |
| if not os.path.exists(token_file): | |
| raise Exception(f"{token_file} missing!") | |
| with open(token_file, "rb") as token: | |
| creds = pickle.load(token) | |
| if creds and creds.expired and creds.refresh_token: | |
| from google.auth.transport.requests import Request | |
| creds.refresh(Request()) | |
| return build("youtube", "v3", credentials=creds) | |
| def get_thumbnail_logic(views): | |
| """Handles thumbnails: fixed message under 1000 views, then steps of 100 views""" | |
| if views < 90: | |
| return "This video will get\nless than 100 views" | |
| elif views < 190: | |
| return "This video will get\nless than 200 views" | |
| elif views < 290: | |
| return "This video will get\nless than 300 views" | |
| elif views < 390: | |
| return "This video will get\nless than 400 views" | |
| elif views < 490: | |
| return "This video will get\nless than 500 views" | |
| elif views < 590: | |
| return "This video will get\nless than 600 views" | |
| elif views < 690: | |
| return "This video will get\nless than 700 views" | |
| elif views < 790: | |
| return "This video will get\nless than 800 views" | |
| elif views < 890: | |
| return "This video will get\nless than 900 views" | |
| elif views < 1000: | |
| return "This video will get\nless than 1,000 views" | |
| elif views < 10000: | |
| # Between 1k and 10k: keep decimal | |
| value_k = (views // 100) / 10.0 | |
| str_k = str(value_k) | |
| return f"This video will get\n{str_k}k views" | |
| elif views < 1000000: | |
| # Between 10k and 999k: no decimal | |
| value_k = views // 1000 | |
| return f"This video will get\n{value_k}k views" | |
| else: | |
| # Round down for millions | |
| value_m = (views // 100000) / 10.0 | |
| str_m = str(value_m) | |
| return f"This video will get\n{str_m}M views" | |
| def create_thumbnail(text): | |
| try: | |
| img = Image.open("background.jpg") | |
| except: | |
| img = Image.new('RGB', (1280, 720), color=(220, 220, 220)) | |
| draw = ImageDraw.Draw(img) | |
| try: | |
| font = ImageFont.truetype("arialbd.ttf", FONT_SIZE) | |
| except: | |
| font = ImageFont.load_default() | |
| draw.text((X_COORD, Y_COORD), text, font=font, fill="black", align="center", spacing=20) | |
| img.save("current_thumbnail.jpg") | |
| return "current_thumbnail.jpg" | |
| def update_loop(): | |
| global last_thumbnail_text | |
| while True: | |
| try: | |
| youtube = get_authenticated_service() | |
| print(f"\n--- CYCLE ({datetime.datetime.now().strftime('%H:%M:%S')}) ---") | |
| # 1. Read | |
| source_data = youtube.videos().list(part="statistics", id=SOURCE_VIDEO_ID).execute() | |
| views = int(source_data["items"][0]["statistics"]["viewCount"]) | |
| # 2. TITLE | |
| formatted_views = f"{views:,}" | |
| new_title = f"It will get {formatted_views} views to be precise" | |
| dest_data = youtube.videos().list(part="snippet", id=DEST_VIDEO_ID).execute() | |
| category_id = dest_data["items"][0]["snippet"]["categoryId"] | |
| youtube.videos().update( | |
| part="snippet", | |
| body={"id": DEST_VIDEO_ID, "snippet": {"title": new_title, "categoryId": category_id}} | |
| ).execute() | |
| print(f"TITLE: {new_title}") | |
| # 3. THUMBNAIL BY STEP | |
| thumbnail_text = get_thumbnail_logic(views) | |
| if thumbnail_text != last_thumbnail_text: | |
| print(f"New step: {thumbnail_text}") | |
| thumb_path = create_thumbnail(thumbnail_text) | |
| youtube.thumbnails().set( | |
| videoId=DEST_VIDEO_ID, | |
| media_body=MediaFileUpload(thumb_path) | |
| ).execute() | |
| last_thumbnail_text = thumbnail_text | |
| print("THUMBNAIL updated.") | |
| else: | |
| print("Step unchanged. Thumbnail kept.") | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| time.sleep(60) | |
| continue | |
| time.sleep(WAIT_MINUTES * 60) | |
| if __name__ == "__main__": | |
| update_loop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment