Initial commit: Functional Winamp MPRIS bridge with Plasma 6 fixes
This commit is contained in:
52
GEMINI.md
Normal file
52
GEMINI.md
Normal 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.
|
||||||
57
sample_Winamp Web Interface.html
Normal file
57
sample_Winamp Web Interface.html
Normal 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>
|
||||||
|
·<a href=/vol?volume=1>1</a>
|
||||||
|
·<a href=/vol?volume=2>2</a>
|
||||||
|
·<a href=/vol?volume=3>3</a>
|
||||||
|
·<a href=/vol?volume=4>4</a>
|
||||||
|
·<a href=/vol?volume=5>5</a>
|
||||||
|
·<a href=/vol?volume=6>6</a>
|
||||||
|
·<a href=/vol?volume=7>7</a>
|
||||||
|
·<a href=/vol?volume=8>8</a>
|
||||||
|
·<a href=/vol?volume=9>9</a>
|
||||||
|
·<a href=/vol?volume=10>10</a>
|
||||||
|
<br>
|
||||||
|
<a href="/prev">Previous</a>
|
||||||
|
· <a href="/play">Play</a>
|
||||||
|
· <a href="/pause">Pause</a>
|
||||||
|
· <a href="/stop">Stop</a>
|
||||||
|
· <a href="/next">Next</A>
|
||||||
|
<br><a href="/main">Main</a>
|
||||||
|
· <a href="/list">Playlist</a>
|
||||||
|
· <a href="/browse?path=%5c">Music Collection</a>
|
||||||
|
· <a href="/admin">Admin</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
226
winamp_mpris.py
Executable file
226
winamp_mpris.py
Executable 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
|
||||||
Reference in New Issue
Block a user