Added Discord RPC support

- Added Allow debug logs to cmakelist instead of debug runs
- Added Discord rpc SDK supporting buttons and type
- Added Discord RPC handling
- Added Discord RPC Events for each stremio site
- Added DiscordRPC settings toggle to disable RPC
- Changed App image to a cleaner one
This commit is contained in:
Zarg 2025-05-26 05:24:28 +02:00
parent a67eebaf0d
commit 76bbdf64a9
12 changed files with 209 additions and 6 deletions

View file

@ -5,20 +5,28 @@ project(stremio VERSION "5.0.17")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
option(DEBUG_LOG "Allow debug logs" ON)
# Locate MPV
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
# 64-bit architecture
set(MPV_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/x86_64/include")
set(MPV_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/x86_64/mpv.lib")
set(MPV_DLL "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/x86_64/libmpv-2.dll")
set(DISCORD_LIB "${CMAKE_CURRENT_SOURCE_DIR}/deps/discord-rpc/win64-static/lib/discord-rpc.lib")
set(DISCORD_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/deps/discord-rpc/win64-static/include")
else()
# 32-bit architecture
set(MPV_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/i686/include")
set(MPV_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/i686/mpv.lib")
set(MPV_DLL "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/i686/libmpv-2.dll")
set(DISCORD_LIB "${CMAKE_CURRENT_SOURCE_DIR}/deps/discord-rpc/win32-static/lib/discord-rpc.lib")
set(DISCORD_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/deps/discord-rpc/win32-static/include")
endif()
include_directories(${DISCORD_INCLUDE_DIR})
include_directories(${MPV_INCLUDE_DIR})
find_package(OpenSSL REQUIRED)
@ -54,6 +62,8 @@ set(SOURCES
src/resource.h
src/utils/extensions.cpp
src/utils/extensions.h
src/utils/discord.cpp
src/utils/discord.h
)
add_executable(${PROJECT_NAME} WIN32 ${SOURCES})
@ -72,9 +82,10 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
OpenSSL::Crypto
CURL::libcurl
${MPV_LIBRARY}
${DISCORD_LIB}
)
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<CONFIG:Debug>:DEBUG_BUILD>)
target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG_LOG)
# Copy MPV DLL
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -67,6 +67,7 @@ HWND g_trayHwnd = nullptr;
bool g_pauseOnMinimize = true;
bool g_pauseOnLostFocus = false;
bool g_allowZoom = false;
bool g_isRpcOn = true;
// Tray sizes
int g_tray_itemH = 31;

View file

@ -111,6 +111,7 @@ extern HWND g_trayHwnd;
extern bool g_pauseOnMinimize;
extern bool g_pauseOnLostFocus;
extern bool g_allowZoom;
extern bool g_isRpcOn;
// Tray sizes
extern int g_tray_itemH;

View file

@ -7,6 +7,7 @@
#include <iostream>
#include <sstream>
#include <VersionHelpers.h>
#include "discord_rpc.h"
#include "ui/mainwindow.h"
#include "core/globals.h"
@ -20,6 +21,7 @@
#include "updater/updater.h"
#include "utils/helpers.h"
#include "utils/config.h"
#include "utils/discord.h"
// This started as 1-week project so please don't take the code to seriously
int main(int argc, char* argv[])
{
@ -85,6 +87,9 @@ int main(int argc, char* argv[])
// Load config
LoadSettings();
// Initialize Discord RPC
InitializeDiscord();
// Updater
g_updaterThread=std::thread(RunAutoUpdaterOnce);
g_updaterThread.detach();
@ -154,6 +159,9 @@ int main(int argc, char* argv[])
while(GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
// Run Discord RPC callbacks
Discord_RunCallbacks();
}
if(g_darkBrush){

View file

@ -15,6 +15,7 @@
#include "../ui/splash.h"
#include "../webview/webview.h"
#include "../updater/updater.h"
#include "../utils/discord.h"
// Single-instance
bool FocusExistingInstance(const std::wstring &protocolArg)
@ -124,7 +125,7 @@ void SendToJS(const std::string &eventName, const nlohmann::json &eventData)
std::wstring wpayload(payload.begin(), payload.end());
g_webview->PostWebMessageAsString(wpayload.c_str());
#ifdef DEBUG_BUILD
#ifdef DEBUG_LOG
std::cout << "[Native->JS] " << payload << "\n";
#endif
}
@ -226,6 +227,8 @@ void HandleEvent(const std::string &ev, std::vector<std::string> &args)
} else {
g_webview->Navigate(uri.c_str());
}
} else if (ev == "activity") {
SetDiscordPresenceFromArgs(args);
} else {
std::cout<<"Unknown event="<<ev<<"\n";
}
@ -235,7 +238,7 @@ void HandleEvent(const std::string &ev, std::vector<std::string> &args)
void HandleInboundJSON(const std::string &msg)
{
try {
#ifdef DEBUG_BUILD
#ifdef DEBUG_LOG
std::cout << "[JS -> NATIVE]: " << msg << std::endl;
#endif

View file

@ -29,6 +29,7 @@ void LoadSettings()
g_allowZoom = GetPrivateProfileIntW(L"General", L"AllowZoom", 0, iniPath.c_str());
g_pauseOnMinimize = (GetPrivateProfileIntW(L"General", L"PauseOnMinimize", 1, iniPath.c_str()) == 1);
g_pauseOnLostFocus = (GetPrivateProfileIntW(L"General", L"PauseOnLostFocus", 0, iniPath.c_str()) == 1);
g_isRpcOn = (GetPrivateProfileIntW(L"General", L"DiscordRPC", 1, iniPath.c_str()) == 1);
//Mpv
wchar_t voBuffer[32];
GetPrivateProfileStringW(L"MPV", L"VideoOutput", L"gpu-next", voBuffer, 32, iniPath.c_str());
@ -47,12 +48,14 @@ void SaveSettings()
const wchar_t* pauseMinVal = g_pauseOnMinimize ? L"1" : L"0";
const wchar_t* pauseFocVal = g_pauseOnLostFocus ? L"1" : L"0";
const wchar_t* allowZoomVal = g_allowZoom ? L"1" : L"0";
const wchar_t* rpcVal = g_isRpcOn ? L"1" : L"0";
WritePrivateProfileStringW(L"General", L"CloseOnExit", closeVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"UseDarkTheme", darkVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"PauseOnMinimize", pauseMinVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"PauseOnLostFocus", pauseFocVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"AllowZoom", allowZoomVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"DiscordRPC", rpcVal, iniPath.c_str());
WriteIntToIni(L"MPV", L"InitialVolume", g_currentVolume, iniPath);
}

View file

@ -9,6 +9,7 @@
#include "../utils/helpers.h"
#include <gdiplus.h>
#include <sstream>
#include "discord_rpc.h"
#include "config.h"
@ -64,4 +65,6 @@ void Cleanup()
if(g_gdiplusToken) {
Gdiplus::GdiplusShutdown(g_gdiplusToken);
}
Discord_Shutdown();
}

166
src/utils/discord.cpp Normal file
View file

@ -0,0 +1,166 @@
#include <iostream>
#include "../core/globals.h"
#include "crashlog.h"
#include "discord_rpc.h"
void Discord_Ready(const DiscordUser* user) {
std::cout << "[DISCORD]: Connected to Discord user: " + std::string(user->username);
}
void Discord_Disconnected(int errorCode, const char* message) {
std::cout << "[DISCORD]: Disconnected (" + std::to_string(errorCode) + "): " + std::string(message);
}
void Discord_Error(int errorCode, const char* message) {
std::cout << "[DISCORD]: Error (" + std::to_string(errorCode) + "): " + std::string(message);
AppendToCrashLog("[DISCORD]: Error (" + std::to_string(errorCode) + "): " + std::string(message));
}
void InitializeDiscord()
{
DiscordEventHandlers handlers{};
memset(&handlers, 0, sizeof(handlers));
handlers.ready = Discord_Ready;
handlers.disconnected = Discord_Disconnected;
handlers.errored = Discord_Error;
Discord_Initialize("1361448446862692492", &handlers, 1, nullptr);
}
// Encapsulated presence setters
static void SetDiscordWatchingPresence(
const std::vector<std::string>& args
) {
// expects:
// 0: generic "watching" identifier
// 1: type (movie, series)
// 2: title
// 3: season
// 4: episode
// 5: episode name
// 6: episode thumbnail (small image) (Optional)
// 7: show/movie image (large image)
// 8: elapsed seconds
// 9: duration seconds
// 10: isPaused ("yes" or "no") (Optional)
// 11: more detail button link (imdb link) (Optional)
// 12: watch on stremio button link (stremio link) (Optional)
DiscordRichPresence discordPresence{};
memset(&discordPresence, 0, sizeof(discordPresence));
discordPresence.type = DISCORD_ACTIVITY_TYPE_WATCHING;
// Common fields (required)
discordPresence.details = args[2].c_str(); // Title
discordPresence.largeImageKey = args[7].c_str();
discordPresence.largeImageText = args[2].c_str();
// Handle paused state (optional)
bool isPaused = (!args[10].empty() && args[10] == "yes");
if (isPaused) {
discordPresence.state = "Paused";
discordPresence.startTimestamp = 0;
discordPresence.endTimestamp = 0;
} else {
std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
int elapsedSeconds = std::stoi(args[8]);
int durationSeconds = std::stoi(args[9]);
discordPresence.startTimestamp = currentTime - elapsedSeconds;
discordPresence.endTimestamp = currentTime + (durationSeconds - elapsedSeconds);
if (args[1] == "series") {
// Series-specific fields
std::string state = args[5] + " (S" + args[3] + "-E" + args[4] + ")";
discordPresence.state = state.c_str();
if (!args[6].empty()) {
discordPresence.smallImageKey = args[6].c_str();
discordPresence.smallImageText = args[5].c_str();
}
} else {
discordPresence.state = "Enjoying a Movie";
}
}
// Buttons setup (optional)
if (!args[11].empty()) {
discordPresence.button1Label = "More Details";
discordPresence.button1Url = args[11].c_str();
}
if (!args[12].empty()) {
discordPresence.button2Label = "Watch on Stremio";
discordPresence.button2Url = args[12].c_str();
}
Discord_UpdatePresence(&discordPresence);
}
static void SetDiscordMetaDetailPresence(const std::vector<std::string>& args) {
// args structure:
// 0: embed type ("meta-detail")
// 1: type ("movie" or "series")
// 2: title
// 3: image URL
DiscordRichPresence discordPresence{};
memset(&discordPresence, 0, sizeof(discordPresence));
discordPresence.type = DISCORD_ACTIVITY_TYPE_WATCHING;
discordPresence.details = args[2].c_str(); // Title (show/movie)
discordPresence.largeImageKey = args[3].c_str();
discordPresence.largeImageText = args[2].c_str();
// Engaging state
discordPresence.state = args[1] == "movie"
? "Exploring a Movie"
: "Exploring a Series";
Discord_UpdatePresence(&discordPresence);
}
static void SetDiscordDiscoverPresence(const char *const details, const char *const state) {
std::cout << "[DISCORD]: Setting discover Presence";
DiscordRichPresence discordPresence{};
memset(&discordPresence, 0, sizeof(discordPresence));
discordPresence.type = DISCORD_ACTIVITY_TYPE_WATCHING;
discordPresence.state = state;
discordPresence.details = details;
discordPresence.largeImageKey = "https://raw.githubusercontent.com/Stremio/stremio-web/refs/heads/development/images/icon.png";
discordPresence.largeImageText = "Stremio";
Discord_UpdatePresence(&discordPresence);
}
void SetDiscordPresenceFromArgs(const std::vector<std::string>& args) {
if (!g_isRpcOn || args.empty()) {
return;
}
const std::string& embedType = args[0];
if (embedType == "watching" && args.size() >= 12) {
SetDiscordWatchingPresence(args);
} else if (embedType == "meta-detail" && args.size() >= 4) {
SetDiscordMetaDetailPresence(args);
} else if (embedType == "board") {
SetDiscordDiscoverPresence("Resuming Favorites", "On Board");
} else if (embedType == "discover") {
SetDiscordDiscoverPresence("Finding New Gems", "In Discover");
} else if (embedType == "library") {
SetDiscordDiscoverPresence("Revisiting Old Favorites", "In Library");
} else if (embedType == "calendar") {
SetDiscordDiscoverPresence("Planning My Next Binge", "On Calendar");
} else if (embedType == "addons") {
SetDiscordDiscoverPresence("Exploring Add-ons", "In Add-ons");
} else if (embedType == "settings") {
SetDiscordDiscoverPresence("Tuning Preferences", "In Settings");
} else if (embedType == "search") {
SetDiscordDiscoverPresence("Searching for Shows & Movies", "In Search");
} else if (embedType == "clear") {
Discord_ClearPresence();
}
// Add more presence types here...
}

7
src/utils/discord.h Normal file
View file

@ -0,0 +1,7 @@
#ifndef STREMIO_DISCORD_H
#define STREMIO_DISCORD_H
void InitializeDiscord();
void SetDiscordPresenceFromArgs(const std::vector<std::string>& args);
#endif

View file

@ -267,7 +267,7 @@ void InitWebView2(HWND hWnd)
if (!settings) return E_FAIL;
if(settings) {
#ifndef DEBUG_BUILD
#ifndef DEBUG_LOG
settings->put_AreDevToolsEnabled(FALSE);
#endif
settings->put_IsStatusBarEnabled(FALSE);
@ -392,7 +392,7 @@ static void SetupWebMessageHandler()
HRESULT hr = args->get_MenuItems(&items);
if (FAILED(hr) || !items) return hr;
#ifdef DEBUG_BUILD
#ifdef DEBUG_LOG
return S_OK; //DEV TOOLS DEBUG ONLY
#endif