music-light/luz-musica.py
2026-04-15 16:28:12 -03:00

165 lines
4.4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import colorsys
import subprocess
import time
from io import BytesIO
from typing import Optional
import requests
from PIL import Image
HA_URL = "http://192.168.1.105:8123"
HA_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkZjM1MmRhNjg1ZWM0MGJiYTc4YTZlNWY3ZmQ0YTA1YSIsImlhdCI6MTc3NjI4MTI0MiwiZXhwIjoyMDkxNjQxMjQyfQ.rR0Kfwk5Smr06_9KJSDn3YPXiRlokEZZFiAa_naNs78"
LIGHT_ENTITY = "light.luz_sala"
# Ajustes visuais
BRIGHTNESS_PCT = 70
SATURATION_BOOST = 1.15
MIN_SATURATION = 0.20
MIN_VALUE = 0.18
REQUEST_TIMEOUT = 10
def run_playerctl(field: str) -> str:
result = subprocess.run(
["playerctl", "metadata", field],
capture_output=True,
text=True,
)
if result.returncode != 0:
return ""
return result.stdout.strip()
def get_current_track_id() -> str:
title = run_playerctl("title")
artist = run_playerctl("artist")
art = run_playerctl("mpris:artUrl")
return f"{artist}::{title}::{art}"
def get_album_art_url() -> str:
return run_playerctl("mpris:artUrl")
def load_image_from_url(url: str) -> Image.Image:
if url.startswith("file://"):
local_path = url[7:]
return Image.open(local_path).convert("RGB")
resp = requests.get(url, timeout=REQUEST_TIMEOUT)
resp.raise_for_status()
return Image.open(BytesIO(resp.content)).convert("RGB")
def good_pixels(img: Image.Image) -> list[tuple[int, int, int]]:
img = img.resize((80, 80))
pixels = list(img.getdata())
filtered: list[tuple[int, int, int]] = []
for r, g, b in pixels:
rf, gf, bf = r / 255.0, g / 255.0, b / 255.0
h, s, v = colorsys.rgb_to_hsv(rf, gf, bf)
# ignora pixels muito escuros e quase cinza
if v < MIN_VALUE:
continue
if s < MIN_SATURATION:
continue
filtered.append((r, g, b))
return filtered
def dominant_color(img: Image.Image) -> tuple[int, int, int]:
pixels = good_pixels(img)
if not pixels:
# fallback simples
small = img.resize((50, 50))
colors = small.getcolors(50 * 50)
if not colors:
return (255, 255, 255)
colors.sort(reverse=True)
return colors[0][1]
# Quantiza para agrupar tons parecidos
buckets: dict[tuple[int, int, int], int] = {}
for r, g, b in pixels:
key = (r // 16 * 16, g // 16 * 16, b // 16 * 16)
buckets[key] = buckets.get(key, 0) + 1
best = max(buckets.items(), key=lambda x: x[1])[0]
return boost_color(best)
def boost_color(rgb: tuple[int, int, int]) -> tuple[int, int, int]:
r, g, b = rgb
rf, gf, bf = r / 255.0, g / 255.0, b / 255.0
h, s, v = colorsys.rgb_to_hsv(rf, gf, bf)
s = min(1.0, s * SATURATION_BOOST)
v = max(v, 0.45)
nr, ng, nb = colorsys.hsv_to_rgb(h, s, v)
return (int(nr * 255), int(ng * 255), int(nb * 255))
def set_light_color(rgb: tuple[int, int, int]) -> None:
response = requests.post(
f"{HA_URL}/api/services/light/turn_on",
headers={
"Authorization": f"Bearer {HA_TOKEN}",
"Content-Type": "application/json",
},
json={
"entity_id": LIGHT_ENTITY,
"rgb_color": list(rgb),
"brightness_pct": BRIGHTNESS_PCT,
},
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
def main() -> None:
print("Monitorando música atual. Ctrl+C para sair.")
last_track = ""
while True:
try:
track_id = get_current_track_id()
if not track_id or track_id == "::":
time.sleep(2)
continue
if track_id != last_track:
art_url = get_album_art_url()
if not art_url:
print("Sem capa disponível para a faixa atual.")
last_track = track_id
time.sleep(2)
continue
print(f"Nova faixa detectada: {track_id}")
img = load_image_from_url(art_url)
rgb = dominant_color(img)
print(f"Aplicando cor: {rgb}")
set_light_color(rgb)
last_track = track_id
time.sleep(2)
except KeyboardInterrupt:
print("\nEncerrado.")
break
except Exception as exc:
print(f"Erro: {exc}")
time.sleep(3)
if __name__ == "__main__":
main()