From d7af92f6b178e80fc04a0ce02169fb95ee1877ab Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Wed, 15 Apr 2026 17:00:53 -0300 Subject: [PATCH] Improve active player artwork detection --- .gitignore | 1 + flake.lock | 27 +++++++++++ luz-musica.py | 132 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 flake.lock diff --git a/.gitignore b/.gitignore index 2612d50..95d7c91 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ result-* # IDE / OS .idea/ .vscode/ +.codex .DS_Store Thumbs.db diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..682004d --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/luz-musica.py b/luz-musica.py index 7fcc761..c98d4ea 100644 --- a/luz-musica.py +++ b/luz-musica.py @@ -5,14 +5,14 @@ import colorsys import subprocess import time from io import BytesIO -from typing import Optional +from urllib.parse import parse_qs, urlparse 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" +LIGHT_ENTITY = "light.luzsala_luz_sala" # Ajustes visuais BRIGHTNESS_PCT = 70 @@ -22,9 +22,14 @@ MIN_VALUE = 0.18 REQUEST_TIMEOUT = 10 -def run_playerctl(field: str) -> str: +def run_playerctl(field: str, player: str | None = None) -> str: + command = ["playerctl"] + if player: + command.extend(["--player", player]) + command.extend(["metadata", field]) + result = subprocess.run( - ["playerctl", "metadata", field], + command, capture_output=True, text=True, ) @@ -33,15 +38,103 @@ def run_playerctl(field: str) -> str: 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 list_players() -> list[str]: + result = subprocess.run( + ["playerctl", "-l"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + return [] + return [line.strip() for line in result.stdout.splitlines() if line.strip()] -def get_album_art_url() -> str: - return run_playerctl("mpris:artUrl") +def player_status(player: str) -> str: + result = subprocess.run( + ["playerctl", "--player", player, "status"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + return "" + return result.stdout.strip() + + +def choose_active_player() -> str: + for player in list_players(): + if player_status(player) == "Playing": + return player + return "" + + +def get_track_url(player: str) -> str: + return run_playerctl("xesam:url", player) + + +def get_player_name(player: str) -> str: + result = subprocess.run( + ["playerctl", "--player", player, "metadata", "--format", "{{playerName}}"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + return player + return result.stdout.strip() or player + + +def youtube_video_id(url: str) -> str: + if not url: + return "" + + parsed = urlparse(url) + host = parsed.netloc.lower() + path = parsed.path.strip("/") + + if host in {"youtu.be", "www.youtu.be"}: + return path.split("/", 1)[0] + + if "youtube.com" not in host: + return "" + + if path == "watch": + return parse_qs(parsed.query).get("v", [""])[0] + + parts = path.split("/") + if len(parts) >= 2 and parts[0] in {"shorts", "live", "embed", "v"}: + return parts[1] + + return "" + + +def youtube_thumbnail_url(player: str) -> str: + video_id = youtube_video_id(get_track_url(player)) + if not video_id: + return "" + return f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg" + + +def get_current_track_id(player: str) -> str: + title = run_playerctl("title", player) + artist = run_playerctl("artist", player) + art = run_playerctl("mpris:artUrl", player) + url = get_track_url(player) + player_name = get_player_name(player) + return f"{player_name}::{artist}::{title}::{art or url}" + + +def get_album_art_url(player: str) -> str: + art_url = run_playerctl("mpris:artUrl", player) + if art_url: + return art_url + + track_url = get_track_url(player) + if youtube_video_id(track_url): + thumb_url = youtube_thumbnail_url(player) + if thumb_url: + print(f"Usando thumbnail do YouTube: {thumb_url}") + return thumb_url + + return "" def load_image_from_url(url: str) -> Image.Image: @@ -54,9 +147,15 @@ def load_image_from_url(url: str) -> Image.Image: return Image.open(BytesIO(resp.content)).convert("RGB") +def image_pixels(img: Image.Image) -> list[tuple[int, int, int]]: + if hasattr(img, "get_flattened_data"): + return list(img.get_flattened_data()) + return list(img.getdata()) + + def good_pixels(img: Image.Image) -> list[tuple[int, int, int]]: img = img.resize((80, 80)) - pixels = list(img.getdata()) + pixels = image_pixels(img) filtered: list[tuple[int, int, int]] = [] for r, g, b in pixels: @@ -130,14 +229,19 @@ def main() -> None: while True: try: - track_id = get_current_track_id() + player = choose_active_player() + if not player: + time.sleep(2) + continue + + track_id = get_current_track_id(player) if not track_id or track_id == "::": time.sleep(2) continue if track_id != last_track: - art_url = get_album_art_url() + art_url = get_album_art_url(player) if not art_url: print("Sem capa disponível para a faixa atual.") last_track = track_id