Instantly share code, notes, and snippets.
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save sogful/8e343ce04da6895ed432e27a21776e91 to your computer and use it in GitHub Desktop.
a little program i use to automate twitter banner updates (outdated, see https://github.com/sogful/banner)
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 requests | |
| from PIL import Image, ImageDraw, ImageFont | |
| import tweepy | |
| from datetime import datetime, timezone, timedelta | |
| import time | |
| import logging | |
| import sys | |
| import os | |
| import threading | |
| import subprocess | |
| from pystray import Icon, Menu, MenuItem | |
| #*//////////////////////////////////////////////////////////////////////*# | |
| BANNER_SIZE = (900, 300) | |
| def base_dir(): | |
| if getattr(sys, "frozen", False): | |
| return os.path.dirname(sys.executable) | |
| return os.path.dirname(os.path.abspath(__file__)) | |
| #*//////////////////////////////////////////////////////////////////////*# | |
| def get_weather_summary(lat, lon): | |
| url = ( | |
| f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}" | |
| f"¤t_weather=true" | |
| ) | |
| r = requests.get(url, timeout=10) | |
| r.raise_for_status() | |
| data = r.json() | |
| cw = data.get('current_weather', {}) | |
| temp = cw.get('temperature', 'N/A') | |
| wind = cw.get('windspeed', 'N/A') | |
| summary = f"{temp}°C, wind speed: {wind}km/hr" | |
| return summary | |
| def get_status_summary(user): | |
| url = f"https://status.cafe/users/{user}/status.json" | |
| r = requests.get(url, timeout=10) | |
| r.raise_for_status() | |
| data = r.json() | |
| content = data.get("content", "N/A") | |
| time_ago = data.get("timeAgo", "") | |
| small = f"{time_ago} (from status.cafe/meow)" | |
| return content, small | |
| def draw_weather_on_banner(weather_text, status_main, status_small, input_image_path, output_image_path): | |
| im = Image.open(input_image_path).convert("RGBA") | |
| draw = ImageDraw.Draw(im) | |
| fontsize = 25 | |
| font_paths = [ # once again, change this (also, the third path is for pythonanywhere, you can use it for deviceless daily updates) | |
| "H:/Users/Admin/Downloads/ruji.ttf", | |
| "ruji.ttf", | |
| "/home/soggycat/banner/ruji.ttf", | |
| ] | |
| font = None | |
| for font_path in font_paths: | |
| try: | |
| font = ImageFont.truetype(font_path, fontsize) | |
| break | |
| except (OSError, IOError): | |
| continue | |
| if font is None: | |
| system_fonts = [ | |
| "C:/Windows/Fonts/arial.ttf", | |
| "C:/Windows/Fonts/calibri.ttf", | |
| "C:/Windows/Fonts/tahoma.ttf" | |
| ] | |
| for sys_font in system_fonts: | |
| try: | |
| font = ImageFont.truetype(sys_font, fontsize) | |
| break | |
| except (OSError, IOError): | |
| continue | |
| if font is None: | |
| font = ImageFont.load_default() | |
| timestamp_fontsize = fontsize // 2 | |
| timestamp_font = None | |
| for font_path in font_paths: | |
| try: | |
| timestamp_font = ImageFont.truetype(font_path, timestamp_fontsize) | |
| break | |
| except (OSError, IOError): | |
| continue | |
| if timestamp_font is None: | |
| for sys_font in system_fonts: | |
| try: | |
| timestamp_font = ImageFont.truetype(sys_font, timestamp_fontsize) | |
| break | |
| except (OSError, IOError): | |
| continue | |
| if timestamp_font is None: | |
| timestamp_font = ImageFont.load_default() | |
| gmt2 = timezone(timedelta(hours=2)) # change this to your timezone!!!! | |
| now = datetime.now(gmt2) | |
| time_str = now.strftime("%H:%M") | |
| timestamp_text = f"last updated {time_str} GMT+2" | |
| def measure(text, fnt): | |
| try: | |
| b = draw.textbbox((0, 0), text, font=fnt) | |
| return b[2]-b[0], b[3]-b[1] | |
| except ValueError: | |
| return draw.textsize(text, font=fnt) | |
| padding = 60 | |
| bg_padding = 10 | |
| corner_radius = 15 | |
| gap = 5 # spacing between main and small text | |
| boxes = [ | |
| (weather_text, timestamp_text, "left"), | |
| (status_main, status_small, "right"), | |
| ] | |
| layouts = [] | |
| for main_text, small_text, side in boxes: | |
| main_w, main_h = measure(main_text, font) | |
| small_w, small_h = measure(small_text, timestamp_font) | |
| box_w = max(main_w, small_w) | |
| box_h = main_h + small_h + gap | |
| y = padding | |
| if side == "left": | |
| x = padding | |
| main_x = small_x = x # left align | |
| else: | |
| x = im.size[0] - padding - box_w | |
| main_x = x + box_w - main_w # right align | |
| small_x = x + box_w - small_w | |
| box = [x-bg_padding, y-bg_padding, x+box_w+bg_padding, y+box_h+bg_padding] | |
| layouts.append((box, main_x, y, main_text, small_x, y + main_h + gap, small_text)) | |
| overlay = Image.new("RGBA", im.size, (0, 0, 0, 0)) | |
| overlay_draw = ImageDraw.Draw(overlay) | |
| for box, mx, my, main_text, sx, sy, small_text in layouts: | |
| overlay_draw.rounded_rectangle(box, radius=corner_radius, fill=(0, 0, 0, 80)) | |
| im = Image.alpha_composite(im, overlay) | |
| draw = ImageDraw.Draw(im) | |
| for box, mx, my, main_text, sx, sy, small_text in layouts: | |
| draw.text((mx, my), main_text, font=font, fill=(255,255,255,255)) | |
| draw.text((sx, sy), small_text, font=timestamp_font, fill=(255,255,255,200)) | |
| # scale down to the exact size twitter needs to keep the alpha channel | |
| # so the fabled 900x300 | |
| if im.size != BANNER_SIZE: | |
| im = im.resize(BANNER_SIZE, Image.LANCZOS) | |
| im.save(output_image_path, format="PNG", optimize=False) | |
| #*//////////////////////////////////////////////////////////////////////*# | |
| def update_twitter_banner(banner_image_path): | |
| consumer_key = "changethis" | |
| consumer_secret = "andthis" | |
| access_token = "andthis" | |
| access_token_secret = "andthis" | |
| auth = tweepy.OAuth1UserHandler( | |
| consumer_key, consumer_secret, access_token, access_token_secret | |
| ) | |
| api = tweepy.API(auth) | |
| api.update_profile_banner(filename=banner_image_path) | |
| def runupd(): | |
| try: | |
| lat, lon = 6.7, 6.7 # change this location to yours | |
| weather_text = get_weather_summary(lat, lon) | |
| logging.info(f"weather: {weather_text}") | |
| status_main, status_small = get_status_summary("user") # and change this to your status.cafe username. if you don't have one just remove the relevant code | |
| logging.info(f"status: {status_main} ({status_small})") | |
| script_dir = base_dir() | |
| input_image = os.path.join(script_dir, "base.png") | |
| output_image = os.path.join(script_dir, "currentbanner.png") | |
| draw_weather_on_banner(weather_text, status_main, status_small, input_image, output_image) | |
| logging.info("updating banner..") | |
| update_twitter_banner(output_image) | |
| return True | |
| except Exception as e: | |
| logging.error(f"{e}", exc_info=True) | |
| return False | |
| #*//////////////////////////////////////////////////////////////////////*# | |
| def make_tray(script_dir, stop_event, update_event): | |
| # builds the buggy tray | |
| def on_update_now(icon, item): | |
| logging.info("tray: update now") | |
| update_event.set() | |
| def on_exit(icon, item): | |
| logging.info("tray: exit") | |
| stop_event.set() | |
| update_event.set() | |
| icon.stop() | |
| def on_recompile(icon, item): | |
| logging.info("tray: recompile and reopen") | |
| recompile = os.path.join(script_dir, ".claude", "recompile.ps1") | |
| DETACHED = 0x00000008 | |
| subprocess.Popen( | |
| ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", | |
| "-WindowStyle", "Hidden", "-File", recompile], | |
| creationflags=DETACHED | subprocess.CREATE_NEW_PROCESS_GROUP, | |
| close_fds=True, | |
| ) | |
| on_exit(icon, item) | |
| try: | |
| tray_image = Image.open(os.path.join(script_dir, "icon.png")) | |
| except (OSError, IOError): | |
| tray_image = Image.new("RGBA", (64, 64), (0, 0, 0, 255)) | |
| menu = Menu( | |
| MenuItem("recompile and reopen", on_recompile), | |
| MenuItem("update now", on_update_now), | |
| MenuItem("exit", on_exit), | |
| ) | |
| return Icon("twitterbanner", tray_image, "twitter banner updater", menu) | |
| def main(): | |
| script_dir = base_dir() | |
| log_file = os.path.join(script_dir, "banner.log") | |
| handlers = [logging.FileHandler(log_file)] | |
| if sys.stdout is not None: | |
| handlers.append(logging.StreamHandler(sys.stdout)) | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(levelname)s - %(message)s', | |
| handlers=handlers | |
| ) | |
| stop_event = threading.Event() | |
| update_event = threading.Event() | |
| def worker(): | |
| runupd() | |
| while not stop_event.is_set(): | |
| logging.info("done, will try again in 1 hour..") | |
| update_event.wait(timeout=3600) # wakes up early on manual update / exit | |
| if stop_event.is_set(): | |
| break | |
| update_event.clear() | |
| runupd() | |
| threading.Thread(target=worker, daemon=True).start() | |
| make_tray(script_dir, stop_event, update_event).run() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment