commit f597c9656c774e0b7fcb921035c3e103c9862d12 Author: ergosteur Date: Thu Mar 19 02:50:17 2026 -0400 Initial commit: Functional Winamp MPRIS bridge with Plasma 6 fixes diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..5810843 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,52 @@ +# Winamp MPRIS Bridge + +A Python-based bridge that provides an MPRIS2 interface for Winamp on Linux, specifically optimized for KDE Plasma 6. It allows Linux desktop environments to control Winamp and display track metadata by interacting with the Winamp Web Interface plugin. + +## Project Overview + +- **Purpose:** Bridges Winamp's Web Interface to the Linux MPRIS2 D-Bus specification. +- **Technologies:** + - **Python 3**: Core logic. + - **pydbus**: D-Bus communication. + - **requests**: Communicating with the Winamp Web Interface. + - **BeautifulSoup4**: Scraping metadata from the Web Interface's HTML. + - **PyGObject (GLib)**: Main event loop and signal handling. +- **Architecture:** The script runs a background thread that polls the Winamp Web Interface (default: `http://localhost:5666`) every 2 seconds, parses the HTML for metadata, and updates the D-Bus properties. + +## Building and Running + +### Prerequisites + +Ensure you have the following Python libraries installed: + +```bash +pip install requests pydbus beautifulsoup4 pygobject +``` + +You also need the `Winamp Web Interface` plugin installed and running in Winamp, configured with: +- **Base URL:** `http://localhost:5666` +- **Username:** `winamp` +- **Password:** `llama` (These are the current defaults in `winamp_mpris.py`) + +### Running the Bridge + +To start the bridge, run: + +```bash +python3 winamp_mpris.py +``` + +The bridge will publish itself on D-Bus as `org.mpris.MediaPlayer2.winamp`. + +## Key Files + +- **`winamp_mpris.py`**: The main, active script. Includes optimizations for Plasma 6 property change detection and correct D-Bus signal emission. +- **`winamp_mpris_old.py`**: A previous iteration of the bridge. +- **`sample_Winamp Web Interface.html`**: A sample of the HTML returned by the Winamp Web Interface, used for reference in parsing logic. + +## Development Conventions + +- **D-Bus Interface:** Defined via XML introspection string within the `WinampMPRIS` class. +- **Polling:** Metadata is updated by polling the web interface every 2 seconds in a daemon thread. +- **Property Changes:** Uses the `PropertiesChanged` signal to notify the desktop environment of track or status updates. +- **Error Handling:** Basic try-except blocks handle network timeouts or parsing errors, defaulting to "Stopped" or "Offline" states. diff --git a/sample_Winamp Web Interface.html b/sample_Winamp Web Interface.html new file mode 100644 index 0000000..7a94257 --- /dev/null +++ b/sample_Winamp Web Interface.html @@ -0,0 +1,57 @@ + + + +Winamp Web Interface + + + + + +

+ +... Winamp Web Interface ... + + +

+
+About Winamp Web Interface v.7.5.10

+

+ +... Winamp Status ... +

+

+Playing track 27 - ARTMS - Verified Beauty - (1:06 / 2:48) +

+

Repeat is off [ on | off | toggle ]
Random is off [ on | off | toggle ]

+

+Volume: +012345678910 +
+PreviousPlayPauseStopNext +
MainPlaylistMusic CollectionAdmin +

+ + + diff --git a/winamp_mpris.py b/winamp_mpris.py new file mode 100755 index 0000000..8d33aa8 --- /dev/null +++ b/winamp_mpris.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +import requests +import time +import re +from threading import Thread +from pydbus import SessionBus +from pydbus.generic import signal +from gi.repository import GLib +from bs4 import BeautifulSoup + +# --- CONFIGURATION --- +BASE_URL = "http://localhost:5666" +AUTH = ('winamp', 'llama') +APP_ID = "org.mpris.MediaPlayer2.winamp" + +class WinampMPRIS: + """ + MPRIS2 specification implementation for Winamp Web Interface. + Optimized for Plasma 6 property change detection. + """ + dbus = f""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + + # Signal for org.freedesktop.DBus.Properties.PropertiesChanged + PropertiesChanged = signal() + + def __init__(self): + self._title = "Winamp" + self._artist = "Unknown" + self._status = "Stopped" + + def _request(self, endpoint): + try: + requests.get(f"{BASE_URL}/{endpoint}", auth=AUTH, timeout=2) + except Exception as e: + print(f"Request failed: {e}") + + # MPRIS Methods + def Next(self): self._request("next") + def Previous(self): self._request("prev") + def Pause(self): self._request("pause") + def PlayPause(self): self._request("pause") + def Play(self): self._request("play") + def Stop(self): self._request("stop") + def Seek(self, offset): pass + def SetPosition(self, track_id, position): pass + def OpenUri(self, uri): pass + + # --- Root Properties --- + @property + def CanQuit(self): return False + @property + def CanRaise(self): return False + @property + def HasTrackList(self): return False + @property + def Identity(self): return "Winamp Web Bridge" + @property + def DesktopEntry(self): return "" + @property + def SupportedUriSchemes(self): return [] + @property + def SupportedMimeTypes(self): return [] + + # --- Player Properties --- + @property + def PlaybackStatus(self): return self._status + @property + def LoopStatus(self): return "None" + @property + def Rate(self): return 1.0 + @property + def Shuffle(self): return False + @property + def CanGoNext(self): return True + @property + def CanGoPrevious(self): return True + @property + def CanPlay(self): return True + @property + def CanPause(self): return True + @property + def CanControl(self): return True + @property + def CanSeek(self): return False + @property + def Position(self): return 0 + @property + def Volume(self): return 1.0 + + @property + def Metadata(self): + # Metadata is a{sv}, so values MUST be wrapped in GLib.Variant + return { + 'mpris:trackid': GLib.Variant('o', '/org/mpris/MediaPlayer2/winamp/track/0'), + 'mpris:length': GLib.Variant('x', 0), + 'xesam:title': GLib.Variant('s', self._title), + 'xesam:artist': GLib.Variant('as', [self._artist]), + 'xesam:album': GLib.Variant('s', ''), + 'mpris:artUrl': GLib.Variant('s', 'https://webamp.org/favicon.ico') + } + +def update_loop(player): + last_known_title = "" + last_known_status = "" + + while True: + try: + r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=2) + if r.status_code == 200: + 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" + + match = re.search(r"track \d+ - (.*?) - (.*?) -", status_raw) + if match: + new_artist = match.group(1).strip() + new_title = match.group(2).strip() + elif "track" in status_raw: + new_title = status_raw.split('-')[0].replace("Playing track ", "").strip() + + if new_status != last_known_status or new_title != last_known_title: + player._status = new_status + player._artist = new_artist + player._title = new_title + last_known_status = new_status + last_known_title = new_title + + print(f"UPDATE: [{player._status}] {player._artist} - {player._title}") + + # pydbus PropertiesChanged helper will wrap these in Variants + # based on the introspection XML. + # Metadata is a{sv}, so its value (the dict) will be wrapped in Variant('a{sv}', ...) + # For that to work, the dict's values must already be Variants. + player.PropertiesChanged( + "org.mpris.MediaPlayer2.Player", + { + "PlaybackStatus": player.PlaybackStatus, + "Metadata": player.Metadata + }, + [] + ) + + except Exception as e: + print(f"Update error: {e}") + import traceback + traceback.print_exc() + time.sleep(2) + +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", + { + "PlaybackStatus": player_logic.PlaybackStatus, + "Metadata": player_logic.Metadata + }, + [] + )) + + thread = Thread(target=update_loop, args=(player_logic,), daemon=True) + thread.start() + + print(f"--- Winamp Bridge Started (Plasma Detection Fix) ---") + loop = GLib.MainLoop() + try: + loop.run() + except KeyboardInterrupt: + pass