[lgogdownloader] 01/05: Imported Upstream version 2.28

Stephen Kitt skitt at moszumanska.debian.org
Mon May 9 21:36:23 UTC 2016


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 <steve at sk2.org>
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



More information about the Pkg-games-commits mailing list