diff --git a/CMakeLists.txt b/CMakeLists.txt index 349ddcd..f13422f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,20 +5,28 @@ project(stremio VERSION "5.0.17") set(CMAKE_CXX_STANDARD 20) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$: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 $<$:DEBUG_BUILD>) +target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG_LOG) # Copy MPV DLL add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD diff --git a/images/stremio2.ico b/images/stremio2.ico index 05137a9..efcecbb 100644 Binary files a/images/stremio2.ico and b/images/stremio2.ico differ diff --git a/images/stremio2.png b/images/stremio2.png index b3df606..37cd137 100644 Binary files a/images/stremio2.png and b/images/stremio2.png differ diff --git a/src/core/globals.cpp b/src/core/globals.cpp index df72935..e42cb76 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -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; diff --git a/src/core/globals.h b/src/core/globals.h index 113888b..ac4b88c 100644 --- a/src/core/globals.h +++ b/src/core/globals.h @@ -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; diff --git a/src/main.cpp b/src/main.cpp index d329602..d9756d9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#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){ diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 612430a..78efcff 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -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 &args) } else { g_webview->Navigate(uri.c_str()); } + } else if (ev == "activity") { + SetDiscordPresenceFromArgs(args); } else { std::cout<<"Unknown event="< &args) void HandleInboundJSON(const std::string &msg) { try { -#ifdef DEBUG_BUILD +#ifdef DEBUG_LOG std::cout << "[JS -> NATIVE]: " << msg << std::endl; #endif diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 35d681f..31249b0 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -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); } diff --git a/src/utils/crashlog.cpp b/src/utils/crashlog.cpp index 8d51bef..83fd9aa 100644 --- a/src/utils/crashlog.cpp +++ b/src/utils/crashlog.cpp @@ -9,6 +9,7 @@ #include "../utils/helpers.h" #include #include +#include "discord_rpc.h" #include "config.h" @@ -64,4 +65,6 @@ void Cleanup() if(g_gdiplusToken) { Gdiplus::GdiplusShutdown(g_gdiplusToken); } + + Discord_Shutdown(); } diff --git a/src/utils/discord.cpp b/src/utils/discord.cpp new file mode 100644 index 0000000..a982d1e --- /dev/null +++ b/src/utils/discord.cpp @@ -0,0 +1,166 @@ +#include +#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& 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& 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& 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... +} diff --git a/src/utils/discord.h b/src/utils/discord.h new file mode 100644 index 0000000..7a25142 --- /dev/null +++ b/src/utils/discord.h @@ -0,0 +1,7 @@ +#ifndef STREMIO_DISCORD_H +#define STREMIO_DISCORD_H + +void InitializeDiscord(); +void SetDiscordPresenceFromArgs(const std::vector& args); + +#endif diff --git a/src/webview/webview.cpp b/src/webview/webview.cpp index fa0ddab..cd15a47 100644 --- a/src/webview/webview.cpp +++ b/src/webview/webview.cpp @@ -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