227 lines
7.9 KiB
Python
Executable File
227 lines
7.9 KiB
Python
Executable File
#!/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
|