Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/msbuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: cmake --install build --config Release --prefix install

- name: Upload a Build Artifact
uses: actions/upload-artifact@v3.1.2
uses: actions/upload-artifact@v4
with:
name: "spotify-volume-controller"
path: |
Expand Down
14 changes: 8 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ project(
include(cmake/project-is-top-level.cmake)
include(cmake/variables.cmake)

find_package(cpprestsdk REQUIRED)
find_package(argparse CONFIG REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(fmt REQUIRED)
find_package(cpr REQUIRED)
find_package(httplib REQUIRED)

# ---- Declare library ----

add_library(
spotify_volume_controller_lib OBJECT
source/Client.cpp
source/Config.cpp
source/http_utils.cpp
source/oauth.cpp
source/SpotifyListener.cpp
source/updater.cpp
source/VolumeController.cpp
source/windows_helpers.cpp
)
Expand All @@ -38,7 +38,9 @@ target_include_directories(

target_compile_features(spotify_volume_controller_lib PUBLIC cxx_std_20)
target_compile_definitions(spotify_volume_controller_lib PRIVATE _SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS=1)
target_link_libraries(spotify_volume_controller_lib PRIVATE cpprestsdk::cpprest)
target_link_libraries(spotify_volume_controller_lib PRIVATE nlohmann_json::nlohmann_json fmt::fmt cpr::cpr httplib::httplib)

set(nlohmann-json_IMPLICIT_CONVERSIONS OFF)

# ---- Declare executable ----

Expand All @@ -55,7 +57,7 @@ set_property(TARGET spotify_volume_controller_exe PROPERTY RESOURCE resources/co
target_compile_features(spotify_volume_controller_exe PRIVATE cxx_std_20)
target_compile_definitions(spotify_volume_controller_exe PRIVATE _SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS=1)
target_compile_definitions(spotify_volume_controller_exe PRIVATE VERSION="${PROJECT_VERSION}")
target_link_libraries(spotify_volume_controller_exe PRIVATE spotify_volume_controller_lib argparse::argparse cpprestsdk::cpprest)
target_link_libraries(spotify_volume_controller_exe PRIVATE spotify_volume_controller_lib argparse::argparse)
# ---- Install rules ----

if(NOT CMAKE_SKIP_INSTALL_RULES)
Expand Down
126 changes: 58 additions & 68 deletions source/Client.cpp
Original file line number Diff line number Diff line change
@@ -1,140 +1,130 @@
#include <codecvt>
#include <iostream>

#include "Client.h"

#include "http_utils.h"
#include <cpr/cpr.h>
#include <fmt/format.h>
#include <nlohmann/json.hpp>

#include "oauth.h"

// for convenience
using json = nlohmann::json;
namespace spotify_volume_controller
{

const web::uri Client::BASE_API_URI(L"https://api.spotify.com");
constexpr std::string_view API_URL {"https://api.spotify.com"};

Client::Client(web::json::value& token_info, const Config& config)
Client::Client(token_t token_info, const Config& config)
: m_token_info(token_info)
, m_config(config)
{
client = new web::http::client::http_client(BASE_API_URI);
}

Client::~Client()
[[nodiscard]] cpr::Response Client::api_request(const std::string_view endpoint)
{
free(client);
}
cpr::Url url {fmt::format("{}{}", API_URL, endpoint)};

web::http::http_response Client::api_request(const utility::string_t& endpoint, const web::http::method http_method)
{
web::http::http_request request;
request.set_request_uri(endpoint);
request.set_method(http_method);
authorize_header(request);
return client->request(request).get();
return cpr::Get(url, cpr::Bearer {get_token()});
}

web::http::http_response Client::api_request(const utility::string_t& endpoint,
const web::http::method http_method,
const utility::string_t& query)
[[nodiscard]] cpr::Response Client::put_api_request(const std::string_view endpoint, const cpr::Payload& payload)
{
web::uri_builder api_uri(BASE_API_URI);
api_uri.append_path(endpoint);
api_uri.append_query(query, true);

return api_request(api_uri.to_string(), http_method);
cpr::Url url {fmt::format("{}{}", API_URL, endpoint)};
return cpr::Put(url, payload, cpr::Bearer {get_token()});
}

std::optional<web::json::array> Client::get_devices()
[[nodiscard]] std::optional<json> Client::get_devices()
{
try {
web::http::http_response response = api_request(L"/v1/me/player/devices", web::http::methods::GET);
if (response.status_code() != web::http::status_codes::OK) {
print_error_message(response);
return {};
}
return get_json_response_body(response).at(L"devices").as_array();
} catch (web::http::http_exception e) {
std::wcout << "Spotify API error:" << std::endl;
std::cout << e.error_code().message() << std::endl;

cpr::Response response = api_request("/v1/me/player/devices");
if (response.status_code != cpr::status::HTTP_OK) {
print_error_message(response);
return {};
}
json response_body = json::parse(response.text);
return response_body.at("devices");
}

std::optional<volume> Client::get_device_volume(const utility::string_t& id)
[[nodiscard]] std::optional<volume> Client::get_device_volume(const std::string_view id)
{
std::optional<web::json::array> devices = get_devices();
std::optional<json> devices = get_devices();
if (!devices.has_value())
return {};

for (auto&& device : devices.value()) {
if (device.has_string_field(L"id") && id == device[L"id"].as_string()) {
return device.has_integer_field(L"volume_percent") ? device[L"volume_percent"].as_number().to_uint32()
: std::optional<volume> {};
if (device.contains("id") && id == device.at("id").template get<std::string>()) {
return device.contains("volume_percent") ? device["volume_percent"].template get<std::uint32_t>()
: std::optional<volume> {};
}
}
return {};
}

[[nodiscard]] web::http::http_response Client::set_device_volume(volume volume, const utility::string_t& device_id)
[[nodiscard]] cpr::Response Client::set_device_volume(volume volume, const std::string& device_id)
{
utility::stringstream_t query;
query << "volume_percent=" << volume;
cpr::Parameters payload {{"volume_percent", fmt::format("{}", volume.m_volume)}};
if (!device_id.empty()) {
query << "&";
query << "device_id=" << device_id;
payload.Add(cpr::Parameter {"device_id", device_id});
}

return api_request(L"/v1/me/player/volume", web::http::methods::PUT, query.str());
cpr::Url url {fmt::format("{}/{}", API_URL, "v1/me/player/volume")};
return cpr::Put(url, payload, cpr::Bearer {get_token()}, cpr::Header {{"Content-Length", "0"}});
}

[[nodiscard]] web::http::http_response Client::set_volume(volume volume)
[[nodiscard]] cpr::Response Client::set_volume(volume volume)
{
return set_device_volume(volume);
}

std::optional<volume> Client::get_current_playing_volume()
{
std::optional<web::json::array> devices = get_devices();
std::optional<json> devices = get_devices();
if (!devices.has_value())
return {};

for (auto&& device : devices.value()) {
if (device[L"is_active"].as_bool()) {
if (device.at("is_active").template get<bool>()) {
// Volume percentage may be null according to documentation
if (device[L"volume_percent"].is_integer())
return device[L"volume_percent"].as_number().to_uint32();
if (device["volume_percent"].is_number_integer())
return device["volume_percent"].template get<std::uint32_t>();
}
}
return {};
}

web::json::value Client::get_desktop_player()
[[nodiscard]] std::optional<std::string> Client::get_desktop_player_id()
{
std::optional<web::json::array> devices = get_devices();
std::optional<json> devices = get_devices();
if (!devices.has_value())
return {};

for (auto&& device : devices.value()) {
if (device[L"type"].as_string().compare(L"Computer") == 0)
return device;
if (device["type"].template get<std::string>() == "Computer")
return device.at("id").template get<std::string>();
}
return web::json::value::Null;
return {};
}

void Client::authorize_header(web::http::http_request request)
[[nodiscard]] std::string Client::get_token()
{
if (oauth::token_is_expired(m_token_info)) {
oauth::refresh_token(m_token_info, m_config);
m_token_info = oauth::refresh_token(m_token_info, m_config);
}
request.headers()[L"Authorization"] = L"Bearer " + m_token_info[ACCESS_TOKEN].as_string();

return m_token_info.access_token;
}

void Client::print_error_message(const web::http::http_response& response)
void Client::print_error_message(const cpr::Response& response) const
{
web::json::value body = get_json_response_body(response);
web::json::value error_body = body[L"error"];
std::wcerr << "Status code: " << error_body[L"status"] << '\n';
std::wcerr << "Error message: " << error_body[L"message"] << '\n';
if (error_body.has_string_field(L"reason")) {
std::wcerr << "Reason: " << error_body[L"reason"];
}
std::wcerr << std::endl;
// web::json::value body = get_json_response_body(response.text);
// web::json::value error_body = body[L"error"];
// std::cerr << "Status code: " << response.error.code << '\n';

std::cerr << "Error message: " << response.error.message << '\n';
// if (error_body.has_string_field(L"reason")) {
std::cerr << "Reason: " << response.reason;
// }
std::cerr << std::endl;
}

} // namespace spotify_volume_controller
71 changes: 29 additions & 42 deletions source/Client.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,50 @@

#include <map>

#include <cpprest/http_client.h>
#include <fmt/format.h>
#include <cpr/cpr.h>
#include <nlohmann/json.hpp>

#include "Config.h"
#include "data_types.h"

// for convenience
using json = nlohmann::json;
namespace spotify_volume_controller
{

class Client
{
public:
Client(web::json::value& token_info, const Config& m_config);
~Client();
/// <summary>
/// Makes a http request to *endpoint* using http method *http_method*
/// </summary>
web::http::http_response api_request(const utility::string_t& endpoint, const web::http::method http_method);
web::http::http_response api_request(const utility::string_t& endpoint,
const web::http::method http_method,
const utility::string_t& query);

/// <summary>
/// Makes a request to the device endpoint
/// </summary>
std::optional<web::json::array> get_devices();

std::optional<volume> get_device_volume(const utility::string_t& id);

/// <summary>
/// Sets the volume of device_id to *volume*
/// </summary>
[[nodiscard]] web::http::http_response set_device_volume(volume volume, const utility::string_t& device_id = L"");

/// <summary>
/// Sets the volume of currently playing device to *volume*
/// </summary>
[[nodiscard]] web::http::http_response set_volume(volume volume);

/// <summary>
/// Returns the volume percent of the current playing device, or -1 if no playing devices
/// </summary>
Client(token_t token_info, const Config& m_config);

[[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<json> get_devices();

[[nodiscard]] std::optional<volume> get_device_volume(const std::string_view id);

// Sets the volume of device_id to *volume*
[[nodiscard]] cpr::Response set_device_volume(volume volume, const std::string& device_id = "");

// Sets the volume of currently playing device to *volume*
[[nodiscard]] cpr::Response set_volume(volume volume);

// Returns the volume percent of the current playing device
std::optional<volume> get_current_playing_volume();

/// <summary>
/// Returns the first desktop device found
/// </summary>
web::json::value get_desktop_player();
// Returns the ID of the first desktop device found
[[nodiscard]] std::optional<std::string> get_desktop_player_id();

void print_error_message(const cpr::Response& response) const;

static void print_error_message(const web::http::http_response& response);
private:
void authorize_header(web::http::http_request request);
[[nodiscard]] std::string get_token();

web::json::value m_token_info;
web::http::client::http_client* client;
token_t m_token_info;
const Config m_config;
const utility::string_t ACCESS_TOKEN = L"access_token";
static const web::uri BASE_API_URI;
};

} // namespace spotify_volume_controller
Loading
Loading