From 06f43992d6214b8a46d8f438465b8c103dd04120 Mon Sep 17 00:00:00 2001 From: ergosteur Date: Thu, 19 Mar 2026 04:00:57 -0400 Subject: [PATCH] Finalize winamp_mpris.py with window title and web UI hybrid logic --- winamp_mpris.py | 179 ++++++++++++++++++++++++++++-------------------- 1 file changed, 104 insertions(+), 75 deletions(-) diff --git a/winamp_mpris.py b/winamp_mpris.py index 8567fa7..37731c6 100755 --- a/winamp_mpris.py +++ b/winamp_mpris.py @@ -2,6 +2,7 @@ import requests import time import re +import subprocess from threading import Thread from pydbus import SessionBus from pydbus.generic import signal @@ -75,6 +76,7 @@ class WinampMPRIS: def __init__(self): self._title = "Winamp" self._artist = "Unknown" + self._album = "" self._status = "Stopped" self._last_position_us = 0 self._total_length_us = 0 @@ -156,19 +158,37 @@ class WinampMPRIS: 'mpris:length': GLib.Variant('x', self._total_length_us), 'xesam:title': GLib.Variant('s', self._title), 'xesam:artist': GLib.Variant('as', [self._artist]), - 'xesam:album': GLib.Variant('s', ''), + 'xesam:album': GLib.Variant('s', self._album), 'mpris:artUrl': GLib.Variant('s', 'https://webamp.org/favicon.ico') } def parse_time_to_us(time_str): """Parses MM:SS or H:MM:SS to microseconds.""" - parts = list(map(int, time_str.split(':'))) - if len(parts) == 2: # MM:SS - return (parts[0] * 60 + parts[1]) * 1_000_000 - elif len(parts) == 3: # H:MM:SS - return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1_000_000 + try: + parts = list(map(int, time_str.split(':'))) + if len(parts) == 2: # MM:SS + return (parts[0] * 60 + parts[1]) * 1_000_000 + elif len(parts) == 3: # H:MM:SS + return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1_000_000 + except Exception: + pass return 0 +def get_winamp_window_title(): + """Extracts the Winamp window title using wmctrl.""" + try: + output = subprocess.check_output(["wmctrl", "-l"], stderr=subprocess.STDOUT).decode('utf-8') + for line in output.splitlines(): + # Match the typical Winamp window title pattern + if " - Winamp" in line: + # wmctrl output: 0x03000001 0 Compy-686 ARTMS––Unf/Air | 0:06/2:45 - Winamp + parts = line.split(None, 3) + if len(parts) >= 4: + return parts[3] + except Exception: + pass + return None + def update_loop(player): last_known_title = "" last_known_status = "" @@ -176,74 +196,81 @@ def update_loop(player): while True: try: - r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=2) - if r.status_code == 200: - offline_logged = False - soup = BeautifulSoup(r.text, 'html.parser') - p_tags = soup.find_all('p') - status_raw = p_tags[0].text if p_tags else "" - - new_status = "Stopped" - if "Playing" in status_raw: new_status = "Playing" - elif "Paused" in status_raw: new_status = "Paused" - - new_artist = "Unknown" - new_title = "Winamp" - new_pos_us = 0 - new_len_us = 0 - - # Metadata match - match_meta = re.search(r"track \d+ - (.*?) - (.*?) -", status_raw) - if match_meta: - new_artist = match_meta.group(1).strip() - new_title = match_meta.group(2).strip() - elif "track" in status_raw: - new_title = status_raw.split('-')[0].replace("Playing track ", "").strip() - - # Time match: (1:06 / 2:48) - match_time = re.search(r"\((\d+:?\d*:\d+) / (\d+:?\d*:\d+)\)", status_raw) - if match_time: - new_pos_us = parse_time_to_us(match_time.group(1)) - new_len_us = parse_time_to_us(match_time.group(2)) - - # Update internal state - player._status = new_status - player._artist = new_artist - player._title = new_title - player._last_position_us = new_pos_us - player._total_length_us = new_len_us - player._last_update_ts = time.time() - - if new_status != last_known_status or new_title != last_known_title: - last_known_status = new_status - last_known_title = new_title - - print(f"UPDATE: [{player._status}] {player._artist} - {player._title} ({match_time.group(1) if match_time else '0:00'})") - - player.PropertiesChanged( - "org.mpris.MediaPlayer2.Player", - { - "PlaybackStatus": player.PlaybackStatus, - "Metadata": player.Metadata - }, - [] - ) - - except (requests.exceptions.RequestException, Exception) as e: - if not offline_logged: - print(f"Winamp Web Interface offline or unreachable: {e}") - offline_logged = True + # 1. Try Window Title (Primary) + window_title = get_winamp_window_title() + title_parsed = False - # Reset state when offline - player._status = "Stopped" - player._artist = "Unknown" - player._title = "Winamp (Offline)" - player._last_position_us = 0 - player._total_length_us = 0 - player._last_update_ts = time.time() + if window_title: + # Pattern: Artist–Album–Title | 0:06/2:45 - Winamp + match = re.search(r"(.*?) \| (\d+:?\d*:\d*)/(\d+:?\d*:\d*) - Winamp", window_title) + if match: + metadata_raw = match.group(1).strip() + new_pos_us = parse_time_to_us(match.group(2)) + new_len_us = parse_time_to_us(match.group(3)) + + # Split metadata (Artist–Album–Title) + meta_parts = [p.strip() for p in re.split(r'[–—-]', metadata_raw)] + + if len(meta_parts) >= 3: + player._artist = meta_parts[0] + player._album = meta_parts[1] + player._title = " – ".join(meta_parts[2:]) + elif len(meta_parts) == 2: + player._artist = meta_parts[0] + player._title = meta_parts[1] + player._album = "" + else: + player._title = metadata_raw + player._artist = "Unknown" + player._album = "" + + player._last_position_us = new_pos_us + player._total_length_us = new_len_us + player._last_update_ts = time.time() + title_parsed = True + + # 2. Poll Web UI (Fallback and Controls/Status) + try: + r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=1) + if r.status_code == 200: + offline_logged = False + soup = BeautifulSoup(r.text, 'html.parser') + p_tags = soup.find_all('p') + status_raw = p_tags[0].text if p_tags else "" + + new_status = "Stopped" + if "Playing" in status_raw: new_status = "Playing" + elif "Paused" in status_raw: new_status = "Paused" + + player._status = new_status + + if not title_parsed: + # Web UI Fallback for metadata + match_meta = re.search(r"track \d+ - (.*?) - (.*?) -", status_raw) + if match_meta: + player._artist = match_meta.group(1).strip() + player._title = match_meta.group(2).strip() + elif "track" in status_raw: + player._title = status_raw.split('-')[0].replace("Playing track ", "").strip() + + match_time = re.search(r"\((\d+:?\d*:\d+) / (\d+:?\d*:\d+)\)", status_raw) + if match_time: + player._last_position_us = parse_time_to_us(match_time.group(1)) + player._total_length_us = parse_time_to_us(match_time.group(2)) + player._last_update_ts = time.time() + except requests.exceptions.RequestException: + if not window_title: + if not offline_logged: + print("Winamp Web Interface offline and no window found.") + offline_logged = True + player._status = "Stopped" + + if player._status != last_known_status or player._title != last_known_title: + last_known_status = player._status + last_known_title = player._title + + print(f"UPDATE: [{player._status}] {player._artist} - {player._title}") - if last_known_status != "Stopped": - last_known_status = "Stopped" player.PropertiesChanged( "org.mpris.MediaPlayer2.Player", { @@ -253,14 +280,16 @@ def update_loop(player): [] ) - time.sleep(2) + except Exception as e: + print(f"Update error: {e}") + + time.sleep(1) if __name__ == "__main__": bus = SessionBus() player_logic = WinampMPRIS() bus.publish(APP_ID, ("/org/mpris/MediaPlayer2", player_logic)) - # Delayed wake-up call GLib.timeout_add(1500, lambda: player_logic.PropertiesChanged( "org.mpris.MediaPlayer2.Player", { @@ -273,7 +302,7 @@ if __name__ == "__main__": thread = Thread(target=update_loop, args=(player_logic,), daemon=True) thread.start() - print(f"--- Winamp Bridge Started (Plasma Detection Fix) ---") + print(f"--- Winamp Bridge Started (Window Title + Web UI) ---") loop = GLib.MainLoop() try: loop.run()