PlayerLink/src/main.cpp

295 lines
11 KiB
C++
Raw Normal View History

2024-11-01 14:10:00 +01:00
#include <discord-rpc/discord_rpc.h>
2024-11-02 17:17:12 +01:00
#include <wx/image.h>
#include <wx/mstream.h>
2024-11-03 12:56:42 +01:00
#include <wx/statline.h>
2024-11-02 17:17:12 +01:00
#include <wx/taskbar.h>
#include <wx/wx.h>
2024-11-01 14:10:00 +01:00
#include <chrono>
#include <thread>
#include "backend.hpp"
2024-11-02 17:17:12 +01:00
#include "rsrc.hpp"
2024-11-01 14:10:00 +01:00
#include "utils.hpp"
2024-11-01 14:10:00 +01:00
std::string lastPlayingSong = "";
std::string lastMediaSource = "";
2024-11-04 16:09:00 +01:00
std::string currentSongTitle = "";
2024-11-01 14:10:00 +01:00
void handleRPCTasks() {
while (true) {
2024-11-04 22:53:19 +01:00
while (true) {
DiscordEventHandlers discordHandler{};
auto app = utils::getApp(lastMediaSource);
Discord_Initialize(app.clientId.c_str(), &discordHandler);
if (Discord_IsConnected())
break;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
while (true) {
Discord_RunCallbacks();
if (!Discord_IsConnected())
break;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
Discord_Shutdown();
2024-11-01 14:10:00 +01:00
}
}
void handleMediaTasks() {
int64_t lastMs = 0;
2024-11-01 14:10:00 +01:00
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
auto mediaInformation = backend::getMediaInformation();
if (!mediaInformation) {
2024-11-04 16:09:00 +01:00
currentSongTitle = "";
2024-11-01 14:10:00 +01:00
Discord_ClearPresence(); // Nothing is playing rn, clear presence
continue;
}
if (mediaInformation->paused) {
lastMs = 0;
lastPlayingSong = "";
2024-11-04 16:09:00 +01:00
currentSongTitle = "";
Discord_ClearPresence();
2024-11-01 14:10:00 +01:00
continue;
}
2024-11-01 16:49:48 +01:00
std::string currentlyPlayingSong = mediaInformation->songTitle + mediaInformation->songArtist +
mediaInformation->songAlbum + std::to_string(mediaInformation->songDuration);
int64_t currentMs = mediaInformation->songElapsedTime;
2024-11-01 14:10:00 +01:00
bool shouldContinue =
currentlyPlayingSong == lastPlayingSong && (lastMs <= currentMs) && (lastMs + 3000 >= currentMs);
lastMs = currentMs;
if (shouldContinue)
2024-11-01 14:10:00 +01:00
continue;
lastPlayingSong = currentlyPlayingSong;
2024-11-04 16:09:00 +01:00
currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle;
2024-11-01 14:10:00 +01:00
std::string currentMediaSource = mediaInformation->playbackSource;
2024-11-01 16:49:48 +01:00
if (currentMediaSource != lastMediaSource) {
lastMediaSource = currentMediaSource;
Discord_Shutdown();
} // reinitialize with new client id
2024-11-01 15:04:43 +01:00
2024-11-04 16:09:00 +01:00
auto app = utils::getApp(lastMediaSource);
if (!app.enabled) {
Discord_ClearPresence();
continue;
}
std::string serviceName = app.appName;
2024-11-01 16:49:48 +01:00
std::string activityState = "by " + mediaInformation->songArtist;
2024-11-01 15:04:43 +01:00
DiscordRichPresence activity{};
2024-11-01 14:10:00 +01:00
activity.type = ActivityType::LISTENING;
2024-11-01 15:04:43 +01:00
activity.details = mediaInformation->songTitle.c_str();
2024-11-01 16:49:48 +01:00
activity.state = activityState.c_str();
2024-11-01 15:04:43 +01:00
activity.smallImageText = serviceName.c_str();
std::string artworkURL = utils::getArtworkURL(mediaInformation->songTitle + " " + mediaInformation->songArtist +
" " + mediaInformation->songAlbum);
2024-11-01 14:10:00 +01:00
2024-11-01 16:49:48 +01:00
activity.smallImageKey = "appicon";
2024-11-01 15:04:43 +01:00
if (artworkURL == "") {
activity.smallImageKey = "";
2024-11-01 16:49:48 +01:00
activity.largeImageKey = "appicon";
2024-11-01 15:04:43 +01:00
} else {
activity.largeImageKey = artworkURL.c_str();
}
2024-11-01 16:49:48 +01:00
activity.largeImageText = mediaInformation->songAlbum.c_str();
2024-11-01 14:10:00 +01:00
2024-11-01 15:04:43 +01:00
if (mediaInformation->songDuration != 0) {
int64_t remainingTime = mediaInformation->songDuration - mediaInformation->songElapsedTime;
2024-11-01 16:49:48 +01:00
activity.startTimestamp = time(nullptr) - (mediaInformation->songElapsedTime / 1000);
activity.endTimestamp = time(nullptr) + (remainingTime / 1000);
2024-11-01 15:04:43 +01:00
}
2024-11-04 16:09:00 +01:00
std::string endpointURL = app.searchEndpoint;
2024-11-01 14:10:00 +01:00
2024-11-01 16:49:48 +01:00
std::string searchQuery = mediaInformation->songTitle + " " + mediaInformation->songArtist;
std::string buttonName = "Search on " + serviceName;
std::string buttonText = endpointURL + utils::urlEncode(searchQuery);
2024-11-01 15:04:43 +01:00
if (endpointURL != "") {
2024-11-01 16:49:48 +01:00
activity.button1name = buttonName.c_str();
activity.button1link = buttonText.c_str();
2024-11-01 15:04:43 +01:00
}
2024-11-01 14:10:00 +01:00
2024-11-01 15:04:43 +01:00
Discord_UpdatePresence(&activity);
2024-11-01 14:10:00 +01:00
}
}
2024-11-03 12:56:42 +01:00
class PlayerLinkIcon : public wxTaskBarIcon {
2024-11-02 17:17:12 +01:00
public:
2024-11-03 12:56:42 +01:00
PlayerLinkIcon(wxFrame* s) : settingsFrame(s) {}
2024-11-02 17:17:12 +01:00
2024-11-03 12:56:42 +01:00
void OnMenuOpen(wxCommandEvent& evt) { settingsFrame->Show(true); }
2024-11-02 17:17:12 +01:00
2024-11-03 12:56:42 +01:00
void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); }
2024-11-02 17:17:12 +01:00
void OnMenuAbout(wxCommandEvent& evt) {
wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION);
}
2024-11-02 17:17:12 +01:00
protected:
virtual wxMenu* CreatePopupMenu() override {
wxMenu* menu = new wxMenu;
menu->Append(10004, _(currentSongTitle == "" ? "Not Playing" : currentSongTitle));
2024-11-03 12:56:42 +01:00
menu->Enable(10004, false);
2024-11-02 17:17:12 +01:00
menu->AppendSeparator();
2024-11-03 12:56:42 +01:00
menu->Append(10001, _("Settings"));
menu->Append(10003, _("About PlayerLink"));
menu->AppendSeparator();
menu->Append(10002, _("Quit PlayerLink..."));
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuOpen, this, 10001);
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuExit, this, 10002);
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuAbout, this, 10003);
2024-11-02 17:17:12 +01:00
return menu;
}
private:
2024-11-03 12:56:42 +01:00
wxFrame* settingsFrame;
};
class PlayerLinkFrame : public wxFrame {
protected:
wxStaticText* settingsText;
wxStaticLine* settingsDivider;
wxStaticText* enabledAppsText;
wxCheckBox* anyOtherCheckbox;
wxStaticLine* appsDivider;
wxStaticText* startupText;
wxCheckBox* autostartCheckbox;
public:
PlayerLinkFrame(wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200),
long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX)
: wxFrame(parent, id, title, pos, size, style) {
this->SetSizeHints(wxDefaultSize, wxDefaultSize);
wxBoxSizer* mainContainer;
mainContainer = new wxBoxSizer(wxVERTICAL);
settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
settingsText->Wrap(-1);
mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5);
settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* enabledAppsContainer;
enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL);
enabledAppsText = new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
enabledAppsText->Wrap(-1);
enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5);
wxBoxSizer* appCheckboxContainer;
appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
2024-11-04 16:09:00 +01:00
auto settings = utils::getSettings();
2024-11-03 12:56:42 +01:00
2024-11-04 16:09:00 +01:00
for (auto app : settings.apps) {
2024-11-03 12:56:42 +01:00
auto checkbox = new wxCheckBox(this, wxID_ANY, _(app.appName), wxDefaultPosition, wxDefaultSize, 0);
2024-11-04 16:09:00 +01:00
checkbox->SetValue(app.enabled);
checkbox->SetClientData(new utils::App(app));
checkbox->Bind(wxEVT_CHECKBOX, [checkbox](wxCommandEvent& event) {
bool isChecked = checkbox->IsChecked();
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
appData->enabled = isChecked;
utils::saveSettings(appData);
});
checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) {
delete static_cast<utils::App*>(checkbox->GetClientData());
});
2024-11-03 12:56:42 +01:00
appCheckboxContainer->Add(checkbox, 0, wxALL, 5);
}
anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
2024-11-04 16:09:00 +01:00
anyOtherCheckbox->SetValue(settings.anyOtherEnabled);
anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) {
bool isChecked = this->anyOtherCheckbox->IsChecked();
auto settings = utils::getSettings();
settings.anyOtherEnabled = isChecked;
utils::saveSettings(settings);
});
2024-11-03 12:56:42 +01:00
appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5);
enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
mainContainer->Add(enabledAppsContainer, 0, 0, 5);
appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* settingsContainer;
settingsContainer = new wxBoxSizer(wxHORIZONTAL);
startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
startupText->Wrap(-1);
settingsContainer->Add(startupText, 0, wxALL, 5);
autostartCheckbox = new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
2024-11-04 16:09:00 +01:00
autostartCheckbox->SetValue(settings.autoStart);
autostartCheckbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) {
bool isChecked = this->autostartCheckbox->IsChecked();
auto settings = utils::getSettings();
settings.autoStart = isChecked;
2024-11-04 18:29:25 +01:00
backend::toggleAutostart(isChecked);
2024-11-04 16:09:00 +01:00
utils::saveSettings(settings);
});
2024-11-03 12:56:42 +01:00
2024-11-04 16:09:00 +01:00
settingsContainer->Add(autostartCheckbox, 0, wxALL, 5);
2024-11-03 12:56:42 +01:00
mainContainer->Add(settingsContainer, 0, wxEXPAND, 5);
this->SetSizerAndFit(mainContainer);
wxSize currentSize = this->GetSize();
this->SetSize(size.GetWidth(), currentSize.GetHeight());
this->Layout();
this->Centre(wxBOTH);
}
2024-11-02 17:17:12 +01:00
};
2024-11-03 12:56:42 +01:00
class PlayerLink : public wxApp {
public:
virtual bool OnInit() override {
2024-11-03 12:56:42 +01:00
if (wxSystemSettings::GetAppearance().IsSystemDark()) // To support the native dark mode on windows 10 and up
this->SetAppearance(wxAppBase::Appearance::Dark);
2024-11-02 17:17:12 +01:00
wxInitAllImageHandlers();
2024-11-03 12:56:42 +01:00
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, wxID_ANY, _("PlayerLink"));
trayIcon = new PlayerLinkIcon(frame);
2024-11-02 17:17:12 +01:00
frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) {
2024-11-03 12:56:42 +01:00
if (event.CanVeto()) {
frame->Hide();
event.Veto();
} else
this->ExitMainLoop();
2024-11-02 17:17:12 +01:00
});
wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
2024-11-03 12:56:42 +01:00
trayIcon->SetIcon(icon, _("PlayerLink"));
return true;
}
2024-11-02 17:17:12 +01:00
private:
2024-11-03 12:56:42 +01:00
PlayerLinkIcon* trayIcon;
};
2024-11-03 12:56:42 +01:00
wxIMPLEMENT_APP_NO_MAIN(PlayerLink);
int main(int argc, char** argv) {
if (!backend::init()) {
wxMessageBox(_("Error initializing platform backend!"), _("PlayerLink"), wxOK | wxICON_ERROR);
return -1;
}
std::thread rpcThread(handleRPCTasks);
rpcThread.detach();
std::thread mediaThread(handleMediaTasks);
mediaThread.detach();
return wxEntry(argc, argv);
2024-11-01 14:10:00 +01:00
}