diff --git a/GEMINI.md b/GEMINI.md
index 5810843..2305381 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -30,17 +30,39 @@ You also need the `Winamp Web Interface` plugin installed and running in Winamp,
### Running the Bridge
-To start the bridge, run:
+### Manual Run
+
+To start the bridge manually, run:
```bash
python3 winamp_mpris.py
```
-The bridge will publish itself on D-Bus as `org.mpris.MediaPlayer2.winamp`.
+### Running as a systemd User Service
+
+A systemd service file is provided to allow the bridge to run automatically in the background.
+
+1. **Install the service file**:
+ ```bash
+ mkdir -p ~/.config/systemd/user/
+ cp winamp-mpris.service ~/.config/systemd/user/
+ ```
+2. **Reload systemd and enable the service**:
+ ```bash
+ systemctl --user daemon-reload
+ systemctl --user enable --now winamp-mpris.service
+ ```
+3. **Check the status**:
+ ```bash
+ systemctl --user status winamp-mpris.service
+ ```
+
+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.
## 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.py`**: The main bridge script. Includes optimizations for Plasma 6 property change detection, live position tracking, and robust error handling.
+- **`winamp-mpris.service`**: Systemd user service unit file.
- **`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.
diff --git a/winamp-mpris.service b/winamp-mpris.service
new file mode 100644
index 0000000..827c0af
--- /dev/null
+++ b/winamp-mpris.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Winamp MPRIS Bridge
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/python3 %h/Scripts/winamp-mpris/winamp_mpris.py
+Restart=always
+RestartSec=5
+
+[Install]
+WantedBy=default.target
diff --git a/winamp_mpris.py b/winamp_mpris.py
index 8d33aa8..8567fa7 100755
--- a/winamp_mpris.py
+++ b/winamp_mpris.py
@@ -61,7 +61,9 @@ class WinampMPRIS:
-
+
+
+
@@ -74,6 +76,9 @@ class WinampMPRIS:
self._title = "Winamp"
self._artist = "Unknown"
self._status = "Stopped"
+ self._last_position_us = 0
+ self._total_length_us = 0
+ self._last_update_ts = time.time()
def _request(self, endpoint):
try:
@@ -129,8 +134,17 @@ class WinampMPRIS:
def CanControl(self): return True
@property
def CanSeek(self): return False
+
@property
- def Position(self): return 0
+ def Position(self):
+ if self._status == "Playing":
+ elapsed_us = int((time.time() - 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)
+ return current_pos
+ return self._last_position_us
+
@property
def Volume(self): return 1.0
@@ -139,21 +153,32 @@ class WinampMPRIS:
# 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),
+ 'mpris:length': GLib.Variant('x', self._total_length_us),
'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 parse_time_to_us(time_str):
+ """Parses MM:SS or H:MM:SS to microseconds."""
+ parts = list(map(int, time_str.split(':')))
+ if len(parts) == 2: # MM:SS
+ return (parts[0] * 60 + parts[1]) * 1_000_000
+ elif len(parts) == 3: # H:MM:SS
+ return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1_000_000
+ return 0
+
def update_loop(player):
last_known_title = ""
last_known_status = ""
+ offline_logged = False
while True:
try:
r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=2)
if r.status_code == 200:
+ offline_logged = False
soup = BeautifulSoup(r.text, 'html.parser')
p_tags = soup.find_all('p')
status_raw = p_tags[0].text if p_tags else ""
@@ -164,27 +189,37 @@ def update_loop(player):
new_artist = "Unknown"
new_title = "Winamp"
+ new_pos_us = 0
+ new_len_us = 0
- match = re.search(r"track \d+ - (.*?) - (.*?) -", status_raw)
- if match:
- new_artist = match.group(1).strip()
- new_title = match.group(2).strip()
+ # Metadata match
+ match_meta = re.search(r"track \d+ - (.*?) - (.*?) -", status_raw)
+ if match_meta:
+ new_artist = match_meta.group(1).strip()
+ new_title = match_meta.group(2).strip()
elif "track" in status_raw:
new_title = status_raw.split('-')[0].replace("Playing track ", "").strip()
+ # Time match: (1:06 / 2:48)
+ match_time = re.search(r"\((\d+:?\d*:\d+) / (\d+:?\d*:\d+)\)", status_raw)
+ if match_time:
+ new_pos_us = parse_time_to_us(match_time.group(1))
+ new_len_us = parse_time_to_us(match_time.group(2))
+
+ # Update internal state
+ player._status = new_status
+ player._artist = new_artist
+ player._title = new_title
+ player._last_position_us = new_pos_us
+ player._total_length_us = new_len_us
+ player._last_update_ts = time.time()
+
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}")
+ print(f"UPDATE: [{player._status}] {player._artist} - {player._title} ({match_time.group(1) if match_time else '0:00'})")
- # 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",
{
@@ -194,10 +229,30 @@ def update_loop(player):
[]
)
- except Exception as e:
- print(f"Update error: {e}")
- import traceback
- traceback.print_exc()
+ except (requests.exceptions.RequestException, Exception) as e:
+ if not offline_logged:
+ print(f"Winamp Web Interface offline or unreachable: {e}")
+ offline_logged = True
+
+ # Reset state when offline
+ player._status = "Stopped"
+ player._artist = "Unknown"
+ player._title = "Winamp (Offline)"
+ player._last_position_us = 0
+ player._total_length_us = 0
+ player._last_update_ts = time.time()
+
+ if last_known_status != "Stopped":
+ last_known_status = "Stopped"
+ player.PropertiesChanged(
+ "org.mpris.MediaPlayer2.Player",
+ {
+ "PlaybackStatus": player.PlaybackStatus,
+ "Metadata": player.Metadata
+ },
+ []
+ )
+
time.sleep(2)
if __name__ == "__main__":