diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..027dcce --- /dev/null +++ b/.clang-format @@ -0,0 +1,10 @@ +BasedOnStyle: Google +UseTab: Never +IndentWidth: 4 +TabWidth: 4 +BreakBeforeBraces: Attach +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false +AccessModifierOffset: -4 +ColumnLimit: 120 +NamespaceIndentation: All \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07ed706 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..adae005 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,90 @@ +{ + "files.associations": { + "iostream": "cpp", + "thread": "cpp", + "algorithm": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "coroutine": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "exception": "cpp", + "format": "cpp", + "forward_list": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "iterator": "cpp", + "limits": "cpp", + "list": "cpp", + "locale": "cpp", + "map": "cpp", + "memory": "cpp", + "mutex": "cpp", + "new": "cpp", + "optional": "cpp", + "ostream": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "string": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "typeinfo": "cpp", + "unordered_map": "cpp", + "utility": "cpp", + "vector": "cpp", + "xfacet": "cpp", + "xhash": "cpp", + "xiosbase": "cpp", + "xlocale": "cpp", + "xlocbuf": "cpp", + "xlocinfo": "cpp", + "xlocmes": "cpp", + "xlocmon": "cpp", + "xlocnum": "cpp", + "xloctime": "cpp", + "xmemory": "cpp", + "xstring": "cpp", + "xtr1common": "cpp", + "xtree": "cpp", + "xutility": "cpp", + "xstddef": "cpp", + "fstream": "cpp", + "any": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "deque": "cpp", + "filesystem": "cpp", + "functional": "cpp", + "numeric": "cpp", + "ranges": "cpp", + "set": "cpp", + "span": "cpp", + "unordered_set": "cpp", + "valarray": "cpp", + "variant": "cpp", + "codecvt": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ab4f704 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required (VERSION 3.8) +project ("globalRPC") +file(GLOB_RECURSE SOURCES "src/*.cpp") +add_executable (rpc ${SOURCES}) +set_property(TARGET rpc PROPERTY CXX_STANDARD 20) +add_subdirectory("vendor") +target_link_libraries(rpc PUBLIC WindowsApp discord-rpc) +target_include_directories(rpc PRIVATE vendor) \ No newline at end of file diff --git a/known.json b/known.json new file mode 100644 index 0000000..1a5c826 --- /dev/null +++ b/known.json @@ -0,0 +1,12 @@ +{ + "apps": [ + { + "name": "Apple Music", + "process_names": [ + "AppleMusic.exe" + ], + "search_endpoint": "https://music.apple.com/search?term=", + "client_id": "1301881165984301106" + } + ] +} \ No newline at end of file diff --git a/rpc.exe b/rpc.exe new file mode 100644 index 0000000..6af8364 Binary files /dev/null and b/rpc.exe differ diff --git a/src/backend.hpp b/src/backend.hpp new file mode 100644 index 0000000..4f7f3f7 --- /dev/null +++ b/src/backend.hpp @@ -0,0 +1,33 @@ +#ifndef _BACKEND_ +#define _BACKEND_ +#include + +#include +#include + +struct MediaInfo { + bool paused; + std::string songTitle; + std::string songArtist; + std::string songAlbum; + std::string songThumbnailData; + int64_t songDuration; + int64_t songElapsedTime; + std::string playbackSource; + MediaInfo(bool p, std::string title, std::string artist, std::string album, std::string source, + std::string thumbnail, int duration, int elapsed) + : paused(p), + songTitle(title), + songArtist(artist), + songAlbum(album), + songDuration(duration), + songElapsedTime(elapsed), + playbackSource(source), + songThumbnailData(thumbnail) {} +}; + +namespace backend { + std::shared_ptr getMediaInformation(); +} + +#endif \ No newline at end of file diff --git a/src/backends/windows.cpp b/src/backends/windows.cpp new file mode 100644 index 0000000..9577fab --- /dev/null +++ b/src/backends/windows.cpp @@ -0,0 +1,61 @@ +#ifdef _WIN32 +#include +#include +#include + +#include + +#include "../backend.hpp" + +using namespace winrt; +using namespace Windows::Media::Control; +using namespace Windows::Storage::Streams; + +// a winrt::hstring is just an extended std::wstring, so we can use std::strings built in conversion. +std::string toStdString(winrt::hstring in) { + std::string converted = std::string(in.begin(), in.end()); + return converted; +} + +std::shared_ptr backend::getMediaInformation() { + auto sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); + auto currentSession = sessionManager.GetCurrentSession(); + if (!currentSession) + return nullptr; + + auto playbackInfo = currentSession.GetPlaybackInfo(); + auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get(); + auto timelineInformation = currentSession.GetTimelineProperties(); + if (!mediaProperties) + return nullptr; + + auto endTime = std::chrono::duration_cast(timelineInformation.EndTime()).count(); + auto elapsedTime = std::chrono::duration_cast(timelineInformation.Position()).count(); + + auto thumbnail = mediaProperties.Thumbnail(); + std::string thumbnailData = ""; + + if (thumbnail) { + auto stream = thumbnail.OpenReadAsync().get(); + size_t size = static_cast(stream.Size()); + + DataReader reader(stream); + reader.LoadAsync(static_cast(size)).get(); + + std::vector buffer(size); + reader.ReadBytes(buffer); + thumbnailData = std::string(buffer.begin(), buffer.end()); + stream.Close(); + } + + std::string artist = toStdString(mediaProperties.Artist()); + if (artist == "") + artist = toStdString(mediaProperties.AlbumArtist()); // Needed for some apps + + return std::make_shared( + playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused, + toStdString(mediaProperties.Title()), artist, toStdString(mediaProperties.AlbumTitle()), + toStdString(currentSession.SourceAppUserModelId()), thumbnailData, endTime, elapsedTime); +} + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..92b2b90 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include +#include +#include + +#include "backend.hpp" +#include "utils.hpp" +std::string lastPlayingSong = ""; +std::string lastMediaSource = ""; + +void handleRPCTasks() { + while (true) { + DiscordEventHandlers discordHandler{}; + Discord_Initialize(utils::getClientID(lastMediaSource).c_str(), &discordHandler); + if (Discord_IsConnected()) + break; + } + while (true) { + Discord_RunCallbacks(); + if (!Discord_IsConnected()) + break; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + handleRPCTasks(); // this could theoretically cause a stack overflow if discord is restarted often enough +} + +int main() { + std::thread rpcThread(handleRPCTasks); + rpcThread.detach(); + + while (true) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto mediaInformation = backend::getMediaInformation(); + if (!mediaInformation) { + Discord_ClearPresence(); // Nothing is playing rn, clear presence + continue; + } + + if (mediaInformation->paused) { + Discord_ClearPresence(); // TODO: allow user to keep presence when paused(because for + // some reason some people want this) + continue; + } + + std::string currentlyPlayingSong = + mediaInformation->songTitle + mediaInformation->songArtist + mediaInformation->songAlbum; + + if (currentlyPlayingSong == lastPlayingSong) + continue; + + lastPlayingSong = currentlyPlayingSong; + + std::string currentMediaSource = mediaInformation->playbackSource; + std::cout << currentMediaSource << std::endl; + if (currentMediaSource != lastMediaSource) + Discord_Shutdown(); //reinitialize with new client id + + std::string serviceName = utils::getAppName(lastMediaSource); + DiscordRichPresence activity{}; + activity.type = ActivityType::LISTENING; + activity.details = mediaInformation->songTitle.c_str(); + activity.state = std::string("by " + mediaInformation->songArtist).c_str(); + activity.smallImageText = serviceName.c_str(); + activity.smallImageKey = "icon"; + + activity.largeImageText = mediaInformation->songAlbum.c_str(); + activity.largeImageKey = ""; + + if(mediaInformation->songDuration != 0) { + int64_t remainingTime = mediaInformation->songDuration - mediaInformation->songElapsedTime; + activity.startTimestamp = time(nullptr) - mediaInformation->songElapsedTime; + activity.endTimestamp = time(nullptr) + remainingTime; + } + std::string endpointURL = utils::getSearchEndpoint(lastMediaSource); + + if(endpointURL != "") { + activity.button1name = std::string("Search on " + serviceName).c_str(); + std::string searchQuery = mediaInformation->songTitle + " " + mediaInformation->songArtist; + activity.button1link = std::string(endpointURL + utils::urlEncode(searchQuery)).c_str(); + } + + lastMediaSource = currentMediaSource; + Discord_UpdatePresence(&activity); + } +} \ No newline at end of file diff --git a/src/utils.hpp b/src/utils.hpp new file mode 100644 index 0000000..5c532c8 --- /dev/null +++ b/src/utils.hpp @@ -0,0 +1,87 @@ +#ifndef _UTILS_ +#define _UTILS_ +#include +#include +#include +#include +#include + +#define DEFAULT_CLIENT_ID "1301849203378622545" +#define DEFAULT_APP_NAME "Music" +#define CONFIG_FILENAME "known.json" + +namespace utils { + inline nlohmann::json getApp(std::string processName) { + std::ifstream i("known.json"); + std::stringstream s; + s << i.rdbuf(); + i.close(); + + try { + nlohmann::json j = nlohmann::json::parse(s.str()); + auto apps = j["apps"]; + for (auto app : apps) { + auto processNames = app["process_names"]; + for (auto process : processNames) { + if (process.get() == processName) + return app; + } + } + } catch (nlohmann::json::parse_error& ex) { + } // TODO: handle parse errors + return nlohmann::json(); + } + + inline std::string getClientID(std::string processName) { + if (!std::filesystem::exists(CONFIG_FILENAME)) + return DEFAULT_CLIENT_ID; + auto app = getApp(processName); + return app["client_id"] == "" ? DEFAULT_CLIENT_ID : app["client_id"]; + } + + inline std::string getAppName(std::string processName) { + if (!std::filesystem::exists(CONFIG_FILENAME)) + return DEFAULT_APP_NAME; + auto app = getApp(processName); + return app["name"] == "" ? DEFAULT_APP_NAME : app["name"]; + } + + inline std::string getSearchEndpoint(std::string processName) { + if (!std::filesystem::exists(CONFIG_FILENAME)) + return ""; + auto app = getApp(processName); + return app["search_endpoint"] == "" ? "" : app["search_endpoint"]; + } + + inline std::string urlEncode(std::string str) { + std::string new_str = ""; + char c; + int ic; + const char* chars = str.c_str(); + char bufHex[10]; + int len = strlen(chars); + + for (int i = 0; i < len; i++) { + c = chars[i]; + ic = c; + if (c == ' ') + new_str += '+'; + else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') + new_str += c; + else { + sprintf(bufHex, "%X", c); + if (ic < 16) + new_str += "%0"; + else + new_str += "%"; + new_str += bufHex; + } + } + return new_str; + } +} // namespace utils + +#undef DEFAULT_APP_NAME +#undef CONFIG_FILENAME +#undef DEFAULT_CLIENT_ID +#endif \ No newline at end of file diff --git a/vendor/.gitkeep b/vendor/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/CMakeLists.txt b/vendor/CMakeLists.txt new file mode 100644 index 0000000..122ae17 --- /dev/null +++ b/vendor/CMakeLists.txt @@ -0,0 +1,12 @@ +add_subdirectory("discord-rpc") +SET(ENABLE_PROGRAMS OFF) +SET(ENABLE_TESTING OFF) +add_subdirectory("mbedtls") +SET(CURL_USE_MBEDTLS ON) +SET(BUILD_STATIC_LIBS ON) +SET(BUILD_SHARED_LIBS OFF) +SET(BUILD_CURL_EXE OFF) +SET(MBEDTLS_INCLUDE_DIRS ../mbedtls/include) +file(REMOVE curl/CMake/FindMbedTLS.cmake) #replace curls FindMbedTLS that expects mbedtls to be prebuilt with a dummy +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dummy ${CMAKE_MODULE_PATH}) +add_subdirectory("curl") diff --git a/vendor/dummy/FindMbedTLS.cmake b/vendor/dummy/FindMbedTLS.cmake new file mode 100644 index 0000000..94a9f75 --- /dev/null +++ b/vendor/dummy/FindMbedTLS.cmake @@ -0,0 +1 @@ +message(STATUS "This is a dummy file to prevent curl from fucking up my build.") \ No newline at end of file