From 853a451d0ac6d8b0e9e9767fa6fcead2e8c36775 Mon Sep 17 00:00:00 2001 From: ergosteur Date: Thu, 19 Mar 2026 03:00:47 -0400 Subject: [PATCH] Add systemd user service and enhance offline robustness --- GEMINI.md | 28 +++++++++++-- winamp-mpris.service | 12 ++++++ winamp_mpris.py | 93 +++++++++++++++++++++++++++++++++++--------- 3 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 winamp-mpris.service 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__":