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 {}; }