diff --git a/.github/workflows/msbuild.yml b/.github/workflows/msbuild.yml index f1e5975..468b900 100644 --- a/.github/workflows/msbuild.yml +++ b/.github/workflows/msbuild.yml @@ -15,7 +15,7 @@ on: env: # Path to the solution file relative to the root of the project. SOLUTION_FILE_PATH: spotify-volume-controller-cpp.sln - VCPKG_COMMIT: "16ee2ecb31788c336ace8bb14c21801efb6836e4" + VCPKG_COMMIT: "b02e341c927f16d991edbd915d8ea43eac52096c" # Configuration type to build. # You can convert this to a build matrix if you need coverage of multiple configuration types. diff --git a/CMakePresets.json b/CMakePresets.json index ad0d824..176cf23 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -65,6 +65,16 @@ "CMAKE_SHARED_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed,-z,noexecstack,-z,relro,-z,now,-z,nodlopen" } }, + { + "name": "flags-windows-gcc-clang", + "description": "These flags are supported by both GCC and Clang", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS=1 -fstack-protector-strong -fcf-protection=full -fstack-clash-protection -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast", + "CMAKE_EXE_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed", + "CMAKE_SHARED_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed" + } + }, { "name": "flags-appleclang", "hidden": true, @@ -106,6 +116,19 @@ "architecture": "x64", "hidden": true }, + { + "name": "ci-win-mingw64", + "inherits": ["flags-windows-gcc-clang", "ci-std"], + "generator": "Ninja", + "hidden": true + }, + { + "name": "vcpkg-mingw64-static", + "hidden": true, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-mingw-static" + } + }, { "name": "coverage-linux", "binaryDir": "${sourceDir}/build/coverage", diff --git a/source/Client.h b/source/Client.h index 91c7649..ec23b66 100644 --- a/source/Client.h +++ b/source/Client.h @@ -2,8 +2,8 @@ #include -#include #include +#include #include #include "Config.h" @@ -21,7 +21,6 @@ class Client [[nodiscard]] cpr::Response api_request(const std::string_view endpoint); [[nodiscard]] cpr::Response put_api_request(const std::string_view endpoint, const cpr::Payload& parameters); - // Makes a request to the device endpoint [[nodiscard]] std::optional get_devices(); diff --git a/source/Config.cpp b/source/Config.cpp index 15373fc..d9bf25a 100644 --- a/source/Config.cpp +++ b/source/Config.cpp @@ -1,16 +1,33 @@ +#include +#include #include #include +#include #include +#include #include "Config.h" #include -constexpr std::string_view VOLUME_INCREMENT_KEY = "volume_increment"; +#include "data_types.h" namespace spotify_volume_controller { +constexpr std::string_view client_id_key = "client_id"; +constexpr std::string_view client_secret_key = "client_secret"; +constexpr std::string_view hide_window_key = "hide_window"; +constexpr std::string_view redirect_url_key = "redirect_url"; +constexpr std::string_view print_keys_key = "print_keys"; +constexpr std::string_view volume_increment_key = "volume_increment"; +constexpr std::string_view volume_up_key = "volume_up"; +constexpr std::string_view volume_down_key = "volume_down"; +constexpr std::string_view batch_delay_key = "batch_delay_ms"; + +constexpr std::chrono::milliseconds default_batch_delay {100}; +constexpr uint32_t default_volume_increment = 1; + Config::Config() { parse_config_file("config.json"); @@ -40,35 +57,36 @@ void Config::parse_config_file(const std::filesystem::path& path) std::string input {}; get_user_input("Please enter your spotify client id", input, true); - config["client_id"] = input; + config[client_id_key] = input; get_user_input("Please enter your spotify client secret", input, true); - config["client_secret"] = input; + config[client_secret_key] = input; get_user_input(fmt::format("Enter callback url, or leave empty for default ({}).", Config::DEFAULT_CALLBACK_URL), input); if (input.empty()) { - config["redirect_url"] = Config::DEFAULT_CALLBACK_URL; + config[redirect_url_key] = Config::DEFAULT_CALLBACK_URL; } else { - config["redirect_url"] = input; + config[redirect_url_key] = input; } get_user_input("Enter volume up virtual keycode as a number, or leave empty for default", input); if (input.empty()) { - config["volume_up"] = "default"; + config[volume_up_key] = "default"; } else { - config["volume_up"] = input; + config[volume_up_key] = input; } get_user_input("Enter volume down virtual keycode as a number, or leave empty for default", input); if (input.empty()) { - config["volume_down"] = "default"; + config[volume_down_key] = "default"; } else { - config["volume_down"] = input; + config[volume_down_key] = input; } - config["print_keys"] = false; - config["hide_window"] = false; + config[print_keys_key] = false; + config[hide_window_key] = false; - config[VOLUME_INCREMENT_KEY.data()] = 1; + config[volume_increment_key] = default_volume_increment; + config[batch_delay_key] = default_batch_delay.count(); new_config_file << config; new_config_file.close(); @@ -76,62 +94,62 @@ void Config::parse_config_file(const std::filesystem::path& path) std::string Config::get_client_id() const { - return config.at("client_id").template get(); + return config.at(client_id_key).template get(); } std::string Config::get_client_secret() const { - return config.at("client_secret").template get(); + return config.at(client_secret_key).template get(); } std::string Config::get_redirect_url() const { - return config.at("redirect_url").template get(); + return config.at(redirect_url_key).template get(); } bool Config::should_print_keys() const { - return config.at("print_keys").template get(); + return config.at(print_keys_key).template get(); } keycode Config::get_volume_up() const { - if (!config.contains("volume_up")) { - throw std::runtime_error("Missing volume_up config"); + if (!config.contains(volume_up_key)) { + throw std::runtime_error(std::format("Missing {} config", volume_up_key)); } - json v_up = config.at("volume_up"); + json v_up = config.at(volume_down_key); if (!v_up.is_number_integer()) { - throw std::runtime_error("volume_up config is not a valid keycode"); + throw std::runtime_error(fmt::format("{} config is not a valid keycode", volume_up_key)); } return v_up.template get(); } keycode Config::get_volume_down() const { - if (!config.contains("volume_down")) { - throw std::runtime_error("Missing volume_down config"); + if (!config.contains(volume_down_key)) { + throw std::runtime_error(fmt::format("Missing {} config", volume_down_key)); } - json v_down = config.at("volume_down"); + json v_down = config.at(volume_down_key); if (!v_down.is_number_integer()) { - throw std::runtime_error("volume_down config is not a valid keycode"); + throw std::runtime_error(std::format("{} config is not a valid keycode", volume_down_key)); } return v_down.template get(); } bool Config::is_default_down() const { - if (!config.contains("volume_down")) { + if (!config.contains(volume_down_key)) { return false; } - json v_down = config.at("volume_down"); + json v_down = config.at(volume_down_key); return v_down.is_string() && v_down.template get() == "default"; } bool Config::is_default_up() const { - if (!config.contains("volume_up")) { + if (!config.contains(volume_down_key)) { return false; } - json v_up = config.at("volume_up"); + json v_up = config.at(volume_down_key); return v_up.is_string() && v_up.template get() == "default"; } @@ -142,11 +160,11 @@ bool Config::is_valid() const bool Config::hide_window() const { - if (!config.contains("hide_window")) { + if (!config.contains(hide_window_key)) { return false; } - return config.at("hide_window").template get(); + return config.at(hide_window_key).template get(); } [[nodiscard]] std::filesystem::path Config::config_directory() const @@ -156,11 +174,16 @@ bool Config::hide_window() const volume Config::volume_increment() const { - if (!config.contains(VOLUME_INCREMENT_KEY.data())) { - return 1; + if (!config.contains(volume_increment_key.data())) { + return default_volume_increment; } - return config.at(VOLUME_INCREMENT_KEY.data()).template get(); + return config.at(volume_increment_key.data()).template get(); +} + +std::chrono::milliseconds Config::batch_delay() const +{ + return std::chrono::milliseconds(config.value(batch_delay_key, default_batch_delay.count())); } void Config::get_user_input(const std::string_view prompt, std::string& input, bool not_empty) const diff --git a/source/Config.h b/source/Config.h index 087a458..e9920eb 100644 --- a/source/Config.h +++ b/source/Config.h @@ -1,14 +1,15 @@ #pragma once +#include #include #include #include "data_types.h" -// for convenience -using json = nlohmann::json; namespace spotify_volume_controller { +// for convenience +using json = nlohmann::json; class Config { @@ -24,6 +25,7 @@ class Config keycode get_volume_up() const; keycode get_volume_down() const; volume volume_increment() const; + std::chrono::milliseconds batch_delay() const; bool is_default_down() const; bool is_default_up() const; diff --git a/source/VolumeController.cpp b/source/VolumeController.cpp index 9187d76..2aacd51 100644 --- a/source/VolumeController.cpp +++ b/source/VolumeController.cpp @@ -1,20 +1,32 @@ +#include +#include +#include #include +#include #include "VolumeController.h" +#include #include +#include +#include "Client.h" +#include "Config.h" +#include "data_types.h" #include "key_hooks.h" -// for convenience -using json = nlohmann::json; + namespace spotify_volume_controller { +// for convenience +using json = nlohmann::json; +using namespace std::chrono_literals; VolumeController::VolumeController(const Config& config, Client& client) : m_config(config) , m_client(client) , m_volume_up_keycode(config.is_default_up() ? VK_VOLUME_UP : config.get_volume_up()) , m_volume_down_keycode(config.is_default_down() ? VK_VOLUME_DOWN : config.get_volume_down()) + , m_client_thread(std::thread(&VolumeController::set_volume_loop, this)) { } @@ -36,35 +48,56 @@ void VolumeController::set_desktop_device() void VolumeController::decrease_volume() { - set_volume(m_volume - m_config.volume_increment()); + { + std::unique_lock lk(m_volume_mutex); + // Assume that the API call will succeed + m_volume = m_volume - m_config.volume_increment(); + m_volume_queue.push(m_volume); + } + if (m_config.batch_delay() > 0ms) { + m_notify_timer.start([this]() { m_volume_cv.notify_one(); }, m_config.batch_delay()); + } else { + m_volume_cv.notify_one(); + } } void VolumeController::increase_volume() { - set_volume(m_volume + m_config.volume_increment()); + { + std::unique_lock lk(m_volume_mutex); + // Assume that the API call will succeed + m_volume = m_volume + m_config.volume_increment(); + m_volume_queue.push(m_volume); + } + if (m_config.batch_delay() > 0ms) { + m_notify_timer.start([this]() { m_volume_cv.notify_one(); }, m_config.batch_delay()); + } else { + m_volume_cv.notify_one(); + } } -void VolumeController::set_volume(const volume to_volume) -{ - volume new_volume = to_volume; +void VolumeController::set_volume_to_desktop_device_volume() { if (!m_desktop_device_id.has_value()) { set_desktop_device(); - if (m_desktop_device_id.has_value()) { - new_volume = m_client.get_device_volume(m_desktop_device_id.value()).value_or(volume(0)); - } } + if (m_desktop_device_id.has_value()) { + m_volume = m_client.get_device_volume(m_desktop_device_id.value()).value_or(volume(0)); + } +} +void VolumeController::set_volume(const volume new_volume) +{ cpr::Response result = m_client.set_volume(new_volume); if (result.status_code == cpr::status::HTTP_NO_CONTENT) { - m_volume = new_volume; return; - } else if ((result.status_code == cpr::status::HTTP_NOT_FOUND || result.status_code == cpr::status::HTTP_FORBIDDEN || cpr::status::HTTP_LENGTH_REQUIRED) - && m_desktop_device_id.has_value()) + } + if ((result.status_code == cpr::status::HTTP_NOT_FOUND || result.status_code == cpr::status::HTTP_FORBIDDEN + || result.status_code == cpr::status::HTTP_LENGTH_REQUIRED) + && m_desktop_device_id.has_value()) { result = m_client.set_device_volume(new_volume, m_desktop_device_id.value()); } if (result.status_code == cpr::status::HTTP_NO_CONTENT) { - m_volume = new_volume; return; } m_client.print_error_message(result); @@ -72,6 +105,7 @@ void VolumeController::set_volume(const volume to_volume) void VolumeController::start() { + set_volume_to_desktop_device_volume(); key_hooks::start_volume_hook(this); } @@ -89,4 +123,19 @@ void VolumeController::print_keys() { return m_volume_down_keycode; } + +void VolumeController::set_volume_loop() +{ + while (true) { + std::unique_lock lk(m_volume_mutex); + m_volume_cv.wait(lk, [&] { return !m_volume_queue.empty(); }); + volume new_volume {0}; + while (!m_volume_queue.empty()) { + new_volume = m_volume_queue.front(); + m_volume_queue.pop(); + } + set_volume(new_volume); + } +} + } // namespace spotify_volume_controller diff --git a/source/VolumeController.h b/source/VolumeController.h index a8e9328..2273356 100644 --- a/source/VolumeController.h +++ b/source/VolumeController.h @@ -1,13 +1,50 @@ #pragma once +#include +#include +#include +#include +#include +#include + #include "Client.h" #include "Config.h" namespace spotify_volume_controller { +class timer +{ +public: + + [[nodiscard]] bool is_running() const { return m_is_running; } + void start(const std::function& callback, std::chrono::milliseconds delay) + { + if (m_is_running) { + return; + } + m_is_running = true; + m_timer_thread = std::jthread + { + [callback, delay, this]() + { + std::this_thread::sleep_for(delay); + callback(); + m_is_running = false; + } + }; + } +private: + std::jthread m_timer_thread; + bool m_is_running = false; +}; + class VolumeController { public: + VolumeController(const VolumeController&) = delete; + VolumeController(VolumeController&&) = delete; + VolumeController& operator=(const VolumeController&) = delete; + VolumeController& operator=(VolumeController&&) = delete; VolumeController(const Config& config, Client& client); ~VolumeController(); @@ -23,13 +60,21 @@ class VolumeController void print_keys(); private: - void set_volume(const volume to_volume); + void set_volume(volume to_volume); + void set_volume_loop(); + void set_volume_to_desktop_device_volume(); volume m_volume = 0; const Config m_config; Client m_client; const keycode m_volume_up_keycode; const keycode m_volume_down_keycode; std::optional m_desktop_device_id = {}; + std::thread m_client_thread; + std::jthread m_notify_timer_thread; + std::mutex m_volume_mutex; + std::condition_variable m_volume_cv; + std::queue m_volume_queue; + timer m_notify_timer{}; }; } // namespace spotify_volume_controller diff --git a/source/data_types.h b/source/data_types.h index 9a7d4b0..60e9f62 100644 --- a/source/data_types.h +++ b/source/data_types.h @@ -8,6 +8,8 @@ namespace spotify_volume_controller using keycode = unsigned int; using volume_t = unsigned int; +constexpr volume_t max_volume{100}; + struct volume { volume(volume_t v) @@ -24,12 +26,12 @@ struct volume [[nodiscard]] volume_t operator+(const volume other) const { - return m_volume + other.m_volume > 100 ? 100 : m_volume + other.m_volume; + return m_volume + other.m_volume > max_volume ? max_volume : m_volume + other.m_volume; } volume& operator++() { - if (m_volume < 100) { + if (m_volume < max_volume) { m_volume++; } return *this; diff --git a/vcpkg.json b/vcpkg.json index 6c4d0f9..c79b012 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -6,7 +6,10 @@ "name": "fmt", "version>=": "10.1.0" }, - + { + "name": "curl", + "version>=": "8.12.1" + }, "cpr", "argparse", "nlohmann-json",