forked from purpl3F0x/TIDAL-Discord-Rich-Presence-UNOFFICIAL
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.cc
435 lines (360 loc) · 12.2 KB
/
main.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
/**
* @file main.cc
* @authors Stavros Avramidis
*/
#ifndef VERSION
#define VERSION "v.1.3.0"
#endif
/* C++ libs */
#include <atomic>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdio>
#include <iomanip>
#include <iostream>
#include <locale>
#include <stack>
#include <string>
#include <thread>
/* Qt */
#include <QAction>
#include <QApplication>
#include <QDesktopServices>
#include <QMenu>
#include <QMessageBox>
#include <QtNetwork>
#include <QSystemTrayIcon>
#include <QTimer>
/* local libs*/
#include "httplib.hh"
#include "json.hh"
#define DISCORD_REQUIRE(x) assert(x == DiscordResult_Ok)
#ifdef WIN32
#include "windows_api_hook.hh"
#elif defined(__APPLE__) or defined(__MACH__)
#include "osx_api_hook.hh"
#else
#error "Not supported target"
#endif
#include "discord_game_sdk.h"
#define CURRENT_TIME std::time(nullptr)
#define HIFI_ASSET "hifi"
static long long APPLICATION_ID = 584458858731405315;
std::atomic<bool> isPresenceActive;
static char *countryCode = nullptr;
static std::string currentStatus;
static std::mutex currentSongMutex;
struct Song {
enum AudioQualityEnum {
master, hifi, normal
};
std::string title;
std::string artist;
std::string album;
std::string url;
std::string cover_id;
char id[10];
int64_t starttime;
int64_t runtime;
uint64_t pausedtime;
uint_fast8_t trackNumber;
uint_fast8_t volumeNumber;
bool isPaused = false;
AudioQualityEnum quality;
bool loaded = false;
void setQuality(const std::string &q) {
if (q == "HI_RES") {
quality = master;
} else {
quality = hifi;
}
}
inline bool isHighRes() const noexcept { return quality == master; }
friend std::ostream &operator<<(std::ostream &out, const Song &song) {
out << song.title << " of " << song.album << " from " << song.artist << "("
<< song.runtime << ")";
return out;
}
};
std::string urlEncode(const std::string &value) {
std::ostringstream escaped;
escaped.fill('0');
escaped << std::hex;
for (std::string::value_type c : value) {
if (isalnum((unsigned char)c) || c == '-' || c == '_' || c == '.' ||
c == '~')
escaped << c;
else {
escaped << std::uppercase;
escaped << '%' << std::setw(2) << int((unsigned char)c);
escaped << std::nouppercase;
}
}
return escaped.str();
}
struct Application {
struct IDiscordCore *core;
struct IDiscordUsers *users;
bool isDiscordOK = false;
};
struct Application app;
static void updateDiscordPresence(const Song &song) {
struct IDiscordActivityManager *manager =
app.core->get_activity_manager(app.core);
if (isPresenceActive && song.loaded) {
struct DiscordActivityTimestamps timestamps{};
memset(×tamps, 0, sizeof(timestamps));
if (song.runtime) {
timestamps.end = song.starttime + song.runtime + song.pausedtime;
}
timestamps.start = song.starttime;
struct DiscordActivity activity{
DiscordActivityType_Listening
};
memset(&activity, 0, sizeof(activity));
activity.type = DiscordActivityType_Listening;
activity.application_id = APPLICATION_ID;
snprintf(activity.details, 128, "%s", song.title.c_str());
snprintf(activity.state, 128, "%s",
(song.artist + " - " + song.album).c_str());
struct DiscordActivityAssets assets{};
memset(&assets, 0, sizeof(assets));
if (song.isPaused) {
snprintf(assets.small_image, 128, "%s", "pause");
snprintf(assets.small_text, 128, "%s", "Paused");
} else {
activity.timestamps = timestamps;
}
// Get url of album picture
auto cover_id_url = song.cover_id;
std::replace(cover_id_url.begin(), cover_id_url.end(), '-', '/');
auto cover_url = "https://resources.tidal.com/images/" + cover_id_url + "/1280x1280.jpg";
std::cout << cover_url << "\n";
snprintf(assets.large_image, 128, "%s", cover_url.c_str());
snprintf(assets.large_text, 128, "%s",
song.isHighRes() ? "Playing High-Res Audio" : "");
if (song.id[0] != '\0') {
struct DiscordActivitySecrets secrets{};
memset(&secrets, 0, sizeof(secrets));
snprintf(secrets.join, 128, "%s", song.id);
activity.secrets = secrets;
}
activity.assets = assets;
activity.instance = false;
manager->update_activity(manager, &activity, nullptr, nullptr);
} else {
// std::clog << "Clearing activity\n";
manager->clear_activity(manager, nullptr, nullptr);
}
}
static void discordInit() {
memset(&app, 0, sizeof(app));
IDiscordCoreEvents events;
memset(&events, 0, sizeof(events));
struct DiscordCreateParams params{};
DiscordCreateParamsSetDefault(¶ms);
params.client_id = APPLICATION_ID;
params.flags = DiscordCreateFlags_NoRequireDiscord;
params.events = &events;
params.event_data = &app;
auto result = DiscordCreate(DISCORD_VERSION, ¶ms, &app.core);
if (result == DiscordResult_Ok) {
app.isDiscordOK = true;
}
auto user_manager = &app.core->get_user_manager;
std::lock_guard<std::mutex> lock(currentSongMutex);
currentStatus = "Connected to Discord";
}
[[noreturn]] inline void rpcLoop() {
using json = nlohmann::json;
using string = std::string;
httplib::Client cli("api.tidal.com", 80, 3);
char getSongInfoBuf[1024];
json j;
static Song curSong;
for (;;) {
if (!app.isDiscordOK) {
std::this_thread::sleep_for(std::chrono::seconds(2));
discordInit();
continue;
}
if (isPresenceActive) {
std::wstring tmpTrack, tmpArtist;
auto localStatus = tidalInfo(tmpTrack, tmpArtist);
// If song is playing
if (localStatus == playing) {
// if new song is playing
if (rawWstringToString(tmpTrack) != curSong.title || rawWstringToString(tmpArtist) != curSong.artist) {
// assign new info to current track
curSong.title = rawWstringToString(tmpTrack);
curSong.artist = rawWstringToString(tmpArtist);
curSong.runtime = 0;
curSong.pausedtime = 0;
curSong.setQuality("");
curSong.id[0] = '\0';
curSong.loaded = true;
std::lock_guard<std::mutex> lock(currentSongMutex);
currentStatus = "Playing " + curSong.title;
// get info form TIDAL api
auto search_param = std::string(curSong.title + " - " + curSong.artist.substr(0, curSong.artist.find('&')));
sprintf(getSongInfoBuf, "/v1/search?query=%s&limit=50&offset=0&types=TRACKS&countryCode=%s",
urlEncode(search_param).c_str(), countryCode ? countryCode : "US");
std::clog << "Querying :" << getSongInfoBuf << "\n";
httplib::Headers headers = {{"x-tidal-token", "zU4XHVVkc2tDPo4t"}};
auto res = cli.Get(getSongInfoBuf, headers);
if (res && res->status == 200) {
try {
j = json::parse(res->body);
for (auto i = 0u;
i < j["tracks"]["totalNumberOfItems"].get<unsigned>(); i++) {
// convert title from windows and from tidal api to strings,
// json lib doesn't support wide string so wstrings are pared as
// strings and have the same convention errors
auto fetched_str =
j["tracks"]["items"][i]["title"].get<std::string>();
auto c_str = rawWstringToString(tmpTrack);
if (fetched_str == c_str) {
if (curSong.runtime == 0 || j["tracks"]["items"][i]["audioQuality"].get<std::string>()
== "HI_RES") { // Ignore songs with same name if you have found
// song
curSong.setQuality(j["tracks"]["items"][i]["audioQuality"].get<std::string>());
curSong.trackNumber = j["tracks"]["items"][i]["trackNumber"].get<uint_fast8_t>();
curSong.volumeNumber = j["tracks"]["items"][i]["volumeNumber"].get<uint_fast8_t>();
curSong.runtime = j["tracks"]["items"][i]["duration"].get<int64_t>();
curSong.cover_id = j["tracks"]["items"][i]["album"]["cover"].get<std::string>();
sprintf(curSong.id, "%u", j["tracks"]["items"][i]["id"].get<unsigned>());
if (curSong.isHighRes())
break; // keep searching for high-res version.
}
}
}
} catch (...) {
std::cerr << "Error getting info from api: " << curSong << "\n";
}
} else {
std::clog << "Did not get results\n";
}
#ifdef DEBUG
std::clog << curSong.title << "\tFrom: " << curSong.artist << "\n";
#endif
// get time just before passing it to RPC handlers
curSong.starttime =
CURRENT_TIME +
2; // add 2 seconds to be more accurate, not a chance
updateDiscordPresence(curSong);
} else {
if (curSong.isPaused) {
curSong.isPaused = false;
updateDiscordPresence(curSong);
std::lock_guard<std::mutex> lock(currentSongMutex);
currentStatus = "Paused " + curSong.title;
}
}
} else if (localStatus == opened) {
curSong.pausedtime += 1;
curSong.isPaused = true;
updateDiscordPresence(curSong);
std::lock_guard<std::mutex> lock(currentSongMutex);
currentStatus = "Paused " + curSong.title;
} else {
curSong = Song();
updateDiscordPresence(curSong);
std::lock_guard<std::mutex> lock(currentSongMutex);
currentStatus = "Waiting for Tidal";
}
} else {
curSong = Song();
updateDiscordPresence(curSong);
std::lock_guard<std::mutex> lock(currentSongMutex);
currentStatus = "Disabled";
}
enum EDiscordResult result = app.core->run_callbacks(app.core);
if (result != DiscordResult_Ok) {
std::clog << "Bad result " << result << "\n";
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
int main(int argc, char **argv) {
// get country code for TIDAL api queries
countryCode = getLocale();
isPresenceActive = true;
// Qt main app setup
QApplication app(argc, argv);
auto appIcon = QIcon(":assets/icon.ico");
app.setWindowIcon(appIcon);
QSystemTrayIcon tray(appIcon, &app);
QAction titleAction(appIcon, "TIDAL - Discord RPC " VERSION, nullptr);
QObject::connect(&titleAction, &QAction::triggered, [&app]() {
QDesktopServices::openUrl(QUrl("https://github.com/purpl3F0x/TIDAL-Discord-Rich-Presence-UNOFFICIAL/", QUrl::TolerantMode));
});
QAction changePresenceStatusAction("Running", nullptr);
changePresenceStatusAction.setCheckable(true);
changePresenceStatusAction.setChecked(true);
QObject::connect(&changePresenceStatusAction, &QAction::triggered,
[&changePresenceStatusAction]() {
isPresenceActive = !isPresenceActive;
changePresenceStatusAction.setText(isPresenceActive ? "Running" : "Disabled (click to re-enable)");
});
QAction quitAction("Exit", nullptr);
QObject::connect(&quitAction, &QAction::triggered, [&app]() {
updateDiscordPresence(Song());
app.quit();
});
QAction currentlyPlayingAction("Status: waiting", nullptr);
currentlyPlayingAction.setDisabled(true);
QMenu trayMenu("TIDAL - RPC", nullptr);
trayMenu.addAction(&titleAction);
trayMenu.addAction(&changePresenceStatusAction);
trayMenu.addAction(¤tlyPlayingAction);
trayMenu.addAction(&quitAction);
tray.setContextMenu(&trayMenu);
tray.show();
// Check for new App version
try {
QNetworkAccessManager *manager = new QNetworkAccessManager();
QNetworkRequest request;
QNetworkReply *reply = nullptr;
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
config.setProtocol(QSsl::TlsV1_2);
request.setSslConfiguration(config);
request.setUrl(QUrl("https://api.github.com/repos/purpl3F0x/TIDAL-Discord-Rich-Presence-UNOFFICIAL/releases/latest"));
request.setHeader(QNetworkRequest::ServerHeader, "application/json");
reply = manager->get(request);
QEventLoop loop;
QObject::connect(reply, &QIODevice::readyRead, &loop, &QEventLoop::quit);
loop.exec();
nlohmann::json j;
j = nlohmann::json::parse(reply->readAll().toStdString());
if (j["tag_name"].get<std::string>() > VERSION) {
tray.showMessage("Tidal Discord RPC", "New Version Available!\nClick to download");
QObject::connect(&tray, &QSystemTrayIcon::messageClicked, &app, []() {
QDesktopServices::openUrl(QUrl("https://github.com/purpl3F0x/TIDAL-Discord-Rich-Presence-UNOFFICIAL/releases/latest",
QUrl::TolerantMode));
});
}
} catch (...) {
//
}
#if defined(__APPLE__) or defined(__MACH__)
if (!macPerms())
{
std::cerr << "No Screen Recording Perms \n";
}
#endif
QTimer timer(&app);
QObject::connect(&timer, &QTimer::timeout, &app, [¤tlyPlayingAction]() {
std::lock_guard<std::mutex> lock(currentSongMutex);
currentlyPlayingAction.setText("Status: " + QString(currentStatus.c_str()));
});
timer.start(1000);
QObject::connect(&app, &QApplication::aboutToQuit,
[&timer]() { timer.stop(); });
discordInit();
std::clog << "Discord Initialized\n";
// RPC loop call
std::thread t1(rpcLoop);
t1.detach();
return app.exec();
}