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:
@@ -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: Artist–Album–Title | 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 (Artist–Album–Title)
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user