#include "modules/mpris/mpris.hpp" #include #include #include #include extern "C" { #include } #include 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(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(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(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(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(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(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(data); if (!mpris) return; spdlog::debug("mpris: player-metadata callback"); // update widget mpris->dp.emit(); } auto Mpris::getPlayerInfo() -> std::optional { 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(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(len); auto len_m = std::chrono::duration_cast(len - len_h); auto len_s = std::chrono::duration_cast(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 << " " << "" << "[" << *info.length << "]" << ""; // 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