Fix position synchronization, add album art fetching, and document busctl usage
- Use Web UI as authoritative time source for better MPRIS position sync - Implement album art fetching via iTunes Search API - Optimize window title parsing to focus on metadata - Add busctl testing commands to README and GEMINI.md
This commit is contained in:
19
GEMINI.md
19
GEMINI.md
@@ -59,6 +59,25 @@ A systemd service file is provided to allow the bridge to run automatically in t
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
### Testing and Debugging
|
||||||
|
|
||||||
|
You can use `busctl` to interact with the MPRIS interface:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get playback status
|
||||||
|
busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlaybackStatus
|
||||||
|
|
||||||
|
# Get current position (in microseconds)
|
||||||
|
busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Position
|
||||||
|
|
||||||
|
# Get metadata
|
||||||
|
busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Metadata
|
||||||
|
|
||||||
|
# Send commands
|
||||||
|
busctl --user call org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlayPause
|
||||||
|
busctl --user call org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Next
|
||||||
|
```
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- **`winamp_mpris.py`**: The main bridge script. Includes optimizations for Plasma 6 property change detection, live position tracking, and robust error handling.
|
- **`winamp_mpris.py`**: The main bridge script. Includes optimizations for Plasma 6 property change detection, live position tracking, and robust error handling.
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -51,6 +51,19 @@ sudo dnf install wmctrl libnotify # or sudo apt install wmctrl libnotify
|
|||||||
python3 winamp_mpris.py
|
python3 winamp_mpris.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Testing and Debugging
|
||||||
|
You can use `busctl` to interact with the MPRIS interface:
|
||||||
|
```bash
|
||||||
|
# Get playback status
|
||||||
|
busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlaybackStatus
|
||||||
|
|
||||||
|
# Get current position (in microseconds)
|
||||||
|
busctl --user get-property org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Position
|
||||||
|
|
||||||
|
# Send commands
|
||||||
|
busctl --user call org.mpris.MediaPlayer2.winamp /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player PlayPause
|
||||||
|
```
|
||||||
|
|
||||||
### Running as a systemd User Service
|
### Running as a systemd User Service
|
||||||
1. **Install the service file**:
|
1. **Install the service file**:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from bs4 import BeautifulSoup
|
|||||||
BASE_URL = "http://localhost:5666"
|
BASE_URL = "http://localhost:5666"
|
||||||
AUTH = ('winamp', 'llama')
|
AUTH = ('winamp', 'llama')
|
||||||
APP_ID = "org.mpris.MediaPlayer2.winamp"
|
APP_ID = "org.mpris.MediaPlayer2.winamp"
|
||||||
|
DEFAULT_ART = "https://webamp.org/favicon.ico"
|
||||||
|
|
||||||
class WinampMPRIS:
|
class WinampMPRIS:
|
||||||
"""
|
"""
|
||||||
@@ -78,6 +79,7 @@ class WinampMPRIS:
|
|||||||
self._artist = "Unknown"
|
self._artist = "Unknown"
|
||||||
self._album = ""
|
self._album = ""
|
||||||
self._status = "Stopped"
|
self._status = "Stopped"
|
||||||
|
self._art_url = DEFAULT_ART
|
||||||
self._last_position_us = 0
|
self._last_position_us = 0
|
||||||
self._total_length_us = 0
|
self._total_length_us = 0
|
||||||
self._last_update_ts = time.time()
|
self._last_update_ts = time.time()
|
||||||
@@ -147,7 +149,8 @@ class WinampMPRIS:
|
|||||||
@property
|
@property
|
||||||
def Position(self):
|
def Position(self):
|
||||||
if self._status == "Playing":
|
if self._status == "Playing":
|
||||||
elapsed_us = int((time.time() - self._last_update_ts) * 1e6)
|
now = time.time()
|
||||||
|
elapsed_us = int((now - self._last_update_ts) * 1e6)
|
||||||
current_pos = self._last_position_us + elapsed_us
|
current_pos = self._last_position_us + elapsed_us
|
||||||
if self._total_length_us > 0:
|
if self._total_length_us > 0:
|
||||||
return min(current_pos, self._total_length_us)
|
return min(current_pos, self._total_length_us)
|
||||||
@@ -166,7 +169,7 @@ class WinampMPRIS:
|
|||||||
'xesam:title': GLib.Variant('s', self._title),
|
'xesam:title': GLib.Variant('s', self._title),
|
||||||
'xesam:artist': GLib.Variant('as', [self._artist]),
|
'xesam:artist': GLib.Variant('as', [self._artist]),
|
||||||
'xesam:album': GLib.Variant('s', self._album),
|
'xesam:album': GLib.Variant('s', self._album),
|
||||||
'mpris:artUrl': GLib.Variant('s', 'https://webamp.org/favicon.ico')
|
'mpris:artUrl': GLib.Variant('s', self._art_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_time_to_us(time_str):
|
def parse_time_to_us(time_str):
|
||||||
@@ -196,9 +199,30 @@ def get_winamp_window_title():
|
|||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def fetch_album_art(artist, album):
|
||||||
|
"""Fetches album art from iTunes Search API."""
|
||||||
|
if not artist or not album or artist == "Unknown":
|
||||||
|
return DEFAULT_ART
|
||||||
|
|
||||||
|
try:
|
||||||
|
query = f"{artist} {album}"
|
||||||
|
url = f"https://itunes.apple.com/search?term={query}&entity=album&limit=1"
|
||||||
|
r = requests.get(url, timeout=3)
|
||||||
|
if r.status_code == 200:
|
||||||
|
data = r.json()
|
||||||
|
if data.get("resultCount", 0) > 0:
|
||||||
|
art_100 = data["results"][0].get("artworkUrl100", DEFAULT_ART)
|
||||||
|
# Upgrade to 600x600 for better quality
|
||||||
|
return art_100.replace("100x100bb.jpg", "600x600bb.jpg")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Art fetch error: {e}")
|
||||||
|
return DEFAULT_ART
|
||||||
|
|
||||||
def update_loop(player):
|
def update_loop(player):
|
||||||
last_known_title = ""
|
last_known_title = ""
|
||||||
last_known_status = ""
|
last_known_status = ""
|
||||||
|
last_known_album = ""
|
||||||
|
last_time_str = ""
|
||||||
offline_logged = False
|
offline_logged = False
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -209,11 +233,10 @@ def update_loop(player):
|
|||||||
|
|
||||||
if window_title:
|
if window_title:
|
||||||
# Pattern: Artist–Album–Title | 0:06/2:45 - Winamp
|
# Pattern: Artist–Album–Title | 0:06/2:45 - Winamp
|
||||||
match = re.search(r"(.*?) \| (\d+:?\d*:\d*)/(\d+:?\d*:\d*) - Winamp", window_title)
|
# Only use window title for metadata as time is unreliable here
|
||||||
|
match = re.search(r"(.*?) \| .* - Winamp", window_title)
|
||||||
if match:
|
if match:
|
||||||
metadata_raw = match.group(1).strip()
|
metadata_raw = match.group(1).strip()
|
||||||
new_pos_us = parse_time_to_us(match.group(2))
|
|
||||||
new_len_us = parse_time_to_us(match.group(3))
|
|
||||||
|
|
||||||
# Split metadata (Artist–Album–Title)
|
# Split metadata (Artist–Album–Title)
|
||||||
meta_parts = [p.strip() for p in re.split(r'[–—-]', metadata_raw)]
|
meta_parts = [p.strip() for p in re.split(r'[–—-]', metadata_raw)]
|
||||||
@@ -231,12 +254,9 @@ def update_loop(player):
|
|||||||
player._artist = "Unknown"
|
player._artist = "Unknown"
|
||||||
player._album = ""
|
player._album = ""
|
||||||
|
|
||||||
player._last_position_us = new_pos_us
|
|
||||||
player._total_length_us = new_len_us
|
|
||||||
player._last_update_ts = time.time()
|
|
||||||
title_parsed = True
|
title_parsed = True
|
||||||
|
|
||||||
# 2. Poll Web UI (Fallback and Controls/Status)
|
# 2. Poll Web UI (Status and Time source)
|
||||||
try:
|
try:
|
||||||
r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=1)
|
r = requests.get(f"{BASE_URL}/main", auth=AUTH, timeout=1)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
@@ -251,20 +271,27 @@ def update_loop(player):
|
|||||||
|
|
||||||
player._status = new_status
|
player._status = new_status
|
||||||
|
|
||||||
|
# Always update time from Web UI
|
||||||
|
match_time = re.search(r"\((\d+:?\d*:\d+) / (\d+:?\d*:\d+)\)", status_raw)
|
||||||
|
if match_time:
|
||||||
|
current_time_str = match_time.group(1)
|
||||||
|
if current_time_str != last_time_str:
|
||||||
|
player._last_position_us = parse_time_to_us(current_time_str)
|
||||||
|
player._total_length_us = parse_time_to_us(match_time.group(2))
|
||||||
|
player._last_update_ts = time.time()
|
||||||
|
last_time_str = current_time_str
|
||||||
|
|
||||||
if not title_parsed:
|
if not title_parsed:
|
||||||
# Web UI Fallback for metadata
|
# Web UI Fallback for metadata
|
||||||
match_meta = re.search(r"track \d+ - (.*?) - (.*?) -", status_raw)
|
match_meta = re.search(r"track \d+ - (.*?) - (.*?) -", status_raw)
|
||||||
if match_meta:
|
if match_meta:
|
||||||
player._artist = match_meta.group(1).strip()
|
player._artist = match_meta.group(1).strip()
|
||||||
player._title = match_meta.group(2).strip()
|
player._title = match_meta.group(2).strip()
|
||||||
|
player._album = ""
|
||||||
elif "track" in status_raw:
|
elif "track" in status_raw:
|
||||||
player._title = status_raw.split('-')[0].replace("Playing track ", "").strip()
|
player._title = status_raw.split('-')[0].replace("Playing track ", "").strip()
|
||||||
|
player._artist = "Unknown"
|
||||||
match_time = re.search(r"\((\d+:?\d*:\d+) / (\d+:?\d*:\d+)\)", status_raw)
|
player._album = ""
|
||||||
if match_time:
|
|
||||||
player._last_position_us = parse_time_to_us(match_time.group(1))
|
|
||||||
player._total_length_us = parse_time_to_us(match_time.group(2))
|
|
||||||
player._last_update_ts = time.time()
|
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
if not window_title:
|
if not window_title:
|
||||||
if not offline_logged:
|
if not offline_logged:
|
||||||
@@ -272,11 +299,22 @@ def update_loop(player):
|
|||||||
offline_logged = True
|
offline_logged = True
|
||||||
player._status = "Stopped"
|
player._status = "Stopped"
|
||||||
|
|
||||||
if player._status != last_known_status or player._title != last_known_title:
|
if (player._status != last_known_status or
|
||||||
|
player._title != last_known_title or
|
||||||
|
player._album != last_known_album):
|
||||||
|
|
||||||
|
# Fetch art if album/artist changed
|
||||||
|
if player._artist != "Unknown" and player._album:
|
||||||
|
player._art_url = fetch_album_art(player._artist, player._album)
|
||||||
|
else:
|
||||||
|
player._art_url = DEFAULT_ART
|
||||||
|
|
||||||
last_known_status = player._status
|
last_known_status = player._status
|
||||||
last_known_title = player._title
|
last_known_title = player._title
|
||||||
|
last_known_album = player._album
|
||||||
|
|
||||||
print(f"UPDATE: [{player._status}] {player._artist} - {player._title}")
|
album_str = f" [{player._album}]" if player._album else ""
|
||||||
|
print(f"UPDATE: [{player._status}] {player._artist}{album_str} - {player._title}")
|
||||||
|
|
||||||
player.PropertiesChanged(
|
player.PropertiesChanged(
|
||||||
"org.mpris.MediaPlayer2.Player",
|
"org.mpris.MediaPlayer2.Player",
|
||||||
@@ -292,6 +330,7 @@ def update_loop(player):
|
|||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bus = SessionBus()
|
bus = SessionBus()
|
||||||
player_logic = WinampMPRIS()
|
player_logic = WinampMPRIS()
|
||||||
@@ -309,7 +348,7 @@ if __name__ == "__main__":
|
|||||||
thread = Thread(target=update_loop, args=(player_logic,), daemon=True)
|
thread = Thread(target=update_loop, args=(player_logic,), daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
print(f"--- Winamp Bridge Started (Window Title + Web UI) ---")
|
print(f"--- Winamp Bridge Started (Window Title + Web UI + Album Art) ---")
|
||||||
loop = GLib.MainLoop()
|
loop = GLib.MainLoop()
|
||||||
try:
|
try:
|
||||||
loop.run()
|
loop.run()
|
||||||
|
|||||||
Reference in New Issue
Block a user