Fix position synchronization, add album art fetching, and document busctl usage

- Use Web UI as authoritative time source for better MPRIS position sync
- Implement album art fetching via iTunes Search API
- Optimize window title parsing to focus on metadata
- Add busctl testing commands to README and GEMINI.md
This commit is contained in:
2026-03-19 05:31:11 -04:00
parent 21afe7650a
commit 7fa164f462
3 changed files with 89 additions and 18 deletions

View File

@@ -13,6 +13,7 @@ from bs4 import BeautifulSoup
BASE_URL = "http://localhost:5666"
AUTH = ('winamp', 'llama')
APP_ID = "org.mpris.MediaPlayer2.winamp"
DEFAULT_ART = "https://webamp.org/favicon.ico"
class WinampMPRIS:
"""
@@ -78,6 +79,7 @@ class WinampMPRIS:
self._artist = "Unknown"
self._album = ""
self._status = "Stopped"
self._art_url = DEFAULT_ART
self._last_position_us = 0
self._total_length_us = 0
self._last_update_ts = time.time()
@@ -147,7 +149,8 @@ class WinampMPRIS:
@property
def Position(self):
if self._status == "Playing":
elapsed_us = int((time.time() - self._last_update_ts) * 1e6)
now = time.time()
elapsed_us = int((now - self._last_update_ts) * 1e6)
current_pos = self._last_position_us + elapsed_us
if self._total_length_us > 0:
return min(current_pos, self._total_length_us)
@@ -166,7 +169,7 @@ class WinampMPRIS:
'xesam:title': GLib.Variant('s', self._title),
'xesam:artist': GLib.Variant('as', [self._artist]),
'xesam:album': GLib.Variant('s', self._album),
'mpris:artUrl': GLib.Variant('s', 'https://webamp.org/favicon.ico')
'mpris:artUrl': GLib.Variant('s', self._art_url)
}
def parse_time_to_us(time_str):
@@ -196,9 +199,30 @@ def get_winamp_window_title():
pass
return None
def fetch_album_art(artist, album):
"""Fetches album art from iTunes Search API."""
if not artist or not album or artist == "Unknown":
return DEFAULT_ART
try:
query = f"{artist} {album}"
url = f"https://itunes.apple.com/search?term={query}&entity=album&limit=1"
r = requests.get(url, timeout=3)
if r.status_code == 200:
data = r.json()
if data.get("resultCount", 0) > 0:
art_100 = data["results"][0].get("artworkUrl100", DEFAULT_ART)
# Upgrade to 600x600 for better quality
return art_100.replace("100x100bb.jpg", "600x600bb.jpg")
except Exception as e:
print(f"Art fetch error: {e}")
return DEFAULT_ART
def update_loop(player):
last_known_title = ""
last_known_status = ""
last_known_album = ""
last_time_str = ""
offline_logged = False
while True:
@@ -209,11 +233,10 @@ def update_loop(player):
if window_title:
# Pattern: ArtistAlbumTitle | 0:06/2:45 - Winamp
match = re.search(r"(.*?) \| (\d+:?\d*:\d*)/(\d+:?\d*:\d*) - Winamp", window_title)
# Only use window title for metadata as time is unreliable here
match = re.search(r"(.*?) \| .* - 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 (ArtistAlbumTitle)
meta_parts = [p.strip() for p in re.split(r'[–—-]', metadata_raw)]
@@ -231,12 +254,9 @@ def update_loop(player):
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)
# 2. Poll Web UI (Status and Time source)
try:
r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=1)
if r.status_code == 200:
@@ -251,20 +271,27 @@ def update_loop(player):
player._status = new_status
# Always update time from Web UI
match_time = re.search(r"\((\d+:?\d*:\d+) / (\d+:?\d*:\d+)\)", status_raw)
if match_time:
current_time_str = match_time.group(1)
if current_time_str != last_time_str:
player._last_position_us = parse_time_to_us(current_time_str)
player._total_length_us = parse_time_to_us(match_time.group(2))
player._last_update_ts = time.time()
last_time_str = current_time_str
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()
player._album = ""
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()
player._artist = "Unknown"
player._album = ""
except requests.exceptions.RequestException:
if not window_title:
if not offline_logged:
@@ -272,11 +299,22 @@ def update_loop(player):
offline_logged = True
player._status = "Stopped"
if player._status != last_known_status or player._title != last_known_title:
if (player._status != last_known_status or
player._title != last_known_title or
player._album != last_known_album):
# Fetch art if album/artist changed
if player._artist != "Unknown" and player._album:
player._art_url = fetch_album_art(player._artist, player._album)
else:
player._art_url = DEFAULT_ART
last_known_status = player._status
last_known_title = player._title
last_known_album = player._album
print(f"UPDATE: [{player._status}] {player._artist} - {player._title}")
album_str = f" [{player._album}]" if player._album else ""
print(f"UPDATE: [{player._status}] {player._artist}{album_str} - {player._title}")
player.PropertiesChanged(
"org.mpris.MediaPlayer2.Player",
@@ -292,6 +330,7 @@ def update_loop(player):
time.sleep(1)
if __name__ == "__main__":
bus = SessionBus()
player_logic = WinampMPRIS()
@@ -309,7 +348,7 @@ if __name__ == "__main__":
thread = Thread(target=update_loop, args=(player_logic,), daemon=True)
thread.start()
print(f"--- Winamp Bridge Started (Window Title + Web UI) ---")
print(f"--- Winamp Bridge Started (Window Title + Web UI + Album Art) ---")
loop = GLib.MainLoop()
try:
loop.run()