Finalize winamp_mpris.py with window title and web UI hybrid logic

This commit is contained in:
2026-03-19 04:00:57 -04:00
parent e59380c929
commit 06f43992d6

View File

@@ -2,6 +2,7 @@
import requests import requests
import time import time
import re import re
import subprocess
from threading import Thread from threading import Thread
from pydbus import SessionBus from pydbus import SessionBus
from pydbus.generic import signal from pydbus.generic import signal
@@ -75,6 +76,7 @@ class WinampMPRIS:
def __init__(self): def __init__(self):
self._title = "Winamp" self._title = "Winamp"
self._artist = "Unknown" self._artist = "Unknown"
self._album = ""
self._status = "Stopped" self._status = "Stopped"
self._last_position_us = 0 self._last_position_us = 0
self._total_length_us = 0 self._total_length_us = 0
@@ -156,19 +158,37 @@ class WinampMPRIS:
'mpris:length': GLib.Variant('x', self._total_length_us), 'mpris:length': GLib.Variant('x', self._total_length_us),
'xesam:title': GLib.Variant('s', self._title), 'xesam:title': GLib.Variant('s', self._title),
'xesam:artist': GLib.Variant('as', [self._artist]), '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') 'mpris:artUrl': GLib.Variant('s', 'https://webamp.org/favicon.ico')
} }
def parse_time_to_us(time_str): def parse_time_to_us(time_str):
"""Parses MM:SS or H:MM:SS to microseconds.""" """Parses MM:SS or H:MM:SS to microseconds."""
parts = list(map(int, time_str.split(':'))) try:
if len(parts) == 2: # MM:SS parts = list(map(int, time_str.split(':')))
return (parts[0] * 60 + parts[1]) * 1_000_000 if len(parts) == 2: # MM:SS
elif len(parts) == 3: # H:MM:SS return (parts[0] * 60 + parts[1]) * 1_000_000
return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 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 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<Dall>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): def update_loop(player):
last_known_title = "" last_known_title = ""
last_known_status = "" last_known_status = ""
@@ -176,74 +196,81 @@ def update_loop(player):
while True: while True:
try: try:
r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=2) # 1. Try Window Title (Primary)
if r.status_code == 200: window_title = get_winamp_window_title()
offline_logged = False title_parsed = 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
# Reset state when offline if window_title:
player._status = "Stopped" # Pattern: ArtistAlbumTitle | 0:06/2:45 - Winamp
player._artist = "Unknown" match = re.search(r"(.*?) \| (\d+:?\d*:\d*)/(\d+:?\d*:\d*) - Winamp", window_title)
player._title = "Winamp (Offline)" if match:
player._last_position_us = 0 metadata_raw = match.group(1).strip()
player._total_length_us = 0 new_pos_us = parse_time_to_us(match.group(2))
player._last_update_ts = time.time() new_len_us = parse_time_to_us(match.group(3))
# Split metadata (ArtistAlbumTitle)
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( player.PropertiesChanged(
"org.mpris.MediaPlayer2.Player", "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__": if __name__ == "__main__":
bus = SessionBus() bus = SessionBus()
player_logic = WinampMPRIS() player_logic = WinampMPRIS()
bus.publish(APP_ID, ("/org/mpris/MediaPlayer2", player_logic)) bus.publish(APP_ID, ("/org/mpris/MediaPlayer2", player_logic))
# Delayed wake-up call
GLib.timeout_add(1500, lambda: player_logic.PropertiesChanged( GLib.timeout_add(1500, lambda: player_logic.PropertiesChanged(
"org.mpris.MediaPlayer2.Player", "org.mpris.MediaPlayer2.Player",
{ {
@@ -273,7 +302,7 @@ if __name__ == "__main__":
thread = Thread(target=update_loop, args=(player_logic,), daemon=True) thread = Thread(target=update_loop, args=(player_logic,), daemon=True)
thread.start() thread.start()
print(f"--- Winamp Bridge Started (Plasma Detection Fix) ---") print(f"--- Winamp Bridge Started (Window Title + Web UI) ---")
loop = GLib.MainLoop() loop = GLib.MainLoop()
try: try:
loop.run() loop.run()