This is an automated email from the git hooks/post-receive script. skitt pushed a commit to branch master in repository lgogdownloader.
commit c3aa4c0c5f9059d025a2c844c747f26c2fb1991c Author: Stephen Kitt <[email protected]> Date: Mon May 9 23:27:42 2016 +0200 Imported Upstream version 2.28 --- CMakeLists.txt | 5 +- include/downloader.h | 17 +- include/gamedetails.h | 6 + include/gamefile.h | 15 +- include/globalconstants.h | 2 + include/progressbar.h | 1 + include/util.h | 24 ++ include/website.h | 39 +++ src/api.cpp | 107 ++++--- src/downloader.cpp | 722 ++++++---------------------------------------- src/gamedetails.cpp | 6 + src/gamefile.cpp | 25 +- src/progressbar.cpp | 29 +- src/util.cpp | 4 +- src/website.cpp | 708 +++++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 989 insertions(+), 721 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 743f955..1670254 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR) -project (lgogdownloader LANGUAGES CXX VERSION 2.27) +project (lgogdownloader LANGUAGES CXX VERSION 2.28) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG=1") @@ -12,7 +12,7 @@ find_package(Boost program_options date_time ) -find_package(CURL REQUIRED) +find_package(CURL 7.32.0 REQUIRED) find_package(OAuth REQUIRED) find_package(Jsoncpp REQUIRED) find_package(Htmlcxx REQUIRED) @@ -22,6 +22,7 @@ find_package(Rhash REQUIRED) file(GLOB SRC_FILES main.cpp src/api.cpp + src/website.cpp src/downloader.cpp src/progressbar.cpp src/util.cpp diff --git a/include/downloader.h b/include/downloader.h index b52153c..a85a1f8 100644 --- a/include/downloader.h +++ b/include/downloader.h @@ -24,6 +24,7 @@ #include "config.h" #include "api.h" #include "progressbar.h" +#include "website.h" #include <curl/curl.h> #include <json/json.h> #include <ctime> @@ -47,14 +48,6 @@ class Timer struct timeval last_update; }; -class gameItem { - public: - std::string name; - std::string id; - std::vector<std::string> dlcnames; - Json::Value gamedetailsjson; -}; - class Downloader { public: @@ -91,22 +84,18 @@ class Downloader int loadGameDetailsCache(); int saveGameDetailsCache(); std::vector<gameDetails> getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0); - int HTTP_Login(const std::string& email, const std::string& password); - std::vector<gameItem> getGames(); - std::vector<gameItem> getFreeGames(); std::vector<gameFile> getExtrasFromJSON(const Json::Value& json, const std::string& gamename); - Json::Value getGameDetailsJSON(const std::string& gameid); std::string getSerialsFromJSON(const Json::Value& json); void saveSerials(const std::string& serials, const std::string& filepath); std::string getChangelogFromJSON(const Json::Value& json); void saveChangelog(const std::string& changelog, const std::string& filepath); - static int progressCallback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow); + static int progressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp); static size_t writeData(void *ptr, size_t size, size_t nmemb, FILE *stream); static size_t readData(void *ptr, size_t size, size_t nmemb, FILE *stream); - + Website *gogWebsite; API *gogAPI; std::vector<gameItem> gameItems; std::vector<gameDetails> games; diff --git a/include/gamedetails.h b/include/gamedetails.h index 1737fff..abda193 100644 --- a/include/gamedetails.h +++ b/include/gamedetails.h @@ -1,3 +1,9 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ + #ifndef GAMEDETAILS_H #define GAMEDETAILS_H diff --git a/include/gamefile.h b/include/gamefile.h index 2c32100..80a37ef 100644 --- a/include/gamefile.h +++ b/include/gamefile.h @@ -1,3 +1,9 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ + #ifndef GAMEFILE_H #define GAMEFILE_H @@ -7,18 +13,25 @@ #include <vector> #include <json/json.h> +// Game file types +const unsigned int GFTYPE_INSTALLER = 1 << 0; +const unsigned int GFTYPE_EXTRA = 1 << 1; +const unsigned int GFTYPE_PATCH = 1 << 2; +const unsigned int GFTYPE_LANGPACK = 1 << 3; + class gameFile { public: gameFile(); - gameFile(const int& t_updated, const std::string& t_id, const std::string& t_name, const std::string& t_path, const std::string& t_size, const unsigned int& t_language = GlobalConstants::LANGUAGE_EN, const unsigned int& t_platform = GlobalConstants::PLATFORM_WINDOWS, const int& t_silent = 0); int updated; + std::string gamename; std::string id; std::string name; std::string path; std::string size; unsigned int platform; unsigned int language; + unsigned int type; int score; int silent; void setFilepath(const std::string& path); diff --git a/include/globalconstants.h b/include/globalconstants.h index 2ae0f96..935ac2f 100644 --- a/include/globalconstants.h +++ b/include/globalconstants.h @@ -12,6 +12,8 @@ namespace GlobalConstants { + const int GAMEDETAILS_CACHE_VERSION = 1; + struct optionsStruct {const unsigned int id; const std::string code; const std::string str; const std::string regexp;}; const std::string PROTOCOL_PREFIX = "gogdownloader://"; diff --git a/include/progressbar.h b/include/progressbar.h index c95230f..4d2f88d 100644 --- a/include/progressbar.h +++ b/include/progressbar.h @@ -16,6 +16,7 @@ class ProgressBar ProgressBar(bool bUnicode, bool bColor); virtual ~ProgressBar(); void draw(unsigned int length, double fraction); + std::string createBarString(unsigned int length, double fraction); protected: private: std::vector<std::string> const m_bar_chars; diff --git a/include/util.h b/include/util.h index 8f77162..7135a5a 100644 --- a/include/util.h +++ b/include/util.h @@ -43,6 +43,30 @@ struct gameSpecificConfig std::vector<unsigned int> vPlatformPriority; }; +struct gameItem +{ + std::string name; + std::string id; + std::vector<std::string> dlcnames; + Json::Value gamedetailsjson; +}; + +struct wishlistItem +{ + std::string title; + unsigned int platform; + std::vector<std::string> tags; + time_t release_date_time; + std::string currency; + std::string price; + std::string discount_percent; + std::string discount; + std::string store_credit; + std::string url; + bool bIsBonusStoreCreditIncluded; + bool bIsDiscounted; +}; + namespace Util { std::string makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename, std::string subdirectory = "", const unsigned int& platformId = 0, const std::string& dlcname = ""); diff --git a/include/website.h b/include/website.h new file mode 100644 index 0000000..0664784 --- /dev/null +++ b/include/website.h @@ -0,0 +1,39 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ + +#ifndef WEBSITE_H +#define WEBSITE_H + +#include "config.h" +#include "util.h" +#include <curl/curl.h> +#include <json/json.h> +#include <fstream> + +class Website +{ + public: + Website(Config &conf); + int Login(const std::string& email, const std::string& password); + std::string getResponse(const std::string& url); + Json::Value getGameDetailsJSON(const std::string& gameid); + std::vector<gameItem> getGames(); + std::vector<gameItem> getFreeGames(); + std::vector<wishlistItem> getWishlistItems(); + bool IsLoggedIn(); + void setConfig(Config &conf); + virtual ~Website(); + protected: + private: + static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp); + CURL* curlhandle; + Config config; + bool IsloggedInSimple(); + bool IsLoggedInComplex(const std::string& email); + int retries; +}; + +#endif // WEBSITE_H diff --git a/src/api.cpp b/src/api.cpp index 13ca1ba..0ada213 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -349,17 +349,19 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int continue; } - game.installers.push_back( - gameFile( installer["notificated"].isInt() ? installer["notificated"].asInt() : std::stoi(installer["notificated"].asString()), - installer["id"].isInt() ? std::to_string(installer["id"].asInt()) : installer["id"].asString(), - installer["name"].asString(), - installer["link"].asString(), - installer["size"].asString(), - language, - installers[i].platform, - installer["silent"].isInt() ? installer["silent"].asInt() : std::stoi(installer["silent"].asString()) - ) - ); + gameFile gf; + gf.type = GFTYPE_INSTALLER; + gf.gamename = game.gamename; + gf.updated = installer["notificated"].isInt() ? installer["notificated"].asInt() : std::stoi(installer["notificated"].asString()); + gf.id = installer["id"].isInt() ? std::to_string(installer["id"].asInt()) : installer["id"].asString(); + gf.name = installer["name"].asString(); + gf.path = installer["link"].asString(); + gf.size = installer["size"].asString(); + gf.language = language; + gf.platform = installers[i].platform; + gf.silent = installer["silent"].isInt() ? installer["silent"].asInt() : std::stoi(installer["silent"].asString()); + + game.installers.push_back(gf); } } @@ -369,14 +371,16 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int { Json::Value extra = extras[index]; - game.extras.push_back( - gameFile( false, /* extras don't have "updated" flag */ - extra["id"].isInt() ? std::to_string(extra["id"].asInt()) : extra["id"].asString(), - extra["name"].asString(), - extra["link"].asString(), - extra["size_mb"].asString() - ) - ); + gameFile gf; + gf.type = GFTYPE_EXTRA; + gf.gamename = game.gamename; + gf.updated = false; // extras don't have "updated" flag + gf.id = extra["id"].isInt() ? std::to_string(extra["id"].asInt()) : extra["id"].asString(); + gf.name = extra["name"].asString(); + gf.path = extra["link"].asString(); + gf.size = extra["size_mb"].asString(); + + game.extras.push_back(gf); } // Patch details @@ -434,16 +438,18 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int continue; } - game.patches.push_back( - gameFile( patch["notificated"].isInt() ? patch["notificated"].asInt() : std::stoi(patch["notificated"].asString()), - patch["id"].isInt() ? std::to_string(patch["id"].asInt()) : patch["id"].asString(), - patch["name"].asString(), - patch["link"].asString(), - patch["size"].asString(), - GlobalConstants::LANGUAGES[i].id, - patches[j].platform - ) - ); + gameFile gf; + gf.type = GFTYPE_PATCH; + gf.gamename = game.gamename; + gf.updated = patch["notificated"].isInt() ? patch["notificated"].asInt() : std::stoi(patch["notificated"].asString()); + gf.id = patch["id"].isInt() ? std::to_string(patch["id"].asInt()) : patch["id"].asString(); + gf.name = patch["name"].asString(); + gf.path = patch["link"].asString(); + gf.size = patch["size"].asString(); + gf.language = GlobalConstants::LANGUAGES[i].id; + gf.platform = patches[j].platform; + + game.patches.push_back(gf); } } else // Patch is a single file @@ -465,16 +471,18 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int continue; } - game.patches.push_back( - gameFile( patchnode["notificated"].isInt() ? patchnode["notificated"].asInt() : std::stoi(patchnode["notificated"].asString()), - patchnode["id"].isInt() ? std::to_string(patchnode["id"].asInt()) : patchnode["id"].asString(), - patchnode["name"].asString(), - patchnode["link"].asString(), - patchnode["size"].asString(), - GlobalConstants::LANGUAGES[i].id, - patches[j].platform - ) - ); + gameFile gf; + gf.type = GFTYPE_PATCH; + gf.gamename = game.gamename; + gf.updated = patchnode["notificated"].isInt() ? patchnode["notificated"].asInt() : std::stoi(patchnode["notificated"].asString()); + gf.id = patchnode["id"].isInt() ? std::to_string(patchnode["id"].asInt()) : patchnode["id"].asString(); + gf.name = patchnode["name"].asString(); + gf.path = patchnode["link"].asString(); + gf.size = patchnode["size"].asString(); + gf.language = GlobalConstants::LANGUAGES[i].id; + gf.platform = patches[j].platform; + + game.patches.push_back(gf); } } } @@ -500,15 +508,18 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int for (unsigned int j = 0; j < langpacknames.size(); ++j) { Json::Value langpack = root["game"][langpacknames[j]]; - game.languagepacks.push_back( - gameFile( false, /* language packs don't have "updated" flag */ - langpack["id"].isInt() ? std::to_string(langpack["id"].asInt()) : langpack["id"].asString(), - langpack["name"].asString(), - langpack["link"].asString(), - langpack["size"].asString(), - GlobalConstants::LANGUAGES[i].id - ) - ); + + gameFile gf; + gf.type = GFTYPE_LANGPACK; + gf.gamename = game.gamename; + gf.updated = false; // language packs don't have "updated" flag + gf.id = langpack["id"].isInt() ? std::to_string(langpack["id"].asInt()) : langpack["id"].asString(); + gf.name = langpack["name"].asString(); + gf.path = langpack["link"].asString(); + gf.size = langpack["size"].asString(); + gf.language = GlobalConstants::LANGUAGES[i].id; + + game.languagepacks.push_back(gf); } } } diff --git a/src/downloader.cpp b/src/downloader.cpp index 650da80..ccb43d1 100644 --- a/src/downloader.cpp +++ b/src/downloader.cpp @@ -23,7 +23,6 @@ #include <json/json.h> #include <htmlcxx/html/ParserDom.h> #include <htmlcxx/html/Uri.h> -#include <boost/algorithm/string/case_conv.hpp> namespace bptime = boost::posix_time; @@ -42,6 +41,7 @@ Downloader::~Downloader() this->report_ofs.close(); delete progressbar; delete gogAPI; + delete gogWebsite; curl_easy_cleanup(curlhandle); curl_global_cleanup(); // Make sure that cookie file is only readable/writable by owner @@ -65,21 +65,23 @@ int Downloader::init() curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, config.sVersionString.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, config.iTimeout); - curl_easy_setopt(curlhandle, CURLOPT_PROGRESSDATA, this); curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true); - curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, config.sCookiePath.c_str()); - curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, config.sCookiePath.c_str()); curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer); curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, config.bVerbose); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData); curl_easy_setopt(curlhandle, CURLOPT_READFUNCTION, Downloader::readData); - curl_easy_setopt(curlhandle, CURLOPT_PROGRESSFUNCTION, Downloader::progressCallback); curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate); + curl_easy_setopt(curlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallback); + curl_easy_setopt(curlhandle, CURLOPT_XFERINFODATA, this); // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30); curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200); + // Create new GOG website handle + gogWebsite = new Website(config); + bool bWebsiteIsLoggedIn = gogWebsite->IsLoggedIn(); + // Create new API handle and set curl options for the API gogAPI = new API(config.sToken, config.sSecret); gogAPI->curlSetOpt(CURLOPT_VERBOSE, config.bVerbose); @@ -89,7 +91,7 @@ int Downloader::init() progressbar = new ProgressBar(config.bUnicode, config.bColor); bool bInitOK = gogAPI->init(); // Initialize the API - if (!bInitOK || config.bLoginHTTP || config.bLoginAPI) + if (!bInitOK || !bWebsiteIsLoggedIn || config.bLoginHTTP || config.bLoginAPI) return 1; if (config.bCover && config.bDownload && !config.bUpdateCheck) @@ -139,7 +141,7 @@ int Downloader::login() // Login to website if (config.bLoginHTTP) { - if (!HTTP_Login(email, password)) + if (!gogWebsite->Login(email, password)) { std::cerr << "HTTP: Login failed" << std::endl; return 0; @@ -197,11 +199,11 @@ void Downloader::getGameList() { if (config.sGameRegex == "free") { - gameItems = this->getFreeGames(); + gameItems = gogWebsite->getFreeGames(); } else { - gameItems = this->getGames(); + gameItems = gogWebsite->getGames(); } } @@ -254,6 +256,11 @@ int Downloader::getGameDetails() std::cerr << "Cache is too old." << std::endl; std::cerr << "Update cache with --update-cache or use bigger --cache-valid" << std::endl; } + else if (result == 5) + { + std::cerr << "Cache version doesn't match current version." << std::endl; + std::cerr << "Update cache with --update-cache" << std::endl; + } return 1; } } @@ -321,19 +328,19 @@ int Downloader::getGameDetails() if (game.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras { if (gameDetailsJSON.empty()) - gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); + gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id); game.extras = this->getExtrasFromJSON(gameDetailsJSON, gameItems[i].name); } if (config.bSaveSerials) { if (gameDetailsJSON.empty()) - gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); + gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id); game.serials = this->getSerialsFromJSON(gameDetailsJSON); } if (config.bSaveChangelogs) { if (gameDetailsJSON.empty()) - gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); + gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id); game.changelog = this->getChangelogFromJSON(gameDetailsJSON); } @@ -341,7 +348,7 @@ int Downloader::getGameDetails() if (game.dlcs.empty() && !bHasDLC && conf.bDLC && conf.bIgnoreDLCCount) { if (gameDetailsJSON.empty()) - gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); + gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id); gameItems[i].dlcnames = Util::getDLCNamesFromJSON(gameDetailsJSON["dlcs"]); bHasDLC = !gameItems[i].dlcnames.empty(); @@ -357,7 +364,7 @@ int Downloader::getGameDetails() if (dlc.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras { if (gameDetailsJSON.empty()) - gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); + gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id); // Make sure we get extras for the right DLC for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k) @@ -379,7 +386,7 @@ int Downloader::getGameDetails() if (config.bSaveSerials) { if (gameDetailsJSON.empty()) - gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); + gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id); // Make sure we save serial for the right DLC for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k) @@ -404,7 +411,7 @@ int Downloader::getGameDetails() if (config.bSaveChangelogs) { if (gameDetailsJSON.empty()) - gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); + gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id); // Make sure we save changelog for the right DLC for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k) @@ -1862,7 +1869,7 @@ std::string Downloader::getResponse(const std::string& url) return response; } -int Downloader::progressCallback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow) +int Downloader::progressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { // on entry: dltotal - how much remains to download till the end of the file (bytes) // dlnow - how much was downloaded from the start of the program (bytes) @@ -1879,7 +1886,7 @@ int Downloader::progressCallback(void *clientp, double dltotal, double dlnow, do // and there is no way to calculate the fraction, so we set to 0 (otherwise it'd be 1). // This is to prevent the progress bar from jumping to 100% and then to lower value. // It's visually better to jump from 0% to higher one. - bool starting = ((0.0 == dlnow) && (0.0 == dltotal)); + bool starting = ((0 == dlnow) && (0 == dltotal)); // (Shmerl): DEBUG: strange thing - when resuming a file which is already downloaded, dlnow is correctly 0.0 // but dltotal is 389.0! This messes things up in the progress bar not showing the very last bar as full. @@ -1889,10 +1896,10 @@ int Downloader::progressCallback(void *clientp, double dltotal, double dlnow, do // // For now making a quirky workaround and setting dltotal to 0.0 in that case. // It's probably better to find a real fix. - if ((0.0 == dlnow) && (389.0 == dltotal)) dltotal = 0.0; + if ((0 == dlnow) && (389 == dltotal)) dltotal = 0; // setting full dlwnow and dltotal - double offset = static_cast<double>(downloader->getResumePosition()); + curl_off_t offset = static_cast<curl_off_t>(downloader->getResumePosition()); if (offset>0) { dlnow += offset; @@ -1944,7 +1951,7 @@ int Downloader::progressCallback(void *clientp, double dltotal, double dlnow, do } // Create progressbar - double fraction = starting ? 0.0 : dlnow / dltotal; + double fraction = starting ? 0.0 : static_cast<double>(dlnow) / static_cast<double>(dltotal); // assuming that config is provided. printf("\033[0K\r%3.0f%% ", fraction * 100); @@ -1962,7 +1969,7 @@ int Downloader::progressCallback(void *clientp, double dltotal, double dlnow, do rate_unit = "kB/s"; } char status_text[200]; // We're probably never going to go as high as 200 characters but it's better to use too big number here than too small - sprintf(status_text, " %0.2f/%0.2fMB @ %0.2f%s ETA: %s\r", dlnow/1024/1024, dltotal/1024/1024, rate, rate_unit.c_str(), eta_ss.str().c_str()); + sprintf(status_text, " %0.2f/%0.2fMB @ %0.2f%s ETA: %s\r", static_cast<double>(dlnow)/1024/1024, static_cast<double>(dltotal)/1024/1024, rate, rate_unit.c_str(), eta_ss.str().c_str()); int status_text_length = strlen(status_text) + 6; if ((status_text_length + bar_length) > iTermWidth) @@ -2000,468 +2007,6 @@ uintmax_t Downloader::getResumePosition() return this->resume_position; } -// Login to GOG website -int Downloader::HTTP_Login(const std::string& email, const std::string& password) -{ - int res = 0; - std::string postdata; - std::ostringstream memory; - std::string token; - std::string tagname_username; - std::string tagname_password; - std::string tagname_login; - std::string tagname_token; - - // Get login token - std::string html = this->getResponse("https://www.gog.com/"); - htmlcxx::HTML::ParserDom parser; - tree<htmlcxx::HTML::Node> dom = parser.parseTree(html); - tree<htmlcxx::HTML::Node>::iterator it = dom.begin(); - tree<htmlcxx::HTML::Node>::iterator end = dom.end(); - // Find auth_url - bool bFoundAuthUrl = false; - for (; it != end; ++it) - { - if (it->tagName()=="script") - { - std::string auth_url; - for (unsigned int i = 0; i < dom.number_of_children(it); ++i) - { - tree<htmlcxx::HTML::Node>::iterator script_it = dom.child(it, i); - if (!script_it->isTag() && !script_it->isComment()) - { - if (script_it->text().find("GalaxyAccounts") != std::string::npos) - { - boost::match_results<std::string::const_iterator> what; - boost::regex expression(".*'(https://auth.gog.com/.*?)'.*"); - boost::regex_match(script_it->text(), what, expression); - auth_url = what[1]; - break; - } - } - } - - if (!auth_url.empty()) - { // Found auth_url, get the necessary info for login - bFoundAuthUrl = true; - std::string login_form_html = this->getResponse(auth_url); - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl; - std::cerr << login_form_html << std::endl; - #endif - if (login_form_html.find("google.com/recaptcha") != std::string::npos) - { - std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl - << "Login with browser and export cookies to \"" << config.sCookiePath << "\"" << std::endl; - return res = 0; - } - - tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html); - tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin(); - tree<htmlcxx::HTML::Node>::iterator login_it_end = login_dom.end(); - for (; login_it != login_it_end; ++login_it) - { - if (login_it->tagName()=="input") - { - login_it->parseAttributes(); - std::string id_login = login_it->attribute("id").second; - if (id_login == "login_username") - { - tagname_username = login_it->attribute("name").second; - } - else if (id_login == "login_password") - { - tagname_password = login_it->attribute("name").second; - } - else if (id_login == "login__token") - { - token = login_it->attribute("value").second; // login token - tagname_token = login_it->attribute("name").second; - } - } - else if (login_it->tagName()=="button") - { - login_it->parseAttributes(); - std::string id_login = login_it->attribute("id").second; - if (id_login == "login_login") - { - tagname_login = login_it->attribute("name").second; - } - } - } - break; - } - } - } - - if (!bFoundAuthUrl) - { - std::cout << "Failed to find url for login form" << std::endl; - } - - if (token.empty()) - { - std::cout << "Failed to get login token" << std::endl; - return res = 0; - } - - //Create postdata - escape characters in email/password to support special characters - postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size()) - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size()) - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "=" - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size()); - curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check"); - curl_easy_setopt(curlhandle, CURLOPT_POST, 1); - curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); - curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback); - curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); - curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); - curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); - curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); - - // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first. - curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); - CURLcode result = curl_easy_perform(curlhandle); - memory.str(std::string()); - - if (result != CURLE_OK) - { - // Expected to hit maximum amount of redirects so don't print error on it - if (result != CURLE_TOO_MANY_REDIRECTS) - std::cout << curl_easy_strerror(result) << std::endl; - } - - // Get redirect url - char *redirect_url; - curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); - - // Handle two step authorization - if (std::string(redirect_url).find("two_step") != std::string::npos) - { - std::string security_code, tagname_two_step_send, tagname_two_step_auth_letter_1, tagname_two_step_auth_letter_2, tagname_two_step_auth_letter_3, tagname_two_step_auth_letter_4, tagname_two_step_token, token_two_step; - std::string two_step_html = this->getResponse(redirect_url); - redirect_url = NULL; - - tree<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html); - tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin(); - tree<htmlcxx::HTML::Node>::iterator two_step_it_end = two_step_dom.end(); - for (; two_step_it != two_step_it_end; ++two_step_it) - { - if (two_step_it->tagName()=="input") - { - two_step_it->parseAttributes(); - std::string id_two_step = two_step_it->attribute("id").second; - if (id_two_step == "second_step_authentication_token_letter_1") - { - tagname_two_step_auth_letter_1 = two_step_it->attribute("name").second; - } - else if (id_two_step == "second_step_authentication_token_letter_2") - { - tagname_two_step_auth_letter_2 = two_step_it->attribute("name").second; - } - else if (id_two_step == "second_step_authentication_token_letter_3") - { - tagname_two_step_auth_letter_3 = two_step_it->attribute("name").second; - } - else if (id_two_step == "second_step_authentication_token_letter_4") - { - tagname_two_step_auth_letter_4 = two_step_it->attribute("name").second; - } - else if (id_two_step == "second_step_authentication__token") - { - token_two_step = two_step_it->attribute("value").second; // two step token - tagname_two_step_token = two_step_it->attribute("name").second; - } - } - else if (two_step_it->tagName()=="button") - { - two_step_it->parseAttributes(); - std::string id_two_step = two_step_it->attribute("id").second; - if (id_two_step == "second_step_authentication_send") - { - tagname_two_step_send = two_step_it->attribute("name").second; - } - } - } - std::cerr << "Security code: "; - std::getline(std::cin,security_code); - if (security_code.size() != 4) - { - std::cerr << "Security code must be 4 characters long" << std::endl; - exit(1); - } - postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "=" - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size()); - - curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step"); - curl_easy_setopt(curlhandle, CURLOPT_POST, 1); - curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); - curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback); - curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); - curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); - curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); - curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); - - // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first. - curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); - result = curl_easy_perform(curlhandle); - memory.str(std::string()); - curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); - } - - curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); - curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1); - curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1); - curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); - result = curl_easy_perform(curlhandle); - - if (result != CURLE_OK) - { - std::cout << curl_easy_strerror(result) << std::endl; - } - - html = this->getResponse("https://www.gog.com/account/settings/personal"); - - std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it - dom = parser.parseTree(html); - it = dom.begin(); - end = dom.end(); - for (; it != end; ++it) - { - if (it->tagName()=="strong") - { - it->parseAttributes(); - if (it->attribute("class").second == "settings-item__value settings-item__section") - { - for (unsigned int i = 0; i < dom.number_of_children(it); ++i) - { - tree<htmlcxx::HTML::Node>::iterator tag_it = dom.child(it, i); - if (!tag_it->isTag() && !tag_it->isComment()) - { - std::string tag_text = boost::algorithm::to_lower_copy(tag_it->text()); - if (tag_text == email_lowercase) - { - res = 1; // Login successful - break; - } - } - } - } - } - if (res == 1) // Login was successful so no need to go through the remaining tags - break; - } - - // Simple login check if complex check failed. Check login by trying to get account page. If response code isn't 200 then login failed. - if (res == 0) - { - std::string url = "https://www.gog.com/account"; - curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); - curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback); - curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); - curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); - curl_easy_perform(curlhandle); - memory.str(std::string()); - long int response_code = 0; - curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); - curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); - if (response_code == 200) - res = 1; // Login successful - } - - return res; -} - -// Get list of games from account page -std::vector<gameItem> Downloader::getGames() -{ - std::vector<gameItem> games; - Json::Value root; - Json::Reader *jsonparser = new Json::Reader; - int i = 1; - bool bAllPagesParsed = false; - - do - { - std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=1&sortBy=title&system=&page=" + std::to_string(i)); - - // Parse JSON - if (!jsonparser->parse(response, root)) - { - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << response << std::endl; - #endif - std::cout << jsonparser->getFormattedErrorMessages(); - delete jsonparser; - if (!response.empty()) - { - if(response[0] != '{') - { - // Response was not JSON. Assume that cookies have expired. - std::cerr << "Response was not JSON. Cookies have most likely expired. Try --login first." << std::endl; - } - } - exit(1); - } - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << root << std::endl; - #endif - if (root["page"].asInt() == root["totalPages"].asInt()) - bAllPagesParsed = true; - if (root["products"].isArray()) - { - for (unsigned int i = 0; i < root["products"].size(); ++i) - { - Json::Value product = root["products"][i]; - gameItem game; - game.name = product["slug"].asString(); - game.id = product["id"].isInt() ? std::to_string(product["id"].asInt()) : product["id"].asString(); - - unsigned int platform = 0; - if (product["worksOn"]["Windows"].asBool()) - platform |= GlobalConstants::PLATFORM_WINDOWS; - if (product["worksOn"]["Mac"].asBool()) - platform |= GlobalConstants::PLATFORM_MAC; - if (product["worksOn"]["Linux"].asBool()) - platform |= GlobalConstants::PLATFORM_LINUX; - - // Skip if platform doesn't match - if (config.bPlatformDetection && !(platform & config.iInstallerPlatform)) - continue; - - // Filter the game list - if (!config.sGameRegex.empty()) - { - // GameRegex filter aliases - if (config.sGameRegex == "all") - config.sGameRegex = ".*"; - - boost::regex expression(config.sGameRegex); - boost::match_results<std::string::const_iterator> what; - if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex - continue; - } - - if (config.bDLC) - { - int dlcCount = product["dlcCount"].asInt(); - - bool bDownloadDLCInfo = (dlcCount != 0); - - if (!bDownloadDLCInfo && !config.sIgnoreDLCCountRegex.empty()) - { - boost::regex expression(config.sIgnoreDLCCountRegex); - boost::match_results<std::string::const_iterator> what; - if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex - { - bDownloadDLCInfo = true; - } - } - - // Check game specific config - if (!config.bUpdateCache) // Disable game specific config files for cache update - { - gameSpecificConfig conf; - conf.bIgnoreDLCCount = false; // Assume false - Util::getGameSpecificConfig(game.name, &conf); - if (conf.bIgnoreDLCCount) - bDownloadDLCInfo = true; - } - - if (bDownloadDLCInfo && !config.sGameRegex.empty()) - { - // don't download unnecessary info if user is only interested in a subset of his account - boost::regex expression(config.sGameRegex); - boost::match_results<std::string::const_iterator> what; - if (!boost::regex_search(game.name, what, expression)) - { - bDownloadDLCInfo = false; - } - } - - if (bDownloadDLCInfo) - { - game.gamedetailsjson = this->getGameDetailsJSON(game.id); - if (!game.gamedetailsjson.empty()) - game.dlcnames = Util::getDLCNamesFromJSON(game.gamedetailsjson["dlcs"]); - } - } - games.push_back(game); - } - } - i++; - } while (!bAllPagesParsed); - - delete jsonparser; - - return games; -} - -// Get list of free games -std::vector<gameItem> Downloader::getFreeGames() -{ - Json::Value root; - Json::Reader *jsonparser = new Json::Reader; - std::vector<gameItem> games; - std::string json = this->getResponse("https://www.gog.com/games/ajax/filtered?mediaType=game&page=1&price=free&sort=title"); - - // Parse JSON - if (!jsonparser->parse(json, root)) - { - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getFreeGames)" << std::endl << json << std::endl; - #endif - std::cout << jsonparser->getFormattedErrorMessages(); - delete jsonparser; - exit(1); - } - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getFreeGames)" << std::endl << root << std::endl; - #endif - - Json::Value products = root["products"]; - for (unsigned int i = 0; i < products.size(); ++i) - { - gameItem game; - game.name = products[i]["slug"].asString(); - game.id = products[i]["id"].isInt() ? std::to_string(products[i]["id"].asInt()) : products[i]["id"].asString(); - games.push_back(game); - } - delete jsonparser; - - return games; -} - -Json::Value Downloader::getGameDetailsJSON(const std::string& gameid) -{ - std::string gameDataUrl = "https://www.gog.com/account/gameDetails/" + gameid + ".json"; - std::string json = this->getResponse(gameDataUrl); - - // Parse JSON - Json::Value root; - Json::Reader *jsonparser = new Json::Reader; - if (!jsonparser->parse(json, root)) - { - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getGameDetailsJSON)" << std::endl << json << std::endl; - #endif - std::cout << jsonparser->getFormattedErrorMessages(); - delete jsonparser; - exit(1); - } - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getGameDetailsJSON)" << std::endl << root << std::endl; - #endif - delete jsonparser; - - return root; -} - std::vector<gameFile> Downloader::getExtrasFromJSON(const Json::Value& json, const std::string& gamename) { std::vector<gameFile> extras; @@ -2516,14 +2061,15 @@ std::vector<gameFile> Downloader::getExtrasFromJSON(const Json::Value& json, con continue; } - extras.push_back( - gameFile ( false, - id, - name, - path, - std::string() - ) - ); + gameFile gf; + gf.type = GFTYPE_EXTRA; + gf.gamename = gamename; + gf.updated = false; + gf.id = id; + gf.name = name; + gf.path = path; + + extras.push_back(gf); } return extras; @@ -3069,6 +2615,7 @@ std::string Downloader::getRemoteFileHash(const std::string& gamename, const std returns 2 if JSON parsing failed returns 3 if cache is too old returns 4 if JSON doesn't contain "games" node + returns 5 if cache version doesn't match */ int Downloader::loadGameDetailsCache() { @@ -3101,14 +2648,25 @@ int Downloader::loadGameDetailsCache() } } - if (root.isMember("games")) + int iCacheVersion = 0; + if (root.isMember("gamedetails-cache-version")) + iCacheVersion = root["gamedetails-cache-version"].asInt(); + + if (iCacheVersion != GlobalConstants::GAMEDETAILS_CACHE_VERSION) { - this->games = getGameDetailsFromJsonNode(root["games"]); - res = 0; + res = 5; } else { - res = 4; + if (root.isMember("games")) + { + this->games = getGameDetailsFromJsonNode(root["games"]); + res = 0; + } + else + { + res = 4; + } } } else @@ -3141,6 +2699,7 @@ int Downloader::saveGameDetailsCache() Json::Value json; + json["gamedetails-cache-version"] = GlobalConstants::GAMEDETAILS_CACHE_VERSION; json["version-string"] = config.sVersionString; json["version-number"] = config.sVersionNumber; json["date"] = bptime::to_iso_string(bptime::second_clock::local_time()); @@ -3224,6 +2783,8 @@ std::vector<gameDetails> Downloader::getGameDetailsFromJsonNode(Json::Value root fileDetails.platform = fileDetailsNode["platform"].asUInt(); fileDetails.language = fileDetailsNode["language"].asUInt(); fileDetails.silent = fileDetailsNode["silent"].asInt(); + fileDetails.gamename = fileDetailsNode["gamename"].asString(); + fileDetails.type = fileDetailsNode["type"].asUInt(); if (nodeName != "extras" && !(fileDetails.platform & conf.iInstallerPlatform)) continue; @@ -3270,6 +2831,7 @@ void Downloader::updateCache() config.vLanguagePriority.clear(); config.vPlatformPriority.clear(); config.sIgnoreDLCCountRegex = ".*"; // Ignore DLC count for all games because GOG doesn't report DLC count correctly + gogWebsite->setConfig(config); // Make sure that website handle has updated config this->getGameList(); this->getGameDetails(); @@ -3387,6 +2949,8 @@ void Downloader::downloadFileWithId(const std::string& fileid_string, const std: url = gogAPI->getInstallerLink(gamename, fileid); else if (fileid.find("patch") != std::string::npos) url = gogAPI->getPatchLink(gamename, fileid); + else if (fileid.find("langpack") != std::string::npos) + url = gogAPI->getLanguagePackLink(gamename, fileid); else url = gogAPI->getExtraLink(gamename, fileid); @@ -3414,141 +2978,37 @@ void Downloader::downloadFileWithId(const std::string& fileid_string, const std: void Downloader::showWishlist() { - Json::Value root; - Json::Reader *jsonparser = new Json::Reader; - int i = 1; - bool bAllPagesParsed = false; - - do - { - std::string response = this->getResponse("https://www.gog.com/account/wishlist/search?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=0&sortBy=title&system=&page=" + std::to_string(i)); - - // Parse JSON - if (!jsonparser->parse(response, root)) - { - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::showWishlist)" << std::endl << response << std::endl; - #endif - std::cout << jsonparser->getFormattedErrorMessages(); - delete jsonparser; - exit(1); - } - #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::showWishlist)" << std::endl << root << std::endl; - #endif - if (root["page"].asInt() >= root["totalPages"].asInt()) - bAllPagesParsed = true; - if (root["products"].isArray()) - { - for (unsigned int i = 0; i < root["products"].size(); ++i) - { - Json::Value product = root["products"][i]; - - unsigned int platform = 0; - std::string platforms_text; - bool bIsMovie = product["isMovie"].asBool(); - if (!bIsMovie) - { - if (product["worksOn"]["Windows"].asBool()) - platform |= GlobalConstants::PLATFORM_WINDOWS; - if (product["worksOn"]["Mac"].asBool()) - platform |= GlobalConstants::PLATFORM_MAC; - if (product["worksOn"]["Linux"].asBool()) - platform |= GlobalConstants::PLATFORM_LINUX; - - // Skip if platform doesn't match - if (config.bPlatformDetection && !(platform & config.iInstallerPlatform)) - continue; - - platforms_text = Util::getOptionNameString(platform, GlobalConstants::PLATFORMS); - } - - std::vector<std::string> tags; - if (product["isComingSoon"].asBool()) - tags.push_back("Coming soon"); - if (product["isDiscounted"].asBool()) - tags.push_back("Discount"); - if (bIsMovie) - tags.push_back("Movie"); - - std::string tags_text; - for (unsigned int j = 0; j < tags.size(); ++j) - { - tags_text += (tags_text.empty() ? "" : ", ")+tags[j]; - } - if (!tags_text.empty()) - tags_text = "[" + tags_text + "]"; - - time_t release_date_time; - std::string release_date; - bool bShowReleaseDate = false; - if (product.isMember("releaseDate") && product["isComingSoon"].asBool()) - { - if (!product["releaseDate"].empty()) - { - if (product["releaseDate"].isInt()) - { - release_date_time = product["releaseDate"].asInt(); - bShowReleaseDate = true; - } - else - { - std::string release_date_time_string = product["releaseDate"].asString(); - if (!release_date_time_string.empty()) - { - try - { - release_date_time = std::stoi(release_date_time_string); - bShowReleaseDate = true; - } - catch (std::invalid_argument& e) - { - bShowReleaseDate = false; - } - } - } - } - - if (bShowReleaseDate) - release_date = bptime::to_simple_string(bptime::from_time_t(release_date_time)); - } - - std::string price_text; - std::string currency = product["price"]["symbol"].asString(); - std::string price = product["price"]["finalAmount"].isDouble() ? std::to_string(product["price"]["finalAmount"].asDouble()) + currency : product["price"]["finalAmount"].asString() + currency; - std::string discount_percent = product["price"]["discountPercentage"].isInt() ? std::to_string(product["price"]["discountPercentage"].asInt()) + "%" : product["price"]["discountPercentage"].asString() + "%"; - std::string discount = product["price"]["discountDifference"].isDouble() ? std::to_string(product["price"]["discountDifference"].asDouble()) + currency : product["price"]["discountDifference"].asString() + currency; - std::string store_credit = product["price"]["bonusStoreCreditAmount"].isDouble() ? std::to_string(product["price"]["bonusStoreCreditAmount"].asDouble()) + currency : product["price"]["bonusStoreCreditAmount"].asString() + currency; - price_text = price; - if (product["isDiscounted"].asBool()) - price_text += " (-" + discount_percent + " | -" + discount + ")"; - - std::string url = product["url"].asString(); - if (url.find("/game/") == 0) - url = "https://www.gog.com" + url; - else if (url.find("/movie/") == 0) - url = "https://www.gog.com" + url; - - std::cout << product["title"].asString(); - if (!tags_text.empty()) - std::cout << " " << tags_text; - std::cout << std::endl; - std::cout << "\t" << url << std::endl; - if (!bIsMovie) - std::cout << "\tPlatforms: " << platforms_text << std::endl; - if (bShowReleaseDate) - std::cout << "\tRelease date: " << release_date << std::endl; - std::cout << "\tPrice: " << price_text << std::endl; - if (product["price"]["isBonusStoreCreditIncluded"].asBool()) - std::cout << "\tStore credit: " << store_credit << std::endl; - - std::cout << std::endl; - } - } - i++; - } while (!bAllPagesParsed); - - delete jsonparser; + std::vector<wishlistItem> wishlistItems = gogWebsite->getWishlistItems(); + for (unsigned int i = 0; i < wishlistItems.size(); ++i) + { + wishlistItem item = wishlistItems[i]; + std::string platforms_text = Util::getOptionNameString(item.platform, GlobalConstants::PLATFORMS); + std::string tags_text; + for (unsigned int j = 0; j < item.tags.size(); ++j) + { + tags_text += (tags_text.empty() ? "" : ", ")+item.tags[j]; + } + if (!tags_text.empty()) + tags_text = "[" + tags_text + "]"; + + std::string price_text = item.price; + if (item.bIsDiscounted) + price_text += " (-" + item.discount_percent + " | -" + item.discount + ")"; + + std::cout << item.title; + if (!tags_text.empty()) + std::cout << " " << tags_text; + std::cout << std::endl; + std::cout << "\t" << item.url << std::endl; + if (item.platform != 0) + std::cout << "\tPlatforms: " << platforms_text << std::endl; + if (item.release_date_time != 0) + std::cout << "\tRelease date: " << bptime::to_simple_string(bptime::from_time_t(item.release_date_time)) << std::endl; + std::cout << "\tPrice: " << price_text << std::endl; + if (item.bIsBonusStoreCreditIncluded) + std::cout << "\tStore credit: " << item.store_credit << std::endl; + std::cout << std::endl; + } return; } diff --git a/src/gamedetails.cpp b/src/gamedetails.cpp index 2ac9067..c3d6a5b 100644 --- a/src/gamedetails.cpp +++ b/src/gamedetails.cpp @@ -1,3 +1,9 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ + #include "gamedetails.h" gameDetails::gameDetails() diff --git a/src/gamefile.cpp b/src/gamefile.cpp index 86ef3c0..d78623e 100644 --- a/src/gamefile.cpp +++ b/src/gamefile.cpp @@ -1,20 +1,17 @@ -#include "gamefile.h" +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ -gameFile::gameFile(const int& t_updated, const std::string& t_id, const std::string& t_name, const std::string& t_path, const std::string& t_size, const unsigned int& t_language, const unsigned int& t_platform, const int& t_silent) -{ - this->updated = t_updated; - this->id = t_id; - this->name = t_name; - this->path = t_path; - this->size = t_size; - this->platform = t_platform; - this->language = t_language; - this->silent = t_silent; -} +#include "gamefile.h" gameFile::gameFile() { - //ctor + this->platform = GlobalConstants::PLATFORM_WINDOWS; + this->language = GlobalConstants::LANGUAGE_EN; + this->silent = 0; + this->type = 0; } gameFile::~gameFile() @@ -44,6 +41,8 @@ Json::Value gameFile::getAsJson() json["platform"] = this->platform; json["language"] = this->language; json["silent"] = this->silent; + json["gamename"] = this->gamename; + json["type"] = this->type; return json; } diff --git a/src/progressbar.cpp b/src/progressbar.cpp index 7f36b2b..1baf3e2 100644 --- a/src/progressbar.cpp +++ b/src/progressbar.cpp @@ -6,6 +6,7 @@ #include "progressbar.h" #include <cmath> +#include <sstream> ProgressBar::ProgressBar(bool bUnicode, bool bColor) : @@ -46,6 +47,12 @@ ProgressBar::~ProgressBar() void ProgressBar::draw(unsigned int length, double fraction) { + std::cout << createBarString(length, fraction); +} + +std::string ProgressBar::createBarString(unsigned int length, double fraction) +{ + std::ostringstream ss; // validation if (!std::isnormal(fraction) || (fraction < 0.0)) fraction = 0.0; else if (fraction > 1.0) fraction = 1.0; @@ -57,29 +64,31 @@ void ProgressBar::draw(unsigned int length, double fraction) unsigned int partial_bar_char_index = (unsigned int) std::floor((bar_part - whole_bar_chars) * 8.0); // left border - if (m_use_color) std::cout << m_border_color; - std::cout << (m_use_unicode ? m_left_border : m_simple_left_border); + if (m_use_color) ss << m_border_color; + ss << (m_use_unicode ? m_left_border : m_simple_left_border); // whole completed bars - if (m_use_color) std::cout << m_bar_color; + if (m_use_color) ss << m_bar_color; unsigned int i = 0; for (; i < whole_bar_chars_i; i++) { - std::cout << (m_use_unicode ? m_bar_chars[8] : m_simple_bar_char); + ss << (m_use_unicode ? m_bar_chars[8] : m_simple_bar_char); } // partial completed bar - if (i < length) std::cout << (m_use_unicode ? m_bar_chars[partial_bar_char_index] : m_simple_empty_fill); + if (i < length) ss << (m_use_unicode ? m_bar_chars[partial_bar_char_index] : m_simple_empty_fill); // whole unfinished bars - if (m_use_color) std::cout << COLOR_RESET; + if (m_use_color) ss << COLOR_RESET; for (i = whole_bar_chars_i + 1; i < length; i++) { // first entry in m_bar_chars is assumed to be the empty bar - std::cout << (m_use_unicode ? m_bar_chars[0] : m_simple_empty_fill); + ss << (m_use_unicode ? m_bar_chars[0] : m_simple_empty_fill); } // right border - if (m_use_color) std::cout << m_border_color; - std::cout << (m_use_unicode ? m_right_border : m_simple_right_border); - if (m_use_color) std::cout << COLOR_RESET; + if (m_use_color) ss << m_border_color; + ss << (m_use_unicode ? m_right_border : m_simple_right_border); + if (m_use_color) ss << COLOR_RESET; + + return ss.str(); } diff --git a/src/util.cpp b/src/util.cpp index 4b98c45..3d9dfed 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -388,11 +388,11 @@ int Util::getTerminalWidth() void Util::getDownloaderUrlsFromJSON(const Json::Value &root, std::vector<std::string> &urls) { if(root.size() > 0) { - for(Json::ValueIterator it = root.begin() ; it != root.end() ; ++it) + for(Json::ValueConstIterator it = root.begin() ; it != root.end() ; ++it) { if (it.key() == "downloaderUrl") { - Json::Value& url = *it; + Json::Value url = *it; urls.push_back(url.asString()); } else diff --git a/src/website.cpp b/src/website.cpp new file mode 100644 index 0000000..2e68bed --- /dev/null +++ b/src/website.cpp @@ -0,0 +1,708 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ + +#include "website.h" +#include "globalconstants.h" + +#include <htmlcxx/html/ParserDom.h> +#include <boost/algorithm/string/case_conv.hpp> + +Website::Website(Config &conf) +{ + this->config = conf; + this->retries = 0; + + curlhandle = curl_easy_init(); + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, config.sVersionString.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); + curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, config.iTimeout); + curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true); + curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, config.sCookiePath.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, config.sCookiePath.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer); + curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, config.bVerbose); + curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate); + + // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds + curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30); + curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200); + +} + +Website::~Website() +{ + curl_easy_cleanup(curlhandle); +} + +size_t Website::writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) +{ + std::ostringstream *stream = (std::ostringstream*)userp; + size_t count = size * nmemb; + stream->write(ptr, count); + return count; +} + +std::string Website::getResponse(const std::string& url) +{ + std::ostringstream memory; + std::string response; + + curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback); + curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); + + CURLcode result; + do + { + if (config.iWait > 0) + usleep(config.iWait); // Delay the request by specified time + result = curl_easy_perform(curlhandle); + response = memory.str(); + memory.str(std::string()); + } + while ((result != CURLE_OK) && response.empty() && (this->retries++ < config.iRetries)); + this->retries = 0; // reset retries counter + + if (result != CURLE_OK) + { + std::cout << curl_easy_strerror(result) << std::endl; + if (result == CURLE_HTTP_RETURNED_ERROR) + { + long int response_code = 0; + result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); + std::cout << "HTTP ERROR: "; + if (result == CURLE_OK) + std::cout << response_code << " (" << url << ")" << std::endl; + else + std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl; + } + } + + return response; +} + +Json::Value Website::getGameDetailsJSON(const std::string& gameid) +{ + std::string gameDataUrl = "https://www.gog.com/account/gameDetails/" + gameid + ".json"; + std::string json = this->getResponse(gameDataUrl); + + // Parse JSON + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + if (!jsonparser->parse(json, root)) + { + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << json << std::endl; + #endif + std::cout << jsonparser->getFormattedErrorMessages(); + delete jsonparser; + exit(1); + } + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << root << std::endl; + #endif + delete jsonparser; + + return root; +} + +// Get list of games from account page +std::vector<gameItem> Website::getGames() +{ + std::vector<gameItem> games; + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + int i = 1; + bool bAllPagesParsed = false; + + do + { + std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=1&sortBy=title&system=&page=" + std::to_string(i)); + + // Parse JSON + if (!jsonparser->parse(response, root)) + { + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << response << std::endl; + #endif + std::cout << jsonparser->getFormattedErrorMessages(); + delete jsonparser; + if (!response.empty()) + { + if(response[0] != '{') + { + // Response was not JSON. Assume that cookies have expired. + std::cerr << "Response was not JSON. Cookies have most likely expired. Try --login first." << std::endl; + } + } + exit(1); + } + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << root << std::endl; + #endif + if (root["page"].asInt() == root["totalPages"].asInt()) + bAllPagesParsed = true; + if (root["products"].isArray()) + { + for (unsigned int i = 0; i < root["products"].size(); ++i) + { + Json::Value product = root["products"][i]; + gameItem game; + game.name = product["slug"].asString(); + game.id = product["id"].isInt() ? std::to_string(product["id"].asInt()) : product["id"].asString(); + + unsigned int platform = 0; + if (product["worksOn"]["Windows"].asBool()) + platform |= GlobalConstants::PLATFORM_WINDOWS; + if (product["worksOn"]["Mac"].asBool()) + platform |= GlobalConstants::PLATFORM_MAC; + if (product["worksOn"]["Linux"].asBool()) + platform |= GlobalConstants::PLATFORM_LINUX; + + // Skip if platform doesn't match + if (config.bPlatformDetection && !(platform & config.iInstallerPlatform)) + continue; + + // Filter the game list + if (!config.sGameRegex.empty()) + { + // GameRegex filter aliases + if (config.sGameRegex == "all") + config.sGameRegex = ".*"; + + boost::regex expression(config.sGameRegex); + boost::match_results<std::string::const_iterator> what; + if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex + continue; + } + + if (config.bDLC) + { + int dlcCount = product["dlcCount"].asInt(); + + bool bDownloadDLCInfo = (dlcCount != 0); + + if (!bDownloadDLCInfo && !config.sIgnoreDLCCountRegex.empty()) + { + boost::regex expression(config.sIgnoreDLCCountRegex); + boost::match_results<std::string::const_iterator> what; + if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex + { + bDownloadDLCInfo = true; + } + } + + // Check game specific config + if (!config.bUpdateCache) // Disable game specific config files for cache update + { + gameSpecificConfig conf; + conf.bIgnoreDLCCount = false; // Assume false + Util::getGameSpecificConfig(game.name, &conf); + if (conf.bIgnoreDLCCount) + bDownloadDLCInfo = true; + } + + if (bDownloadDLCInfo && !config.sGameRegex.empty()) + { + // don't download unnecessary info if user is only interested in a subset of his account + boost::regex expression(config.sGameRegex); + boost::match_results<std::string::const_iterator> what; + if (!boost::regex_search(game.name, what, expression)) + { + bDownloadDLCInfo = false; + } + } + + if (bDownloadDLCInfo) + { + game.gamedetailsjson = this->getGameDetailsJSON(game.id); + if (!game.gamedetailsjson.empty()) + game.dlcnames = Util::getDLCNamesFromJSON(game.gamedetailsjson["dlcs"]); + } + } + games.push_back(game); + } + } + i++; + } while (!bAllPagesParsed); + + delete jsonparser; + + return games; +} + +// Get list of free games +std::vector<gameItem> Website::getFreeGames() +{ + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + std::vector<gameItem> games; + std::string json = this->getResponse("https://www.gog.com/games/ajax/filtered?mediaType=game&page=1&price=free&sort=title"); + + // Parse JSON + if (!jsonparser->parse(json, root)) + { + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << json << std::endl; + #endif + std::cout << jsonparser->getFormattedErrorMessages(); + delete jsonparser; + exit(1); + } + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << root << std::endl; + #endif + + Json::Value products = root["products"]; + for (unsigned int i = 0; i < products.size(); ++i) + { + gameItem game; + game.name = products[i]["slug"].asString(); + game.id = products[i]["id"].isInt() ? std::to_string(products[i]["id"].asInt()) : products[i]["id"].asString(); + games.push_back(game); + } + delete jsonparser; + + return games; +} + +// Login to GOG website +int Website::Login(const std::string& email, const std::string& password) +{ + int res = 0; + std::string postdata; + std::ostringstream memory; + std::string token; + std::string tagname_username; + std::string tagname_password; + std::string tagname_login; + std::string tagname_token; + + // Get login token + std::string html = this->getResponse("https://www.gog.com/"); + htmlcxx::HTML::ParserDom parser; + tree<htmlcxx::HTML::Node> dom = parser.parseTree(html); + tree<htmlcxx::HTML::Node>::iterator it = dom.begin(); + tree<htmlcxx::HTML::Node>::iterator end = dom.end(); + // Find auth_url + bool bFoundAuthUrl = false; + for (; it != end; ++it) + { + if (it->tagName()=="script") + { + std::string auth_url; + for (unsigned int i = 0; i < dom.number_of_children(it); ++i) + { + tree<htmlcxx::HTML::Node>::iterator script_it = dom.child(it, i); + if (!script_it->isTag() && !script_it->isComment()) + { + if (script_it->text().find("GalaxyAccounts") != std::string::npos) + { + boost::match_results<std::string::const_iterator> what; + boost::regex expression(".*'(https://auth.gog.com/.*?)'.*"); + boost::regex_match(script_it->text(), what, expression); + auth_url = what[1]; + break; + } + } + } + + if (!auth_url.empty()) + { // Found auth_url, get the necessary info for login + bFoundAuthUrl = true; + std::string login_form_html = this->getResponse(auth_url); + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::Login)" << std::endl; + std::cerr << login_form_html << std::endl; + #endif + if (login_form_html.find("google.com/recaptcha") != std::string::npos) + { + std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl + << "Login with browser and export cookies to \"" << config.sCookiePath << "\"" << std::endl; + return res = 0; + } + + tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html); + tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin(); + tree<htmlcxx::HTML::Node>::iterator login_it_end = login_dom.end(); + for (; login_it != login_it_end; ++login_it) + { + if (login_it->tagName()=="input") + { + login_it->parseAttributes(); + std::string id_login = login_it->attribute("id").second; + if (id_login == "login_username") + { + tagname_username = login_it->attribute("name").second; + } + else if (id_login == "login_password") + { + tagname_password = login_it->attribute("name").second; + } + else if (id_login == "login__token") + { + token = login_it->attribute("value").second; // login token + tagname_token = login_it->attribute("name").second; + } + } + else if (login_it->tagName()=="button") + { + login_it->parseAttributes(); + std::string id_login = login_it->attribute("id").second; + if (id_login == "login_login") + { + tagname_login = login_it->attribute("name").second; + } + } + } + break; + } + } + } + + if (!bFoundAuthUrl) + { + std::cout << "Failed to find url for login form" << std::endl; + } + + if (token.empty()) + { + std::cout << "Failed to get login token" << std::endl; + return res = 0; + } + + //Create postdata - escape characters in email/password to support special characters + postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size()) + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size()) + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "=" + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size()); + curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check"); + curl_easy_setopt(curlhandle, CURLOPT_POST, 1); + curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback); + curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); + curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); + curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); + + // Don't follow to redirect location because we need to check it for two step authorization. + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); + CURLcode result = curl_easy_perform(curlhandle); + memory.str(std::string()); + + if (result != CURLE_OK) + { + // Expected to hit maximum amount of redirects so don't print error on it + if (result != CURLE_TOO_MANY_REDIRECTS) + std::cout << curl_easy_strerror(result) << std::endl; + } + + // Get redirect url + char *redirect_url; + curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); + + // Handle two step authorization + if (std::string(redirect_url).find("two_step") != std::string::npos) + { + std::string security_code, tagname_two_step_send, tagname_two_step_auth_letter_1, tagname_two_step_auth_letter_2, tagname_two_step_auth_letter_3, tagname_two_step_auth_letter_4, tagname_two_step_token, token_two_step; + std::string two_step_html = this->getResponse(redirect_url); + redirect_url = NULL; + + tree<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html); + tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin(); + tree<htmlcxx::HTML::Node>::iterator two_step_it_end = two_step_dom.end(); + for (; two_step_it != two_step_it_end; ++two_step_it) + { + if (two_step_it->tagName()=="input") + { + two_step_it->parseAttributes(); + std::string id_two_step = two_step_it->attribute("id").second; + if (id_two_step == "second_step_authentication_token_letter_1") + { + tagname_two_step_auth_letter_1 = two_step_it->attribute("name").second; + } + else if (id_two_step == "second_step_authentication_token_letter_2") + { + tagname_two_step_auth_letter_2 = two_step_it->attribute("name").second; + } + else if (id_two_step == "second_step_authentication_token_letter_3") + { + tagname_two_step_auth_letter_3 = two_step_it->attribute("name").second; + } + else if (id_two_step == "second_step_authentication_token_letter_4") + { + tagname_two_step_auth_letter_4 = two_step_it->attribute("name").second; + } + else if (id_two_step == "second_step_authentication__token") + { + token_two_step = two_step_it->attribute("value").second; // two step token + tagname_two_step_token = two_step_it->attribute("name").second; + } + } + else if (two_step_it->tagName()=="button") + { + two_step_it->parseAttributes(); + std::string id_two_step = two_step_it->attribute("id").second; + if (id_two_step == "second_step_authentication_send") + { + tagname_two_step_send = two_step_it->attribute("name").second; + } + } + } + std::cerr << "Security code: "; + std::getline(std::cin,security_code); + if (security_code.size() != 4) + { + std::cerr << "Security code must be 4 characters long" << std::endl; + exit(1); + } + postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "=" + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size()); + + curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step"); + curl_easy_setopt(curlhandle, CURLOPT_POST, 1); + curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback); + curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); + curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); + curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); + + // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first. + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); + result = curl_easy_perform(curlhandle); + memory.str(std::string()); + curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); + } + + curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); + curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1); + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); + result = curl_easy_perform(curlhandle); + + if (result != CURLE_OK) + { + std::cout << curl_easy_strerror(result) << std::endl; + } + + if (this->IsLoggedInComplex(email)) + { + res = 1; // Login was successful + } + else + { + if (this->IsloggedInSimple()) + res = 1; // Login was successful + } + + if (res == 1) + { + curl_easy_setopt(curlhandle, CURLOPT_COOKIELIST, "FLUSH"); // Write all known cookies to the file specified by CURLOPT_COOKIEJAR + } + + return res; +} + +bool Website::IsLoggedIn() +{ + return this->IsloggedInSimple(); +} + +/* Complex login check. Check login by checking email address on the account settings page. + returns true if we are logged in + returns false if we are not logged in +*/ +bool Website::IsLoggedInComplex(const std::string& email) +{ + bool bIsLoggedIn = false; + std::string html = this->getResponse("https://www.gog.com/account/settings/personal"); + std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it + + htmlcxx::HTML::ParserDom parser; + tree<htmlcxx::HTML::Node> dom = parser.parseTree(html); + tree<htmlcxx::HTML::Node>::iterator it = dom.begin(); + tree<htmlcxx::HTML::Node>::iterator end = dom.end(); + dom = parser.parseTree(html); + it = dom.begin(); + end = dom.end(); + for (; it != end; ++it) + { + if (it->tagName()=="strong") + { + it->parseAttributes(); + if (it->attribute("class").second == "settings-item__value settings-item__section") + { + for (unsigned int i = 0; i < dom.number_of_children(it); ++i) + { + tree<htmlcxx::HTML::Node>::iterator tag_it = dom.child(it, i); + if (!tag_it->isTag() && !tag_it->isComment()) + { + std::string tag_text = boost::algorithm::to_lower_copy(tag_it->text()); + if (tag_text == email_lowercase) + { + bIsLoggedIn = true; // We are logged in + break; + } + } + } + } + } + if (bIsLoggedIn) // We are logged in so no need to go through the remaining tags + break; + } + + return bIsLoggedIn; +} + +/* Simple login check. Check login by trying to get account page. If response code isn't 200 then login failed. + returns true if we are logged in + returns false if we are not logged in +*/ +bool Website::IsloggedInSimple() +{ + bool bIsLoggedIn = false; + std::ostringstream memory; + std::string url = "https://www.gog.com/account"; + long int response_code = 0; + + curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); + curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback); + curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); + curl_easy_perform(curlhandle); + memory.str(std::string()); + + curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); + if (response_code == 200) + bIsLoggedIn = true; // We are logged in + + return bIsLoggedIn; +} + +std::vector<wishlistItem> Website::getWishlistItems() +{ + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + int i = 1; + bool bAllPagesParsed = false; + std::vector<wishlistItem> wishlistItems; + + do + { + std::string response = this->getResponse("https://www.gog.com/account/wishlist/search?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=0&sortBy=title&system=&page=" + std::to_string(i)); + + // Parse JSON + if (!jsonparser->parse(response, root)) + { + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << response << std::endl; + #endif + std::cout << jsonparser->getFormattedErrorMessages(); + delete jsonparser; + exit(1); + } + #ifdef DEBUG + std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << root << std::endl; + #endif + if (root["page"].asInt() >= root["totalPages"].asInt()) + bAllPagesParsed = true; + if (root["products"].isArray()) + { + for (unsigned int i = 0; i < root["products"].size(); ++i) + { + wishlistItem item; + Json::Value product = root["products"][i]; + + item.platform = 0; + std::string platforms_text; + bool bIsMovie = product["isMovie"].asBool(); + if (!bIsMovie) + { + if (product["worksOn"]["Windows"].asBool()) + item.platform |= GlobalConstants::PLATFORM_WINDOWS; + if (product["worksOn"]["Mac"].asBool()) + item.platform |= GlobalConstants::PLATFORM_MAC; + if (product["worksOn"]["Linux"].asBool()) + item.platform |= GlobalConstants::PLATFORM_LINUX; + + // Skip if platform doesn't match + if (config.bPlatformDetection && !(item.platform & config.iInstallerPlatform)) + continue; + } + + if (product["isComingSoon"].asBool()) + item.tags.push_back("Coming soon"); + if (product["isDiscounted"].asBool()) + item.tags.push_back("Discount"); + if (bIsMovie) + item.tags.push_back("Movie"); + + item.release_date_time = 0; + if (product.isMember("releaseDate") && product["isComingSoon"].asBool()) + { + if (!product["releaseDate"].empty()) + { + if (product["releaseDate"].isInt()) + { + item.release_date_time = product["releaseDate"].asInt(); + } + else + { + std::string release_date_time_string = product["releaseDate"].asString(); + if (!release_date_time_string.empty()) + { + try + { + item.release_date_time = std::stoi(release_date_time_string); + } + catch (std::invalid_argument& e) + { + item.release_date_time = 0; + } + } + } + } + } + + item.currency = product["price"]["symbol"].asString(); + item.price = product["price"]["finalAmount"].isDouble() ? std::to_string(product["price"]["finalAmount"].asDouble()) + item.currency : product["price"]["finalAmount"].asString() + item.currency; + item.discount_percent = product["price"]["discountPercentage"].isInt() ? std::to_string(product["price"]["discountPercentage"].asInt()) + "%" : product["price"]["discountPercentage"].asString() + "%"; + item.discount = product["price"]["discountDifference"].isDouble() ? std::to_string(product["price"]["discountDifference"].asDouble()) + item.currency : product["price"]["discountDifference"].asString() + item.currency; + item.store_credit = product["price"]["bonusStoreCreditAmount"].isDouble() ? std::to_string(product["price"]["bonusStoreCreditAmount"].asDouble()) + item.currency : product["price"]["bonusStoreCreditAmount"].asString() + item.currency; + + item.url = product["url"].asString(); + if (item.url.find("/game/") == 0) + item.url = "https://www.gog.com" + item.url; + else if (item.url.find("/movie/") == 0) + item.url = "https://www.gog.com" + item.url; + + item.title = product["title"].asString(); + item.bIsBonusStoreCreditIncluded = product["price"]["isBonusStoreCreditIncluded"].asBool(); + item.bIsDiscounted = product["isDiscounted"].asBool(); + + wishlistItems.push_back(item); + } + } + i++; + } while (!bAllPagesParsed); + + delete jsonparser; + + return wishlistItems; +} + +void Website::setConfig(Config &conf) +{ + this->config = conf; +} -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/lgogdownloader.git _______________________________________________ Pkg-games-commits mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-games-commits

