Aus dem Backup und Live-Pull vom Pi (10.11.3.144): - dashboard.py: Grid-Linien nur im freien Hintergrund (nicht durch Widgets) - templates/index.html: komplett redesigned mit Sidebar + Topbar + Toast + Modal - plugins/clock.py: responsive Layout (1x1 bis 4x4) - plugins/system.py, weather.py, minimax.py: mit Threshold-Bars und Color-Variants - plugins/base.py: NEU — fetch_with_retry Helper (3x retry mit backoff) + render_error_banner für fehlgeschlagene API-Plugins (grosses rotes "!" Icon mit Plugin-Name und Fehler statt Crash) Cleanup: Helfer-Chaos (renderer.py/2/3, design_a/b/c.html, clock_classic.py, 23x clock_*.png, alte test_*.py) wurde bereits im vorherigen Commit entfernt. Co-Authored-By: Hermes <noreply@hermes.local>
79 lines
3.3 KiB
Python
79 lines
3.3 KiB
Python
"""Spotify via Last.fm Scrobble - responsive."""
|
|
import os, sys, json
|
|
import urllib.request, urllib.error, urllib.parse
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
from plugins.base import Widget
|
|
from palette import FG, INFO, OK, WARN, measure, fit_font, centered_text, is_small, is_wide
|
|
|
|
|
|
def _fetch_lastfm(api_key, user):
|
|
url = (f"http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks"
|
|
f"&user={urllib.parse.quote(user)}&api_key={api_key}"
|
|
f"&format=json&limit=2")
|
|
with urllib.request.urlopen(url, timeout=8) as r:
|
|
return json.loads(r.read())
|
|
|
|
|
|
class Widget(Widget):
|
|
name = "spotify"
|
|
label = "Spotify (Last.fm)"
|
|
description = "Zeigt aktuell gespielten Spotify-Track via Last.fm Scrobble."
|
|
category = "info"
|
|
|
|
config_schema = [
|
|
{"key": "api_key", "label": "Last.fm API Key", "type": "secret"},
|
|
{"key": "username", "label": "Last.fm Username", "type": "string", "default": ""},
|
|
]
|
|
default_config = {"api_key": "", "username": ""}
|
|
|
|
def fetch(self):
|
|
api_key = self.cfg("api_key")
|
|
user = self.cfg("username")
|
|
if not api_key or not user:
|
|
return {"_error": "API-Key oder Username fehlt"}
|
|
try:
|
|
return _fetch_lastfm(api_key, user)
|
|
except Exception as e:
|
|
return {"_error": str(e)}
|
|
|
|
def render(self, draw, fonts, x, y, w, h):
|
|
pad = 8
|
|
draw.text((x + pad, y + pad), "SPOTIFY", font=fonts.get("24", fonts.get("20")), fill=INFO)
|
|
d = self.fetch()
|
|
if "_error" in d:
|
|
font = fit_font(draw, "Konfig fehlt", fonts, w - 2 * pad, h - 80)
|
|
centered_text(draw, "Konfig fehlt", x, y + 60, w, h - 60, font, WARN)
|
|
return
|
|
try:
|
|
tracks = d.get("recenttracks", {}).get("track", [])
|
|
if isinstance(tracks, dict): tracks = [tracks]
|
|
if not tracks:
|
|
font = fit_font(draw, "Kein Track", fonts, w - 2 * pad, h - 80)
|
|
centered_text(draw, "Kein Track", x, y + 60, w, h - 60, font, FG)
|
|
return
|
|
current = tracks[0]
|
|
is_playing = current.get("@attr", {}).get("nowplaying") == "true"
|
|
artist = current.get("artist", {}).get("#text", "?")
|
|
track = current.get("name", "?")
|
|
color = OK if is_playing else WARN
|
|
|
|
if is_small(w, h):
|
|
txt = "▶" if is_playing else "⏸"
|
|
font = fit_font(draw, txt, fonts, w - 2 * pad, h - 2 * pad)
|
|
centered_text(draw, txt, x, y, w, h, font, color)
|
|
return
|
|
|
|
# Header symbol
|
|
status_str = "▶ " if is_playing else "⏸"
|
|
font_st = fit_font(draw, status_str, fonts, 50, h // 4)
|
|
draw.text((x + pad, y + 50), status_str, font=font_st, fill=color)
|
|
|
|
# Track info
|
|
font_a = fit_font(draw, artist, fonts, w - 60, h // 4)
|
|
font_t = fit_font(draw, track, fonts, w - 20, h // 4)
|
|
draw.text((x + 60, y + 50), artist[:30], font=font_a, fill=FG)
|
|
draw.text((x + 60, y + 50 + font_a.size + 8), track[:35], font=font_t, fill=FG)
|
|
except Exception as e:
|
|
font = fit_font(draw, f"Fehler: {e}", fonts, w - 2 * pad, h - 80)
|
|
centered_text(draw, f"Fehler: {str(e)[:40]}", x, y + 60, w, h - 60, font, WARN)
|