diff --git a/.gitmodules b/.gitmodules index 623a00c..3f89aef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "SpotifyDemo/3rdparty/vcpkg"] path = SpotifyDemo/3rdparty/vcpkg url = https://github.com/microsoft/vcpkg +[submodule "SpotifyDemo/3rdparty/cpp-base64"] + path = SpotifyDemo/3rdparty/cpp-base64 + url = https://github.com/ReneNyffenegger/cpp-base64 diff --git a/SpotifyDemo/3rdparty/cpp-base64 b/SpotifyDemo/3rdparty/cpp-base64 new file mode 160000 index 0000000..80525f9 --- /dev/null +++ b/SpotifyDemo/3rdparty/cpp-base64 @@ -0,0 +1 @@ +Subproject commit 80525f93cbabaa11bbcae137f24467f97d74345d diff --git a/SpotifyDemo/CMakeLists.txt b/SpotifyDemo/CMakeLists.txt index fa572c1..c2411e0 100644 --- a/SpotifyDemo/CMakeLists.txt +++ b/SpotifyDemo/CMakeLists.txt @@ -3,7 +3,35 @@ # cmake_minimum_required (VERSION 3.8) -# Add source to this project's executable. -add_executable (SpotifyDemo "SpotifyDemo.cpp" "SpotifyDemo.h") +set (CMAKE_CXX_STANDARD_REQUIRED ON) +set (CMAKE_CXX_STANDARD 14) -# TODO: Add tests and install targets if needed. + +find_package (nlohmann_json CONFIG REQUIRED) +find_package (cpr CONFIG REQUIRED) + +find_path (CPPCODEC_INCLUDE_DIRS "cppcodec/base32_crockford.hpp") + + +set (SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/SpotifyDemo.cpp" +) + +set (SD_INCLUDE_DIR + + "${CMAKE_CURRENT_SOURCE_DIR}/include" + ) + +set (HEADERS + "${SD_INCLUDE_DIR}/Models.h" +) + +include_directories ("${SD_INCLUDE_DIR}") + +configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/client_information.json" "${CMAKE_BINARY_DIR}/SpotifyDemo/client_information.json" COPYONLY) + +# Add source to this project's executable. +add_executable (SpotifyDemo ${SOURCES}) + +target_link_libraries (SpotifyDemo PRIVATE nlohmann_json nlohmann_json::nlohmann_json cpr) +target_include_directories (SpotifyDemo PRIVATE ${CPPCODEC_INCLUDE_DIRS}) diff --git a/SpotifyDemo/SpotifyDemo.cpp b/SpotifyDemo/SpotifyDemo.cpp deleted file mode 100644 index d36512f..0000000 --- a/SpotifyDemo/SpotifyDemo.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// SpotifyDemo.cpp : Defines the entry point for the application. -// - -#include "SpotifyDemo.h" - -using namespace std; - -int main() -{ - cout << "Hello CMake." << endl; - return 0; -} diff --git a/SpotifyDemo/SpotifyDemo.h b/SpotifyDemo/SpotifyDemo.h deleted file mode 100644 index 398ce8c..0000000 --- a/SpotifyDemo/SpotifyDemo.h +++ /dev/null @@ -1,8 +0,0 @@ -// SpotifyDemo.h : Include file for standard system include files, -// or project specific include files. - -#pragma once - -#include - -// TODO: Reference additional headers your program requires here. diff --git a/SpotifyDemo/client_information.json b/SpotifyDemo/client_information.json new file mode 100644 index 0000000..93adec0 --- /dev/null +++ b/SpotifyDemo/client_information.json @@ -0,0 +1,4 @@ +{ + "client_id": "id", + "client_secret": "secret" +} \ No newline at end of file diff --git a/SpotifyDemo/include/Models.h b/SpotifyDemo/include/Models.h new file mode 100644 index 0000000..16e9f27 --- /dev/null +++ b/SpotifyDemo/include/Models.h @@ -0,0 +1,76 @@ +#ifndef _MODELS_H_ +#define _MODELS_H_ + +#include +#include + +namespace Models +{ + template + class Song + { + public: + Song() = default; + + Str title; + Str artist; + Str album; + Str released; + Dur duration; + }; + + template + class Album + { + public: + Album() = default; + + Str title; + Str released; + std::vector> songs; + }; + + template + class ClientInformation + { + public: + ClientInformation() = default; + ClientInformation(Str& id, Str& secret) : + client_id(std::move(id)), client_secret(std::move(secret)) + { + } + ClientInformation(Str&& id, Str&& secret) : + client_id(id), client_secret(secret) + { + } + + auto is_empty() + { + return (client_id.empty() && client_secret.empty()) ? true : false; + } + + Str client_id; + Str client_secret; + }; + + template + class TokenResp + { + public: + auto is_empty() + { + return this->access_token.empty(); + } + + Str access_token; + Str token_type; + Num expires_in; + Str scope; + }; +} + +#endif \ No newline at end of file diff --git a/SpotifyDemo/include/SpotifyCall.hpp b/SpotifyDemo/include/SpotifyCall.hpp new file mode 100644 index 0000000..47089de --- /dev/null +++ b/SpotifyDemo/include/SpotifyCall.hpp @@ -0,0 +1,175 @@ +#ifndef _SPOTIFYCALL_H_ +#define _SPOTIFYCALL_H_ + +#include +#include +#include +#include + +#include "cpr/cpr.h" +#include "nlohmann/json.hpp" + +#include "Models.h" + +namespace Spotify +{ + template> + class SpotifyCall + { + public: + SpotifyCall() = default; + SpotifyCall(Tok& tok) : token(tok) + { + } + SpotifyCall(Tok&& tok) : token(std::move(tok)) + { + } + + + template + auto search_songs(Str&& title) + { + constexpr auto limit = 49; + + std::vector> albums; + albums.reserve(limit); + + std::string search_query("name:"); + search_query.append(delimit_search_query(title)); + + + const auto uri = search_uri(); + const auto authorization = bearer_authorization(); + + Str limit_str(std::to_string(limit)); + + auto resp = cpr::Get(cpr::Url{ uri }, + cpr::Parameters{ {"type", "track"}, + {"limit", limit_str}, + {"q", search_query} }, + cpr::Header{ {"Authorization", authorization.c_str()} }); + + if (resp.status_code == 200) { + parse_albums(albums, std::move(resp.text)); + } + + + return albums; + } + + template>> + void print_songs_to_console(Cont &albums) + { + std::cout << "Printing songs to the console\n"; + + std::for_each(albums.begin(), albums.end(), [](auto album) { + auto song = album.songs.at(0); + + std::cout << "Title: " << song.title << "\n"; + std::cout << "Artist: " << song.artist << "\n"; + std::cout << "Album: " << album.title << "\n"; + std::cout << "Released Date: " << song.released << "\n"; + std::cout << "Duration (ms): " << song.duration << "\n"; + std::cout << "\n"; + }); + + std::cout << "\n"; + } + + private: + template + auto delimit_search_query(Str &&title) + { + return std::regex_replace(title, std::regex(" "), "%20"); + } + + auto bearer_authorization() + { + std::string authorization("Bearer "); + authorization.append(this->token.access_token); + + return authorization; + } + + template, typename Vec = std::vector, + typename Str = std::string> + void parse_albums(Vec &alb, Str &&resp) + { + auto obj = nlohmann::json::parse(resp); + + for (auto& item : obj["tracks"]["items"]) { + auto album_json = item["album"]; + auto artists_json = item["artists"]; + + Alb album; + album.title = album_json["name"].get(); + album.released = album_json["release_date"].get(); + + Models::Song<> song; + song.title = item["name"].get(); + song.duration = item["duration_ms"].get(); + song.artist = artists_json[0]["name"].get(); + song.released = album.released; + song.album = album.title; + + std::vector> songs; + songs.push_back(song); + + album.songs = songs; + + alb.push_back(album); + } + } + + auto saved_albums_response_body() + { + nlohmann::json obj; + + std::string uri(base_uri()); + uri.append("/"); + uri.append(album_saved()); + + std::string auth_header("Bearer "); + auth_header.append(this->token.access_token); + + auto resp = cpr::Get(cpr::Url(uri), + cpr::Header{{"Authorization", auth_header.c_str()}} + ); + + if (resp.status_code == 200) { + obj = nlohmann::json::parse(resp.text); + } + + + return obj; + } + + template + auto search_uri() + { + Str uri(base_uri()); + uri.append("/"); + uri.append(search_item_endpoint()); + + return uri; + } + + constexpr auto base_uri() noexcept + { + return "https://api.spotify.com/v1"; + } + + constexpr auto album_saved() noexcept + { + return "me/albums"; + } + constexpr auto search_item_endpoint() noexcept + { + return "search"; + } + + Tok token; + }; +} + +#endif \ No newline at end of file diff --git a/SpotifyDemo/include/TokenRetrieval.hpp b/SpotifyDemo/include/TokenRetrieval.hpp new file mode 100644 index 0000000..1466b98 --- /dev/null +++ b/SpotifyDemo/include/TokenRetrieval.hpp @@ -0,0 +1,113 @@ +#ifndef _TOKENRETRIEVAL_H_ +#define _TOKENRETRIEVAL_H_ + +#include +#include +#include +#include + +#include "cppcodec/base64_rfc4648.hpp" +#include "cpr/cpr.h" +#include "nlohmann/json.hpp" + +#include "Models.h" + +namespace Manager +{ + template + class TokenRetrieval + { + public: + TokenRetrieval() = default; + TokenRetrieval(Models::ClientInformation &info) : + client_information(std::move(info)) + { + } + TokenRetrieval(Models::ClientInformation &&info) : + client_information(info) + { + } + + template + auto retrieve_token() + { + if (this->client_information.is_empty()) { + return TokResp(); + } + + auto token = make_call(); + + + return (token.is_empty()) ? TokResp() : token; + } + + + private: + template + auto make_call() + { + TokResp resp; + + Str authorization("Basic "); + authorization.append(encoded_authorization()); + + + const auto uri = construct_uri(spotify_uri(), spotify_token_endpoint()); + auto r = cpr::Post(cpr::Url{ uri }, + cpr::Parameters{ {"grant_type", "client_credentials" }}, + cpr::Header{ {"Authorization", authorization.c_str()} } + ); + + if (r.status_code == 200) { + auto token = nlohmann::json::parse(r.text); + resp.access_token = token["access_token"].get(); + resp.token_type = token["token_type"].get(); + resp.expires_in = token["expires_in"].get(); + resp.scope = token["scope"].get(); + } + + + return resp; + } + + auto encoded_authorization() + { + Str unencoded(client_information.client_id); + unencoded.append(":"); + unencoded.append(client_information.client_secret); + + std::vector v(unencoded.begin(), unencoded.end()); + + return cppcodec::base64_rfc4648::encode(v); + } + + template + auto construct_uri(Ch *base, Ch *endpoint) + { + Str uri(base); + + if (uri.at(uri.size() - 1) != '/') { + uri.append("/"); + } + + uri.append(endpoint); + + return uri; + } + + auto constexpr spotify_uri() noexcept + { + return "https://accounts.spotify.com"; + } + + auto constexpr spotify_token_endpoint() noexcept + { + return "api/token"; + } + + Models::ClientInformation client_information; + }; + +} + +#endif \ No newline at end of file diff --git a/SpotifyDemo/src/SpotifyDemo.cpp b/SpotifyDemo/src/SpotifyDemo.cpp new file mode 100644 index 0000000..4636e41 --- /dev/null +++ b/SpotifyDemo/src/SpotifyDemo.cpp @@ -0,0 +1,65 @@ +// SpotifyDemo.cpp : Defines the entry point for the application. +// +#include +#include +#include +#include +#include + +#include "cpr/cpr.h" +#include "nlohmann/json.hpp" + +#include "Models.h" +#include "SpotifyCall.hpp" +#include "TokenRetrieval.hpp" + +using namespace Models; + + +template +auto retrieve_client_information(Str &&path) +{ + std::fstream config_file(path, std::ios::in); + + std::stringstream buffer; + buffer << config_file.rdbuf(); + + config_file.close(); + + auto client_json = nlohmann::json::parse(buffer.str()); + std::string id(client_json["client_id"].get()); + std::string secret(client_json["client_secret"].get()); + + Conf client_information(std::move(id), std::move(secret)); + + return client_information; +} + + +int main(int argc, char **argv) +{ + std::cout << "Starting SpotfyDemo\n"; + + if (argc < 2) { + std::cout << "Provide path to the client information json file\n"; + + return -1; + } + + std::string path(std::move(argv[1])); + + auto client_info = retrieve_client_information, + std::string>(std::move(path)); + + Manager::TokenRetrieval<> tok_mgr(std::move(client_info)); + auto resp = tok_mgr.retrieve_token>(); + + Spotify::SpotifyCall<> spotify(std::move(resp)); + std::string song_title("Heave"); + + auto albums = spotify.search_songs(song_title); + spotify.print_songs_to_console(albums); + + + return 0; +}