Files
nwg-panel/nwg_panel/modules/playerctl.py
Colin 7aa759990b playerctl: remove backward/forward/music-note icons
these aren't worth the space cost on narrow devices (moby)
2024-06-19 19:13:07 +00:00

311 lines
11 KiB
Python

#!/usr/bin/env python3
from enum import Enum
import os.path
import threading
from urllib.parse import unquote, urlparse
import gi
gi.require_version('Playerctl', '2.0')
from gi.repository import GLib, Gtk, Gdk
from gi.repository import Playerctl as Ctl
import requests
from nwg_panel.tools import check_key, eprint, local_dir, update_image
class Playerctl(Gtk.EventBox):
PlayerOps = Enum('PlayerOps', ['PLAY_PAUSE', 'NEXT', 'PREVIOUS'])
def __init__(self, settings, voc, icons_path=""):
self.settings = settings
self.icons_path = icons_path
Gtk.EventBox.__init__(self)
check_key(settings, "interval", 1)
check_key(settings, "label-css-name", "")
check_key(settings, "button-css-name", "")
check_key(settings, "icon-size", 16)
check_key(settings, "buttons-position", "left")
check_key(settings, "chars", 30)
check_key(settings, "scroll", True)
check_key(settings, "show-cover", True)
check_key(settings, "cover-size", 24)
check_key(settings, "angle", 0.0)
check_key(settings, "button-css-name", "")
self.voc = voc
self.old_cover_url = ""
self.old_media_info = ""
self.player = None
self.player_handler_ids = []
self.num_players = 0
self.player_idx = 0
self.add_events(Gdk.EventMask.SCROLL_MASK)
self.connect('scroll-event', self.on_scroll)
self.build_box()
self.subscribe()
# Hide on start if no player presents
def hide_self(_):
if not self.player:
self.hide()
self.connect("realize", hide_self)
def subscribe(self):
# Must associate manager with self to increase its reference count
self.manager = Ctl.PlayerManager()
self.manager.connect('name-appeared', self.on_name_appeared)
self.manager.connect('player-vanished', self.on_player_vanished)
# Manage all players from old to new, so that the newest one comes
# first in props.players
for name in reversed(self.manager.props.player_names):
self.manage_player_by_name(self.manager, name)
self.num_players = len(self.manager.props.players)
if self.num_players > 1:
self.num_players_lbl.set_text(f" {self.player_idx + 1}/{self.num_players} ")
self.num_players_lbl.set_tooltip_text(
f"Player {self.player_idx + 1}/{self.num_players}, {self.voc['scroll-to-switch']}")
else:
self.num_players_lbl.set_text("")
if len(self.manager.props.players) > 0:
self.init_player(self.manager.props.players[self.player_idx])
@staticmethod
def manage_player_by_name(manager, name):
player = Ctl.Player.new_from_name(name)
manager.manage_player(player)
def on_name_appeared(self, manager, name):
self.subscribe()
self.deinit_player()
self.manage_player_by_name(manager, name)
self.init_player(manager.props.players[self.player_idx])
def on_player_vanished(self, manager, player):
self.subscribe()
# Non-active player vanished, do nothing
if self.player and player.props.player_name != self.player.props.player_name:
return
# Active player vanished, populate another one if exists
self.deinit_player()
if len(manager.props.players) > 0:
self.init_player(manager.props.players[self.player_idx])
def init_player(self, player):
self.player = player
self.show()
# connect signals
self.player_handler_ids.append(
player.connect('metadata', self.on_metadata))
self.player_handler_ids.append(
player.connect('playback-status', self.on_playback_status))
# Manually set the initial state
# self.on_metadata(player, player.props.metadata)
self.on_metadata(player, [])
def deinit_player(self):
if self.player:
for handler_id in self.player_handler_ids:
self.player.disconnect(handler_id)
self.player = None
self.player_handler_ids.clear()
self.hide()
def on_playback_status(self, player, status):
artist = player.get_artist()
title = player.get_title()
status_text = None
if status == Ctl.PlaybackStatus.PLAYING:
update_image(self.play_pause_btn.get_image(), "media-playback-pause-symbolic",
self.settings["icon-size"], icons_path=self.icons_path)
else:
update_image(self.play_pause_btn.get_image(), "media-playback-start-symbolic",
self.settings["icon-size"], icons_path=self.icons_path)
if status == Ctl.PlaybackStatus.PAUSED:
status_text = "paused"
elif status == Ctl.PlaybackStatus.STOPPED:
status_text = "stopped"
# Filter out empty value when building info
info = [x for x in (artist, title, status_text) if x]
info = " - ".join(info)
self.set_media_info(info)
def on_metadata(self, player, metadata):
try:
cover_url = metadata["mpris:artUrl"]
except: # used to be on KeyError, but actual error is 'mpris:artUrl' for some reason (playerctl bug?)
cover_url = ""
if cover_url != self.old_cover_url:
self.old_cover_url = cover_url
self.update_cover_image(cover_url)
self.on_playback_status(player, player.props.playback_status)
def update_remote_cover(self, url):
try:
r = requests.get(url, allow_redirects=True)
cover_path = os.path.join(local_dir(), "cover.jpg")
with open(cover_path, 'wb') as f:
f.write(r.content)
cover_path = "file://" + cover_path
except Exception as e:
eprint("Couldn't update remote cover: {}".format(e))
cover_path = ""
GLib.idle_add(self.update_cover_image, cover_path)
def update_cover_image(self, url):
url = urlparse(url)
path = unquote(url.path)
if url.scheme.startswith("http"):
threading.Thread(target=self.update_remote_cover(url.geturl()), daemon=True).start()
return
if url.scheme == "file" and path:
try:
update_image(self.cover_img, path, self.settings["cover-size"], fallback=False)
except Exception as e:
eprint("Error creating pixbuf: {}".format(e))
path = ""
if not path:
update_image(self.cover_img, "music", self.settings["cover-size"], self.icons_path)
def on_scroll(self, widget, event):
if event.direction == Gdk.ScrollDirection.UP:
if self.player_idx < self.num_players - 1:
self.player_idx += 1
else:
self.player_idx = 0
if event.direction == Gdk.ScrollDirection.DOWN:
if self.player_idx > 0:
self.player_idx -= 1
else:
self.player_idx = self.num_players - 1
print(f"Switched to player {self.player_idx}")
self.subscribe()
def build_box(self):
self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
if self.settings["angle"] != 0.0:
self.box.set_orientation(Gtk.Orientation.VERTICAL)
self.box.set_property("name", "task-box")
self.add(self.box)
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
if self.settings["angle"] != 0.0:
button_box.set_orientation(Gtk.Orientation.VERTICAL)
self.play_pause_btn = Gtk.Button()
if self.settings["button-css-name"]:
self.play_pause_btn.set_property("name", self.settings["button-css-name"])
img = Gtk.Image()
update_image(img, "media-playback-start-symbolic", self.settings["icon-size"], icons_path=self.icons_path)
self.play_pause_btn.set_image(img)
self.play_pause_btn.connect("clicked", self.launch, self.PlayerOps.PLAY_PAUSE)
button_box.pack_start(self.play_pause_btn, False, False, 1)
self.num_players_lbl = Gtk.Label.new("")
if self.settings["label-css-name"]:
self.num_players_lbl.set_property("name", self.settings["label-css-name"])
self.num_players_lbl.set_angle(self.settings["angle"])
self.label = AutoScrollLabel(self.settings["scroll"],
self.settings["chars"],
self.settings["interval"])
if self.settings["label-css-name"]:
self.label.set_property("name", self.settings["label-css-name"])
self.label.set_angle(self.settings["angle"])
self.cover_img = Gtk.Image()
update_image(self.cover_img, "music", self.settings["cover-size"], self.icons_path)
if self.settings["buttons-position"] == "left":
self.box.pack_start(button_box, False, False, 2)
if self.settings["show-cover"]:
self.box.pack_start(self.cover_img, False, False, 0)
else:
if self.settings["show-cover"]:
self.box.pack_start(self.cover_img, False, False, 2)
self.box.pack_start(button_box, False, False, 10)
def launch(self, button, op):
if not self.player:
return
props = self.player.props
if op == self.PlayerOps.PLAY_PAUSE:
status = props.playback_status
if status == Ctl.PlaybackStatus.PLAYING:
if not props.can_pause:
return
else:
if not props.can_play:
return
self.player.play_pause()
elif op == self.PlayerOps.PREVIOUS:
if props.can_go_previous:
self.player.previous()
elif op == self.PlayerOps.NEXT:
if props.can_go_next:
self.player.next()
def set_media_info(self, text):
if self.old_media_info != text:
self.old_media_info = text
self.label.set_tooltip_text(text)
self.label.set_text(text)
class AutoScrollLabel(Gtk.Label):
def __init__(self, scroll, chars, interval):
super().__init__(self)
self.chars = chars
self.interval = interval if scroll else 0
self.output_start_idx = 0
self.text = ""
self.src = 0
def set_text(self, text):
self.text = text
self.output_start_idx = 0
super().set_text(text[:self.chars])
if self.interval == 0 or len(text) <= self.chars:
# Disable scroll
if self.src > 0:
GLib.Source.remove(self.src)
self.src = 0
else:
# Enable scroll
if self.src == 0:
self.src = GLib.timeout_add_seconds(self.interval,
self.scroll_text,
priority=GLib.PRIORITY_LOW)
def scroll_text(self):
self.output_start_idx += 1
if self.output_start_idx + self.chars > len(self.text):
self.output_start_idx = 0
super().set_text(
self.text[self.output_start_idx:self.output_start_idx + self.chars])
return True