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 from spotiplayer_pi.lib import LCD_2inch from spotiplayer_pi.api import Api 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/GothamMedium.ttf", 13) 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), # fill=cfg["color_theme"]["text"], ) 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().time() draw.rectangle([(00, 120), (119, 170)], 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), # fill=cfg["color_theme"]["text"], ) draw.text( (20, 152), current_time.strftime("%a %d"), font=Font2, fill=(255, 255, 255), # fill=cfg["color_theme"]["text"], ) 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("updating track") 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 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( [(7, h - 23), (w + 3, h - 7)], # outline=cfg["color_theme"]["text"], outline=cfg["color_theme"]["bar_outline"], ) draw.rectangle( [(8, h - 22), (w + 2, h - 8)], # outline=cfg["color_theme"]["text"], outline=cfg["color_theme"]["bar_outline"], ) draw.rectangle( [(10, h - 20), (w, h - 10)], fill=cfg["color_theme"]["background"] ) draw.rectangle( [(10, h - 20), (10 + 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( [(239, 215), (320, 235)], fill=cfg["color_theme"]["background"] ) draw.text( (240, 220), 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)