Initial commit: Functional Winamp MPRIS bridge with Plasma 6 fixes

This commit is contained in:
2026-03-19 02:50:17 -04:00
commit f597c9656c
3 changed files with 335 additions and 0 deletions

52
GEMINI.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,57 @@
<html>
<head>
<title>
Winamp Web Interface
</title>
<link rel=STYLESHEET href="wawi.css" type="text/css">
<Script language="JavaScript">
function about()
{
window.open("about","newWin","width=300,height=150");
}
</script>
</head>
<body bgcolor="#EEEEEE"><h1 align=center>
<i>
... Winamp Web Interface ...
</i>
</h1>
<h6 align=center>
<a href="JavaScript:about()">About Winamp Web Interface v.7.5.10</a></h6><hr>
<h3>
<i>
... Winamp Status ...
</i></h3>
<p>
Playing track 27 - ARTMS - Verified Beauty - (1:06 / 2:48)
</p>
<p>Repeat is <b>off</b> [ <a href="/playmode?repeat=on">on</a> | <a href="/playmode?repeat=off">off</a> | <a href="/playmode?repeat=toggle">toggle</a> ]<br>Random is <b>off</b> [ <a href="/playmode?random=on">on</a> | <a href="/playmode?random=off">off</a> | <a href="/playmode?random=toggle">toggle</a> ]</p>
<p align=center>
Volume:
<a href=/vol?volume=0>0</a>
&middot;<a href=/vol?volume=1>1</a>
&middot;<a href=/vol?volume=2>2</a>
&middot;<a href=/vol?volume=3>3</a>
&middot;<a href=/vol?volume=4>4</a>
&middot;<a href=/vol?volume=5>5</a>
&middot;<a href=/vol?volume=6>6</a>
&middot;<a href=/vol?volume=7>7</a>
&middot;<a href=/vol?volume=8>8</a>
&middot;<a href=/vol?volume=9>9</a>
&middot;<a href=/vol?volume=10>10</a>
<br>
<a href="/prev">Previous</a>
&middot; <a href="/play">Play</a>
&middot; <a href="/pause">Pause</a>
&middot; <a href="/stop">Stop</a>
&middot; <a href="/next">Next</A>
<br><a href="/main">Main</a>
&middot; <a href="/list">Playlist</a>
&middot; <a href="/browse?path=%5c">Music Collection</a>
&middot; <a href="/admin">Admin</a>
</p>
</body>
</html>

226
winamp_mpris.py Executable file
View File

@@ -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"""
<node>
<interface name="org.mpris.MediaPlayer2">
<property name="CanQuit" type="b" access="read" />
<property name="CanRaise" type="b" access="read" />
<property name="HasTrackList" type="b" access="read" />
<property name="Identity" type="s" access="read" />
<property name="DesktopEntry" type="s" access="read" />
<property name="SupportedUriSchemes" type="as" access="read" />
<property name="SupportedMimeTypes" type="as" access="read" />
</interface>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Next" />
<method name="Previous" />
<method name="Pause" />
<method name="PlayPause" />
<method name="Stop" />
<method name="Play" />
<method name="Seek">
<arg type="x" name="Offset" direction="in" />
</method>
<method name="SetPosition">
<arg type="o" name="TrackId" direction="in" />
<arg type="x" name="Position" direction="in" />
</method>
<method name="OpenUri">
<arg type="s" name="Uri" direction="in" />
</method>
<property name="PlaybackStatus" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="LoopStatus" type="s" access="read" />
<property name="Rate" type="d" access="read" />
<property name="Shuffle" type="b" access="read" />
<property name="Metadata" type="a{{sv}}" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="CanGoNext" type="b" access="read" />
<property name="CanGoPrevious" type="b" access="read" />
<property name="CanPlay" type="b" access="read" />
<property name="CanPause" type="b" access="read" />
<property name="CanControl" type="b" access="read" />
<property name="CanSeek" type="b" access="read" />
<property name="Position" type="x" access="read" />
<property name="Volume" type="d" access="read" />
</interface>
</node>
"""
# 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