Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions MediaAction.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,19 @@ def get_player_name(self):
if settings is not None:
return settings.get("player_name")

def get_display_player_name(self):
"""Get the player name for display purposes (status, title, thumbnail).
When 'All Players' is selected, returns the currently-playing player
so the UI shows the active player's info instead of an arbitrary one."""
name = self.get_player_name()
if name is not None:
return name
return self.plugin_base.mc.get_active_player_name()

def show_title(self, reload_key = True) -> bool:
if self.get_settings() == None:
return False
title = self.plugin_base.mc.title(self.get_player_name())
title = self.plugin_base.mc.title(self.get_display_player_name())
if self.get_settings().setdefault("show_label", True) and title is not None:
label = None
if isinstance(title, list):
Expand Down Expand Up @@ -143,6 +152,27 @@ def on_toggle_thumbnail(self, switch, *args):
# Update image
self.on_tick()

def _load_thumbnail(self, player_name: str = None) -> "Image.Image | None":
"""Load the thumbnail image for the given player.
Returns a PIL Image if a thumbnail is available, or None."""
import io as _io
settings = self.get_settings()
if settings is None:
return None
if not settings.setdefault("show_thumbnail", True):
return None
thumbnail = self.plugin_base.mc.thumbnail(player_name)
if thumbnail is None:
return None
if isinstance(thumbnail, list):
if thumbnail[0] is not None:
try:
return Image.open(thumbnail[0])
except Exception as e:
return None
return None
return None

def generate_image(self, icon:Image.Image = None, background:Image.Image=None, valign: float = 0, halign: float = 0, size: float = 1):
if background is None:
background = Image.new("RGBA", (self.deck_controller.deck.key_image_format()["size"]), (0, 0, 0, 0))
Expand All @@ -151,12 +181,11 @@ def generate_image(self, icon:Image.Image = None, background:Image.Image=None, v

if icon is not None:
# Resize
lenght = int(self.deck_controller.deck.key_image_format()["size"][0] * size)
icon = icon.resize((lenght, lenght))
length = int(self.deck_controller.deck.key_image_format()["size"][0] * size)
icon = icon.resize((length, length))

left_margin = int((background.width - icon.width) * (halign + 1) / 2)
top_margin = int((background.height - icon.height) * (valign + 1) / 2)

background.paste(icon, (left_margin, top_margin), icon)

return background
return background
132 changes: 114 additions & 18 deletions MediaController.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ class MediaController:
def __init__(self):
self.session_bus = dbus.SessionBus()

# Tracks the D-Bus bus names of players that were playing before a pause action.
# When pause is pressed, all currently-playing sources are recorded here.
# When play is pressed, only these remembered sources are resumed.
self._previously_playing: set[str] = set()

# Remembers the last player that was seen playing, so that when all
# players are paused the UI doesn't jump back to an arbitrary player.
self._last_active_player: str | None = None

self.update_players()

def update_players(self):
Expand All @@ -29,6 +38,19 @@ def update_players(self):
return
self.mpris_players = mpris_players

def _get_bus_name(self, player) -> str:
"""Get the D-Bus bus name for a player proxy object."""
return str(player.bus_name)

def _get_playback_status(self, iface) -> str | None:
"""Get the playback status of a player interface."""
try:
properties = dbus.Interface(iface, 'org.freedesktop.DBus.Properties')
return str(properties.Get('org.mpris.MediaPlayer2.Player', 'PlaybackStatus'))
except dbus.exceptions.DBusException as e:
log.error(e)
return None

def get_player_names(self, remove_duplicates = False):
names = []
try:
Expand All @@ -43,6 +65,22 @@ def get_player_names(self, remove_duplicates = False):
log.error("Could not connect to D-Bus session bus. Is the D-Bus daemon running?", e)
return names

def get_active_player_name(self) -> str | None:
"""Return the Identity of the first currently-playing MPRIS player.
Falls back to the last known active player if none are currently playing."""
self.update_players()
for player in self.mpris_players:
try:
properties = dbus.Interface(player, 'org.freedesktop.DBus.Properties')
status = str(properties.Get('org.mpris.MediaPlayer2.Player', 'PlaybackStatus'))
if status == "Playing":
name = str(properties.Get('org.mpris.MediaPlayer2', 'Identity'))
self._last_active_player = name
return name
except dbus.exceptions.DBusException:
pass
return self._last_active_player

def get_matching_ifaces(self, player_name: str = None) -> list[dbus.Interface]:
self.update_players()
"""
Expand All @@ -65,10 +103,30 @@ def get_matching_ifaces(self, player_name: str = None) -> list[dbus.Interface]:
except dbus.exceptions.DBusException as e:
log.warning(e)
return ifaces

def get_matching_players(self, player_name: str = None) -> list[tuple[str, dbus.Interface]]:
"""
Like get_matching_ifaces, but returns (bus_name, iface) tuples
so callers can identify individual player instances.
"""
self.update_players()
players = []
for player in self.mpris_players:
properties = dbus.Interface(player, 'org.freedesktop.DBus.Properties')
try:
if player_name in [None, "", properties.Get('org.mpris.MediaPlayer2', 'Identity')]:
bus_name = self._get_bus_name(player)
iface = dbus.Interface(player, 'org.mpris.MediaPlayer2.Player')
players.append((bus_name, iface))
except dbus.exceptions.DBusException as e:
log.warning(e)
return players

def pause(self, player_name: str = None):
"""
Pauses the media player specified by the `player_name` parameter.
Before pausing, records which players are currently playing so they
can be resumed later with play().

Args:
player_name (str, optional): The name of the media player to pause.
Expand All @@ -78,19 +136,32 @@ def pause(self, player_name: str = None):
None
"""
status = []
ifaces = self.get_matching_ifaces(player_name)
for iface in ifaces:
players = self.get_matching_players(player_name)
currently_playing = set()
for bus_name, iface in players:
try:
playback_status = self._get_playback_status(iface)
if playback_status == "Playing":
currently_playing.add(bus_name)
iface.Pause()
status.append(True)
except dbus.exceptions.DBusException as e:
log.error(e)
status.append(False)

# Only update the remembered set if at least one player was actually playing.
# This avoids clearing the set if pause is pressed while already paused.
if currently_playing:
self._previously_playing = currently_playing

return self.compress_list(status)

def play(self, player_name: str = None):
"""
Plays the media player specified by the `player_name` parameter.
If there are remembered previously-playing sources (from a prior pause),
only those sources will be resumed. Otherwise, all matching players
will be played.

Args:
player_name (str, optional): The name of the media player to play.
Expand All @@ -100,19 +171,37 @@ def play(self, player_name: str = None):
None
"""
status = []
ifaces = self.get_matching_ifaces(player_name)
for iface in ifaces:
players = self.get_matching_players(player_name)
for bus_name, iface in players:
try:
iface.Play()
status.append(True)
if player_name is not None:
# Specific player requested: always play it
iface.Play()
status.append(True)
elif self._previously_playing:
# General play with remembered set: only resume those
if bus_name in self._previously_playing:
iface.Play()
status.append(True)
else:
status.append(True) # skip, not an error
else:
iface.Play()
status.append(True)
except dbus.exceptions.DBusException as e:
log.error(e)
status.append(False)

# Clear the remembered set after resuming
self._previously_playing.clear()

return self.compress_list(status)

def toggle(self, player_name: str = None):
"""
Toggles the playback state of the media player specified by the `player_name` parameter.
Uses the smart pause/play logic: when pausing, remembers which sources were playing;
when playing, only resumes those remembered sources.

Args:
player_name (str, optional): The name of the media player to toggle.
Expand All @@ -121,16 +210,19 @@ def toggle(self, player_name: str = None):
Returns:
None
"""
status = []
ifaces = self.get_matching_ifaces(player_name)
for iface in ifaces:
try:
iface.PlayPause()
status.append(True)
except dbus.exceptions.DBusException as e:
log.error(e)
status.append(False)
return self.compress_list(status)
# Check if any matching player is currently playing
players = self.get_matching_players(player_name)
any_playing = False
for bus_name, iface in players:
playback_status = self._get_playback_status(iface)
if playback_status == "Playing":
any_playing = True
break

if any_playing:
return self.pause(player_name)
else:
return self.play(player_name)

def stop(self, player_name: str = None):
"""
Expand Down Expand Up @@ -256,8 +348,12 @@ def thumbnail(self, player_name: str = None) -> list[str]:
try:
properties = dbus.Interface(iface, 'org.freedesktop.DBus.Properties')
metadata = properties.Get('org.mpris.MediaPlayer2.Player', 'Metadata')
path = str(metadata.get('mpris:artUrl'))
if path in [None, ""]:
art_url = metadata.get('mpris:artUrl')
if art_url is None:
thumbnails.append(None)
continue
path = str(art_url)
if not path:
thumbnails.append(None)
continue
if path.startswith("data:"):
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# MediaPlugin

This plugin controls your player using the
[MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/) interface.

It will let you play/pause the current playing item and start the previous/next
track using the [StreamController](https://github.com/StreamController/StreamController).

## Actions

- **Play** - Starts media playback if the player is not currently playing.
- **Pause** - Pauses media playback if the player is currently playing.
- **PlayPause** - Toggles between play and pause states based on the current
playback status. If more than one item is playing, it will pause/play all
- **Next** - Skips to the next track in the media queue.
- **Previous** - Returns to the previous track in the media queue.
- **Info** - Displays the current media title and artist information with and
optional separator.
- **ThumbnailBackground** - Sets the current media thumbnail as the deck
background image.
Loading