From 13649f87088ec5e8824498b41d43f151b9a6c69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Birath?= Date: Sat, 27 Sep 2025 16:36:18 +0200 Subject: [PATCH] Update background volume polling logic Change from periodically polling the volume in the background to instead fetch the current volume the user changes the volume. The frequency of the fetching is configured via the "fetch_cooldown" setting (default 2 seconds) --- CMakeLists.txt | 2 +- source/Client.cpp | 7 ++++-- source/Config.cpp | 28 ++++++++++++------------ source/Config.h | 2 +- source/VolumeController.cpp | 43 ++++++++++++++++++++++++------------- source/VolumeController.h | 6 +++++- source/data_types.h | 17 +++++++++++++++ source/key_hooks.cpp | 4 ++-- source/main.cpp | 15 +++++++------ source/oauth.cpp | 27 +++++++++++------------ 10 files changed, 93 insertions(+), 58 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0084892..17fb1fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ target_include_directories( "$" ) -target_compile_features(spotify_volume_controller_lib PUBLIC cxx_std_20) +target_compile_features(spotify_volume_controller_lib PUBLIC cxx_std_23) target_compile_definitions(spotify_volume_controller_lib PRIVATE _SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS=1) target_link_libraries(spotify_volume_controller_lib PRIVATE nlohmann_json::nlohmann_json fmt::fmt cpr::cpr httplib::httplib) diff --git a/source/Client.cpp b/source/Client.cpp index 75243d0..93ffd0c 100644 --- a/source/Client.cpp +++ b/source/Client.cpp @@ -132,8 +132,11 @@ std::optional Client::get_current_playing_volume() void Client::print_error_message(const cpr::Response& response) { - std::cerr << "Error message: " << response.error.message << '\n'; - std::cerr << "Reason: " << response.reason << '\n'; + fmt::println(stderr, "Error when requesting {} [{}]", response.url.str(), response.status_code); + if (!response.error.message.empty()) { + fmt::println("Error message: {}", response.error.message); + } + fmt::println("Reason: {}", response.reason); } } // namespace spotify_volume_controller diff --git a/source/Config.cpp b/source/Config.cpp index 8d44916..80d0709 100644 --- a/source/Config.cpp +++ b/source/Config.cpp @@ -1,8 +1,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -27,12 +27,12 @@ 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::string_view poll_rate_key = "poll_rate_ms"; +constexpr std::string_view fetch_cooldown_key = "fetch_cooldown_ms"; constexpr std::chrono::milliseconds default_batch_delay {100}; constexpr uint32_t default_volume_increment = 1; -constexpr std::chrono::milliseconds default_poll_rate {250}; -constexpr std::chrono::milliseconds min_poll_rate {100}; +constexpr std::chrono::milliseconds default_fetch_cooldown {2000}; +constexpr std::chrono::milliseconds min_fetch_cooldown {100}; constexpr std::string_view default_callback_url = "http://127.0.0.1:5000/callback"; Config::Config() @@ -53,13 +53,12 @@ void Config::parse_config_file(const std::filesystem::path& path) try { m_config = json::parse(config_file); } catch (const json::exception& e) { - std::cout << "Invalid config file." << '\n'; - std::cerr << e.what() << '\n'; + fmt::println(stderr, "Invalid config file: {}", e.what()); m_config = json {}; } return; } - std::cout << "No config file found, creating." << '\n'; + fmt::println("No config file found, creating."); std::ofstream new_config_file(m_directory / "config.json"); std::string input {}; @@ -93,7 +92,7 @@ void Config::parse_config_file(const std::filesystem::path& path) m_config[volume_increment_key] = default_volume_increment; m_config[batch_delay_key] = default_batch_delay.count(); - m_config[poll_rate_key] = default_poll_rate.count(); + m_config[fetch_cooldown_key] = default_fetch_cooldown.count(); new_config_file << m_config; new_config_file.close(); @@ -120,7 +119,7 @@ bool Config::should_print_keys() const keycode Config::get_volume_up() const { if (!m_config.contains(volume_up_key)) { - throw std::runtime_error(std::format("Missing {} config", volume_up_key)); + throw std::runtime_error(fmt::format("Missing {} config", volume_up_key)); } json const v_up = m_config.at(volume_down_key); if (!v_up.is_number_integer()) { @@ -137,7 +136,7 @@ keycode Config::get_volume_down() const json const v_down = m_config.at(volume_down_key); if (!v_down.is_number_integer()) { - throw std::runtime_error(std::format("{} config is not a valid keycode", volume_down_key)); + throw std::runtime_error(fmt::format("{} config is not a valid keycode", volume_down_key)); } return v_down.template get(); } @@ -193,18 +192,19 @@ std::chrono::milliseconds Config::batch_delay() const return std::chrono::milliseconds(m_config.value(batch_delay_key, default_batch_delay.count())); } -std::chrono::milliseconds Config::poll_rate() const +std::chrono::milliseconds Config::fetch_cooldown() const { - return std::max(std::chrono::milliseconds(m_config.value(poll_rate_key, default_poll_rate.count())), min_poll_rate); + return std::max(std::chrono::milliseconds(m_config.value(fetch_cooldown_key, default_fetch_cooldown.count())), + min_fetch_cooldown); } void Config::get_user_input(const std::string_view prompt, std::string& input, bool not_empty) { input.clear(); - std::cout << prompt << '\n'; + fmt::println("{}", prompt); std::getline(std::cin, input); while (not_empty && input.empty()) { - std::cout << "Input can't be empty" << '\n'; + fmt::println("Input can't be empty"); std::getline(std::cin, input); } } diff --git a/source/Config.h b/source/Config.h index a39cfd5..ff314ca 100644 --- a/source/Config.h +++ b/source/Config.h @@ -33,7 +33,7 @@ class Config keycode get_volume_down() const; volume volume_increment() const; std::chrono::milliseconds batch_delay() const; - std::chrono::milliseconds poll_rate() const; + std::chrono::milliseconds fetch_cooldown() const; bool is_default_down() const; bool is_default_up() const; diff --git a/source/VolumeController.cpp b/source/VolumeController.cpp index 9c044ab..3a85b79 100644 --- a/source/VolumeController.cpp +++ b/source/VolumeController.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -49,9 +50,9 @@ void VolumeController::decrease_volume() { std::lock_guard const lock(m_volume_mutex); // Assume that the API call will succeed - m_volume = m_volume - m_config.volume_increment(); - m_volume_queue.push(m_volume); + m_volume_queue.push(volume_change::decrease); } + m_update_current_volume_cv.notify_one(); if (m_config.batch_delay() > std::chrono::milliseconds(0)) { m_notify_timer.start([this]() { m_volume_cv.notify_one(); }, m_config.batch_delay()); } else { @@ -64,9 +65,9 @@ void VolumeController::increase_volume() { std::lock_guard const lock(m_volume_mutex); // Assume that the API call will succeed - m_volume = m_volume + m_config.volume_increment(); - m_volume_queue.push(m_volume); + m_volume_queue.push(volume_change::increase); } + m_update_current_volume_cv.notify_one(); if (m_config.batch_delay() > std::chrono::milliseconds(0)) { m_notify_timer.start([this]() { m_volume_cv.notify_one(); }, m_config.batch_delay()); } else { @@ -118,11 +119,22 @@ void VolumeController::set_volume_loop() while (true) { std::unique_lock lock(m_volume_mutex); m_volume_cv.wait(lock, [&] { return !m_volume_queue.empty(); }); - volume new_volume {0}; + if (m_updating_current_volume) { + std::unique_lock update_lock(m_update_current_volume_mutex); + m_updating_current_volume_cv.wait(update_lock, [&] { return !m_updating_current_volume; }); + } + volume new_volume = m_volume; while (!m_volume_queue.empty()) { - new_volume = m_volume_queue.front(); + const volume_change change = m_volume_queue.front(); + if (change == volume_change::increase) { + new_volume += m_config.volume_increment(); + } else { + new_volume -= m_config.volume_increment(); + } m_volume_queue.pop(); } + m_volume = new_volume; + fmt::println("Setting volume to {}", new_volume.m_volume); set_volume(new_volume); } } @@ -131,17 +143,18 @@ void VolumeController::update_current_volume_loop() { while (true) { { - std::lock_guard const lock(m_volume_mutex); - if (m_volume_queue.empty()) { - std::optional current_volume = m_client.get_current_playing_volume(); - if (current_volume.has_value()) { - { - m_volume = current_volume.value(); - } - } + std::unique_lock update_lock(m_update_current_volume_mutex); + m_update_current_volume_cv.wait(update_lock); + m_updating_current_volume = true; + fmt::println("Fetching current volume from Spotify API"); + std::optional current_volume = m_client.get_current_playing_volume(); + if (current_volume.has_value()) { + m_volume = current_volume.value(); } + m_updating_current_volume_cv.notify_all(); + m_updating_current_volume = false; } - std::this_thread::sleep_for(m_config.poll_rate()); + std::this_thread::sleep_for(m_config.fetch_cooldown()); } } diff --git a/source/VolumeController.h b/source/VolumeController.h index d0da8db..f8872ff 100644 --- a/source/VolumeController.h +++ b/source/VolumeController.h @@ -77,7 +77,11 @@ class VolumeController std::mutex m_volume_mutex; std::condition_variable m_volume_cv; - std::queue m_volume_queue; + std::queue m_volume_queue; + std::condition_variable m_update_current_volume_cv; + std::mutex m_update_current_volume_mutex; + std::atomic m_updating_current_volume; + std::condition_variable m_updating_current_volume_cv; Timer m_notify_timer {}; }; diff --git a/source/data_types.h b/source/data_types.h index f288044..d035e2e 100644 --- a/source/data_types.h +++ b/source/data_types.h @@ -14,6 +14,12 @@ using volume_t = unsigned int; constexpr volume_t max_volume {100}; +enum class volume_change : std::uint8_t +{ + increase, + decrease +}; + struct volume { volume(const volume&) = default; @@ -72,6 +78,17 @@ struct volume return old; } + volume& operator+=(const volume& other) + { + m_volume = (m_volume + other.m_volume > max_volume) ? max_volume : m_volume + other.m_volume; + return *this; + } + + volume& operator-=(const volume& other) + { + m_volume = (m_volume < other.m_volume) ? 0 : m_volume - other.m_volume; + return *this; + } explicit operator volume_t() const { return m_volume; } }; diff --git a/source/key_hooks.cpp b/source/key_hooks.cpp index 294303f..224ee89 100644 --- a/source/key_hooks.cpp +++ b/source/key_hooks.cpp @@ -1,9 +1,9 @@ #include -#include #include #include "key_hooks.h" +#include #include #include #include @@ -44,7 +44,7 @@ LRESULT CALLBACK print_v_key(int n_code, WPARAM w_param, LPARAM l_param) } if (w_param == WM_KEYDOWN) { auto* keyboard_struct = std::bit_cast(l_param); - std::cout << keyboard_struct->vkCode << '\n'; + fmt::println("{}", keyboard_struct->vkCode); } return CallNextHookEx(nullptr, n_code, w_param, l_param); } diff --git a/source/main.cpp b/source/main.cpp index 868adea..5d85669 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,11 +1,12 @@ +#include #include #include #include #include +#include #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers #include -#include #include "Client.h" #include "Config.h" @@ -24,30 +25,30 @@ int main(int argc, char* argv[]) try { program.parse_args(argc, argv); } catch (const std::exception& err) { - std::cerr << err.what() << '\n'; - std::cerr << program; + fmt::println(stderr, "{}", err.what()); + fmt::println(stderr, "{}", program.usage()); return 1; } - std::cout << "Starting..." << '\n'; + fmt::println("Starting..."); spotify_volume_controller::Config const config = program.is_used("--config") ? spotify_volume_controller::Config(program.get("--config")) : spotify_volume_controller::Config(); if (!config.is_valid()) { - std::cerr << "Failed to read config file." << '\n'; + fmt::println(stderr, "Failed to read config file."); std::cin.get(); return 1; } std::optional token = spotify_volume_controller::oauth::get_token(config); if (!token.has_value()) { - std::cout << "Failed to connect to spotify, exiting..." << '\n'; + fmt::println(stderr, "Failed to connect to Spotify, exiting..."); std::cin.get(); return 1; } spotify_volume_controller::Client client(token.value(), config); - std::cout << "Connected to spotify successfully!" << '\n'; + fmt::println("Connected to spotify successfully!"); if (config.hide_window()) { FreeConsole(); } diff --git a/source/oauth.cpp b/source/oauth.cpp index 3af0821..605feda 100644 --- a/source/oauth.cpp +++ b/source/oauth.cpp @@ -148,9 +148,8 @@ void open_uri(const std::string_view uri) {"scope", std::string(scopes)}, {"show_dialog", "true"}}; std::string const auth_url = fmt::format("{}?{}", authorization_url, params.GetContent(curl_holder)); - std::cout << "Please accept the prompt in your browser." << '\n'; - std::cout << "If your browser did not open, please go to the following link to accept the prompt:" << '\n'; - std::cout << auth_url << '\n'; + fmt::println("Please accept the prompt in your browser."); + fmt::println("If your browser did not open, please go to the following link to accept the prompt: {}", auth_url); open_uri(auth_url); } std::string authorization_code {}; @@ -164,13 +163,13 @@ void open_uri(const std::string_view uri) authorization_code = req.get_param_value("code"); res.set_content("

Successfully authenticated


You can close this tab

", "text/html"); } else if (req.has_param("error")) { - std::cerr << "Failed to get authorization code: " << req.get_param_value("error") << '\n'; + fmt::println(stderr, "Failed to get authorization code: {}", req.get_param_value("error")); res.set_content(fmt::format("

Failed to authenticate due to error \"{}\". Check logs

", req.get_param_value("error")), "text/html"); res.status = cpr::status::HTTP_INTERNAL_SERVER_ERROR; } else { - std::cerr << "Failed to get authorization code: Unknown error" << '\n'; + fmt::println(stderr, "Failed to get authorization code: Unknown error"); res.set_content("

Failed to authenticate due to unkown error. Check logs

", "text/html"); res.status = cpr::status::HTTP_INTERNAL_SERVER_ERROR; } @@ -224,10 +223,10 @@ void open_uri(const std::string_view uri) [[nodiscard]] std::optional get_token(const Config& config) { - std::cout << "Getting authorization token..." << '\n'; + fmt::println("Getting authorization token..."); std::optional token = read_token(config.config_directory()); if (!token.has_value()) { - std::cout << "No existing token found, creating new" << '\n'; + fmt::println("No existing token found, creating new"); std::optional const authorization_code = get_authorization_code(config.get_redirect_url(), config); if (!authorization_code.has_value() || authorization_code->empty()) { return {}; @@ -244,11 +243,11 @@ void open_uri(const std::string_view uri) { json j_new_token = fetch_token(token.refresh_token, grant_type::refresh_token, config); if (j_new_token.contains("error")) { - std::cout << "Failed to refresh token." << '\n'; - std::cout << "Error: " << j_new_token["error"] << '\n'; - std::cout << "Error description: " << j_new_token["error_description"] << '\n'; + fmt::println(stderr, "Failed to refresh token."); + fmt::println(stderr, "Error: {}", j_new_token["error"].dump()); + fmt::println(stderr, "Error description: {}", j_new_token["error_description"].dump()); if (j_new_token["error"].template get() == "invalid_client") { - std::cout << "Check that the client id/secret is correct." << '\n'; + fmt::println(stderr, "Check that the client id/secret is correct."); } std::cin.get(); exit(1); @@ -276,8 +275,7 @@ void save_token(const token_t& token, const std::filesystem::path& token_directo token_file << token.as_json(); token_file.close(); } else { - std::cerr << "Failed to write token to file" - << "\n"; + fmt::println(stderr, "Failed to write token to file"); } } @@ -288,8 +286,7 @@ std::optional read_token(const std::filesystem::path& token_directory) if (token_file) { return {token_t(json::parse(token_file))}; } - std::cerr << "Failed to read token from file" - << "\n"; + fmt::println(stderr, "Failed to read token to file"); return {}; }