Waybar/src/modules/mpd/mpd.cpp

418 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "modules/mpd/mpd.hpp"
#include <fmt/chrono.h>
#include <glibmm/ustring.h>
#include <spdlog/spdlog.h>
#include <system_error>
#include <util/sanitize_str.hpp>
using namespace waybar::util;
#include "modules/mpd/state.hpp"
#if defined(MPD_NOINLINE)
namespace waybar::modules {
#include "modules/mpd/state.inl.hpp"
} // namespace waybar::modules
#endif
waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
: ALabel(config, "mpd", id, "{album} - {artist} - {title}", 5, false, true),
module_name_(id.empty() ? "mpd" : "mpd#" + id),
server_(nullptr),
port_(config_["port"].isUInt() ? config["port"].asUInt() : 0),
password_(config_["password"].empty() ? "" : config_["password"].asString()),
timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000),
connection_(nullptr, &mpd_connection_free),
status_(nullptr, &mpd_status_free),
song_(nullptr, &mpd_song_free) {
if (!config_["port"].isNull() && !config_["port"].isUInt()) {
spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_);
}
if (!config_["timeout"].isNull() && !config_["timeout"].isUInt()) {
spdlog::warn("{}: `timeout` configuration should be an unsigned int", module_name_);
}
if (!config["server"].isNull()) {
if (!config_["server"].isString()) {
spdlog::warn("{}:`server` configuration should be a string", module_name_);
}
server_ = config["server"].asCString();
}
event_box_.add_events(Gdk::BUTTON_PRESS_MASK);
event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &MPD::handlePlayPause));
}
auto waybar::modules::MPD::update() -> void {
context_.update();
// Call parent update
ALabel::update();
}
void waybar::modules::MPD::queryMPD() {
if (connection_ != nullptr) {
spdlog::trace("{}: fetching state information", module_name_);
try {
fetchState();
spdlog::trace("{}: fetch complete", module_name_);
} catch (std::exception const& e) {
spdlog::error("{}: {}", module_name_, e.what());
state_ = MPD_STATE_UNKNOWN;
}
dp.emit();
}
}
std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const {
std::string result =
config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A";
const char* tag = mpd_song_get_tag(song_.get(), type, idx);
// mpd_song_get_tag can return NULL, so make sure it's valid before setting
if (tag) result = tag;
return result;
}
Glib::ustring waybar::modules::MPD::getArtistTitleOrFilename() const {
const char* artist = mpd_song_get_tag(this->song_.get(), MPD_TAG_ARTIST, 0);
const char* title = mpd_song_get_tag(this->song_.get(), MPD_TAG_TITLE, 0);
// https://mpd.readthedocs.io/en/latest/protocol.html#tags
const char* name = mpd_song_get_tag(this->song_.get(), MPD_TAG_NAME, 0);
// it is important to use Glib::ustring here, as filenames may be utf8 encoded
// DECO27 x ピノキオピー - デビルじゃないもん feat. 初音ミク-lMEt3RdqB9Y.m4a
Glib::ustring result;
auto starts_with = [&](const Glib::ustring& str, const char* other) {
size_t other_len = strlen(other);
if (str.size() < other_len) {
return false;
}
for (size_t i = 0; i < other_len; i++) {
if (str[i] != other[i]) {
return false;
}
}
return true;
};
if (title || name) {
result = title ? title : name;
bool prepend_artist = artist != nullptr && !starts_with(result, artist);
if (config_["title-len"].isInt()) {
result = result.substr(0, config_["title-len"].asInt());
}
if (prepend_artist) {
result = std::string(artist) + " - " + std::move(result);
}
return result;
}
result = this->getFilename();
if (config_["title-len"].isInt()) {
result = result.substr(0, config_["title-len"].asInt());
}
return result;
}
std::string waybar::modules::MPD::getFilename() const {
std::string path = mpd_song_get_uri(song_.get());
size_t position = path.find_last_of("/");
if (position == std::string::npos) {
return path;
} else {
return path.substr(position + 1);
}
}
void waybar::modules::MPD::setLabel() {
if (connection_ == nullptr) {
label_.get_style_context()->add_class("disconnected");
label_.get_style_context()->remove_class("stopped");
label_.get_style_context()->remove_class("playing");
label_.get_style_context()->remove_class("paused");
auto format = config_["format-disconnected"].isString()
? config_["format-disconnected"].asString()
: "disconnected";
if (format.empty()) {
label_.set_markup(format);
label_.show();
} else {
label_.hide();
}
if (tooltipEnabled()) {
std::string tooltip_format;
tooltip_format = config_["tooltip-format-disconnected"].isString()
? config_["tooltip-format-disconnected"].asString()
: "MPD (disconnected)";
// Nothing to format
label_.set_tooltip_text(tooltip_format);
}
return;
}
label_.get_style_context()->remove_class("disconnected");
auto format = format_;
Glib::ustring artist, album_artist, album, title, artist_title_or_filename;
std::string date, filename;
int song_pos = 0, queue_length = 0, volume = 0;
std::chrono::seconds elapsedTime, totalTime;
std::string stateIcon = "";
bool no_song = song_.get() == nullptr;
if (stopped() || no_song) {
if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped.");
format =
config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped";
label_.get_style_context()->add_class("stopped");
label_.get_style_context()->remove_class("playing");
label_.get_style_context()->remove_class("paused");
} else {
label_.get_style_context()->remove_class("stopped");
if (playing()) {
label_.get_style_context()->add_class("playing");
label_.get_style_context()->remove_class("paused");
} else if (paused()) {
format = config_["format-paused"].isString() ? config_["format-paused"].asString()
: config_["format"].asString();
label_.get_style_context()->add_class("paused");
label_.get_style_context()->remove_class("playing");
}
stateIcon = getStateIcon();
artist = sanitize_string(getTag(MPD_TAG_ARTIST));
album_artist = sanitize_string(getTag(MPD_TAG_ALBUM_ARTIST));
album = sanitize_string(getTag(MPD_TAG_ALBUM));
title = sanitize_string(getTag(MPD_TAG_TITLE));
date = sanitize_string(getTag(MPD_TAG_DATE));
filename = sanitize_string(getFilename());
artist_title_or_filename = sanitize_string(this->getArtistTitleOrFilename());
song_pos = mpd_status_get_song_pos(status_.get()) + 1;
volume = mpd_status_get_volume(status_.get());
if (volume < 0) {
volume = 0;
}
queue_length = mpd_status_get_queue_length(status_.get());
elapsedTime = std::chrono::seconds(mpd_status_get_elapsed_time(status_.get()));
totalTime = std::chrono::seconds(mpd_status_get_total_time(status_.get()));
}
bool consumeActivated = mpd_status_get_consume(status_.get());
std::string consumeIcon = getOptionIcon("consume", consumeActivated);
bool randomActivated = mpd_status_get_random(status_.get());
std::string randomIcon = getOptionIcon("random", randomActivated);
bool repeatActivated = mpd_status_get_repeat(status_.get());
std::string repeatIcon = getOptionIcon("repeat", repeatActivated);
bool singleActivated = mpd_status_get_single(status_.get());
std::string singleIcon = getOptionIcon("single", singleActivated);
if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt());
if (config_["album-artist-len"].isInt())
album_artist = album_artist.substr(0, config_["album-artist-len"].asInt());
if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt());
if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt());
try {
auto text = fmt::format(
fmt::runtime(format), fmt::arg("artist", artist.raw()),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()),
fmt::arg("title", title.raw()), fmt::arg("artistTitleOrFilename", artist_title_or_filename.raw()),
fmt::arg("date", date), fmt::arg("volume", volume),
fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime),
fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length),
fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon),
fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon),
fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename));
if (text.empty()) {
label_.hide();
} else {
label_.show();
label_.set_markup(text);
}
} catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error: {}", e.what());
}
if (tooltipEnabled()) {
std::string tooltip_format;
tooltip_format = config_["tooltip-format"].isString() ? config_["tooltip-format"].asString()
: "MPD (connected)";
try {
auto tooltip_text =
fmt::format(fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()),
fmt::arg("title", title.raw()), fmt::arg("date", date),
fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime),
fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos),
fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon),
fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon),
fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon));
label_.set_tooltip_text(tooltip_text);
} catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error (tooltip): {}", e.what());
}
}
}
std::string waybar::modules::MPD::getStateIcon() const {
if (!config_["state-icons"].isObject()) {
return "";
}
if (connection_ == nullptr) {
spdlog::warn("{}: Trying to fetch state icon while disconnected", module_name_);
return "";
}
if (stopped()) {
spdlog::warn("{}: Trying to fetch state icon while stopped", module_name_);
return "";
}
if (playing()) {
return config_["state-icons"]["playing"].asString();
} else {
return config_["state-icons"]["paused"].asString();
}
}
std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool activated) const {
if (!config_[optionName + "-icons"].isObject()) {
return "";
}
if (connection_ == nullptr) {
spdlog::warn("{}: Trying to fetch option icon while disconnected", module_name_);
return "";
}
if (activated) {
return config_[optionName + "-icons"]["on"].asString();
} else {
return config_[optionName + "-icons"]["off"].asString();
}
}
static bool isServerUnavailable(const std::error_code& ec) {
if (ec.category() == std::system_category()) {
switch (ec.value()) {
case ECONNREFUSED:
case ECONNRESET:
case ENETDOWN:
case ENETUNREACH:
case EHOSTDOWN:
case ENOENT:
return true;
}
}
return false;
}
void waybar::modules::MPD::tryConnect() {
if (connection_ != nullptr) {
return;
}
connection_ =
detail::unique_connection(mpd_connection_new(server_, port_, timeout_), &mpd_connection_free);
if (connection_ == nullptr) {
spdlog::error("{}: Failed to connect to MPD", module_name_);
connection_.reset();
return;
}
try {
checkErrors(connection_.get());
spdlog::debug("{}: Connected to MPD", module_name_);
if (!password_.empty()) {
bool res = mpd_run_password(connection_.get(), password_.c_str());
if (!res) {
spdlog::error("{}: Wrong MPD password", module_name_);
connection_.reset();
return;
}
checkErrors(connection_.get());
}
} catch (std::system_error& e) {
/* Tone down logs if it's likely that the mpd server is not running */
auto level = isServerUnavailable(e.code()) ? spdlog::level::debug : spdlog::level::err;
spdlog::log(level, "{}: Failed to connect to MPD: {}", module_name_, e.what());
connection_.reset();
} catch (std::runtime_error& e) {
spdlog::error("{}: Failed to connect to MPD: {}", module_name_, e.what());
connection_.reset();
}
}
void waybar::modules::MPD::checkErrors(mpd_connection* conn) {
switch (mpd_connection_get_error(conn)) {
case MPD_ERROR_SUCCESS:
mpd_connection_clear_error(conn);
return;
case MPD_ERROR_TIMEOUT:
case MPD_ERROR_CLOSED:
mpd_connection_clear_error(conn);
connection_.reset();
state_ = MPD_STATE_UNKNOWN;
throw std::runtime_error("Connection to MPD closed");
case MPD_ERROR_SYSTEM:
if (auto ec = mpd_connection_get_system_error(conn); ec != 0) {
mpd_connection_clear_error(conn);
throw std::system_error(ec, std::system_category());
}
G_GNUC_FALLTHROUGH;
default:
if (conn) {
auto error_message = mpd_connection_get_error_message(conn);
std::string error(error_message);
mpd_connection_clear_error(conn);
throw std::runtime_error(error);
}
throw std::runtime_error("Invalid connection");
}
}
void waybar::modules::MPD::fetchState() {
if (connection_ == nullptr) {
spdlog::error("{}: Not connected to MPD", module_name_);
return;
}
auto conn = connection_.get();
status_ = detail::unique_status(mpd_run_status(conn), &mpd_status_free);
checkErrors(conn);
state_ = mpd_status_get_state(status_.get());
checkErrors(conn);
song_ = detail::unique_song(mpd_run_current_song(conn), &mpd_song_free);
checkErrors(conn);
}
bool waybar::modules::MPD::handlePlayPause(GdkEventButton* const& e) {
if (e->type == GDK_2BUTTON_PRESS || e->type == GDK_3BUTTON_PRESS || connection_ == nullptr) {
return false;
}
if (e->button == 1) {
if (state_ == MPD_STATE_PLAY)
context_.pause();
else
context_.play();
} else if (e->button == 3) {
context_.stop();
}
return true;
}