Compare commits

..

9 Commits

Author SHA1 Message Date
Jonas Kvinge
7eee74a2e9 Release 1.2.10 2025-04-18 20:04:22 +02:00
Jonas Kvinge
d9e38fb3be Remove Genius lyrics
No longer working properly because of website changes.
2025-04-18 15:56:30 +02:00
Jonas Kvinge
81cc90e54a Update Changelog 2025-04-18 02:38:37 +02:00
Jonas Kvinge
bd9771a88f TagReaderTagLib: Use TagLib::Tag::comment
Makes it use only commercial frames without description for comments, reading other commercial frames picks different iTunes tags we don't want.
2025-04-18 02:15:17 +02:00
Jonas Kvinge
f5cd81fe09 nsi: Re-enable Spotify 2025-04-16 23:25:03 +02:00
Gregor Santner
277e2cff59 Linux: Add Clementine search keyword to .desktop shortcut 2025-04-15 21:46:15 +02:00
Jonas Kvinge
6fa9514059 RichPresence: Only initialize discord when enabled 2025-04-13 21:45:55 +02:00
Jonas Kvinge
c5e38b71f7 discord_rpc: Use anonymous namespace 2025-04-13 21:34:40 +02:00
Jonas Kvinge
3746915ae7 RichPresence: Always include album 2025-04-13 19:19:53 +02:00
27 changed files with 642 additions and 877 deletions

View File

@@ -37,6 +37,5 @@ if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32) target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif() endif()
target_link_libraries(discord-rpc PRIVATE Qt${QT_VERSION_MAJOR}::Core) target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -24,13 +24,11 @@
#ifndef DISCORD_REGISTER_H #ifndef DISCORD_REGISTER_H
#define DISCORD_REGISTER_H #define DISCORD_REGISTER_H
#include <QString>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
void Discord_Register(const QString &applicationId, const char *command); void Discord_Register(const char *applicationId, const char *command);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -21,6 +21,9 @@
* *
*/ */
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio> #include <cstdio>
#include <errno.h> #include <errno.h>
#include <cstdlib> #include <cstdlib>
@@ -29,11 +32,6 @@
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include <QString>
#include "discord_rpc.h"
#include "discord_register.h"
namespace { namespace {
static bool Mkdir(const char *path) { static bool Mkdir(const char *path) {
@@ -50,7 +48,7 @@ static bool Mkdir(const char *path) {
} // namespace } // namespace
// We want to register games so we can run them from Discord client as discord-<appid>:// // We want to register games so we can run them from Discord client as discord-<appid>://
extern "C" void Discord_Register(const QString &applicationId, const char *command) { extern "C" void Discord_Register(const char *applicationId, const char *command) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing. // Add a desktop file and update some mime handlers so that xdg-open does the right thing.
@@ -77,13 +75,13 @@ extern "C" void Discord_Register(const QString &applicationId, const char *comma
"Categories=Discord;Games;\n" "Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n"; "MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]{}; char desktopFile[2048]{};
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId.toUtf8().constData(), command, applicationId.toUtf8().constData()); int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
if (fileLen <= 0) { if (fileLen <= 0) {
return; return;
} }
char desktopFilename[256]{}; char desktopFilename[256]{};
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId.toUtf8().constData()); (void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
char desktopFilePath[1024]{}; char desktopFilePath[1024]{};
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home); (void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
@@ -113,8 +111,8 @@ extern "C" void Discord_Register(const QString &applicationId, const char *comma
snprintf(xdgMimeCommand, snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand), sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s", "xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId.toUtf8().constData(), applicationId,
applicationId.toUtf8().constData()); applicationId);
if (system(xdgMimeCommand) < 0) { if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n"); fprintf(stderr, "Failed to register mime handler\n");
} }

View File

@@ -84,17 +84,15 @@ static void RegisterURL(const char *applicationId) {
} }
void Discord_Register(const QString &applicationId, const char *command) { void Discord_Register(const char *applicationId, const char *command) {
const QByteArray applicationIdData = applicationId.toUtf8();
if (command) { if (command) {
RegisterCommand(applicationIdData.constData(), command); RegisterCommand(applicationId, command);
} }
else { else {
// raii lite // raii lite
@autoreleasepool { @autoreleasepool {
RegisterURL(applicationIdData.constData()); RegisterURL(applicationId);
} }
} }

View File

@@ -147,10 +147,10 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
} }
extern "C" void Discord_Register(const QString &applicationId, const char *command) { extern "C" void Discord_Register(const char *applicationId, const char *command) {
wchar_t appId[32]{}; wchar_t appId[32]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId.toUtf8().constData(), -1, appId, 32); MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t openCommand[1024]{}; wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr; const wchar_t *wcommand = nullptr;

View File

@@ -27,10 +27,6 @@
#include <condition_variable> #include <condition_variable>
#include <thread> #include <thread>
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc.h" #include "discord_rpc.h"
#include "discord_backoff.h" #include "discord_backoff.h"
#include "discord_register.h" #include "discord_register.h"
@@ -38,9 +34,11 @@
#include "discord_rpc_connection.h" #include "discord_rpc_connection.h"
#include "discord_serialization.h" #include "discord_serialization.h"
using namespace Qt::Literals::StringLiterals; using namespace discord_rpc;
namespace discord_rpc { static void Discord_UpdateConnection();
namespace {
constexpr size_t MaxMessageSize { 16 * 1024 }; constexpr size_t MaxMessageSize { 16 * 1024 };
constexpr size_t MessageQueueSize { 8 }; constexpr size_t MessageQueueSize { 8 };
@@ -58,19 +56,17 @@ struct QueuedMessage {
} }
}; };
class User { struct User {
public:
explicit User() {}
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null // snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21 // terminator = 21
QString userId; char userId[32];
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null // 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
// terminator = 129 // terminator = 129
QString username; char username[344];
// 4 decimal digits + 1 null terminator = 5 // 4 decimal digits + 1 null terminator = 5
QString discriminator; char discriminator[8];
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35 // optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
QString avatar; char avatar[128];
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes // Rounded way up because I'm paranoid about games breaking from future changes in these sizes
}; };
@@ -83,12 +79,12 @@ static std::atomic_bool GotErrorMessage { false };
static std::atomic_bool WasJoinGame { false }; static std::atomic_bool WasJoinGame { false };
static std::atomic_bool WasSpectateGame { false }; static std::atomic_bool WasSpectateGame { false };
static std::atomic_bool UpdatePresence { false }; static std::atomic_bool UpdatePresence { false };
static QString JoinGameSecret; static char JoinGameSecret[256];
static QString SpectateGameSecret; static char SpectateGameSecret[256];
static int LastErrorCode { 0 }; static int LastErrorCode { 0 };
static QString LastErrorMessage; static char LastErrorMessage[256];
static int LastDisconnectErrorCode { 0 }; static int LastDisconnectErrorCode { 0 };
static QString LastDisconnectErrorMessage; static char LastDisconnectErrorMessage[256];
static std::mutex PresenceMutex; static std::mutex PresenceMutex;
static std::mutex HandlerMutex; static std::mutex HandlerMutex;
static QueuedMessage QueuedPresence {}; static QueuedMessage QueuedPresence {};
@@ -102,7 +98,6 @@ static auto NextConnect = std::chrono::system_clock::now();
static int Pid { 0 }; static int Pid { 0 };
static int Nonce { 1 }; static int Nonce { 1 };
static void Discord_UpdateConnection(void);
class IoThreadHolder { class IoThreadHolder {
private: private:
std::atomic_bool keepRunning { true }; std::atomic_bool keepRunning { true };
@@ -136,6 +131,7 @@ class IoThreadHolder {
~IoThreadHolder() { Stop(); } ~IoThreadHolder() { Stop(); }
}; };
static IoThreadHolder *IoThread { nullptr }; static IoThreadHolder *IoThread { nullptr };
static void UpdateReconnectTime() { static void UpdateReconnectTime() {
@@ -144,110 +140,6 @@ static void UpdateReconnectTime() {
} }
static void Discord_UpdateConnection() {
if (!Connection) {
return;
}
if (!Connection->IsOpen()) {
if (std::chrono::system_clock::now() >= NextConnect) {
UpdateReconnectTime();
Connection->Open();
}
}
else {
// reads
for (;;) {
QJsonDocument json_document;
if (!Connection->Read(json_document)) {
break;
}
const QJsonObject json_object = json_document.object();
const QString event_name = json_object["evt"_L1].toString();
const QString nonce = json_object["nonce"_L1].toString();
if (json_object.contains("nonce"_L1)) {
// in responses only -- should use to match up response when needed.
if (event_name == "ERROR"_L1) {
const QJsonObject data = json_object["data"_L1].toObject();
LastErrorCode = data["code"_L1].toInt();
LastErrorMessage = data["message"_L1].toString();
GotErrorMessage.store(true);
}
}
else {
// should have evt == name of event, optional data
if (event_name.isEmpty()) {
continue;
}
const QJsonObject data = json_object["data"_L1].toObject();
if (event_name == "ACTIVITY_JOIN"_L1) {
if (data.contains("secret"_L1)) {
JoinGameSecret = data["secret"_L1].toString();
WasJoinGame.store(true);
}
}
else if (event_name == "ACTIVITY_SPECTATE"_L1) {
if (data.contains("secret"_L1)) {
SpectateGameSecret = data["secret"_L1].toString();
WasSpectateGame.store(true);
}
}
else if (event_name == "ACTIVITY_JOIN_REQUEST"_L1) {
const QJsonObject user = data["user"_L1].toObject();
const QString userId = user["id"_L1].toString();
const QString username = user["username"_L1].toString();
const QString avatar = user["avatar"_L1].toString();
const auto joinReq = JoinAskQueue.GetNextAddMessage();
if (!userId.isEmpty() && !username.isEmpty() && joinReq) {
joinReq->userId = userId;
joinReq->username = username;
const QString discriminator = user["discriminator"_L1].toString();
if (!discriminator.isEmpty()) {
joinReq->discriminator = discriminator;
}
if (!avatar.isEmpty()) {
joinReq->avatar = avatar;
}
else {
joinReq->avatar.clear();
}
JoinAskQueue.CommitAdd();
}
}
}
}
// writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
QueuedMessage local;
{
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence);
}
if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.Copy(local);
UpdatePresence.exchange(true);
}
}
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
SendQueue.CommitSend();
}
}
}
static void SignalIOActivity() { static void SignalIOActivity() {
if (IoThread != nullptr) { if (IoThread != nullptr) {
@@ -284,7 +176,115 @@ static bool DeregisterForEvent(const char *evtName) {
} }
extern "C" void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister) { } // namespace
static void Discord_UpdateConnection() {
if (!Connection) {
return;
}
if (!Connection->IsOpen()) {
if (std::chrono::system_clock::now() >= NextConnect) {
UpdateReconnectTime();
Connection->Open();
}
}
else {
// reads
for (;;) {
JsonDocument message;
if (!Connection->Read(message)) {
break;
}
const char *evtName = GetStrMember(&message, "evt");
const char *nonce = GetStrMember(&message, "nonce");
if (nonce) {
// in responses only -- should use to match up response when needed.
if (evtName && strcmp(evtName, "ERROR") == 0) {
auto data = GetObjMember(&message, "data");
LastErrorCode = GetIntMember(data, "code");
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
GotErrorMessage.store(true);
}
}
else {
// should have evt == name of event, optional data
if (evtName == nullptr) {
continue;
}
auto data = GetObjMember(&message, "data");
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(JoinGameSecret, secret);
WasJoinGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(SpectateGameSecret, secret);
WasSpectateGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
auto joinReq = JoinAskQueue.GetNextAddMessage();
if (userId && username && joinReq) {
StringCopy(joinReq->userId, userId);
StringCopy(joinReq->username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(joinReq->discriminator, discriminator);
}
if (avatar) {
StringCopy(joinReq->avatar, avatar);
}
else {
joinReq->avatar[0] = 0;
}
JoinAskQueue.CommitAdd();
}
}
}
}
// writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
QueuedMessage local;
{
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence);
}
if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.Copy(local);
UpdatePresence.exchange(true);
}
}
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
SendQueue.CommitSend();
}
}
}
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
IoThread = new (std::nothrow) IoThreadHolder(); IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) { if (IoThread == nullptr) {
@@ -315,38 +315,37 @@ extern "C" void Discord_Initialize(const QString &applicationId, DiscordEventHan
} }
Connection = RpcConnection::Create(applicationId); Connection = RpcConnection::Create(applicationId);
Connection->onConnect = [](QJsonDocument &readyMessage) { Connection->onConnect = [](JsonDocument &readyMessage) {
Discord_UpdateHandlers(&QueuedHandlers); Discord_UpdateHandlers(&QueuedHandlers);
if (QueuedPresence.length > 0) { if (QueuedPresence.length > 0) {
UpdatePresence.exchange(true); UpdatePresence.exchange(true);
SignalIOActivity(); SignalIOActivity();
} }
const QJsonValue json_object = readyMessage.object(); auto data = GetObjMember(&readyMessage, "data");
auto data = json_object["data"_L1].toObject(); auto user = GetObjMember(data, "user");
auto user = data["user"_L1].toObject(); auto userId = GetStrMember(user, "id");
auto userId = user["id"_L1].toString(); auto username = GetStrMember(user, "username");
auto username = user["username"_L1].toString(); auto avatar = GetStrMember(user, "avatar");
auto avatar = user["avatar"_L1].toString(); if (userId && username) {
if (!userId.isEmpty() && !username.isEmpty()) { StringCopy(connectedUser.userId, userId);
connectedUser.userId = userId; StringCopy(connectedUser.username, username);
connectedUser.username = username; auto discriminator = GetStrMember(user, "discriminator");
const QString discriminator = user["discriminator"_L1].toString(); if (discriminator) {
if (!discriminator.isEmpty()) { StringCopy(connectedUser.discriminator, discriminator);
connectedUser.discriminator = discriminator;
} }
if (!avatar.isEmpty()) { if (avatar) {
connectedUser.avatar = avatar; StringCopy(connectedUser.avatar, avatar);
} }
else { else {
connectedUser = User(); connectedUser.avatar[0] = 0;
} }
} }
WasJustConnected.exchange(true); WasJustConnected.exchange(true);
ReconnectTimeMs.reset(); ReconnectTimeMs.reset();
}; };
Connection->onDisconnect = [](int err, QString &message) { Connection->onDisconnect = [](int err, const char *message) {
LastDisconnectErrorCode = err; LastDisconnectErrorCode = err;
LastDisconnectErrorMessage = message; StringCopy(LastDisconnectErrorMessage, message);
WasJustDisconnected.exchange(true); WasJustDisconnected.exchange(true);
UpdateReconnectTime(); UpdateReconnectTime();
}; };
@@ -355,7 +354,7 @@ extern "C" void Discord_Initialize(const QString &applicationId, DiscordEventHan
} }
extern "C" void Discord_Shutdown(void) { extern "C" void Discord_Shutdown() {
if (!Connection) { if (!Connection) {
return; return;
@@ -375,7 +374,7 @@ extern "C" void Discord_Shutdown(void) {
} }
extern "C" void Discord_UpdatePresence(const DiscordRichPresence &presence) { extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
{ {
std::lock_guard<std::mutex> guard(PresenceMutex); std::lock_guard<std::mutex> guard(PresenceMutex);
@@ -387,8 +386,8 @@ extern "C" void Discord_UpdatePresence(const DiscordRichPresence &presence) {
} }
extern "C" void Discord_ClearPresence() { extern "C" void Discord_ClearPresence(void) {
Discord_UpdatePresence(); Discord_UpdatePresence(nullptr);
} }
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) { extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
@@ -509,5 +508,3 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
} }
} }
} // namespace discord_rpc

View File

@@ -25,49 +25,45 @@
#define DISCORD_RPC_H #define DISCORD_RPC_H
#include <cstdint> #include <cstdint>
#include <QString>
namespace discord_rpc {
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
class DiscordRichPresence { typedef struct DiscordRichPresence {
public:
int type; int type;
QString name; /* max 128 bytes */ const char *name; /* max 128 bytes */
QString state; /* max 128 bytes */ const char *state; /* max 128 bytes */
QString details; /* max 128 bytes */ const char *details; /* max 128 bytes */
qint64 startTimestamp; int64_t startTimestamp;
qint64 endTimestamp; int64_t endTimestamp;
QString largeImageKey; /* max 32 bytes */ const char *largeImageKey; /* max 32 bytes */
QString largeImageText; /* max 128 bytes */ const char *largeImageText; /* max 128 bytes */
QString smallImageKey; /* max 32 bytes */ const char *smallImageKey; /* max 32 bytes */
QString smallImageText; /* max 128 bytes */ const char *smallImageText; /* max 128 bytes */
QString partyId; /* max 128 bytes */ const char *partyId; /* max 128 bytes */
int partySize; int partySize;
int partyMax; int partyMax;
int partyPrivacy; int partyPrivacy;
QString matchSecret; /* max 128 bytes */ const char *matchSecret; /* max 128 bytes */
QString joinSecret; /* max 128 bytes */ const char *joinSecret; /* max 128 bytes */
QString spectateSecret; /* max 128 bytes */ const char *spectateSecret; /* max 128 bytes */
qint8 instance; int8_t instance;
}; } DiscordRichPresence;
typedef struct DiscordUser { typedef struct DiscordUser {
const QString userId; const char *userId;
const QString username; const char *username;
const QString discriminator; const char *discriminator;
const QString avatar; const char *avatar;
} DiscordUser; } DiscordUser;
typedef struct DiscordEventHandlers { typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser *request); void (*ready)(const DiscordUser *request);
void (*disconnected)(int errorCode, const QString &message); void (*disconnected)(int errorCode, const char *message);
void (*errored)(int errorCode, const QString &message); void (*errored)(int errorCode, const char *message);
void (*joinGame)(const QString &joinSecret); void (*joinGame)(const char *joinSecret);
void (*spectateGame)(const QString &spectateSecret); void (*spectateGame)(const char *spectateSecret);
void (*joinRequest)(const DiscordUser *request); void (*joinRequest)(const DiscordUser *request);
} DiscordEventHandlers; } DiscordEventHandlers;
@@ -77,14 +73,14 @@ typedef struct DiscordEventHandlers {
#define DISCORD_PARTY_PRIVATE 0 #define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1 #define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister); void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown(); void Discord_Shutdown(void);
// checks for incoming messages, dispatches callbacks // checks for incoming messages, dispatches callbacks
void Discord_RunCallbacks(); void Discord_RunCallbacks(void);
void Discord_UpdatePresence(const DiscordRichPresence &presence = DiscordRichPresence()); void Discord_UpdatePresence(const DiscordRichPresence *presence);
void Discord_ClearPresence(); void Discord_ClearPresence(void);
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply); void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
@@ -94,6 +90,4 @@ void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
} /* extern "C" */ } /* extern "C" */
#endif #endif
} // namespace discord_rpc
#endif // DISCORD_RPC_H #endif // DISCORD_RPC_H

View File

@@ -21,24 +21,18 @@
* *
*/ */
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc_connection.h" #include "discord_rpc_connection.h"
#include "discord_serialization.h" #include "discord_serialization.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc { namespace discord_rpc {
static const int RpcVersion = 1; static const int RpcVersion = 1;
static RpcConnection Instance; static RpcConnection Instance;
RpcConnection *RpcConnection::Create(const QString &applicationId) { RpcConnection *RpcConnection::Create(const char *applicationId) {
Instance.connection = BaseConnection::Create(); Instance.connection = BaseConnection::Create();
Instance.appId = applicationId; StringCopy(Instance.appId, applicationId);
return &Instance; return &Instance;
} }
@@ -62,15 +56,14 @@ void RpcConnection::Open() {
} }
if (state == State::SentHandshake) { if (state == State::SentHandshake) {
QJsonDocument json_document; JsonDocument message;
if (Read(json_document)) { if (Read(message)) {
const QJsonObject json_object = json_document.object(); auto cmd = GetStrMember(&message, "cmd");
const QString cmd = json_object["cmd"_L1].toString(); auto evt = GetStrMember(&message, "evt");
const QString evt = json_object["evt"_L1].toString(); if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
if (cmd == "DISPATCH"_L1 && evt == "READY"_L1) {
state = State::Connected; state = State::Connected;
if (onConnect) { if (onConnect) {
onConnect(json_document); onConnect(message);
} }
} }
} }
@@ -113,7 +106,7 @@ bool RpcConnection::Write(const void *data, size_t length) {
} }
bool RpcConnection::Read(QJsonDocument &message) { bool RpcConnection::Read(JsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) { if (state != State::Connected && state != State::SentHandshake) {
return false; return false;
@@ -124,7 +117,7 @@ bool RpcConnection::Read(QJsonDocument &message) {
if (!didRead) { if (!didRead) {
if (!connection->isOpen) { if (!connection->isOpen) {
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed); lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
lastErrorMessage = "Pipe closed"_L1; StringCopy(lastErrorMessage, "Pipe closed");
Close(); Close();
} }
return false; return false;
@@ -134,7 +127,7 @@ bool RpcConnection::Read(QJsonDocument &message) {
didRead = connection->Read(readFrame.message, readFrame.length); didRead = connection->Read(readFrame.message, readFrame.length);
if (!didRead) { if (!didRead) {
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt); lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
lastErrorMessage = "Partial data in frame"_L1; StringCopy(lastErrorMessage, "Partial data in frame");
Close(); Close();
return false; return false;
} }
@@ -143,14 +136,14 @@ bool RpcConnection::Read(QJsonDocument &message) {
switch (readFrame.opcode) { switch (readFrame.opcode) {
case Opcode::Close: { case Opcode::Close: {
message = QJsonDocument::fromJson(readFrame.message); message.ParseInsitu(readFrame.message);
lastErrorCode = message["code"_L1].toInt(); lastErrorCode = GetIntMember(&message, "code");
lastErrorMessage = message["message"_L1].toString(); StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
Close(); Close();
return false; return false;
} }
case Opcode::Frame: case Opcode::Frame:
message = QJsonDocument::fromJson(readFrame.message); message.ParseInsitu(readFrame.message);
return true; return true;
case Opcode::Ping: case Opcode::Ping:
readFrame.opcode = Opcode::Pong; readFrame.opcode = Opcode::Pong;
@@ -164,7 +157,7 @@ bool RpcConnection::Read(QJsonDocument &message) {
default: default:
// something bad happened // something bad happened
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt); lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
lastErrorMessage = "Bad ipc frame"_L1; StringCopy(lastErrorMessage, "Bad ipc frame");
Close(); Close();
return false; return false;
} }

View File

@@ -24,9 +24,6 @@
#ifndef DISCORD_RPC_CONNECTION_H #ifndef DISCORD_RPC_CONNECTION_H
#define DISCORD_RPC_CONNECTION_H #define DISCORD_RPC_CONNECTION_H
#include <QString>
#include <QJsonDocument>
#include "discord_connection.h" #include "discord_connection.h"
#include "discord_serialization.h" #include "discord_serialization.h"
@@ -68,14 +65,14 @@ struct RpcConnection {
BaseConnection *connection { nullptr }; BaseConnection *connection { nullptr };
State state { State::Disconnected }; State state { State::Disconnected };
void (*onConnect)(QJsonDocument &message) { nullptr }; void (*onConnect)(JsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, QString &message) { nullptr }; void (*onDisconnect)(int errorCode, const char *message) { nullptr };
QString appId; char appId[64] {};
int lastErrorCode { 0 }; int lastErrorCode { 0 };
QString lastErrorMessage; char lastErrorMessage[256] {};
RpcConnection::MessageFrame sendFrame; RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const QString &applicationId); static RpcConnection *Create(const char *applicationId);
static void Destroy(RpcConnection *&); static void Destroy(RpcConnection *&);
inline bool IsOpen() const { return state == State::Connected; } inline bool IsOpen() const { return state == State::Connected; }
@@ -83,7 +80,7 @@ struct RpcConnection {
void Open(); void Open();
void Close(); void Close();
bool Write(const void *data, size_t length); bool Write(const void *data, size_t length);
bool Read(QJsonDocument &message); bool Read(JsonDocument &message);
}; };
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -21,15 +21,10 @@
* *
*/ */
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_serialization.h" #include "discord_serialization.h"
#include "discord_connection.h"
#include "discord_rpc.h" #include "discord_rpc.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc { namespace discord_rpc {
template<typename T> template<typename T>
@@ -58,129 +53,229 @@ void NumberToString(char *dest, T number) {
} }
void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value); // it's ever so slightly faster to not have to strlen the key
void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value) { template<typename T>
void WriteKey(JsonWriter &w, T &k) {
w.Key(k, sizeof(T) - 1);
}
if (!value.isEmpty()) { struct WriteObject {
json_object[key] = value; JsonWriter &writer;
WriteObject(JsonWriter &w)
: writer(w) {
writer.StartObject();
}
template<typename T>
WriteObject(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartObject();
}
~WriteObject() { writer.EndObject(); }
};
struct WriteArray {
JsonWriter &writer;
template<typename T>
WriteArray(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartArray();
}
~WriteArray() { writer.EndArray(); }
};
template<typename T>
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
if (value && value[0]) {
w.Key(k, sizeof(T) - 1);
w.String(value);
} }
} }
static QString JsonWriteNonce(const int nonce) { static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
char nonce_buffer[32]{}; WriteKey(writer, "nonce");
NumberToString(nonce_buffer, nonce); char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
return QString::fromLatin1(nonce_buffer); writer.String(nonceBuffer);
} }
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence &presence) { size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
QJsonObject json_object; JsonWriter writer(dest, maxLen);
json_object["nonce"_L1] = JsonWriteNonce(nonce); {
json_object["cmd"_L1] = "SET_ACTIVITY"_L1; WriteObject top(writer);
QJsonObject args; JsonWriteNonce(writer, nonce);
args["pid"_L1] = pid;
QJsonObject activity; WriteKey(writer, "cmd");
writer.String("SET_ACTIVITY");
if (presence.type >= 0 && presence.type <= 5) { {
activity["type"_L1] = presence.type; WriteObject args(writer, "args");
}
activity["state"_L1] = presence.state; WriteKey(writer, "pid");
activity["details"_L1] = presence.details; writer.Int(pid);
if (presence.startTimestamp != 0 || presence.endTimestamp != 0) { if (presence != nullptr) {
QJsonObject timestamps; WriteObject activity(writer, "activity");
if (presence.startTimestamp != 0) {
timestamps["start"_L1] = presence.startTimestamp; if (presence->type >= 0 && presence->type <= 5) {
WriteKey(writer, "type");
writer.Int(presence->type);
}
WriteOptionalString(writer, "name", presence->name);
WriteOptionalString(writer, "state", presence->state);
WriteOptionalString(writer, "details", presence->details);
if (presence->startTimestamp || presence->endTimestamp) {
WriteObject timestamps(writer, "timestamps");
if (presence->startTimestamp) {
WriteKey(writer, "start");
writer.Int64(presence->startTimestamp);
}
if (presence->endTimestamp) {
WriteKey(writer, "end");
writer.Int64(presence->endTimestamp);
}
}
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
(presence->largeImageText && presence->largeImageText[0]) ||
(presence->smallImageKey && presence->smallImageKey[0]) ||
(presence->smallImageText && presence->smallImageText[0])) {
WriteObject assets(writer, "assets");
WriteOptionalString(writer, "large_image", presence->largeImageKey);
WriteOptionalString(writer, "large_text", presence->largeImageText);
WriteOptionalString(writer, "small_image", presence->smallImageKey);
WriteOptionalString(writer, "small_text", presence->smallImageText);
}
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
presence->partyMax || presence->partyPrivacy) {
WriteObject party(writer, "party");
WriteOptionalString(writer, "id", presence->partyId);
if (presence->partySize && presence->partyMax) {
WriteArray size(writer, "size");
writer.Int(presence->partySize);
writer.Int(presence->partyMax);
}
if (presence->partyPrivacy) {
WriteKey(writer, "privacy");
writer.Int(presence->partyPrivacy);
}
}
if ((presence->matchSecret && presence->matchSecret[0]) ||
(presence->joinSecret && presence->joinSecret[0]) ||
(presence->spectateSecret && presence->spectateSecret[0])) {
WriteObject secrets(writer, "secrets");
WriteOptionalString(writer, "match", presence->matchSecret);
WriteOptionalString(writer, "join", presence->joinSecret);
WriteOptionalString(writer, "spectate", presence->spectateSecret);
}
writer.Key("instance");
writer.Bool(presence->instance != 0);
}
} }
if (presence.endTimestamp != 0) {
timestamps["end"_L1] = presence.endTimestamp;
}
activity["timestamps"_L1] = timestamps;
} }
if (!presence.largeImageKey.isEmpty() || !presence.largeImageText.isEmpty() || !presence.smallImageKey.isEmpty() || !presence.smallImageText.isEmpty()) { return writer.Size();
QJsonObject assets; }
WriteOptionalString(assets, "large_image"_L1, presence.largeImageKey);
WriteOptionalString(assets, "large_text"_L1, presence.largeImageText); size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
WriteOptionalString(assets, "small_image"_L1, presence.smallImageKey);
WriteOptionalString(assets, "small_text"_L1, presence.smallImageText); JsonWriter writer(dest, maxLen);
activity["assets"_L1] = assets;
{
WriteObject obj(writer);
WriteKey(writer, "v");
writer.Int(version);
WriteKey(writer, "client_id");
writer.String(applicationId);
} }
activity["instance"_L1] = presence.instance != 0; return writer.Size();
args["activity"_L1] = activity;
json_object["args"_L1] = args;
QJsonDocument json_document(json_object);
QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return data.length();
} }
size_t JsonWriteHandshakeObj(char *dest, const size_t maxLen, const int version, const QString &applicationId) { size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
QJsonObject json_object; JsonWriter writer(dest, maxLen);
json_object["v"_L1] = version;
json_object["client_id"_L1] = applicationId;
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return data.length(); {
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
} }
size_t JsonWriteSubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) { size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
QJsonObject json_object; JsonWriter writer(dest, maxLen);
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = "SUBSCRIBE"_L1;
json_object["evt"_L1] = QLatin1String(evtName);
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return data.length(); {
WriteObject obj(writer);
} JsonWriteNonce(writer, nonce);
size_t JsonWriteUnsubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) { WriteKey(writer, "cmd");
writer.String("UNSUBSCRIBE");
QJsonObject json_object; WriteKey(writer, "evt");
json_object["nonce"_L1] = JsonWriteNonce(nonce); writer.String(evtName);
json_object["cmd"_L1] = "UNSUBSCRIBE"_L1; }
json_object["evt"_L1] = QLatin1String(evtName);
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return data.length(); return writer.Size();
} }
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) { size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
QJsonObject json_object; JsonWriter writer(dest, maxLen);
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = reply == DISCORD_REPLY_YES ? "SEND_ACTIVITY_JOIN_INVITE"_L1 : "CLOSE_ACTIVITY_JOIN_REQUEST"_L1;
QJsonObject args;
args["user_id"_L1] = QLatin1String(userId);
json_object["args"_L1] = args;
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return data.length(); {
WriteObject obj(writer);
WriteKey(writer, "cmd");
if (reply == DISCORD_REPLY_YES) {
writer.String("SEND_ACTIVITY_JOIN_INVITE");
}
else {
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
}
WriteKey(writer, "args");
{
WriteObject args(writer);
WriteKey(writer, "user_id");
writer.String(userId);
}
JsonWriteNonce(writer, nonce);
}
return writer.Size();
} }

View File

@@ -24,18 +24,190 @@
#ifndef DISCORD_SERIALIZATION_H #ifndef DISCORD_SERIALIZATION_H
#define DISCORD_SERIALIZATION_H #define DISCORD_SERIALIZATION_H
#include <cstddef> #include <rapidjson/document.h>
#include <QString> #include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
struct DiscordRichPresence;
namespace discord_rpc { namespace discord_rpc {
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const QString &applicationId); // if only there was a standard library function for this
class DiscordRichPresence; template<size_t Len>
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence &presence); inline size_t StringCopy(char (&dest)[Len], const char *src) {
if (!src || !Len) {
return 0;
}
size_t copied;
char *out = dest;
for (copied = 1; *src && copied < Len; ++copied) {
*out++ = *src++;
}
*out = 0;
return copied - 1;
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
// Commands
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName); size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName); size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce); size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
// to supply some of your own allocators for stuff rather than use the defaults
class LinearAllocator {
public:
char *buffer_;
char *end_;
LinearAllocator() {
assert(0); // needed for some default case in rapidjson, should not use
}
LinearAllocator(char *buffer, size_t size)
: buffer_(buffer), end_(buffer + size) {
}
static const bool kNeedFree = false;
void *Malloc(size_t size) {
char *res = buffer_;
buffer_ += size;
if (buffer_ > end_) {
buffer_ = res;
return nullptr;
}
return res;
}
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
if (newSize == 0) {
return nullptr;
}
// allocate how much you need in the first place
assert(!originalPtr && !originalSize);
// unused parameter warning
(void)(originalPtr);
(void)(originalSize);
return Malloc(newSize);
}
static void Free(void *ptr) {
/* shrug */
(void)ptr;
}
};
template<size_t Size>
class FixedLinearAllocator : public LinearAllocator {
public:
char fixedBuffer_[Size];
FixedLinearAllocator()
: LinearAllocator(fixedBuffer_, Size) {
}
static const bool kNeedFree = false;
};
// wonder why this isn't a thing already, maybe I missed it
class DirectStringBuffer {
public:
using Ch = char;
char *buffer_;
char *end_;
char *current_;
DirectStringBuffer(char *buffer, size_t maxLen)
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
}
void Put(char c) {
if (current_ < end_) {
*current_++ = c;
}
}
void Flush() {}
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
};
using MallocAllocator = rapidjson::CrtAllocator;
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
using UTF8 = rapidjson::UTF8<char>;
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
using StackAllocator = FixedLinearAllocator<2048>;
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
using JsonWriterBase =
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
class JsonWriter : public JsonWriterBase {
public:
DirectStringBuffer stringBuffer_;
StackAllocator stackAlloc_;
JsonWriter(char *dest, size_t maxLen)
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
}
size_t Size() const { return stringBuffer_.GetSize(); }
};
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
class JsonDocument : public JsonDocumentBase {
public:
static const int kDefaultChunkCapacity = 32 * 1024;
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
// send any messages that would use all of this, though.
char parseBuffer_[32 * 1024];
MallocAllocator mallocAllocator_;
PoolAllocator poolAllocator_;
StackAllocator stackAllocator_;
JsonDocument()
: JsonDocumentBase(rapidjson::kObjectType,
&poolAllocator_,
sizeof(stackAllocator_.fixedBuffer_),
&stackAllocator_),
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
}
};
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsObject()) {
return &member->value;
}
}
return nullptr;
}
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsInt()) {
return member->value.GetInt();
}
}
return notFoundDefault;
}
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsString()) {
return member->value.GetString();
}
}
return notFoundDefault;
}
} // namespace discord_rpc } // namespace discord_rpc
#endif // DISCORD_SERIALIZATION_H #endif // DISCORD_SERIALIZATION_H

View File

@@ -212,6 +212,8 @@ find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash) pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
find_package(RapidJSON)
set(QT_VERSION_MAJOR 6) set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0) set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR}) set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
@@ -364,7 +366,9 @@ optional_component(STREAMTAGREADER ON "Stream tagreader"
DEPENDS "sparsehash" LIBSPARSEHASH_FOUND DEPENDS "sparsehash" LIBSPARSEHASH_FOUND
) )
optional_component(DISCORD_RPC ON "Discord Rich Presence") optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "RapidJSON" RapidJSON_FOUND
)
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ) if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON) set(HAVE_CHROMAPRINT ON)
@@ -678,7 +682,6 @@ set(SOURCES
src/lyrics/htmllyricsprovider.cpp src/lyrics/htmllyricsprovider.cpp
src/lyrics/ovhlyricsprovider.cpp src/lyrics/ovhlyricsprovider.cpp
src/lyrics/lololyricsprovider.cpp src/lyrics/lololyricsprovider.cpp
src/lyrics/geniuslyricsprovider.cpp
src/lyrics/musixmatchlyricsprovider.cpp src/lyrics/musixmatchlyricsprovider.cpp
src/lyrics/chartlyricsprovider.cpp src/lyrics/chartlyricsprovider.cpp
src/lyrics/songlyricscomlyricsprovider.cpp src/lyrics/songlyricscomlyricsprovider.cpp
@@ -976,7 +979,6 @@ set(HEADERS
src/lyrics/htmllyricsprovider.h src/lyrics/htmllyricsprovider.h
src/lyrics/ovhlyricsprovider.h src/lyrics/ovhlyricsprovider.h
src/lyrics/lololyricsprovider.h src/lyrics/lololyricsprovider.h
src/lyrics/geniuslyricsprovider.h
src/lyrics/musixmatchlyricsprovider.h src/lyrics/musixmatchlyricsprovider.h
src/lyrics/chartlyricsprovider.h src/lyrics/chartlyricsprovider.h
src/lyrics/songlyricscomlyricsprovider.h src/lyrics/songlyricscomlyricsprovider.h

View File

@@ -2,6 +2,17 @@ Strawberry Music Player
======================= =======================
ChangeLog ChangeLog
Version 1.2.10 (2025.04.18):
Bugfixes:
* Fixed Discord rich presence showing bogus artist and album.
* Fixed incorrect ID3v2 comment tag.
* (macOS|Windows MSVC) Fixed stuck playback of some streams.
Enhancements:
* Removed Genius lyrics (longer working properly because of website changes).
* (macOS|Windows MSVC) Added back Spotify
Version 1.2.9 (2025.04.08): Version 1.2.9 (2025.04.08):
Bugfixes: Bugfixes:

View File

@@ -53,7 +53,7 @@ Funding developers is a way to contribute to open source projects you appreciate
* Edit tags on audio files * Edit tags on audio files
* Fetch tags from MusicBrainz * Fetch tags from MusicBrainz
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/) * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/) * Song lyrics from [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
* Support for multiple backends * Support for multiple backends
* Audio analyzer * Audio analyzer
* Audio equalizer * Audio equalizer
@@ -94,6 +94,7 @@ Optional dependencies:
* MTP devices: [libmtp](http://libmtp.sourceforge.net/) * MTP devices: [libmtp](http://libmtp.sourceforge.net/)
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/) * iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128) * EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
* Discord rich presence [RapidJSON](https://rapidjson.org/)
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats. You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.

View File

@@ -1,9 +1,9 @@
set(STRAWBERRY_VERSION_MAJOR 1) set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 2) set(STRAWBERRY_VERSION_MINOR 2)
set(STRAWBERRY_VERSION_PATCH 9) set(STRAWBERRY_VERSION_PATCH 10)
#set(STRAWBERRY_VERSION_PRERELEASE rc1) #set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION ON) set(INCLUDE_GIT_REVISION OFF)
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}") set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")

2
debian/control vendored
View File

@@ -60,7 +60,7 @@ Description: music player and music collection organizer
- Edit tags on audio files - Edit tags on audio files
- Automatically retrieve tags from MusicBrainz - Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net - Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
- Audio analyzer - Audio analyzer
- Audio equalizer - Audio equalizer
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic - Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic

View File

@@ -31,7 +31,7 @@
<li>Edit tags on audio files</li> <li>Edit tags on audio files</li>
<li>Automatically retrieve tags from MusicBrainz</li> <li>Automatically retrieve tags from MusicBrainz</li>
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li> <li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li> <li>Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
<li>Audio analyzer and equalizer</li> <li>Audio analyzer and equalizer</li>
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li> <li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li> <li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
@@ -51,6 +51,7 @@
</screenshots> </screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact> <update_contact>eclipseo@fedoraproject.org</update_contact>
<releases> <releases>
<release version="1.2.10" date="2025-04-18"/>
<release version="1.2.9" date="2025-04-08"/> <release version="1.2.9" date="2025-04-08"/>
<release version="1.2.8" date="2025-04-05"/> <release version="1.2.8" date="2025-04-05"/>
<release version="1.2.7" date="2025-01-31"/> <release version="1.2.7" date="2025-01-31"/>

View File

@@ -13,7 +13,7 @@ TryExec=strawberry
Icon=strawberry Icon=strawberry
Terminal=false Terminal=false
Categories=AudioVideo;Player;Qt;Audio; Categories=AudioVideo;Player;Qt;Audio;
Keywords=Audio;Player; Keywords=Audio;Player;Clementine;
StartupNotify=false StartupNotify=false
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal; MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
StartupWMClass=strawberry StartupWMClass=strawberry

View File

@@ -29,7 +29,7 @@ Features:
.br .br
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
.br .br
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com - Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
.br .br
- Support for multiple backends - Support for multiple backends
.br .br

View File

@@ -93,7 +93,7 @@ Features:
- Edit tags on audio files - Edit tags on audio files
- Automatically retrieve tags from MusicBrainz - Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net - Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
- Support for multiple backends - Support for multiple backends
- Audio analyzer - Audio analyzer
- Audio equalizer - Audio equalizer

View File

@@ -720,7 +720,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll" File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll" File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
!ifdef arch_x64 !ifdef arch_x64
;File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll" File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
!endif !endif
!endif ; MSVC !endif ; MSVC
@@ -1179,7 +1179,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll" Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll" Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
!ifdef arch_x64 !ifdef arch_x64
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll" Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
!endif !endif
!endif ; msvc !endif ; msvc

View File

@@ -64,7 +64,6 @@
#include "covermanager/opentidalcoverprovider.h" #include "covermanager/opentidalcoverprovider.h"
#include "lyrics/lyricsproviders.h" #include "lyrics/lyricsproviders.h"
#include "lyrics/geniuslyricsprovider.h"
#include "lyrics/ovhlyricsprovider.h" #include "lyrics/ovhlyricsprovider.h"
#include "lyrics/lololyricsprovider.h" #include "lyrics/lololyricsprovider.h"
#include "lyrics/musixmatchlyricsprovider.h" #include "lyrics/musixmatchlyricsprovider.h"
@@ -173,7 +172,6 @@ class ApplicationImpl {
lyrics_providers_([app]() { lyrics_providers_([app]() {
LyricsProviders *lyrics_providers = new LyricsProviders(app); LyricsProviders *lyrics_providers = new LyricsProviders(app);
// Initialize the repository of lyrics providers. // Initialize the repository of lyrics providers.
lyrics_providers->AddProvider(new GeniusLyricsProvider(lyrics_providers->network()));
lyrics_providers->AddProvider(new OVHLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new OVHLyricsProvider(lyrics_providers->network()));
lyrics_providers->AddProvider(new LoloLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new LoloLyricsProvider(lyrics_providers->network()));
lyrics_providers->AddProvider(new MusixmatchLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new MusixmatchLyricsProvider(lyrics_providers->network()));

View File

@@ -38,8 +38,6 @@ constexpr char kStrawberryIconResourceName[] = "embedded_cover";
constexpr char kStrawberryIconDescription[] = "Strawberry Music Player"; constexpr char kStrawberryIconDescription[] = "Strawberry Music Player";
} // namespace } // namespace
using namespace discord_rpc;
namespace discord { namespace discord {
RichPresence::RichPresence(const SharedPtr<Player> player, RichPresence::RichPresence(const SharedPtr<Player> player,
@@ -48,9 +46,7 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
: QObject(parent), : QObject(parent),
player_(player), player_(player),
playlist_manager_(playlist_manager), playlist_manager_(playlist_manager),
enabled_(false) { initialized_(false) {
Discord_Initialize(QLatin1String(kDiscordApplicationId), nullptr, 1);
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged); QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged); QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged);
@@ -61,7 +57,11 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
} }
RichPresence::~RichPresence() { RichPresence::~RichPresence() {
Discord_Shutdown();
if (initialized_) {
Discord_Shutdown();
}
} }
void RichPresence::ReloadSettings() { void RichPresence::ReloadSettings() {
@@ -71,16 +71,22 @@ void RichPresence::ReloadSettings() {
const bool enabled = s.value(DiscordRPCSettings::kEnabled, false).toBool(); const bool enabled = s.value(DiscordRPCSettings::kEnabled, false).toBool();
s.endGroup(); s.endGroup();
if (enabled_ && !enabled) { if (enabled && !initialized_) {
Discord_ClearPresence(); Discord_Initialize(kDiscordApplicationId, nullptr, 1);
initialized_ = true;
}
else if (!enabled && initialized_) {
Discord_ClearPresence();
Discord_Shutdown();
initialized_ = false;
} }
enabled_ = enabled;
} }
void RichPresence::EngineStateChanged(const EngineBase::State state) { void RichPresence::EngineStateChanged(const EngineBase::State state) {
if (!initialized_) return;
if (state == EngineBase::State::Playing) { if (state == EngineBase::State::Playing) {
SetTimestamp(player_->engine()->position_nanosec() / kNsecPerSec); SetTimestamp(player_->engine()->position_nanosec() / kNsecPerSec);
SendPresenceUpdate(); SendPresenceUpdate();
@@ -93,6 +99,8 @@ void RichPresence::EngineStateChanged(const EngineBase::State state) {
void RichPresence::CurrentSongChanged(const Song &song) { void RichPresence::CurrentSongChanged(const Song &song) {
if (!initialized_) return;
SetTimestamp(0LL); SetTimestamp(0LL);
activity_.length_secs = song.length_nanosec() / kNsecPerSec; activity_.length_secs = song.length_nanosec() / kNsecPerSec;
activity_.title = song.title(); activity_.title = song.title();
@@ -105,48 +113,55 @@ void RichPresence::CurrentSongChanged(const Song &song) {
void RichPresence::SendPresenceUpdate() { void RichPresence::SendPresenceUpdate() {
if (!enabled_) { if (!initialized_) return;
return;
}
DiscordRichPresence presence_data{}; ::DiscordRichPresence presence_data{};
memset(&presence_data, 0, sizeof(presence_data));
presence_data.type = 2; // Listening presence_data.type = 2; // Listening
presence_data.largeImageKey = QLatin1String(kStrawberryIconResourceName); presence_data.largeImageKey = kStrawberryIconResourceName;
presence_data.smallImageKey = QLatin1String(kStrawberryIconResourceName); presence_data.smallImageKey = kStrawberryIconResourceName;
presence_data.smallImageText = QLatin1String(kStrawberryIconDescription); presence_data.smallImageText = kStrawberryIconDescription;
presence_data.instance = 0; presence_data.instance = 0;
QByteArray artist;
if (!activity_.artist.isEmpty()) { if (!activity_.artist.isEmpty()) {
QString artist = activity_.artist; artist = activity_.artist.toUtf8();
artist.prepend(tr("by ")); artist.prepend(tr("by ").toUtf8());
presence_data.state = artist; presence_data.state = artist.constData();
} }
if (!activity_.album.isEmpty() && activity_.album != activity_.title) { QByteArray album;
QString album = activity_.album; if (!activity_.album.isEmpty()) {
album.prepend(tr("on ")); album = activity_.album.toUtf8();
presence_data.largeImageText = album; album.prepend(tr("on ").toUtf8());
presence_data.largeImageText = album.constData();
} }
const QString title = activity_.title; const QByteArray title = activity_.title.toUtf8();
presence_data.details = title; presence_data.details = title.constData();
const qint64 start_timestamp = activity_.start_timestamp - activity_.seek_secs; const qint64 start_timestamp = activity_.start_timestamp - activity_.seek_secs;
presence_data.startTimestamp = start_timestamp; presence_data.startTimestamp = start_timestamp;
presence_data.endTimestamp = start_timestamp + activity_.length_secs; presence_data.endTimestamp = start_timestamp + activity_.length_secs;
Discord_UpdatePresence(presence_data); Discord_UpdatePresence(&presence_data);
} }
void RichPresence::SetTimestamp(const qint64 seconds) { void RichPresence::SetTimestamp(const qint64 seconds) {
activity_.start_timestamp = QDateTime::currentSecsSinceEpoch(); activity_.start_timestamp = QDateTime::currentSecsSinceEpoch();
activity_.seek_secs = seconds; activity_.seek_secs = seconds;
} }
void RichPresence::Seeked(const qint64 seek_microseconds) { void RichPresence::Seeked(const qint64 seek_microseconds) {
if (!initialized_) return;
SetTimestamp(seek_microseconds / 1000LL); SetTimestamp(seek_microseconds / 1000LL);
SendPresenceUpdate(); SendPresenceUpdate();
} }
} // namespace discord } // namespace discord

View File

@@ -69,7 +69,7 @@ class RichPresence : public QObject {
qint64 seek_secs; qint64 seek_secs;
}; };
Activity activity_; Activity activity_;
bool enabled_; bool initialized_;
}; };
} // namespace discord } // namespace discord

View File

@@ -1,406 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2020-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <memory>
#include <QApplication>
#include <QThread>
#include <QByteArray>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QRegularExpression>
#include <QSettings>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QJsonParseError>
#include <QMessageBox>
#include <QMutexLocker>
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/oauthenticator.h"
#include "jsonlyricsprovider.h"
#include "htmllyricsprovider.h"
#include "geniuslyricsprovider.h"
using namespace Qt::Literals::StringLiterals;
using std::make_shared;
namespace {
constexpr char kSettingsGroup[] = "GeniusLyrics";
constexpr char kOAuthAuthorizeUrl[] = "https://api.genius.com/oauth/authorize";
constexpr char kOAuthAccessTokenUrl[] = "https://api.genius.com/oauth/token";
constexpr char kOAuthRedirectUrl[] = "http://localhost:63111/"; // Genius does not accept a random port number. This port must match the URL of the ClientID.
constexpr char kOAuthScope[] = "me";
constexpr char kUrlSearch[] = "https://api.genius.com/search/";
constexpr char kClientIDB64[] = "RUNTNXU4U1VyMU1KUU5hdTZySEZteUxXY2hkanFiY3lfc2JjdXBpNG5WMU9SNUg4dTBZelEtZTZCdFg2dl91SQ==";
constexpr char kClientSecretB64[] = "VE9pMU9vUjNtTXZ3eFR3YVN0QVRyUjVoUlhVWDI1Ylp5X240eEt1M0ZkYlNwRG5JUnd0LXFFbHdGZkZkRWY2VzJ1S011UnQzM3c2Y3hqY0tVZ3NGN2c=";
} // namespace
GeniusLyricsProvider::GeniusLyricsProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: JsonLyricsProvider(u"Genius"_s, true, true, network, parent),
oauth_(new OAuthenticator(network, this)) {
oauth_->set_settings_group(QLatin1String(kSettingsGroup));
oauth_->set_type(OAuthenticator::Type::Authorization_Code);
oauth_->set_authorize_url(QUrl(QLatin1String(kOAuthAuthorizeUrl)));
oauth_->set_redirect_url(QUrl(QLatin1String(kOAuthRedirectUrl)));
oauth_->set_access_token_url(QUrl(QLatin1String(kOAuthAccessTokenUrl)));
oauth_->set_client_id(QString::fromLatin1(QByteArray::fromBase64(kClientIDB64)));
oauth_->set_client_secret(QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64)));
oauth_->set_scope(QLatin1String(kOAuthScope));
oauth_->set_use_local_redirect_server(true);
oauth_->set_random_port(false);
QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &GeniusLyricsProvider::OAuthFinished);
oauth_->LoadSession();
}
bool GeniusLyricsProvider::authenticated() const {
return oauth_->authenticated();
}
bool GeniusLyricsProvider::use_authorization_header() const {
return true;
}
void GeniusLyricsProvider::Authenticate() {
oauth_->Authenticate();
}
void GeniusLyricsProvider::ClearSession() {
oauth_->ClearSession();
}
QByteArray GeniusLyricsProvider::authorization_header() const {
return oauth_->authorization_header();
}
void GeniusLyricsProvider::OAuthFinished(const bool success, const QString &error) {
if (success) {
qLog(Debug) << "Genius: Authentication was successful.";
Q_EMIT AuthenticationComplete(true);
Q_EMIT AuthenticationSuccess();
}
else {
qLog(Debug) << "Genius: Authentication failed.";
Q_EMIT AuthenticationFailure(error);
Q_EMIT AuthenticationComplete(false, error);
}
}
void GeniusLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &request) {
Q_ASSERT(QThread::currentThread() != qApp->thread());
if (!authenticated()) {
EndSearch(id, request);
return;
}
GeniusLyricsSearchContextPtr search = make_shared<GeniusLyricsSearchContext>();
search->id = id;
search->request = request;
requests_search_.insert(id, search);
QUrlQuery url_query;
url_query.addQueryItem(u"q"_s, QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral("%1 %2").arg(request.artist, request.title))));
QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kUrlSearch)), url_query);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); });
}
GeniusLyricsProvider::JsonObjectResult GeniusLyricsProvider::ParseJsonObject(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
return JsonObjectResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
JsonObjectResult result(ErrorCode::Success);
result.network_error = reply->error();
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_parse_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error);
if (json_parse_error.error == QJsonParseError::NoError) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("errors"_L1) && json_object["errors"_L1].isArray()) {
const QJsonArray array_errors = json_object["errors"_L1].toArray();
for (const auto &value : array_errors) {
if (!value.isObject()) continue;
const QJsonObject object_error = value.toObject();
if (!object_error.contains("category"_L1) || !object_error.contains("code"_L1) || !object_error.contains("detail"_L1)) {
continue;
}
const QString category = object_error["category"_L1].toString();
const QString code = object_error["code"_L1].toString();
const QString detail = object_error["detail"_L1].toString();
result.error_code = ErrorCode::APIError;
result.error_message = QStringLiteral("%1 (%2) (%3)").arg(category, code, detail);
}
}
else {
result.json_object = json_document.object();
}
}
else {
result.error_code = ErrorCode::ParseError;
result.error_message = json_parse_error.errorString();
}
}
if (result.error_code != ErrorCode::APIError) {
if (reply->error() != QNetworkReply::NoError) {
result.error_code = ErrorCode::NetworkError;
result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else if (result.http_status_code != 200) {
result.error_code = ErrorCode::HttpError;
result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code);
}
}
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
oauth_->ClearSession();
}
return result;
}
void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
Q_ASSERT(QThread::currentThread() != qApp->thread());
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
if (!requests_search_.contains(id)) return;
GeniusLyricsSearchContextPtr search = requests_search_.value(id);
const QScopeGuard end_search = qScopeGuard([this, search]() { EndSearch(search); });
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
Error(json_object_result.error_message);
return;
}
const QJsonObject &json_object = json_object_result.json_object;
if (json_object.isEmpty()) {
return;
}
if (!json_object.contains("meta"_L1)) {
Error(u"Json reply is missing meta object."_s, json_object);
return;
}
if (!json_object["meta"_L1].isObject()) {
Error(u"Json reply meta is not an object."_s, json_object);
return;
}
const QJsonObject object_meta = json_object["meta"_L1].toObject();
if (!object_meta.contains("status"_L1)) {
Error(u"Json reply meta object is missing status."_s, object_meta);
return;
}
const int status = object_meta["status"_L1].toInt();
if (status != 200) {
if (object_meta.contains("message"_L1)) {
Error(QStringLiteral("Received error %1: %2.").arg(status).arg(object_meta["message"_L1].toString()));
}
else {
Error(QStringLiteral("Received error %1.").arg(status));
}
return;
}
if (!json_object.contains("response"_L1)) {
Error(u"Json reply is missing response."_s, json_object);
return;
}
if (!json_object["response"_L1].isObject()) {
Error(u"Json response is not an object."_s, json_object);
return;
}
const QJsonObject obj_response = json_object["response"_L1].toObject();
if (!obj_response.contains("hits"_L1)) {
Error(u"Json response is missing hits."_s, obj_response);
return;
}
if (!obj_response["hits"_L1].isArray()) {
Error(u"Json hits is not an array."_s, obj_response);
return;
}
const QJsonArray array_hits = obj_response["hits"_L1].toArray();
for (const QJsonValue &value_hit : array_hits) {
if (!value_hit.isObject()) {
continue;
}
const QJsonObject object_hit = value_hit.toObject();
if (!object_hit.contains("result"_L1)) {
continue;
}
if (!object_hit["result"_L1].isObject()) {
continue;
}
const QJsonObject object_result = object_hit["result"_L1].toObject();
if (!object_result.contains("title"_L1) || !object_result.contains("primary_artist"_L1) || !object_result.contains("url"_L1) || !object_result["primary_artist"_L1].isObject()) {
Error(u"Missing one or more values in result object"_s, object_result);
continue;
}
const QJsonObject primary_artist = object_result["primary_artist"_L1].toObject();
if (!primary_artist.contains("name"_L1)) continue;
const QString artist = primary_artist["name"_L1].toString();
const QString title = object_result["title"_L1].toString();
// Ignore results where both the artist and title don't match.
if (!artist.startsWith(search->request.albumartist, Qt::CaseInsensitive) &&
!artist.startsWith(search->request.artist, Qt::CaseInsensitive) &&
!title.startsWith(search->request.title, Qt::CaseInsensitive)) {
continue;
}
const QUrl url(object_result["url"_L1].toString());
if (!url.isValid()) continue;
if (search->requests_lyric_.contains(url)) continue;
GeniusLyricsLyricContext lyric;
lyric.artist = artist;
lyric.title = title;
lyric.url = url;
search->requests_lyric_.insert(url, lyric);
QNetworkReply *new_reply = CreateGetRequest(url);
QObject::connect(new_reply, &QNetworkReply::finished, this, [this, new_reply, search, url]() { HandleLyricReply(new_reply, search->id, url); });
}
}
void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url) {
Q_ASSERT(QThread::currentThread() != qApp->thread());
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
if (!requests_search_.contains(search_id)) return;
GeniusLyricsSearchContextPtr search = requests_search_.value(search_id);
if (!search->requests_lyric_.contains(url)) {
EndSearch(search);
return;
}
const GeniusLyricsLyricContext lyric = search->requests_lyric_.value(url);
if (reply->error() != QNetworkReply::NoError) {
Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
EndSearch(search, lyric);
return;
}
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
EndSearch(search, lyric);
return;
}
const QByteArray data = reply->readAll();
if (data.isEmpty()) {
Error(u"Empty reply received from server."_s);
EndSearch(search, lyric);
return;
}
const QString content = QString::fromUtf8(data);
QString lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div data-lyrics-container=[^>]+>"_s), true);
if (lyrics.isEmpty()) {
lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div class=\"lyrics\">"_s), true);
}
if (!lyrics.isEmpty()) {
LyricsSearchResult result(lyrics);
result.artist = lyric.artist;
result.title = lyric.title;
search->results.append(result);
}
EndSearch(search, lyric);
}
void GeniusLyricsProvider::EndSearch(GeniusLyricsSearchContextPtr search, const GeniusLyricsLyricContext &lyric) {
if (search->requests_lyric_.contains(lyric.url)) {
search->requests_lyric_.remove(lyric.url);
}
if (search->requests_lyric_.count() == 0) {
requests_search_.remove(search->id);
EndSearch(search->id, search->request, search->results);
}
}
void GeniusLyricsProvider::EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results) {
if (results.isEmpty()) {
qLog(Debug) << "GeniusLyrics: No lyrics for" << request.artist << request.title;
}
else {
qLog(Debug) << "GeniusLyrics: Got lyrics for" << request.artist << request.title;
}
Q_EMIT SearchFinished(id, results);
}

View File

@@ -1,88 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2020-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef GENIUSLYRICSPROVIDER_H
#define GENIUSLYRICSPROVIDER_H
#include "config.h"
#include <QMap>
#include <QString>
#include <QUrl>
#include <QMutex>
#include "includes/shared_ptr.h"
#include "jsonlyricsprovider.h"
#include "lyricssearchrequest.h"
#include "lyricssearchresult.h"
class QNetworkReply;
class NetworkAccessManager;
class OAuthenticator;
class GeniusLyricsProvider : public JsonLyricsProvider {
Q_OBJECT
public:
explicit GeniusLyricsProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
void Authenticate() override;
void ClearSession() override;
virtual bool authenticated() const override;
virtual bool use_authorization_header() const override;
virtual QByteArray authorization_header() const override;
protected Q_SLOTS:
void StartSearch(const int id, const LyricsSearchRequest &request) override;
private:
struct GeniusLyricsLyricContext {
explicit GeniusLyricsLyricContext() {}
QString artist;
QString title;
QUrl url;
};
struct GeniusLyricsSearchContext {
explicit GeniusLyricsSearchContext() : id(-1) {}
int id;
LyricsSearchRequest request;
QMap<QUrl, GeniusLyricsLyricContext> requests_lyric_;
LyricsSearchResults results;
};
using GeniusLyricsSearchContextPtr = SharedPtr<GeniusLyricsSearchContext>;
private:
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
void EndSearch(GeniusLyricsSearchContextPtr search, const GeniusLyricsLyricContext &lyric = GeniusLyricsLyricContext());
void EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results = LyricsSearchResults());
private Q_SLOTS:
void OAuthFinished(const bool success, const QString &error);
void HandleSearchReply(QNetworkReply *reply, const int id);
void HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url);
private:
OAuthenticator *oauth_;
mutable QMutex mutex_access_token_;
QMap<int, SharedPtr<GeniusLyricsSearchContext>> requests_search_;
};
#endif // GENIUSLYRICSPROVIDER_H

View File

@@ -123,7 +123,6 @@ constexpr char kID3v2_OriginalReleaseTime[] = "TDOR";
constexpr char kID3v2_OriginalReleaseYear[] = "TORY"; constexpr char kID3v2_OriginalReleaseYear[] = "TORY";
constexpr char kID3v2_UnsychronizedLyrics[] = "USLT"; constexpr char kID3v2_UnsychronizedLyrics[] = "USLT";
constexpr char kID3v2_CoverArt[] = "APIC"; constexpr char kID3v2_CoverArt[] = "APIC";
constexpr char kID3v2_CommercialFrame[] = "COMM";
constexpr char kID3v2_FMPS_Playcount[] = "FMPS_Playcount"; constexpr char kID3v2_FMPS_Playcount[] = "FMPS_Playcount";
constexpr char kID3v2_FMPS_Rating[] = "FMPS_Rating"; constexpr char kID3v2_FMPS_Rating[] = "FMPS_Rating";
constexpr char kID3v2_Unique_File_Identifier[] = "UFID"; constexpr char kID3v2_Unique_File_Identifier[] = "UFID";
@@ -337,6 +336,7 @@ TagReaderResult TagReaderTagLib::Read(SharedPtr<TagLib::FileRef> fileref, Song *
song->set_genre(tag->genre()); song->set_genre(tag->genre());
song->set_year(static_cast<int>(tag->year())); song->set_year(static_cast<int>(tag->year()));
song->set_track(static_cast<int>(tag->track())); song->set_track(static_cast<int>(tag->track()));
song->set_comment(tag->comment());
song->set_valid(true); song->set_valid(true);
} }
@@ -616,16 +616,6 @@ void TagReaderTagLib::ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QSt
if (map.contains(kID3v2_CoverArt) && song->url().isLocalFile()) song->set_art_embedded(true); if (map.contains(kID3v2_CoverArt) && song->url().isLocalFile()) song->set_art_embedded(true);
// Find a suitable comment tag. For now we ignore iTunNORM comments.
for (uint i = 0; i < map[kID3v2_CommercialFrame].size(); ++i) {
const TagLib::ID3v2::CommentsFrame *frame = dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map[kID3v2_CommercialFrame][i]);
if (frame && TagLibStringToQString(frame->description()) != "iTunNORM"_L1) {
song->set_comment(TagLibStringToQString(frame->text()));
break;
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, kID3v2_FMPS_Playcount)) { if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, kID3v2_FMPS_Playcount)) {
TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList(); TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList();
if (frame_field_list.size() > 1) { if (frame_field_list.size() > 1) {