Add mpris module
Uses libplayerctl to use the MPRIS dbus protocol to query, listen and control media players. Signed-off-by: Robert Günzler <r@gnzler.io>
This commit is contained in:
parent
b5c686c0dd
commit
0bc5314e08
|
@ -41,6 +41,9 @@
|
||||||
#ifdef HAVE_DBUSMENU
|
#ifdef HAVE_DBUSMENU
|
||||||
#include "modules/sni/tray.hpp"
|
#include "modules/sni/tray.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_MPRIS
|
||||||
|
#include "modules/mpris/mpris.hpp"
|
||||||
|
#endif
|
||||||
#ifdef HAVE_LIBNL
|
#ifdef HAVE_LIBNL
|
||||||
#include "modules/network.hpp"
|
#include "modules/network.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "gtkmm/box.h"
|
||||||
|
#include "gtkmm/label.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <playerctl/playerctl.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "ALabel.hpp"
|
||||||
|
#include "util/sleeper_thread.hpp"
|
||||||
|
|
||||||
|
namespace waybar::modules::mpris {
|
||||||
|
|
||||||
|
class Mpris : public AModule {
|
||||||
|
public:
|
||||||
|
Mpris(const std::string&, const Json::Value&);
|
||||||
|
~Mpris();
|
||||||
|
auto update() -> void;
|
||||||
|
bool handleToggle(GdkEventButton* const&);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static auto onPlayerNameAppeared(PlayerctlPlayerManager*, PlayerctlPlayerName*, gpointer) -> void;
|
||||||
|
static auto onPlayerNameVanished(PlayerctlPlayerManager*, PlayerctlPlayerName*, gpointer) -> void;
|
||||||
|
static auto onPlayerPlay(PlayerctlPlayer*, gpointer) -> void;
|
||||||
|
static auto onPlayerPause(PlayerctlPlayer*, gpointer) -> void;
|
||||||
|
static auto onPlayerStop(PlayerctlPlayer*, gpointer) -> void;
|
||||||
|
static auto onPlayerMetadata(PlayerctlPlayer*, GVariant*, gpointer) -> void;
|
||||||
|
|
||||||
|
struct PlayerInfo {
|
||||||
|
std::string name;
|
||||||
|
PlayerctlPlaybackStatus status;
|
||||||
|
std::string status_string;
|
||||||
|
|
||||||
|
std::optional<std::string> artist;
|
||||||
|
std::optional<std::string> album;
|
||||||
|
std::optional<std::string> title;
|
||||||
|
std::optional<std::string> length; // as HH:MM:SS
|
||||||
|
};
|
||||||
|
|
||||||
|
auto getPlayerInfo() -> std::optional<PlayerInfo>;
|
||||||
|
auto getIcon(const Json::Value&, const std::string&) -> std::string;
|
||||||
|
|
||||||
|
Gtk::Box box_;
|
||||||
|
Gtk::Label label_;
|
||||||
|
|
||||||
|
// config
|
||||||
|
std::string format_;
|
||||||
|
std::string format_playing_;
|
||||||
|
std::string format_paused_;
|
||||||
|
std::string format_stopped_;
|
||||||
|
std::chrono::seconds interval_;
|
||||||
|
std::string player_;
|
||||||
|
std::vector<std::string> ignored_players_;
|
||||||
|
|
||||||
|
PlayerctlPlayerManager* manager;
|
||||||
|
PlayerctlPlayer* player;
|
||||||
|
std::string lastStatus;
|
||||||
|
std::string lastPlayer;
|
||||||
|
|
||||||
|
util::SleeperThread thread_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace waybar::modules::mpris
|
|
@ -0,0 +1,103 @@
|
||||||
|
waybar-mpris(5)
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
waybar - MPRIS module
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
The *mpris* module displays currently playing media via libplayerctl.
|
||||||
|
|
||||||
|
# CONFIGURATION
|
||||||
|
|
||||||
|
*player*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: playerctld ++
|
||||||
|
Name of the MPRIS player to attach to. Using the default value always
|
||||||
|
follows the currenly active player.
|
||||||
|
|
||||||
|
*ignored-players*: ++
|
||||||
|
typeof: []string ++
|
||||||
|
Ignore updates of the listed players, when using playerctld.
|
||||||
|
|
||||||
|
*interval*: ++
|
||||||
|
typeof: integer ++
|
||||||
|
Refresh MPRIS information on a timer.
|
||||||
|
|
||||||
|
*format*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: {player} ({status}) {dynamic} ++
|
||||||
|
The text format.
|
||||||
|
|
||||||
|
*format-[status]*: ++
|
||||||
|
typeof: string ++
|
||||||
|
The status-specific text format.
|
||||||
|
|
||||||
|
*on-click*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: play-pause ++
|
||||||
|
Overwrite default action toggles.
|
||||||
|
|
||||||
|
*on-middle-click*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: previous track ++
|
||||||
|
Overwrite default action toggles.
|
||||||
|
|
||||||
|
*on-right-click*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: next track ++
|
||||||
|
Overwrite default action toggles.
|
||||||
|
|
||||||
|
*player-icons*: ++
|
||||||
|
typeof: map[string]string
|
||||||
|
Allows setting _{player-icon}_ based on player-name property.
|
||||||
|
|
||||||
|
*status-icons*: ++
|
||||||
|
typeof: map[string]string
|
||||||
|
Allows setting _{status-icon}_ based on player status (playing, paused,
|
||||||
|
stopped).
|
||||||
|
|
||||||
|
|
||||||
|
# FORMAT REPLACEMENTS
|
||||||
|
|
||||||
|
*{player}*: The name of the current media player
|
||||||
|
|
||||||
|
*{status}*: The current status (playing, paused, stopped)
|
||||||
|
|
||||||
|
*{artist}*: The artist of the current track
|
||||||
|
|
||||||
|
*{album}*: The album title of the current track
|
||||||
|
|
||||||
|
*{title}*: The title of the current track
|
||||||
|
|
||||||
|
*{length}*: Length of the track, formatted as HH:MM:SS
|
||||||
|
|
||||||
|
*{dynamic}*: Use _{artist}_, _{album}_, _{title}_ and _{length}_, automatically omit++
|
||||||
|
empty values
|
||||||
|
|
||||||
|
*{player-icon}*: Chooses an icon from _player-icons_ based on _{player}_
|
||||||
|
|
||||||
|
*{status-icon}*: Chooses an icon from _status-icons_ based on _{status}_
|
||||||
|
|
||||||
|
# EXAMPLES
|
||||||
|
|
||||||
|
```
|
||||||
|
"mpris": {
|
||||||
|
"format": "DEFAULT: {player_icon} {dynamic}",
|
||||||
|
"format-paused": "DEFAULT: {status_icon} <i>{dynamic}</i>",
|
||||||
|
"player-icons": {
|
||||||
|
"default": "▶",
|
||||||
|
"mpv": "🎵"
|
||||||
|
},
|
||||||
|
"status-icons": {
|
||||||
|
"paused": "⏸"
|
||||||
|
},
|
||||||
|
// "ignored-players": ["firefox"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# STYLE
|
||||||
|
|
||||||
|
- *#mpris*
|
||||||
|
- *#mpris.${status}*
|
||||||
|
- *#mpris.${player}*
|
|
@ -266,6 +266,7 @@ A module group is defined by specifying a module named "group/some-group-name".
|
||||||
- *waybar-keyboard-state(5)*
|
- *waybar-keyboard-state(5)*
|
||||||
- *waybar-memory(5)*
|
- *waybar-memory(5)*
|
||||||
- *waybar-mpd(5)*
|
- *waybar-mpd(5)*
|
||||||
|
- *waybar-mpris(5)*
|
||||||
- *waybar-network(5)*
|
- *waybar-network(5)*
|
||||||
- *waybar-pulseaudio(5)*
|
- *waybar-pulseaudio(5)*
|
||||||
- *waybar-river-mode(5)*
|
- *waybar-river-mode(5)*
|
||||||
|
|
13
meson.build
13
meson.build
|
@ -86,7 +86,10 @@ wayland_cursor = dependency('wayland-cursor')
|
||||||
wayland_protos = dependency('wayland-protocols')
|
wayland_protos = dependency('wayland-protocols')
|
||||||
gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0'])
|
gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0'])
|
||||||
dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk'))
|
dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk'))
|
||||||
giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled() or get_option('upower_glib').enabled()))
|
giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or
|
||||||
|
get_option('logind').enabled() or
|
||||||
|
get_option('upower_glib').enabled() or
|
||||||
|
get_option('mpris').enabled()))
|
||||||
jsoncpp = dependency('jsoncpp', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep'])
|
jsoncpp = dependency('jsoncpp', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep'])
|
||||||
sigcpp = dependency('sigc++-2.0')
|
sigcpp = dependency('sigc++-2.0')
|
||||||
libinotify = dependency('libinotify', required: false)
|
libinotify = dependency('libinotify', required: false)
|
||||||
|
@ -95,6 +98,7 @@ libinput = dependency('libinput', required: get_option('libinput'))
|
||||||
libnl = dependency('libnl-3.0', required: get_option('libnl'))
|
libnl = dependency('libnl-3.0', required: get_option('libnl'))
|
||||||
libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl'))
|
libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl'))
|
||||||
upower_glib = dependency('upower-glib', required: get_option('upower_glib'))
|
upower_glib = dependency('upower-glib', required: get_option('upower_glib'))
|
||||||
|
playerctl = dependency('playerctl', version : ['>=2.0.0'], required: get_option('mpris'))
|
||||||
libpulse = dependency('libpulse', required: get_option('pulseaudio'))
|
libpulse = dependency('libpulse', required: get_option('pulseaudio'))
|
||||||
libudev = dependency('libudev', required: get_option('libudev'))
|
libudev = dependency('libudev', required: get_option('libudev'))
|
||||||
libevdev = dependency('libevdev', required: get_option('libevdev'))
|
libevdev = dependency('libevdev', required: get_option('libevdev'))
|
||||||
|
@ -238,6 +242,11 @@ if (upower_glib.found() and giounix.found() and not get_option('logind').disable
|
||||||
src_files += 'src/modules/upower/upower_tooltip.cpp'
|
src_files += 'src/modules/upower/upower_tooltip.cpp'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if (playerctl.found() and giounix.found() and not get_option('logind').disabled())
|
||||||
|
add_project_arguments('-DHAVE_MPRIS', language: 'cpp')
|
||||||
|
src_files += 'src/modules/mpris/mpris.cpp'
|
||||||
|
endif
|
||||||
|
|
||||||
if libpulse.found()
|
if libpulse.found()
|
||||||
add_project_arguments('-DHAVE_LIBPULSE', language: 'cpp')
|
add_project_arguments('-DHAVE_LIBPULSE', language: 'cpp')
|
||||||
src_files += 'src/modules/pulseaudio.cpp'
|
src_files += 'src/modules/pulseaudio.cpp'
|
||||||
|
@ -334,6 +343,7 @@ executable(
|
||||||
libnl,
|
libnl,
|
||||||
libnlgen,
|
libnlgen,
|
||||||
upower_glib,
|
upower_glib,
|
||||||
|
playerctl,
|
||||||
libpulse,
|
libpulse,
|
||||||
libjack,
|
libjack,
|
||||||
libwireplumber,
|
libwireplumber,
|
||||||
|
@ -387,6 +397,7 @@ if scdoc.found()
|
||||||
'waybar-keyboard-state.5.scd',
|
'waybar-keyboard-state.5.scd',
|
||||||
'waybar-memory.5.scd',
|
'waybar-memory.5.scd',
|
||||||
'waybar-mpd.5.scd',
|
'waybar-mpd.5.scd',
|
||||||
|
'waybar-mpris.5.scd',
|
||||||
'waybar-network.5.scd',
|
'waybar-network.5.scd',
|
||||||
'waybar-pulseaudio.5.scd',
|
'waybar-pulseaudio.5.scd',
|
||||||
'waybar-river-mode.5.scd',
|
'waybar-river-mode.5.scd',
|
||||||
|
|
|
@ -5,6 +5,7 @@ option('libudev', type: 'feature', value: 'auto', description: 'Enable libudev s
|
||||||
option('libevdev', type: 'feature', value: 'auto', description: 'Enable libevdev support for evdev related features')
|
option('libevdev', type: 'feature', value: 'auto', description: 'Enable libevdev support for evdev related features')
|
||||||
option('pulseaudio', type: 'feature', value: 'auto', description: 'Enable support for pulseaudio')
|
option('pulseaudio', type: 'feature', value: 'auto', description: 'Enable support for pulseaudio')
|
||||||
option('upower_glib', type: 'feature', value: 'auto', description: 'Enable support for upower')
|
option('upower_glib', type: 'feature', value: 'auto', description: 'Enable support for upower')
|
||||||
|
option('mpris', type: 'feature', value: 'auto', description: 'Enable support for mpris')
|
||||||
option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit')
|
option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit')
|
||||||
option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable support for tray')
|
option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable support for tray')
|
||||||
option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages')
|
option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages')
|
||||||
|
|
|
@ -22,6 +22,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const {
|
||||||
return new waybar::modules::upower::UPower(id, config_[name]);
|
return new waybar::modules::upower::UPower(id, config_[name]);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_MPRIS
|
||||||
|
if (ref == "mpris") {
|
||||||
|
return new waybar::modules::mpris::Mpris(id, config_[name]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#ifdef HAVE_SWAY
|
#ifdef HAVE_SWAY
|
||||||
if (ref == "sway/mode") {
|
if (ref == "sway/mode") {
|
||||||
return new waybar::modules::sway::Mode(id, config_[name]);
|
return new waybar::modules::sway::Mode(id, config_[name]);
|
||||||
|
|
|
@ -0,0 +1,394 @@
|
||||||
|
#include "modules/mpris/mpris.hpp"
|
||||||
|
|
||||||
|
#include <fmt/core.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <playerctl/playerctl.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
namespace waybar::modules::mpris {
|
||||||
|
|
||||||
|
const std::string DEFAULT_FORMAT = "{player} ({status}): {dynamic}";
|
||||||
|
|
||||||
|
Mpris::Mpris(const std::string& id, const Json::Value& config)
|
||||||
|
: AModule(config, "mpris", id),
|
||||||
|
box_(Gtk::ORIENTATION_HORIZONTAL, 0),
|
||||||
|
label_(),
|
||||||
|
format_(DEFAULT_FORMAT),
|
||||||
|
interval_(0),
|
||||||
|
player_("playerctld"),
|
||||||
|
manager(),
|
||||||
|
player() {
|
||||||
|
box_.pack_start(label_);
|
||||||
|
box_.set_name(name_);
|
||||||
|
event_box_.add(box_);
|
||||||
|
event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &Mpris::handleToggle));
|
||||||
|
|
||||||
|
if (config_["format"].isString()) {
|
||||||
|
format_ = config_["format"].asString();
|
||||||
|
}
|
||||||
|
if (config_["format-playing"].isString()) {
|
||||||
|
format_playing_ = config_["format-playing"].asString();
|
||||||
|
}
|
||||||
|
if (config_["format-paused"].isString()) {
|
||||||
|
format_paused_ = config_["format-paused"].asString();
|
||||||
|
}
|
||||||
|
if (config_["format-stopped"].isString()) {
|
||||||
|
format_stopped_ = config_["format-stopped"].asString();
|
||||||
|
}
|
||||||
|
if (config_["interval"].isUInt()) {
|
||||||
|
interval_ = std::chrono::seconds(config_["interval"].asUInt());
|
||||||
|
}
|
||||||
|
if (config_["player"].isString()) {
|
||||||
|
player_ = config_["player"].asString();
|
||||||
|
}
|
||||||
|
if (config_["ignored-players"].isArray()) {
|
||||||
|
for (auto it = config_["ignored-players"].begin(); it != config_["ignored-players"].end();
|
||||||
|
++it) {
|
||||||
|
ignored_players_.push_back(it->asString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GError* error = nullptr;
|
||||||
|
manager = playerctl_player_manager_new(&error);
|
||||||
|
if (error) {
|
||||||
|
throw std::runtime_error(fmt::format("unable to create MPRIS client: {}", error->message));
|
||||||
|
}
|
||||||
|
|
||||||
|
g_object_connect(manager, "signal::name-appeared", G_CALLBACK(onPlayerNameAppeared), this, NULL);
|
||||||
|
g_object_connect(manager, "signal::name-vanished", G_CALLBACK(onPlayerNameVanished), this, NULL);
|
||||||
|
|
||||||
|
if (player_ == "playerctld") {
|
||||||
|
// use playerctld proxy
|
||||||
|
PlayerctlPlayerName name = {
|
||||||
|
.instance = (gchar*)player_.c_str(),
|
||||||
|
.source = PLAYERCTL_SOURCE_DBUS_SESSION,
|
||||||
|
};
|
||||||
|
player = playerctl_player_new_from_name(&name, &error);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
GList* players = playerctl_list_players(&error);
|
||||||
|
if (error) {
|
||||||
|
auto e = fmt::format("unable to list players: {}", error->message);
|
||||||
|
g_error_free(error);
|
||||||
|
throw std::runtime_error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto p = players; p != NULL; p = p->next) {
|
||||||
|
auto pn = static_cast<PlayerctlPlayerName*>(p->data);
|
||||||
|
if (strcmp(pn->name, player_.c_str()) == 0) {
|
||||||
|
player = playerctl_player_new_from_name(pn, &error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
fmt::format("unable to connect to player {}: {}", player_, error->message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
g_object_connect(player, "signal::play", G_CALLBACK(onPlayerPlay), this, "signal::pause",
|
||||||
|
G_CALLBACK(onPlayerPause), this, "signal::stop", G_CALLBACK(onPlayerStop),
|
||||||
|
this, "signal::stop", G_CALLBACK(onPlayerStop), this, "signal::metadata",
|
||||||
|
G_CALLBACK(onPlayerMetadata), this, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow setting an interval count that triggers periodic refreshes
|
||||||
|
if (interval_.count() > 0) {
|
||||||
|
thread_ = [this] {
|
||||||
|
dp.emit();
|
||||||
|
thread_.sleep_for(interval_);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger initial update
|
||||||
|
dp.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mpris::~Mpris() {
|
||||||
|
if (manager != NULL) g_object_unref(manager);
|
||||||
|
if (player != NULL) g_object_unref(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::getIcon(const Json::Value& icons, const std::string& key) -> std::string {
|
||||||
|
if (icons.isObject()) {
|
||||||
|
if (icons[key].isString()) {
|
||||||
|
return icons[key].asString();
|
||||||
|
} else if (icons["default"].isString()) {
|
||||||
|
return icons["default"].asString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name,
|
||||||
|
gpointer data) -> void {
|
||||||
|
Mpris* mpris = static_cast<Mpris*>(data);
|
||||||
|
if (!mpris) return;
|
||||||
|
|
||||||
|
spdlog::debug("mpris: name-appeared callback: {}", player_name->name);
|
||||||
|
|
||||||
|
if (std::string(player_name->name) != mpris->player_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GError* error = nullptr;
|
||||||
|
mpris->player = playerctl_player_new_from_name(player_name, &error);
|
||||||
|
g_object_connect(mpris->player, "signal::play", G_CALLBACK(onPlayerPlay), mpris, "signal::pause",
|
||||||
|
G_CALLBACK(onPlayerPause), mpris, "signal::stop", G_CALLBACK(onPlayerStop),
|
||||||
|
mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::metadata",
|
||||||
|
G_CALLBACK(onPlayerMetadata), mpris, NULL);
|
||||||
|
|
||||||
|
mpris->dp.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name,
|
||||||
|
gpointer data) -> void {
|
||||||
|
Mpris* mpris = static_cast<Mpris*>(data);
|
||||||
|
if (!mpris) return;
|
||||||
|
|
||||||
|
spdlog::debug("mpris: player-vanished callback: {}", player_name->name);
|
||||||
|
|
||||||
|
if (std::string(player_name->name) == mpris->player_) {
|
||||||
|
mpris->player = nullptr;
|
||||||
|
mpris->dp.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void {
|
||||||
|
Mpris* mpris = static_cast<Mpris*>(data);
|
||||||
|
if (!mpris) return;
|
||||||
|
|
||||||
|
spdlog::debug("mpris: player-play callback");
|
||||||
|
// update widget
|
||||||
|
mpris->dp.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void {
|
||||||
|
Mpris* mpris = static_cast<Mpris*>(data);
|
||||||
|
if (!mpris) return;
|
||||||
|
|
||||||
|
spdlog::debug("mpris: player-pause callback");
|
||||||
|
// update widget
|
||||||
|
mpris->dp.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void {
|
||||||
|
Mpris* mpris = static_cast<Mpris*>(data);
|
||||||
|
if (!mpris) return;
|
||||||
|
|
||||||
|
spdlog::debug("mpris: player-stop callback");
|
||||||
|
|
||||||
|
// hide widget
|
||||||
|
mpris->event_box_.set_visible(false);
|
||||||
|
// update widget
|
||||||
|
mpris->dp.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::onPlayerMetadata(PlayerctlPlayer* player, GVariant* metadata, gpointer data) -> void {
|
||||||
|
Mpris* mpris = static_cast<Mpris*>(data);
|
||||||
|
if (!mpris) return;
|
||||||
|
|
||||||
|
spdlog::debug("mpris: player-metadata callback");
|
||||||
|
// update widget
|
||||||
|
mpris->dp.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
|
||||||
|
if (!player) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
GError* error = nullptr;
|
||||||
|
|
||||||
|
char* player_status = nullptr;
|
||||||
|
auto player_playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED;
|
||||||
|
g_object_get(player, "status", &player_status, "playback-status", &player_playback_status, NULL);
|
||||||
|
|
||||||
|
std::string player_name = player_;
|
||||||
|
if (player_name == "playerctld") {
|
||||||
|
GList* players = playerctl_list_players(&error);
|
||||||
|
if (error) {
|
||||||
|
auto e = fmt::format("unable to list players: {}", error->message);
|
||||||
|
g_error_free(error);
|
||||||
|
throw std::runtime_error(e);
|
||||||
|
}
|
||||||
|
// > get the list of players [..] in order of activity
|
||||||
|
// https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249
|
||||||
|
players = g_list_first(players);
|
||||||
|
if (players) player_name = static_cast<PlayerctlPlayerName*>(players->data)->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::any_of(ignored_players_.begin(), ignored_players_.end(),
|
||||||
|
[&](const std::string& pn) { return player_name == pn; })) {
|
||||||
|
spdlog::warn("mpris[{}]: ignoring player update", player_name);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make status lowercase
|
||||||
|
player_status[0] = std::tolower(player_status[0]);
|
||||||
|
|
||||||
|
PlayerInfo info = {
|
||||||
|
.name = player_name,
|
||||||
|
.status = player_playback_status,
|
||||||
|
.status_string = player_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (auto artist_ = playerctl_player_get_artist(player, &error)) {
|
||||||
|
spdlog::debug("mpris[{}]: artist = {}", info.name, artist_);
|
||||||
|
info.artist = Glib::Markup::escape_text(artist_);
|
||||||
|
g_free(artist_);
|
||||||
|
}
|
||||||
|
if (error) goto errorexit;
|
||||||
|
|
||||||
|
if (auto album_ = playerctl_player_get_album(player, &error)) {
|
||||||
|
spdlog::debug("mpris[{}]: album = {}", info.name, album_);
|
||||||
|
info.album = Glib::Markup::escape_text(album_);
|
||||||
|
g_free(album_);
|
||||||
|
}
|
||||||
|
if (error) goto errorexit;
|
||||||
|
|
||||||
|
if (auto title_ = playerctl_player_get_title(player, &error)) {
|
||||||
|
spdlog::debug("mpris[{}]: title = {}", info.name, title_);
|
||||||
|
info.title = Glib::Markup::escape_text(title_);
|
||||||
|
g_free(title_);
|
||||||
|
}
|
||||||
|
if (error) goto errorexit;
|
||||||
|
|
||||||
|
if (auto length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) {
|
||||||
|
spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_);
|
||||||
|
std::chrono::microseconds len = std::chrono::microseconds(std::strtol(length_, nullptr, 10));
|
||||||
|
auto len_h = std::chrono::duration_cast<std::chrono::hours>(len);
|
||||||
|
auto len_m = std::chrono::duration_cast<std::chrono::minutes>(len - len_h);
|
||||||
|
auto len_s = std::chrono::duration_cast<std::chrono::seconds>(len - len_m);
|
||||||
|
info.length = fmt::format("{:02}:{:02}:{:02}", len_h.count(), len_m.count(), len_s.count());
|
||||||
|
g_free(length_);
|
||||||
|
}
|
||||||
|
if (error) goto errorexit;
|
||||||
|
|
||||||
|
return info;
|
||||||
|
|
||||||
|
errorexit:
|
||||||
|
spdlog::error("mpris[{}]: {}", info.name, error->message);
|
||||||
|
g_error_free(error);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Mpris::handleToggle(GdkEventButton* const& e) {
|
||||||
|
GError* error = nullptr;
|
||||||
|
|
||||||
|
auto info = getPlayerInfo();
|
||||||
|
if (!info) return false;
|
||||||
|
|
||||||
|
if (e->type == GdkEventType::GDK_BUTTON_PRESS) {
|
||||||
|
switch (e->button) {
|
||||||
|
case 1: // left-click
|
||||||
|
if (config_["on-click"].isString()) {
|
||||||
|
return AModule::handleToggle(e);
|
||||||
|
}
|
||||||
|
playerctl_player_play_pause(player, &error);
|
||||||
|
break;
|
||||||
|
case 2: // middle-click
|
||||||
|
if (config_["on-middle-click"].isString()) {
|
||||||
|
return AModule::handleToggle(e);
|
||||||
|
}
|
||||||
|
playerctl_player_previous(player, &error);
|
||||||
|
break;
|
||||||
|
case 3: // right-click
|
||||||
|
if (config_["on-right-click"].isString()) {
|
||||||
|
return AModule::handleToggle(e);
|
||||||
|
}
|
||||||
|
playerctl_player_next(player, &error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
spdlog::error("mpris[{}]: error running builtin on-click action: {}", (*info).name,
|
||||||
|
error->message);
|
||||||
|
g_error_free(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mpris::update() -> void {
|
||||||
|
auto opt = getPlayerInfo();
|
||||||
|
if (!opt) {
|
||||||
|
event_box_.set_visible(false);
|
||||||
|
AModule::update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto info = *opt;
|
||||||
|
|
||||||
|
if (info.status == PLAYERCTL_PLAYBACK_STATUS_STOPPED) {
|
||||||
|
spdlog::debug("mpris[{}]: player stopped, skipping update", info.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::debug("mpris[{}]: running update", info.name);
|
||||||
|
|
||||||
|
// dynamic is the auto-formatted string containing a nice out-of-the-box
|
||||||
|
// format text
|
||||||
|
std::stringstream dynamic;
|
||||||
|
if (info.artist) dynamic << *info.artist << " - ";
|
||||||
|
if (info.album) dynamic << *info.album << " - ";
|
||||||
|
if (info.title) dynamic << *info.title;
|
||||||
|
if (info.length)
|
||||||
|
dynamic << " "
|
||||||
|
<< "<small>"
|
||||||
|
<< "[" << *info.length << "]"
|
||||||
|
<< "</small>";
|
||||||
|
|
||||||
|
// set css class for player status
|
||||||
|
if (!lastStatus.empty() && box_.get_style_context()->has_class(lastStatus)) {
|
||||||
|
box_.get_style_context()->remove_class(lastStatus);
|
||||||
|
}
|
||||||
|
if (!box_.get_style_context()->has_class(info.status_string)) {
|
||||||
|
box_.get_style_context()->add_class(info.status_string);
|
||||||
|
}
|
||||||
|
lastStatus = info.status_string;
|
||||||
|
|
||||||
|
// set css class for player name
|
||||||
|
if (!lastPlayer.empty() && box_.get_style_context()->has_class(lastPlayer)) {
|
||||||
|
box_.get_style_context()->remove_class(lastPlayer);
|
||||||
|
}
|
||||||
|
if (!box_.get_style_context()->has_class(info.name)) {
|
||||||
|
box_.get_style_context()->add_class(info.name);
|
||||||
|
}
|
||||||
|
lastPlayer = info.name;
|
||||||
|
|
||||||
|
auto formatstr = format_;
|
||||||
|
switch (info.status) {
|
||||||
|
case PLAYERCTL_PLAYBACK_STATUS_PLAYING:
|
||||||
|
if (!format_playing_.empty()) formatstr = format_playing_;
|
||||||
|
break;
|
||||||
|
case PLAYERCTL_PLAYBACK_STATUS_PAUSED:
|
||||||
|
if (!format_paused_.empty()) formatstr = format_paused_;
|
||||||
|
break;
|
||||||
|
case PLAYERCTL_PLAYBACK_STATUS_STOPPED:
|
||||||
|
if (!format_stopped_.empty()) formatstr = format_stopped_;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto label_format =
|
||||||
|
fmt::format(formatstr, fmt::arg("player", info.name), fmt::arg("status", info.status_string),
|
||||||
|
fmt::arg("artist", *info.artist), fmt::arg("title", *info.title),
|
||||||
|
fmt::arg("album", *info.album), fmt::arg("length", *info.length),
|
||||||
|
fmt::arg("dynamic", dynamic.str()),
|
||||||
|
fmt::arg("player_icon", getIcon(config_["player-icons"], info.name)),
|
||||||
|
fmt::arg("status_icon", getIcon(config_["status-icons"], info.status_string)));
|
||||||
|
label_.set_markup(label_format);
|
||||||
|
|
||||||
|
event_box_.set_visible(true);
|
||||||
|
// call parent update
|
||||||
|
AModule::update();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace waybar::modules::mpris
|
Loading…
Reference in New Issue