from PIL import Image, ImageDraw, ImageFont import textwrap import time import requests from io import BytesIO import warnings from datetime import datetime import yaml from typing import Dict import subprocess from spotiplayer_pi.lib import LCD_2inch from spotiplayer_pi.api import Api def get_cpu_temp(): try: result = subprocess.run( ["vcgencmd", "measure_temp"], capture_output=True, text=True, check=True ) temp_str = result.stdout.strip() temp_value = temp_str.split("=")[1].replace("'C", "") return float(temp_value) except subprocess.CalledProcessError as e: warnings.warn(f"Error executing vcgencmd: {e}") return "None" except (IndexError, ValueError) as e: warnings.warn(f"Error parsing temperature: {e}") return "None" def parse_config(path="spotiplayer_pi/config.yaml"): with open(path, "r") as file: data = yaml.safe_load(file) for key, value in data["color_theme"].items(): data["color_theme"][key] = tuple(value) return data def display_loop(api: Api, cfg: Dict): try: d = LCD_2inch() d.Init() d.clear() bg = Image.new("RGB", (d.height, d.width), (0, 0, 0)) d.ShowImage(bg) Font0 = ImageFont.truetype("Font/GothamBold.ttf", 14) Font1 = ImageFont.truetype("Font/GothamMedium.ttf", 18) Font1b = ImageFont.truetype("Font/GothamBold.ttf", 19) Font2 = ImageFont.truetype("Font/GothamMedium.ttf", 20) Font3 = ImageFont.truetype("Font/GothamMedium.ttf", 36) unavailable_img = Image.open("imgs/unavailable.jpg") error_img = Image.new("RGB", (240, 320), color=(255, 0, 0)) draw = ImageDraw.Draw(error_img) draw.text((150, 150), "Error :(", font=Font1, fill=cfg["color_theme"]["text"]) del draw spoti_logo = Image.open("imgs/logo64.jpg") qr = Image.open("imgs/qr.jpg") spoti_logo = spoti_logo.resize((64, 64)) not_playing_img = Image.new("RGB", (320, 240), (0, 0, 0)) not_playing_img.paste(spoti_logo, (25, 40)) not_playing_img.paste(qr, (120, 40)) draw = ImageDraw.Draw(not_playing_img) draw.text( (112, 15), "Connect to speakers", font=Font2, fill=(255, 255, 255), ) del spoti_logo, qr, draw last_api_call = 0 last_auth_refresh = 0 current_mode = None # 0: not-playing, 1: playing last_track = None data = None auth_interval = 0 while True: if time.time() - last_auth_refresh >= auth_interval: auth_interval = api.refreshAuth() - 120 last_auth_refresh = time.time() print(f"Refreshed auth at {datetime.now().strftime('%d-%m %H:%M:%S')}") if time.time() - last_api_call >= cfg["api_interval"]: data = api.getPlaying() last_api_call = time.time() if data == None: warnings.warn("No data found") d.ShowImage(error_img) elif data == "not-playing": draw = ImageDraw.Draw(not_playing_img) current_time = datetime.now() draw.rectangle([(00, 120), (119, 190)], fill=(0, 0, 0)) offset = 10 if current_time.hour < 9 else 0 draw.text( (10 + offset, 120), f"{current_time.hour}:{current_time.minute:02d}", font=Font3, fill=(255, 255, 255), ) draw.text( (25, 152), current_time.strftime("%a %d"), font=Font2, fill=(255, 255, 255), ) draw.text( (20, 175), f"CPU: {get_cpu_temp():.2f}°", font=Font0, fill=(255, 255, 255), ) if current_mode != 0: current_mode = 0 print("Standby mode") d.ShowImage(not_playing_img) time.sleep(cfg["api_interval"]) elif type(data) == dict: current_mode = 1 current_track = data["track"] + data["artists"][0] + data["album"] if current_track != last_track: print(f"Updating track to : {data['track']} - {data['artists'][0]}") last_track = current_track img = None try: img = requests.get( data["img_url"], timeout=cfg["refresh_interval"] / 2 ) img = Image.open(BytesIO(img.content)) except Exception as e: warnings.warn( f"Failed to fetch album cover at: {data['img_url']}\n{e}" ) if img == None: img = unavailable_img img = img.resize((128, 128)) bg = Image.new("RGB", (320, 240), cfg["color_theme"]["background"]) bg.paste(img, (10, 30)) draw = ImageDraw.Draw(bg) draw.text( (145, 34), "\n".join(textwrap.wrap(", ".join(data["artists"]), width=16)), font=Font1, fill=cfg["color_theme"]["text"], ) draw.text( (11, 164), "\n".join(textwrap.wrap(data["track"], width=28)), font=Font1b, fill=cfg["color_theme"]["text"], ) w, h = bg.size w -= 90 h += 4 progress_time = min( data["duration_ms"], data["progress_ms"] + int((time.time() - last_api_call) * 1000), ) progress = min(1, progress_time / data["duration_ms"]) bar_width = int((w - 10) * progress) draw.rectangle( [(9, h - 23), (w + 3, h - 7)], outline=cfg["color_theme"]["text"], ) draw.rectangle( [(10, h - 22), (w + 2, h - 8)], outline=cfg["color_theme"]["text"], ) draw.rectangle( [(12, h - 20), (w, h - 10)], fill=cfg["color_theme"]["background"] ) draw.rectangle( [(12, h - 20), (12 + bar_width, h - 10)], fill=cfg["color_theme"]["bar_inside"], ) f_current_time = "{}:{:02.0f}".format( *divmod(progress_time // 1000, 60) ) f_total_time = "{}:{:02d}".format( *divmod(data["duration_ms"] // 1000, 60) ) draw.rectangle( [(237, 218), (320, 238)], fill=cfg["color_theme"]["background"] ) draw.text( (238, 223), f"{f_current_time}/{f_total_time}", font=Font0, fill=cfg["color_theme"]["text"], ) d.ShowImage(bg) time.sleep(cfg["refresh_interval"]) except IOError as e: raise e except KeyboardInterrupt: d.module_exit() if __name__ == "__main__": cfg = parse_config() api = Api(cfg["refresh_token"], cfg["base_64"]) display_loop(api, cfg)