diff --git a/CMakeLists.txt b/CMakeLists.txt index 306c15b82..30f03eb96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -364,6 +364,8 @@ optional_component(STREAMTAGREADER ON "Stream tagreader" DEPENDS "sparsehash" LIBSPARSEHASH_FOUND ) +optional_component(DISCORD_RPC ON "Discord Rich Presence") + if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ) set(HAVE_CHROMAPRINT ON) endif() @@ -1237,6 +1239,11 @@ optional_source(HAVE_STREAMTAGREADER HEADERS src/tagreader/tagreaderreadstreamreply.h ) +optional_source(HAVE_DISCORD_RPC + SOURCES src/discord/richpresence.cpp + HEADERS src/discord/richpresence.h +) + if(HAVE_GLOBALSHORTCUTS) optional_source(HAVE_GLOBALSHORTCUTS @@ -1481,6 +1488,11 @@ if(LINUX AND LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE) add_subdirectory(debian) endif() +if(HAVE_DISCORD_RPC) + add_subdirectory(thirdparty/discord-rpc) + target_include_directories(strawberry_lib PUBLIC thirdparty/discord-rpc/include) +endif() + if(HAVE_TRANSLATIONS) qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete) file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts) @@ -1559,6 +1571,10 @@ if(APPLE) endif() endif() +if(HAVE_DISCORD_RPC) + target_link_libraries(strawberry_lib PRIVATE discord-rpc) +endif() + target_link_libraries(strawberry PUBLIC strawberry_lib) if(NOT APPLE) diff --git a/src/config.h.in b/src/config.h.in index 4e1fb1b56..84b474d01 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -32,6 +32,7 @@ #cmakedefine HAVE_TIDAL #cmakedefine HAVE_SPOTIFY #cmakedefine HAVE_QOBUZ +#cmakedefine HAVE_DISCORD_RPC #cmakedefine HAVE_TAGLIB_DSFFILE #cmakedefine HAVE_TAGLIB_DSDIFFFILE diff --git a/src/constants/notificationssettings.h b/src/constants/notificationssettings.h index cd4e95888..617884fcf 100644 --- a/src/constants/notificationssettings.h +++ b/src/constants/notificationssettings.h @@ -65,4 +65,12 @@ constexpr QRgb kPresetRed = qRgb(202, 22, 16); } // namespace +namespace DiscordRPCSettings { + +constexpr char kSettingsGroup[] = "DiscordRPC"; + +constexpr char kEnabled[] = "enabled"; + +} // namespace + #endif // NOTIFICATIONSSETTINGS_H diff --git a/src/discord/richpresence.cpp b/src/discord/richpresence.cpp new file mode 100644 index 000000000..e03573255 --- /dev/null +++ b/src/discord/richpresence.cpp @@ -0,0 +1,149 @@ +/* + * Strawberry Music Player + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "richpresence.h" + +#include "core/logging.h" +#include "core/player.h" +#include "core/settings.h" +#include "engine/enginebase.h" +#include "constants/notificationssettings.h" + +#include + +namespace { + +constexpr char kDiscordApplicationId[] = "1352351827206733974"; +constexpr char kStrawberryIconResourceName[] = "embedded_cover"; +constexpr char kStrawberryIconDescription[] = "Strawberry Music Player"; +constexpr qint64 kDiscordPresenceUpdateRateLimitMs = 2000; + +} // namespace + +namespace discord { + +RichPresence::RichPresence(const SharedPtr player, + const SharedPtr playlist_manager, + QObject *parent) + : QObject(parent), + player_(player), + playlist_manager_(playlist_manager), + activity_({ {}, {}, {}, 0, 0, 0 }), + send_presence_timestamp_(0), + is_enabled_(false) { + Discord_Initialize(kDiscordApplicationId, nullptr, true, nullptr); + + QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged); + QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged); + QObject::connect(&*player_, &Player::Seeked, this, &RichPresence::Seeked); +} + +RichPresence::~RichPresence() { + Discord_Shutdown(); +} + +void RichPresence::EngineStateChanged(EngineBase::State newState) { + if (newState == EngineBase::State::Playing) { + SetTimestamp(player_->engine()->position_nanosec() / 1e3); + SendPresenceUpdate(); + } + else + Discord_ClearPresence(); +} + +void RichPresence::CurrentSongChanged(const Song &song) { + SetTimestamp(0); + activity_.length_secs = song.length_nanosec() / 1e9; + activity_.title = song.title(); + activity_.artist = song.artist(); + activity_.album = song.album(); + + SendPresenceUpdate(); +} + +void RichPresence::CheckEnabled() { + Settings s; + s.beginGroup(DiscordRPCSettings::kSettingsGroup); + + is_enabled_ = s.value(DiscordRPCSettings::kEnabled).toBool(); + + s.endGroup(); + + if (!is_enabled_) + Discord_ClearPresence(); +} + +void RichPresence::SendPresenceUpdate() { + CheckEnabled(); + if (!is_enabled_) + return; + + qint64 nowTimestamp = QDateTime::currentMSecsSinceEpoch(); + if (nowTimestamp - send_presence_timestamp_ < kDiscordPresenceUpdateRateLimitMs) { + qLog(Debug) << "Not sending rich presence due to rate limit of " << kDiscordPresenceUpdateRateLimitMs << "ms"; + return; + } + + send_presence_timestamp_ = nowTimestamp; + + ::DiscordRichPresence presence_data; + memset(&presence_data, 0, sizeof(presence_data)); + QByteArray title; + QByteArray artist; + QByteArray album; + + presence_data.type = 2 /* Listening */; + presence_data.largeImageKey = kStrawberryIconResourceName; + presence_data.smallImageKey = kStrawberryIconResourceName; + presence_data.smallImageText = kStrawberryIconDescription; + presence_data.instance = false; + + if (!activity_.artist.isEmpty()) { + artist = activity_.artist.toUtf8(); + artist.prepend(tr("by ").toUtf8()); + presence_data.state = artist.constData(); + } + + if (!activity_.album.isEmpty() && !(activity_.album == activity_.title)) { + album = activity_.album.toUtf8(); + album.prepend(tr("on ").toUtf8()); + presence_data.largeImageText = album.constData(); + } + + title = activity_.title.toUtf8(); + presence_data.details = title.constData(); + + const qint64 startTimestamp = activity_.start_timestamp - activity_.seek_secs; + + presence_data.startTimestamp = startTimestamp; + presence_data.endTimestamp = startTimestamp + activity_.length_secs; + + Discord_UpdatePresence(&presence_data); +} + +void RichPresence::SetTimestamp(const qint64 seekMicroseconds) { + activity_.start_timestamp = time(nullptr); + activity_.seek_secs = seekMicroseconds / 1e6; +} + +void RichPresence::Seeked(const qint64 microseconds) { + SetTimestamp(microseconds); + SendPresenceUpdate(); +} + +} // namespace discord diff --git a/src/discord/richpresence.h b/src/discord/richpresence.h new file mode 100644 index 000000000..58d1e2ed3 --- /dev/null +++ b/src/discord/richpresence.h @@ -0,0 +1,70 @@ +/* + * Strawberry Music Player + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef RICHPRESENCE_H +#define RICHPRESENCE_H + +#include "config.h" + +#include + +#include "core/player.h" +#include "playlist/playlistmanager.h" +#include "includes/shared_ptr.h" + +namespace discord { + +class RichPresence : public QObject { + Q_OBJECT + + public: + explicit RichPresence(const SharedPtr player, + const SharedPtr playlist_manager, + QObject *parent = nullptr); + ~RichPresence(); + + void Stop(); + + private Q_SLOTS: + void EngineStateChanged(EngineBase::State newState); + void CurrentSongChanged(const Song &song); + void Seeked(const qint64 microseconds); + + private: + void CheckEnabled(); + void SendPresenceUpdate(); + void SetTimestamp(const qint64 seekMicroseconds = 0); + + const SharedPtr player_; + const SharedPtr playlist_manager_; + + struct { + QString title; + QString artist; + QString album; + qint64 start_timestamp; + qint64 length_secs; + qint64 seek_secs; + } activity_; + qint64 send_presence_timestamp_; + bool is_enabled_; +}; + +} // namespace discord + +#endif // RICHPRESENCE_H diff --git a/src/main.cpp b/src/main.cpp index a98998283..5ef538254 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -89,6 +89,10 @@ # include "mpris2/mpris2.h" #endif +#ifdef HAVE_DISCORD_RPC +# include "discord/richpresence.h" +#endif + #include "core/iconloader.h" #include "core/commandlineoptions.h" #include "core/networkproxyfactory.h" @@ -314,6 +318,9 @@ int main(int argc, char *argv[]) { #ifdef HAVE_MPRIS2 mpris::Mpris2 mpris2(app.player(), app.playlist_manager(), app.current_albumcover_loader()); #endif +#ifdef HAVE_DISCORD_RPC + discord::RichPresence discord_rich_presence(app.player(), app.playlist_manager()); +#endif // Window MainWindow w(&app, tray_icon, &osd, options); diff --git a/src/settings/notificationssettingspage.cpp b/src/settings/notificationssettingspage.cpp index f8ec9d02b..37c6ca984 100644 --- a/src/settings/notificationssettingspage.cpp +++ b/src/settings/notificationssettingspage.cpp @@ -129,6 +129,7 @@ NotificationsSettingsPage::NotificationsSettingsPage(SettingsDialog *dialog, OSD ui_->notifications_exp_chooser2->setIcon(IconLoader::Load(u"list-add"_s)); QObject::connect(pretty_popup_, &OSDPretty::PositionChanged, this, &NotificationsSettingsPage::PrettyOSDChanged); + QObject::connect(ui_->richpresence_enabled, &QCheckBox::toggled, this, &NotificationsSettingsPage::DiscordRPCChanged); } @@ -205,6 +206,11 @@ void NotificationsSettingsPage::Load() { ui_->notifications_fading->setChecked(pretty_popup_->fading()); + // Discord + s.beginGroup(DiscordRPCSettings::kSettingsGroup); + ui_->richpresence_enabled->setChecked(s.value(DiscordRPCSettings::kEnabled).toBool()); + s.endGroup(); + UpdatePopupVisible(); Init(ui_->layout_notificationssettingspage->parentWidget()); @@ -247,6 +253,9 @@ void NotificationsSettingsPage::Save() { s.setValue(OSDPrettySettings::kFading, ui_->notifications_fading->isChecked()); s.endGroup(); + s.beginGroup(DiscordRPCSettings::kSettingsGroup); + s.setValue(DiscordRPCSettings::kEnabled, ui_->richpresence_enabled->isChecked()); + s.endGroup(); } void NotificationsSettingsPage::PrettyOpacityChanged(int value) { @@ -385,3 +394,7 @@ void NotificationsSettingsPage::NotificationTypeChanged() { void NotificationsSettingsPage::PrettyOSDChanged() { set_changed(); } + +void NotificationsSettingsPage::DiscordRPCChanged() { + set_changed(); +} diff --git a/src/settings/notificationssettingspage.h b/src/settings/notificationssettingspage.h index d63df587c..fec049c8a 100644 --- a/src/settings/notificationssettingspage.h +++ b/src/settings/notificationssettingspage.h @@ -70,6 +70,8 @@ class NotificationsSettingsPage : public SettingsPage { void PrettyOSDChanged(); + void DiscordRPCChanged(); + private: Ui_NotificationsSettingsPage *ui_; OSDBase *osd_; diff --git a/src/settings/notificationssettingspage.ui b/src/settings/notificationssettingspage.ui index 6906e0d06..9ab88fb13 100644 --- a/src/settings/notificationssettingspage.ui +++ b/src/settings/notificationssettingspage.ui @@ -367,6 +367,22 @@ + + + + Discord + + + + + + Enable Rich Presence + + + + + +