diff --git a/GEMINI.md b/GEMINI.md index 2305381..83fe543 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -59,6 +59,25 @@ A systemd service file is provided to allow the bridge to run automatically in t The bridge will publish itself on D-Bus as `org.mpris.MediaPlayer2.winamp`. It gracefully handles Winamp being offline, automatically reconnecting when the web interface becomes available. +### Testing and Debugging + +You can use `busctl` to interact with the MPRIS interface: + +```bash +# Get playback status +busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlaybackStatus + +# Get current position (in microseconds) +busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Position + +# Get metadata +busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Metadata + +# Send commands +busctl --user call org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlayPause +busctl --user call org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Next +``` + ## Key Files - **`winamp_mpris.py`**: The main bridge script. Includes optimizations for Plasma 6 property change detection, live position tracking, and robust error handling. diff --git a/README.md b/README.md index 0e1f5fe..24f94cf 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,19 @@ sudo dnf install wmctrl libnotify # or sudo apt install wmctrl libnotify python3 winamp_mpris.py ``` +### Testing and Debugging +You can use `busctl` to interact with the MPRIS interface: +```bash +# Get playback status +busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlaybackStatus + +# Get current position (in microseconds) +busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Position + +# Send commands +busctl --user call org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlayPause +``` + ### Running as a systemd User Service 1. **Install the service file**: ```bash diff --git a/winamp_mpris.py b/winamp_mpris.py index a4aec2b..2d0c1a2 100755 --- a/winamp_mpris.py +++ b/winamp_mpris.py @@ -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()