[lgogdownloader] 01/08: New upstream version 3.3

Stephen Kitt skitt at moszumanska.debian.org
Tue Nov 7 22:06:02 UTC 2017


This is an automated email from the git hooks/post-receive script.

skitt pushed a commit to branch master
in repository lgogdownloader.

commit f75ae64344dd2e65c71fd9ab1e6a20f8250ec6c8
Author: Stephen Kitt <steve at sk2.org>
Date:   Tue Nov 7 20:24:03 2017 +0100

    New upstream version 3.3
---
 CMakeLists.txt                        |   2 +-
 include/config.h                      |  15 +-
 include/downloader.h                  |   4 +-
 include/galaxyapi.h                   |   8 +
 include/gamefile.h                    |   1 +
 include/globalconstants.h             |  18 +-
 include/util.h                        |   3 +-
 main.cpp                              |  34 +-
 man/CMakeLists.txt                    |   4 +-
 man/lgogdownloader.supplemental.groff |   2 -
 src/api.cpp                           |  21 +-
 src/downloader.cpp                    | 936 ++++++++++++++++------------------
 src/galaxyapi.cpp                     | 206 +++++++-
 src/gamedetails.cpp                   |   6 +
 src/gamefile.cpp                      |   1 +
 src/util.cpp                          |  27 +-
 src/website.cpp                       |  20 +-
 17 files changed, 779 insertions(+), 529 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7614b6f..147dd8b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR)
-project (lgogdownloader LANGUAGES C CXX VERSION 3.2)
+project (lgogdownloader LANGUAGES C CXX VERSION 3.3)
 
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
 set(LINK_LIBCRYPTO 0)
diff --git a/include/config.h b/include/config.h
index 5f00b5b..2dbcc21 100644
--- a/include/config.h
+++ b/include/config.h
@@ -36,6 +36,7 @@ struct DownloadConfig
     unsigned int iInclude;
     unsigned int iGalaxyPlatform;
     unsigned int iGalaxyLanguage;
+    unsigned int iGalaxyArch;
 
     bool bRemoteXML;
     bool bCover;
@@ -65,7 +66,7 @@ class GalaxyConfig
         bool isExpired()
         {
             std::unique_lock<std::mutex> lock(m);
-            bool bExpired = false;
+            bool bExpired = true; // assume that token is expired
             intmax_t time_now = time(NULL);
             if (this->token_json.isMember("expires_at"))
                 bExpired = (time_now > this->token_json["expires_at"].asLargestInt());
@@ -74,14 +75,20 @@ class GalaxyConfig
 
         std::string getAccessToken()
         {
+            std:: string access_token;
             std::unique_lock<std::mutex> lock(m);
-            return this->token_json["access_token"].asString();
+            if (this->token_json.isMember("access_token"))
+                access_token = this->token_json["access_token"].asString();
+            return access_token;
         }
 
         std::string getRefreshToken()
         {
+            std::string refresh_token;
             std::unique_lock<std::mutex> lock(m);
-            return this->token_json["refresh_token"].asString();
+            if (this->token_json.isMember("refresh_token"))
+                refresh_token = this->token_json["refresh_token"].asString();
+            return refresh_token;
         }
 
         Json::Value getJSON()
@@ -180,6 +187,7 @@ struct CurlConfig
     bool bVerbose;
     std::string sCACertPath;
     std::string sCookiePath;
+    std::string sUserAgent;
     long int iTimeout;
     curl_off_t iDownloadRate;
 };
@@ -276,6 +284,7 @@ class Config
         unsigned int iThreads;
         int iWait;
         size_t iChunkSize;
+        int iProgressInterval;
 };
 
 #endif // CONFIG_H__
diff --git a/include/downloader.h b/include/downloader.h
index 3d91a5c..f760bec 100644
--- a/include/downloader.h
+++ b/include/downloader.h
@@ -90,7 +90,7 @@ class Downloader
         std::deque< std::pair<time_t, uintmax_t> > TimeAndSize;
         void saveGalaxyJSON();
 
-        void galaxyInstallGame(const std::string& product_id, int build_index = -1);
+        void galaxyInstallGame(const std::string& product_id, int build_index = -1, const unsigned int& iGalaxyArch = GlobalConstants::ARCH_X64);
         void galaxyShowBuilds(const std::string& product_id, int build_index = -1);
     protected:
     private:
@@ -122,6 +122,8 @@ class Downloader
         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);
 
+        std::vector<std::string> galaxyGetOrphanedFiles(const std::vector<galaxyDepotItem>& items, const std::string& install_path);
+
         Website *gogWebsite;
         API *gogAPI;
         galaxyAPI *gogGalaxy;
diff --git a/include/galaxyapi.h b/include/galaxyapi.h
index 50260e6..465039e 100644
--- a/include/galaxyapi.h
+++ b/include/galaxyapi.h
@@ -11,6 +11,7 @@
 #include "globals.h"
 #include "config.h"
 #include "util.h"
+#include "gamedetails.h"
 
 #include <iostream>
 #include <vector>
@@ -53,11 +54,18 @@ class galaxyAPI
         std::string getResponse(const std::string& url, const bool& zlib_decompress = false);
         std::string hashToGalaxyPath(const std::string& hash);
         std::vector<galaxyDepotItem> getDepotItemsVector(const std::string& hash);
+        Json::Value getProductInfo(const std::string& product_id);
+        gameDetails productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf);
     protected:
     private:
         CurlConfig curlConf;
         static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
         CURL* curlhandle;
+        std::vector<gameFile> installerJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf);
+        std::vector<gameFile> patchJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf);
+        std::vector<gameFile> languagepackJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf);
+        std::vector<gameFile> extraJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json);
+        std::vector<gameFile> fileJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const unsigned int& type = GFTYPE_INSTALLER, const unsigned int& platform = (GlobalConstants::PLATFORM_WINDOWS | GlobalConstants::PLATFORM_LINUX), const unsigned int& lang = GlobalConstants::LANGUAGE_EN, const bool& useDuplicateHandler = false);
 };
 
 #endif // GALAXYAPI_H
diff --git a/include/gamefile.h b/include/gamefile.h
index 11f2744..e314046 100644
--- a/include/gamefile.h
+++ b/include/gamefile.h
@@ -31,6 +31,7 @@ class gameFile
         std::string name;
         std::string path;
         std::string size;
+        std::string galaxy_downlink_json_url;
         unsigned int platform;
         unsigned int language;
         unsigned int type;
diff --git a/include/globalconstants.h b/include/globalconstants.h
index ab522bd..c939d49 100644
--- a/include/globalconstants.h
+++ b/include/globalconstants.h
@@ -12,7 +12,7 @@
 
 namespace GlobalConstants
 {
-    const int GAMEDETAILS_CACHE_VERSION = 2;
+    const int GAMEDETAILS_CACHE_VERSION = 3;
     const int ZLIB_WINDOW_SIZE = 15;
 
     struct optionsStruct {const unsigned int id; const std::string code; const std::string str; const std::string regexp;};
@@ -41,6 +41,8 @@ namespace GlobalConstants
     const unsigned int LANGUAGE_PT_BR = 1 << 19;
     const unsigned int LANGUAGE_SK = 1 << 20;
     const unsigned int LANGUAGE_BL = 1 << 21;
+    const unsigned int LANGUAGE_UK = 1 << 22;
+    const unsigned int LANGUAGE_ES_419 = 1 << 23;
 
     const std::vector<optionsStruct> LANGUAGES =
     {
@@ -65,7 +67,9 @@ namespace GlobalConstants
         { LANGUAGE_FI, "fi", "Finnish"   , "fi|fin|finnish"        },
         { LANGUAGE_PT_BR, "br", "Brazilian Portuguese", "br|pt_br|pt-br|ptbr|brazilian_portuguese" },
         { LANGUAGE_SK, "sk", "Slovak"    , "sk|slk|slo|slovak"     },
-        { LANGUAGE_BL, "bl", "Bulgarian" , "bl|bg|bul|bulgarian"   }
+        { LANGUAGE_BL, "bl", "Bulgarian" , "bl|bg|bul|bulgarian"   },
+        { LANGUAGE_UK, "uk", "Ukrainian" , "uk|ukr|ukrainian"      },
+        { LANGUAGE_ES_419, "es_mx", "Spanish (Latin American)", "es_mx|es-mx|esmx|es-419|spanish_latin_american" }
     };
 
     // Platform constants
@@ -79,6 +83,16 @@ namespace GlobalConstants
         { PLATFORM_MAC,     "mac",   "Mac"     , "m|mac|osx"     },
         { PLATFORM_LINUX,   "linux", "Linux"   , "l|lin|linux"   }
     };
+
+    // Galaxy platform arch
+    const unsigned int ARCH_X86 = 1 << 0;
+    const unsigned int ARCH_X64 = 1 << 1;
+
+    const std::vector<optionsStruct> GALAXY_ARCHS =
+    {
+        { ARCH_X86, "32", "32-bit", "32|x86|32bit|32-bit" },
+        { ARCH_X64, "64", "64-bit", "64|x64|64bit|64-bit" }
+    };
 }
 
 #endif // GLOBALCONSTANTS_H_INCLUDED
diff --git a/include/util.h b/include/util.h
index b31ea1a..57a5f3b 100644
--- a/include/util.h
+++ b/include/util.h
@@ -66,11 +66,12 @@ namespace Util
     std::string getConfigHome();
     std::string getCacheHome();
     std::vector<std::string> tokenize(const std::string& str, const std::string& separator = ",");
-    unsigned int getOptionValue(const std::string& str, const std::vector<GlobalConstants::optionsStruct>& options);
+    unsigned int getOptionValue(const std::string& str, const std::vector<GlobalConstants::optionsStruct>& options, const bool& bAllowStringToIntConversion = true);
     std::string getOptionNameString(const unsigned int& value, const std::vector<GlobalConstants::optionsStruct>& options);
     void parseOptionString(const std::string &option_string, std::vector<unsigned int> &priority, unsigned int &type, const std::vector<GlobalConstants::optionsStruct>& options);
     std::string getLocalFileHash(const std::string& xml_dir, const std::string& filepath, const std::string& gamename = std::string());
     void shortenStringToTerminalWidth(std::string& str);
+    std::string getJsonUIntValueAsString(const Json::Value& json);
 
     template<typename ... Args> std::string formattedString(const std::string& format, Args ... args)
     {
diff --git a/main.cpp b/main.cpp
index 3298781..8adcc17 100644
--- a/main.cpp
+++ b/main.cpp
@@ -100,6 +100,13 @@ int main(int argc, char *argv[])
         galaxy_language_text +=  GlobalConstants::LANGUAGES[i].str + " = " + GlobalConstants::LANGUAGES[i].regexp + "|" + std::to_string(GlobalConstants::LANGUAGES[i].id) + "\n";
     }
 
+    // Create help text for --galaxy-arch option
+    std::string galaxy_arch_text = "Select architecture\n";
+    for (unsigned int i = 0; i < GlobalConstants::GALAXY_ARCHS.size(); ++i)
+    {
+        galaxy_arch_text +=  GlobalConstants::GALAXY_ARCHS[i].str + " = " + GlobalConstants::GALAXY_ARCHS[i].regexp + "\n";
+    }
+
     // Create help text for --check-orphans
     std::string orphans_regex_default = ".*\\.(zip|exe|bin|dmg|old|deb|tar\\.gz|pkg|sh)$"; // Limit to files with these extensions (".old" is for renamed older version files)
     std::string check_orphans_text = "Check for orphaned files (files found on local filesystem that are not found on GOG servers). Sets regular expression filter (Perl syntax) for files to check. If no argument is given then the regex defaults to '" + orphans_regex_default + "'";
@@ -126,6 +133,7 @@ int main(int argc, char *argv[])
     bpo::options_description options_cli_no_cfg;
     bpo::options_description options_cli_no_cfg_hidden;
     bpo::options_description options_cli_all_include_hidden;
+    bpo::options_description options_cli_experimental("Experimental");
     bpo::options_description options_cli_cfg;
     bpo::options_description options_cfg_only;
     bpo::options_description options_cfg_all("Configuration");
@@ -145,6 +153,7 @@ int main(int argc, char *argv[])
         std::string sExcludeOptions;
         std::string sGalaxyPlatform;
         std::string sGalaxyLanguage;
+        std::string sGalaxyArch;
         Globals::globalConfig.bReport = false;
         // Commandline options (no config file)
         options_cli_no_cfg.add_options()
@@ -172,6 +181,7 @@ int main(int argc, char *argv[])
             ("login-website", bpo::value<bool>(&Globals::globalConfig.bLoginHTTP)->zero_tokens()->default_value(false), "Login (website only)")
             ("cacert", bpo::value<std::string>(&Globals::globalConfig.curlConf.sCACertPath)->default_value(""), "Path to CA certificate bundle in PEM format")
             ("respect-umask", bpo::value<bool>(&Globals::globalConfig.bRespectUmask)->zero_tokens()->default_value(false), "Do not adjust permissions of sensitive files")
+            ("user-agent", bpo::value<std::string>(&Globals::globalConfig.curlConf.sUserAgent)->default_value(Globals::globalConfig.sVersionString), "Set user agent")
         ;
         // Commandline options (config file)
         options_cli_cfg.add_options()
@@ -192,7 +202,7 @@ int main(int argc, char *argv[])
             ("retries", bpo::value<int>(&Globals::globalConfig.iRetries)->default_value(3), "Set maximum number of retries on failed download")
             ("wait", bpo::value<int>(&Globals::globalConfig.iWait)->default_value(0), "Time to wait between requests (milliseconds)")
             ("cover-list", bpo::value<std::string>(&Globals::globalConfig.sCoverList)->default_value("https://raw.githubusercontent.com/Sude-/lgogdownloader-lists/master/covers.xml"), "Set URL for cover list")
-            ("subdir-installers", bpo::value<std::string>(&Globals::globalConfig.dirConf.sInstallersSubdir)->default_value(""), ("Set subdirectory for extras" + subdir_help_text).c_str())
+            ("subdir-installers", bpo::value<std::string>(&Globals::globalConfig.dirConf.sInstallersSubdir)->default_value(""), ("Set subdirectory for installers" + subdir_help_text).c_str())
             ("subdir-extras", bpo::value<std::string>(&Globals::globalConfig.dirConf.sExtrasSubdir)->default_value("extras"), ("Set subdirectory for extras" + subdir_help_text).c_str())
             ("subdir-patches", bpo::value<std::string>(&Globals::globalConfig.dirConf.sPatchesSubdir)->default_value("patches"), ("Set subdirectory for patches" + subdir_help_text).c_str())
             ("subdir-language-packs", bpo::value<std::string>(&Globals::globalConfig.dirConf.sLanguagePackSubdir)->default_value("languagepacks"), ("Set subdirectory for language packs" + subdir_help_text).c_str())
@@ -208,6 +218,7 @@ int main(int argc, char *argv[])
             ("save-changelogs", bpo::value<bool>(&Globals::globalConfig.dlConf.bSaveChangelogs)->zero_tokens()->default_value(false), "Save changelogs when downloading")
             ("threads", bpo::value<unsigned int>(&Globals::globalConfig.iThreads)->default_value(4), "Number of download threads")
             ("dlc-list", bpo::value<std::string>(&Globals::globalConfig.sGameHasDLCList)->default_value("https://raw.githubusercontent.com/Sude-/lgogdownloader-lists/master/game_has_dlc.txt"), "Set URL for list of games that have DLC")
+            ("progress-interval", bpo::value<int>(&Globals::globalConfig.iProgressInterval)->default_value(100), "Set interval for progress bar update (milliseconds)\nValue must be between 1 and 10000")
         ;
         // Options read from config file
         options_cfg_only.add_options()
@@ -216,15 +227,19 @@ int main(int argc, char *argv[])
         ;
 
         options_cli_no_cfg_hidden.add_options()
+            ("login-email", bpo::value<std::string>(&Globals::globalConfig.sEmail)->default_value(""), "login email")
+            ("login-password", bpo::value<std::string>(&Globals::globalConfig.sPassword)->default_value(""), "login password")
+        ;
+
+        options_cli_experimental.add_options()
             ("galaxy-install", bpo::value<std::string>(&galaxy_product_id_install)->default_value(""), "Install game using product id")
             ("galaxy-show-builds", bpo::value<std::string>(&galaxy_product_id_show_builds)->default_value(""), "Show game builds using product id")
             ("galaxy-platform", bpo::value<std::string>(&sGalaxyPlatform)->default_value("w"), galaxy_platform_text.c_str())
             ("galaxy-language", bpo::value<std::string>(&sGalaxyLanguage)->default_value("en"), galaxy_language_text.c_str())
-            ("login-email", bpo::value<std::string>(&Globals::globalConfig.sEmail)->default_value(""), "login email")
-            ("login-password", bpo::value<std::string>(&Globals::globalConfig.sPassword)->default_value(""), "login password")
+            ("galaxy-arch", bpo::value<std::string>(&sGalaxyArch)->default_value("x64"), galaxy_arch_text.c_str())
         ;
 
-        options_cli_all.add(options_cli_no_cfg).add(options_cli_cfg);
+        options_cli_all.add(options_cli_no_cfg).add(options_cli_cfg).add(options_cli_experimental);
         options_cfg_all.add(options_cfg_only).add(options_cli_cfg);
         options_cli_all_include_hidden.add(options_cli_all).add(options_cli_no_cfg_hidden);
 
@@ -402,6 +417,11 @@ int main(int argc, char *argv[])
         if (Globals::globalConfig.iWait > 0)
             Globals::globalConfig.iWait *= 1000;
 
+        if (Globals::globalConfig.iProgressInterval < 1)
+            Globals::globalConfig.iProgressInterval = 1;
+        else if (Globals::globalConfig.iProgressInterval > 10000)
+            Globals::globalConfig.iProgressInterval = 10000;
+
         if (Globals::globalConfig.iThreads < 1)
         {
             Globals::globalConfig.iThreads = 1;
@@ -450,6 +470,10 @@ int main(int argc, char *argv[])
 
         Globals::globalConfig.dlConf.iGalaxyPlatform = Util::getOptionValue(sGalaxyPlatform, GlobalConstants::PLATFORMS);
         Globals::globalConfig.dlConf.iGalaxyLanguage = Util::getOptionValue(sGalaxyLanguage, GlobalConstants::LANGUAGES);
+        Globals::globalConfig.dlConf.iGalaxyArch = Util::getOptionValue(sGalaxyArch, GlobalConstants::GALAXY_ARCHS, false);
+
+        if (Globals::globalConfig.dlConf.iGalaxyArch == 0 || Globals::globalConfig.dlConf.iGalaxyArch == Util::getOptionValue("all", GlobalConstants::GALAXY_ARCHS, false))
+            Globals::globalConfig.dlConf.iGalaxyArch = GlobalConstants::ARCH_X64;
 
         unsigned int include_value = 0;
         unsigned int exclude_value = 0;
@@ -740,7 +764,7 @@ int main(int argc, char *argv[])
         {
             build_index = std::stoi(tokens[1]);
         }
-        downloader.galaxyInstallGame(product_id, build_index);
+        downloader.galaxyInstallGame(product_id, build_index, Globals::globalConfig.dlConf.iGalaxyArch);
     }
     else
     {
diff --git a/man/CMakeLists.txt b/man/CMakeLists.txt
index 7f123b0..5fe72d0 100644
--- a/man/CMakeLists.txt
+++ b/man/CMakeLists.txt
@@ -3,6 +3,8 @@ find_program(GZIP gzip DOC "Location of the gzip program")
 mark_as_advanced(HELP2MAN)
 mark_as_advanced(GZIP)
 
+include(GNUInstallDirs)
+
 if(HELP2MAN AND GZIP)	
   set(H2M_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.supplemental.groff)
   set(MAN_PAGE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.1)
@@ -16,7 +18,7 @@ if(HELP2MAN AND GZIP)
 	VERBATIM
 	)
   add_custom_target(manpage ALL DEPENDS ${MAN_FILE} ${PROJECT_NAME})
-  install(FILES ${MAN_FILE} DESTINATION ${INSTALL_SHARE_DIR}/man/man1)
+  install(FILES ${MAN_FILE} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
 else(HELP2MAN AND GZIP)
   message("WARNING: One of the following is missing: help2man, gzip; man page will not be generated")
 endif(HELP2MAN AND GZIP)
diff --git a/man/lgogdownloader.supplemental.groff b/man/lgogdownloader.supplemental.groff
index d3d4800..4a73c62 100644
--- a/man/lgogdownloader.supplemental.groff
+++ b/man/lgogdownloader.supplemental.groff
@@ -6,8 +6,6 @@
 An open-source GOG.com downloader for Linux users which uses the same API as the official GOGDownloader.
 .PP
 LGOGDownloader can download purchased games, query GOG.com to see if game files have changed, as well as downloading extras such as artwork and manuals. It is capable of downloading language-specific installers for games where they exist.
-.PP
-These games are currently offered only for the Microsoft Windows\[rg] and Apple OS X\[rg] operating systems. To play these games under GNU/Linux will require a compatibility layer such as Wine. Usage of such a program is outside the scope of this document.
 
 /--update-check/
 .nf
diff --git a/src/api.cpp b/src/api.cpp
index 6ff2480..b84226b 100644
--- a/src/api.cpp
+++ b/src/api.cpp
@@ -356,6 +356,8 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                 {
                     Json::Value installer = installers[i].jsonNode[index];
                     unsigned int language = installers[i].language;
+                    std::string path = installer["link"].asString();
+                    path = (std::string)curl_easy_unescape(curlhandle, path.c_str(), path.size(), NULL);
 
                     // Check for duplicate installers in different languages and add languageId of duplicate installer to the original installer
                     // https://secure.gog.com/forum/general/introducing_the_beta_release_of_the_new_gogcom_downloader/post1483
@@ -364,7 +366,7 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                         bool bDuplicate = false;
                         for (unsigned int j = 0; j < game.installers.size(); ++j)
                         {
-                            if (game.installers[j].path == installer["link"].asString())
+                            if (game.installers[j].path == path)
                             {
                                 game.installers[j].language |= language; // Add language code to installer
                                 bDuplicate = true;
@@ -381,7 +383,7 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                     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.path = path;
                     gf.size = installer["size"].asString();
                     gf.language = language;
                     gf.platform = installers[i].platform;
@@ -404,6 +406,7 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                 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.path = (std::string)curl_easy_unescape(curlhandle, gf.path.c_str(), gf.path.size(), NULL);
                 gf.size = extra["size_mb"].asString();
 
                 game.extras.push_back(gf);
@@ -446,6 +449,8 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                                 for ( unsigned int index = 0; index < patchnode.size(); ++index )
                                 {
                                     Json::Value patch = patchnode[index];
+                                    std::string path = patch["link"].asString();
+                                    path = (std::string)curl_easy_unescape(curlhandle, path.c_str(), path.size(), NULL);
 
                                     // Check for duplicate patches in different languages and add languageId of duplicate patch to the original patch
                                     if (useDuplicateHandler)
@@ -453,7 +458,7 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                                         bool bDuplicate = false;
                                         for (unsigned int j = 0; j < game.patches.size(); ++j)
                                         {
-                                            if (game.patches[j].path == patch["link"].asString())
+                                            if (game.patches[j].path == path)
                                             {
                                                 game.patches[j].language |= GlobalConstants::LANGUAGES[i].id; // Add language code to patch
                                                 bDuplicate = true;
@@ -470,7 +475,7 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                                     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.path = path;
                                     gf.size = patch["size"].asString();
                                     gf.language = GlobalConstants::LANGUAGES[i].id;
                                     gf.platform = patches[j].platform;
@@ -480,13 +485,16 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                             }
                             else // Patch is a single file
                             {
+                                std::string path = patchnode["link"].asString();
+                                path = (std::string)curl_easy_unescape(curlhandle, path.c_str(), path.size(), NULL);
+
                                 // Check for duplicate patches in different languages and add languageId of duplicate patch to the original patch
                                 if (useDuplicateHandler)
                                 {
                                     bool bDuplicate = false;
                                     for (unsigned int k = 0; k < game.patches.size(); ++k)
                                     {
-                                        if (game.patches[k].path == patchnode["link"].asString())
+                                        if (game.patches[k].path == path)
                                         {
                                             game.patches[k].language |= GlobalConstants::LANGUAGES[i].id; // Add language code to patch
                                             bDuplicate = true;
@@ -503,7 +511,7 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                                 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.path = path;
                                 gf.size = patchnode["size"].asString();
                                 gf.language = GlobalConstants::LANGUAGES[i].id;
                                 gf.platform = patches[j].platform;
@@ -542,6 +550,7 @@ gameDetails API::getGameDetails(const std::string& game_name, const unsigned int
                             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.path = (std::string)curl_easy_unescape(curlhandle, gf.path.c_str(), gf.path.size(), NULL);
                             gf.size = langpack["size"].asString();
                             gf.language = GlobalConstants::LANGUAGES[i].id;
 
diff --git a/src/downloader.cpp b/src/downloader.cpp
index 66fc36e..2e8a2b2 100644
--- a/src/downloader.cpp
+++ b/src/downloader.cpp
@@ -82,7 +82,7 @@ Downloader::Downloader()
     // Initialize curl and set curl options
     curlhandle = curl_easy_init();
     curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
-    curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, Globals::globalConfig.sVersionString.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, Globals::globalConfig.curlConf.sUserAgent.c_str());
     curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0);
     curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
     curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, Globals::globalConfig.curlConf.iTimeout);
@@ -152,6 +152,10 @@ Downloader::~Downloader()
     if (Globals::globalConfig.bReport)
         if (this->report_ofs)
             this->report_ofs.close();
+
+    if (!gogGalaxy->isTokenExpired())
+        this->saveGalaxyJSON();
+
     delete progressbar;
     delete gogGalaxy;
     delete gogAPI;
@@ -178,11 +182,24 @@ bool Downloader::isLoggedIn()
     if (!bWebsiteIsLoggedIn)
         Globals::globalConfig.bLoginHTTP = true;
 
+    bool bGalaxyIsLoggedIn = !gogGalaxy->isTokenExpired();
+    if (!bGalaxyIsLoggedIn)
+    {
+        if (gogGalaxy->refreshLogin())
+            bGalaxyIsLoggedIn = true;
+        else
+            Globals::globalConfig.bLoginHTTP = true;
+    }
+
     bool bIsLoggedInAPI = gogAPI->isLoggedIn();
     if (!bIsLoggedInAPI)
         Globals::globalConfig.bLoginAPI = true;
 
-    if (bIsLoggedInAPI && bWebsiteIsLoggedIn)
+    /* Check that website and Galaxy API are logged in.
+        Allows users to use most of the functionality without having valid API login credentials.
+        Globals::globalConfig.bLoginAPI can still be set to true at this point which means that if website is not logged in we still try to login to API.
+    */
+    if (bWebsiteIsLoggedIn && bGalaxyIsLoggedIn)
         bIsLoggedIn = true;
 
     return bIsLoggedIn;
@@ -289,8 +306,8 @@ int Downloader::login()
                 if (!boost::filesystem::remove(Globals::globalConfig.curlConf.sCookiePath))
                     std::cerr << "Failed to delete " << Globals::globalConfig.curlConf.sCookiePath << std::endl;
 
-            //if (!gogWebsite->Login(email, password))
-            if (!gogWebsite->Login(email, password))
+            int iWebsiteLoginResult = gogWebsite->Login(email, password);
+            if (iWebsiteLoginResult < 1)
             {
                 std::cerr << "HTTP: Login failed" << std::endl;
                 return 0;
@@ -298,6 +315,16 @@ int Downloader::login()
             else
             {
                 std::cerr << "HTTP: Login successful" << std::endl;
+            }
+
+            if (iWebsiteLoginResult < 2)
+            {
+                std::cerr << "Galaxy: Login failed" << std::endl;
+                return 0;
+            }
+            else
+            {
+                std::cerr << "Galaxy: Login successful" << std::endl;
 
                 if (!Globals::galaxyConf.getJSON().empty())
                 {
@@ -313,7 +340,7 @@ int Downloader::login()
         {
             if (!gogAPI->login(email, password))
             {
-                std::cerr << "API: Login failed" << std::endl;
+                std::cerr << "API: Login failed (some features may not work)" << std::endl;
                 return 0;
             }
             else
@@ -500,7 +527,7 @@ int Downloader::listGames()
             std::cout   << "gamename: " << games[i].gamename << std::endl
                         << "product id: " << games[i].product_id << std::endl
                         << "title: " << games[i].title << std::endl
-                        << "icon: " << "http://static.gog.com" << games[i].icon << std::endl;
+                        << "icon: " << games[i].icon << std::endl;
             if (!games[i].serials.empty())
                 std::cout << "serials:" << std::endl << games[i].serials << std::endl;
 
@@ -621,6 +648,7 @@ int Downloader::listGames()
                         }
 
                         std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+                                    << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
                                     << "\tid: " << games[i].dlcs[j].installers[k].id << std::endl
                                     << "\tname: " << games[i].dlcs[j].installers[k].name << std::endl
                                     << "\tpath: " << games[i].dlcs[j].installers[k].path << std::endl
@@ -638,6 +666,7 @@ int Downloader::listGames()
                         }
 
                         std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+                                    << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
                                     << "\tid: " << games[i].dlcs[j].patches[k].id << std::endl
                                     << "\tname: " << games[i].dlcs[j].patches[k].name << std::endl
                                     << "\tpath: " << games[i].dlcs[j].patches[k].path << std::endl
@@ -654,6 +683,7 @@ int Downloader::listGames()
                         }
 
                         std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+                                    << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
                                     << "\tid: " << games[i].dlcs[j].extras[k].id << std::endl
                                     << "\tname: " << games[i].dlcs[j].extras[k].name << std::endl
                                     << "\tpath: " << games[i].dlcs[j].extras[k].path << std::endl
@@ -670,6 +700,7 @@ int Downloader::listGames()
                         }
 
                         std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+                                    << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
                                     << "\tid: " << games[i].dlcs[j].languagepacks[k].id << std::endl
                                     << "\tname: " << games[i].dlcs[j].languagepacks[k].name << std::endl
                                     << "\tpath: " << games[i].dlcs[j].languagepacks[k].path << std::endl
@@ -708,283 +739,96 @@ void Downloader::repair()
     if (this->games.empty())
         this->getGameDetails();
 
+    Json::Reader *jsonparser = new Json::Reader;
+
+    // Create a vector containing all game files
+    std::vector<gameFile> vGameFiles;
     for (unsigned int i = 0; i < games.size(); ++i)
     {
-        // Installers (use remote or local file)
-        if (Globals::globalConfig.dlConf.bInstallers)
-        {
-            for (unsigned int j = 0; j < games[i].installers.size(); ++j)
-            {
-                std::string filepath = games[i].installers[j].getFilepath();
-                if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
-                {
-                    if (Globals::globalConfig.bVerbose)
-                        std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                    continue;
-                }
+        std::vector<gameFile> vec = games[i].getGameFileVector();
+        vGameFiles.insert(std::end(vGameFiles), std::begin(vec), std::end(vec));
+    }
 
-                // Get XML data
-                std::string XML = "";
-                if (Globals::globalConfig.dlConf.bRemoteXML)
-                {
-                    XML = gogAPI->getXML(games[i].gamename, games[i].installers[j].id);
-                    if (gogAPI->getError())
-                    {
-                        std::cerr << gogAPI->getErrorMessage() << std::endl;
-                        gogAPI->clearError();
-                        continue;
-                    }
-                }
+    for (unsigned int i = 0; i < vGameFiles.size(); ++i)
+    {
+        gameSpecificConfig conf;
+        conf.dlConf = Globals::globalConfig.dlConf;
+        conf.dirConf = Globals::globalConfig.dirConf;
 
-                // Repair
-                bool bUseLocalXML = !Globals::globalConfig.dlConf.bRemoteXML;
-                if (!XML.empty() || bUseLocalXML)
-                {
-                    std::string url = gogAPI->getInstallerLink(games[i].gamename, games[i].installers[j].id);
-                    if (gogAPI->getError())
-                    {
-                        std::cerr << gogAPI->getErrorMessage() << std::endl;
-                        gogAPI->clearError();
-                        continue;
-                    }
-                    std::cout << "Repairing file " << filepath << std::endl;
-                    this->repairFile(url, filepath, XML, games[i].gamename);
-                    std::cout << std::endl;
-                }
-            }
-        }
+        unsigned int type = vGameFiles[i].type;
+        if (!conf.dlConf.bDLC && (type & GFTYPE_DLC))
+            continue;
+        if (!conf.dlConf.bInstallers && (type & GFTYPE_INSTALLER))
+            continue;
+        if (!conf.dlConf.bExtras && (type & GFTYPE_EXTRA))
+            continue;
+        if (!conf.dlConf.bPatches && (type & GFTYPE_PATCH))
+            continue;
+        if (!conf.dlConf.bLanguagePacks && (type & GFTYPE_LANGPACK))
+            continue;
 
-        // Extras (GOG doesn't provide XML data for extras, use local file)
-        if (Globals::globalConfig.dlConf.bExtras)
+        std::string filepath = vGameFiles[i].getFilepath();
+        if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
         {
-            for (unsigned int j = 0; j < games[i].extras.size(); ++j)
-            {
-                std::string filepath = games[i].extras[j].getFilepath();
-                if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
-                {
-                    if (Globals::globalConfig.bVerbose)
-                        std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                    continue;
-                }
-
-                std::string url = gogAPI->getExtraLink(games[i].gamename, games[i].extras[j].id);
-                if (gogAPI->getError())
-                {
-                    std::cerr << gogAPI->getErrorMessage() << std::endl;
-                    gogAPI->clearError();
-                    continue;
-                }
-                std::cout << "Repairing file " << filepath << std::endl;
-                this->repairFile(url, filepath, std::string(), games[i].gamename);
-                std::cout << std::endl;
-            }
+            if (Globals::globalConfig.bVerbose)
+                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+            continue;
         }
 
-        // Patches (use remote or local file)
-        if (Globals::globalConfig.dlConf.bPatches)
+        // Refresh Galaxy login if token is expired
+        if (gogGalaxy->isTokenExpired())
         {
-            for (unsigned int j = 0; j < games[i].patches.size(); ++j)
+            if (!gogGalaxy->refreshLogin())
             {
-                std::string filepath = games[i].patches[j].getFilepath();
-                if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
-                {
-                    if (Globals::globalConfig.bVerbose)
-                        std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                    continue;
-                }
-
-                // Get XML data
-                std::string XML = "";
-                if (Globals::globalConfig.dlConf.bRemoteXML)
-                {
-                    XML = gogAPI->getXML(games[i].gamename, games[i].patches[j].id);
-                    if (gogAPI->getError())
-                    {
-                        std::cerr << gogAPI->getErrorMessage() << std::endl;
-                        gogAPI->clearError();
-                    }
-                }
-
-                std::string url = gogAPI->getPatchLink(games[i].gamename, games[i].patches[j].id);
-                if (gogAPI->getError())
-                {
-                    std::cerr << gogAPI->getErrorMessage() << std::endl;
-                    gogAPI->clearError();
-                    continue;
-                }
-                std::cout << "Repairing file " << filepath << std::endl;
-                this->repairFile(url, filepath, XML, games[i].gamename);
-                std::cout << std::endl;
+                std::cerr << "Galaxy API failed to refresh login" << std::endl;
+                break;
             }
         }
 
-        // Language packs (GOG doesn't provide XML data for language packs, use local file)
-        if (Globals::globalConfig.dlConf.bLanguagePacks)
-        {
-            for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j)
-            {
-                std::string filepath = games[i].languagepacks[j].getFilepath();
-                if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
-                {
-                    if (Globals::globalConfig.bVerbose)
-                        std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                    continue;
-                }
+        Json::Value downlinkJson;
+        std::string response = gogGalaxy->getResponse(vGameFiles[i].galaxy_downlink_json_url);
 
-                std::string url = gogAPI->getLanguagePackLink(games[i].gamename, games[i].languagepacks[j].id);
-                if (gogAPI->getError())
-                {
-                    std::cerr << gogAPI->getErrorMessage() << std::endl;
-                    gogAPI->clearError();
-                    continue;
-                }
-                std::cout << "Repairing file " << filepath << std::endl;
-                this->repairFile(url, filepath, std::string(), games[i].gamename);
-                std::cout << std::endl;
-            }
-        }
-        if (Globals::globalConfig.dlConf.bDLC && !games[i].dlcs.empty())
+        if (response.empty())
         {
-            for (unsigned int j = 0; j < games[i].dlcs.size(); ++j)
-            {
-                if (Globals::globalConfig.dlConf.bInstallers)
-                {
-                    for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k)
-                    {
-                        std::string filepath = games[i].dlcs[j].installers[k].getFilepath();
-                        if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
-                        {
-                            if (Globals::globalConfig.bVerbose)
-                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                            continue;
-                        }
+            std::cerr << "Found nothing in " << vGameFiles[i].galaxy_downlink_json_url << ", skipping file" << std::endl;
+            continue;
+        }
+        jsonparser->parse(response, downlinkJson);
 
-                        // Get XML data
-                        std::string XML = "";
-                        if (Globals::globalConfig.dlConf.bRemoteXML)
-                        {
-                            XML = gogAPI->getXML(games[i].dlcs[j].gamename, games[i].dlcs[j].installers[k].id);
-                            if (gogAPI->getError())
-                            {
-                                std::cerr << gogAPI->getErrorMessage() << std::endl;
-                                gogAPI->clearError();
-                                continue;
-                            }
-                        }
+        if (!downlinkJson.isMember("downlink"))
+        {
+            std::cerr << "Invalid JSON response, skipping file" << std::endl;
+            continue;
+        }
 
-                        // Repair
-                        bool bUseLocalXML = !Globals::globalConfig.dlConf.bRemoteXML;
-                        if (!XML.empty() || bUseLocalXML)
-                        {
-                            std::string url = gogAPI->getInstallerLink(games[i].dlcs[j].gamename, games[i].dlcs[j].installers[k].id);
-                            if (gogAPI->getError())
-                            {
-                                std::cerr << gogAPI->getErrorMessage() << std::endl;
-                                gogAPI->clearError();
-                                continue;
-                            }
-                            std::cout << "Repairing file " << filepath << std::endl;
-                            this->repairFile(url, filepath, XML, games[i].dlcs[j].gamename);
-                            std::cout << std::endl;
-                        }
-                    }
-                }
-                if (Globals::globalConfig.dlConf.bPatches)
-                {
-                    for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k)
-                    {
-                        std::string filepath = games[i].dlcs[j].patches[k].getFilepath();
-                        if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) {
-                            if (Globals::globalConfig.bVerbose)
-                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                            continue;
-                        }
+        std::string xml_url;
+        if (downlinkJson.isMember("checksum"))
+            if (!downlinkJson["checksum"].empty())
+                xml_url = downlinkJson["checksum"].asString();
 
-                        // Get XML data
-                        std::string XML = "";
-                        if (Globals::globalConfig.dlConf.bRemoteXML)
-                        {
-                            XML = gogAPI->getXML(games[i].dlcs[j].gamename, games[i].dlcs[j].patches[k].id);
-                            if (gogAPI->getError())
-                            {
-                                std::cerr << gogAPI->getErrorMessage() << std::endl;
-                                gogAPI->clearError();
-                            }
-                        }
+        // Get XML data
+        std::string XML = "";
+        if (conf.dlConf.bRemoteXML && !xml_url.empty())
+            XML = gogGalaxy->getResponse(xml_url);
 
-                        std::string url = gogAPI->getPatchLink(games[i].dlcs[j].gamename, games[i].dlcs[j].patches[k].id);
-                        if (gogAPI->getError())
-                        {
-                            std::cerr << gogAPI->getErrorMessage() << std::endl;
-                            gogAPI->clearError();
-                            continue;
-                        }
-                        std::cout << "Repairing file " << filepath << std::endl;
-                        this->repairFile(url, filepath, XML, games[i].dlcs[j].gamename);
-                        std::cout << std::endl;
-                    }
-                }
-                if (Globals::globalConfig.dlConf.bExtras)
-                {
-                    for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k)
-                    {
-                        std::string filepath = games[i].dlcs[j].extras[k].getFilepath();
-                        if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) {
-                            if (Globals::globalConfig.bVerbose)
-                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                            continue;
-                        }
+        // Repair
+        bool bUseLocalXML = !conf.dlConf.bRemoteXML;
 
-                        std::string url = gogAPI->getExtraLink(games[i].dlcs[j].gamename, games[i].dlcs[j].extras[k].id);
-                        if (gogAPI->getError())
-                        {
-                            std::cerr << gogAPI->getErrorMessage() << std::endl;
-                            gogAPI->clearError();
-                            continue;
-                        }
-                        std::cout << "Repairing file " << filepath << std::endl;
-                        this->repairFile(url, filepath, std::string(), games[i].dlcs[j].gamename);
-                        std::cout << std::endl;
-                    }
-                }
-                if (Globals::globalConfig.dlConf.bLanguagePacks)
-                {
-                    for (unsigned int k = 0; k < games[i].dlcs[j].languagepacks.size(); ++k)
-                    {
-                        std::string filepath = games[i].dlcs[j].languagepacks[k].getFilepath();
-                        if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) {
-                            if (Globals::globalConfig.bVerbose)
-                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
-                            continue;
-                        }
+        // Use local XML data for extras
+        if (XML.empty() && (type & GFTYPE_EXTRA))
+            bUseLocalXML = true;
 
-                        // Get XML data
-                        std::string XML = "";
-                        if (Globals::globalConfig.dlConf.bRemoteXML)
-                        {
-                            XML = gogAPI->getXML(games[i].dlcs[j].gamename, games[i].dlcs[j].languagepacks[k].id);
-                            if (gogAPI->getError())
-                            {
-                                std::cerr << gogAPI->getErrorMessage() << std::endl;
-                                gogAPI->clearError();
-                            }
-                        }
+        if (!XML.empty() || bUseLocalXML)
+        {
+            std::string url = downlinkJson["downlink"].asString();
 
-                        std::string url = gogAPI->getLanguagePackLink(games[i].dlcs[j].gamename, games[i].dlcs[j].languagepacks[k].id);
-                        if (gogAPI->getError())
-                        {
-                            std::cerr << gogAPI->getErrorMessage() << std::endl;
-                            gogAPI->clearError();
-                            continue;
-                        }
-                        std::cout << "Repairing file " << filepath << std::endl;
-                        this->repairFile(url, filepath, XML, games[i].dlcs[j].gamename);
-                        std::cout << std::endl;
-                    }
-                }
-            }
+            std::cout << "Repairing file " << filepath << std::endl;
+            this->repairFile(url, filepath, XML, vGameFiles[i].gamename);
+            std::cout << std::endl;
         }
     }
+
+    delete jsonparser;
 }
 
 void Downloader::download()
@@ -997,20 +841,24 @@ void Downloader::download()
 
     for (unsigned int i = 0; i < games.size(); ++i)
     {
-        if (Globals::globalConfig.dlConf.bSaveSerials && !games[i].serials.empty())
+        gameSpecificConfig conf;
+        conf.dlConf = Globals::globalConfig.dlConf;
+        conf.dirConf = Globals::globalConfig.dirConf;
+
+        if (conf.dlConf.bSaveSerials && !games[i].serials.empty())
         {
             std::string filepath = games[i].getSerialsFilepath();
             this->saveSerials(games[i].serials, filepath);
         }
 
-        if (Globals::globalConfig.dlConf.bSaveChangelogs && !games[i].changelog.empty())
+        if (conf.dlConf.bSaveChangelogs && !games[i].changelog.empty())
         {
             std::string filepath = games[i].getChangelogFilepath();
             this->saveChangelog(games[i].changelog, filepath);
         }
 
         // Download covers
-        if (Globals::globalConfig.dlConf.bCover && !Globals::globalConfig.bUpdateCheck)
+        if (conf.dlConf.bCover && !Globals::globalConfig.bUpdateCheck)
         {
             if (!games[i].installers.empty())
             {
@@ -1024,71 +872,71 @@ void Downloader::download()
             }
         }
 
-        if (Globals::globalConfig.dlConf.bInstallers)
+        if (conf.dlConf.bInstallers)
         {
             for (unsigned int j = 0; j < games[i].installers.size(); ++j)
             {
                 dlQueue.push(games[i].installers[j]);
             }
         }
-        if (Globals::globalConfig.dlConf.bPatches)
+        if (conf.dlConf.bPatches)
         {
             for (unsigned int j = 0; j < games[i].patches.size(); ++j)
             {
                 dlQueue.push(games[i].patches[j]);
             }
         }
-        if (Globals::globalConfig.dlConf.bExtras)
+        if (conf.dlConf.bExtras)
         {
             for (unsigned int j = 0; j < games[i].extras.size(); ++j)
             {
                 dlQueue.push(games[i].extras[j]);
             }
         }
-        if (Globals::globalConfig.dlConf.bLanguagePacks)
+        if (conf.dlConf.bLanguagePacks)
         {
             for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j)
             {
                 dlQueue.push(games[i].languagepacks[j]);
             }
         }
-        if (Globals::globalConfig.dlConf.bDLC && !games[i].dlcs.empty())
+        if (conf.dlConf.bDLC && !games[i].dlcs.empty())
         {
             for (unsigned int j = 0; j < games[i].dlcs.size(); ++j)
             {
-                if (Globals::globalConfig.dlConf.bSaveSerials && !games[i].dlcs[j].serials.empty())
+                if (conf.dlConf.bSaveSerials && !games[i].dlcs[j].serials.empty())
                 {
                     std::string filepath = games[i].dlcs[j].getSerialsFilepath();
                     this->saveSerials(games[i].dlcs[j].serials, filepath);
                 }
-                if (Globals::globalConfig.dlConf.bSaveChangelogs && !games[i].dlcs[j].changelog.empty())
+                if (conf.dlConf.bSaveChangelogs && !games[i].dlcs[j].changelog.empty())
                 {
                     std::string filepath = games[i].dlcs[j].getChangelogFilepath();
                     this->saveChangelog(games[i].dlcs[j].changelog, filepath);
                 }
 
-                if (Globals::globalConfig.dlConf.bInstallers)
+                if (conf.dlConf.bInstallers)
                 {
                     for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k)
                     {
                         dlQueue.push(games[i].dlcs[j].installers[k]);
                     }
                 }
-                if (Globals::globalConfig.dlConf.bPatches)
+                if (conf.dlConf.bPatches)
                 {
                     for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k)
                     {
                         dlQueue.push(games[i].dlcs[j].patches[k]);
                     }
                 }
-                if (Globals::globalConfig.dlConf.bExtras)
+                if (conf.dlConf.bExtras)
                 {
                     for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k)
                     {
                         dlQueue.push(games[i].dlcs[j].extras[k]);
                     }
                 }
-                if (Globals::globalConfig.dlConf.bLanguagePacks)
+                if (conf.dlConf.bLanguagePacks)
                 {
                     for (unsigned int k = 0; k < games[i].dlcs[j].languagepacks.size(); ++k)
                     {
@@ -1302,6 +1150,7 @@ CURLcode Downloader::downloadFile(const std::string& url, const std::string& fil
 
     curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
     curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, outfile);
+    curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 1L);
     res = this->beginDownload();
 
     fclose(outfile);
@@ -1342,7 +1191,16 @@ CURLcode Downloader::downloadFile(const std::string& url, const std::string& fil
     else
     {
         this->retries = 0; // Reset retries counter
+        // Set timestamp for downloaded file to same value as file on server
+        long filetime = -1;
+        CURLcode result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime);
+        if (result == CURLE_OK && filetime >= 0)
+        {
+            std::time_t timestamp = (std::time_t)filetime;
+            boost::filesystem::last_write_time(filepath, timestamp);
+        }
     }
+    curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L);
 
     return res;
 }
@@ -1545,6 +1403,9 @@ int Downloader::repairFile(const std::string& url, const std::string& filepath,
 
     // Check all chunks
     int iChunksRepaired = 0;
+    int iChunkRetryCount = 0;
+    int iChunkRetryLimit = 3;
+    bool bChunkRetryLimitReached = false;
     for (int i=0; i<chunks; i++)
     {
         off_t chunk_begin = chunk_from.at(i);
@@ -1573,21 +1434,41 @@ int Downloader::repairFile(const std::string& url, const std::string& filepath,
         std::string hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5);
         if (hash != chunk_hash.at(i))
         {
-            std::cout << "Failed - downloading chunk" << std::endl;
+            if (bChunkRetryLimitReached)
+            {
+                std::cout << "Failed - chunk retry limit reached\r" << std::flush;
+                free(chunk);
+                res = 0;
+                break;
+            }
+
+            if (iChunkRetryCount < 1)
+                std::cout << "Failed - downloading chunk" << std::endl;
+            else
+                std::cout << "Failed - retrying chunk download" << std::endl;
             // use fseeko to support large files on 32 bit platforms
             fseeko(outfile, chunk_begin, SEEK_SET);
             curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
             curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, outfile);
             curl_easy_setopt(curlhandle, CURLOPT_RANGE, range.c_str()); //download range
+            curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 1L);
             this->beginDownload(); //begin chunk download
             std::cout << std::endl;
             if (Globals::globalConfig.bReport)
                 iChunksRepaired++;
             i--; //verify downloaded chunk
+
+            iChunkRetryCount++;
+            if (iChunkRetryCount >= iChunkRetryLimit)
+            {
+                bChunkRetryLimitReached = true;
+            }
         }
         else
         {
             std::cout << "OK\r" << std::flush;
+            iChunkRetryCount = 0; // reset retry count
+            bChunkRetryLimitReached = false;
         }
         free(chunk);
         res = 1;
@@ -1597,10 +1478,27 @@ int Downloader::repairFile(const std::string& url, const std::string& filepath,
 
     if (Globals::globalConfig.bReport)
     {
-        std::string report_line = "Repaired [" + std::to_string(iChunksRepaired) + "/" + std::to_string(chunks) + "] " + filepath;
+        std::string report_line;
+        if (bChunkRetryLimitReached)
+            report_line = "Repair failed: " + filepath;
+        else
+            report_line = "Repaired [" + std::to_string(iChunksRepaired) + "/" + std::to_string(chunks) + "] " + filepath;
         this->report_ofs << report_line << std::endl;
     }
 
+    if (bChunkRetryLimitReached)
+        return res;
+
+    // Set timestamp for downloaded file to same value as file on server
+    long filetime = -1;
+    CURLcode result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime);
+    if (result == CURLE_OK && filetime >= 0)
+    {
+        std::time_t timestamp = (std::time_t)filetime;
+        boost::filesystem::last_write_time(filepath, timestamp);
+    }
+    curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L);
+
     return res;
 }
 
@@ -1981,7 +1879,9 @@ std::string Downloader::getSerialsFromJSON(const Json::Value& json)
 
     if (cdkey.find("<span>") == std::string::npos)
     {
-        serials << cdkey << std::endl;
+        boost::regex expression("<br\\h*/?>");
+        std::string text = boost::regex_replace(cdkey, expression, "\n");
+        serials << text << std::endl;
     }
     else
     {
@@ -2486,6 +2386,7 @@ std::vector<gameDetails> Downloader::getGameDetailsFromJsonNode(Json::Value root
                         fileDetails.silent = fileDetailsNode["silent"].asInt();
                         fileDetails.gamename = fileDetailsNode["gamename"].asString();
                         fileDetails.type = fileDetailsNode["type"].asUInt();
+                        fileDetails.galaxy_downlink_json_url = fileDetailsNode["galaxy_downlink_json_url"].asString();
 
                         if (nodeName != "extras" && !(fileDetails.platform & conf.dlConf.iInstallerPlatform))
                             continue;
@@ -2630,6 +2531,13 @@ void Downloader::saveChangelog(const std::string& changelog, const std::string&
 
 int Downloader::downloadFileWithId(const std::string& fileid_string, const std::string& output_filepath)
 {
+    if (!gogAPI->isLoggedIn())
+    {
+        std::cout << "API not logged in. This feature doesn't work without valid API login." << std::endl;
+        std::cout << "Try to login with --login-api" << std::endl;
+        exit(1);
+    }
+
     int res = 1;
     size_t pos = fileid_string.find("/");
     if (pos == std::string::npos)
@@ -2718,23 +2626,23 @@ void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
 {
     std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
 
-    API* api = new API(conf.apiConf.sToken, conf.apiConf.sSecret);
-    api->curlSetOpt(CURLOPT_SSL_VERIFYPEER, conf.curlConf.bVerifyPeer);
-    api->curlSetOpt(CURLOPT_CONNECTTIMEOUT, conf.curlConf.iTimeout);
-    if (!conf.curlConf.sCACertPath.empty())
-        api->curlSetOpt(CURLOPT_CAINFO, conf.curlConf.sCACertPath.c_str());
-
-    if (!api->init())
+    galaxyAPI* galaxy = new galaxyAPI(Globals::globalConfig.curlConf);
+    if (!galaxy->init())
     {
-        delete api;
-        msgQueue.push(Message("API init failed", MSGTYPE_ERROR, msg_prefix));
-        vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
-        return;
+        if (!galaxy->refreshLogin())
+        {
+            delete galaxy;
+            msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+            vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+            return;
+        }
     }
 
+    Json::Reader *jsonparser = new Json::Reader;
+
     CURL* dlhandle = curl_easy_init();
     curl_easy_setopt(dlhandle, CURLOPT_FOLLOWLOCATION, 1);
-    curl_easy_setopt(dlhandle, CURLOPT_USERAGENT, conf.sVersionString.c_str());
+    curl_easy_setopt(dlhandle, CURLOPT_USERAGENT, conf.curlConf.sUserAgent.c_str());
     curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0);
     curl_easy_setopt(dlhandle, CURLOPT_NOSIGNAL, 1);
 
@@ -2745,6 +2653,7 @@ void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
     curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
     curl_easy_setopt(dlhandle, CURLOPT_READFUNCTION, Downloader::readData);
     curl_easy_setopt(dlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, conf.curlConf.iDownloadRate);
+    curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 1L);
 
     // 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(dlhandle, CURLOPT_LOW_SPEED_TIME, 30);
@@ -2820,32 +2729,62 @@ void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
         bool bSameVersion = true; // assume same version
         bool bLocalXMLExists = boost::filesystem::exists(local_xml_file); // This is additional check to see if remote xml should be saved to speed up future version checks
 
-        std::string xml;
-        if (gf.type & (GFTYPE_INSTALLER | GFTYPE_PATCH) && conf.dlConf.bRemoteXML)
+        // Refresh Galaxy login if token is expired
+        if (galaxy->isTokenExpired())
         {
-            xml = api->getXML(gf.gamename, gf.id);
-            if (api->getError())
+            if (!galaxy->refreshLogin())
             {
-                msgQueue.push(Message(api->getErrorMessage(), MSGTYPE_ERROR, msg_prefix));
-                api->clearError();
+                msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+                vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+                delete jsonparser;
+                delete galaxy;
+                return;
             }
-            else
+        }
+
+        // Get downlink JSON from Galaxy API
+        Json::Value downlinkJson;
+        std::string response = galaxy->getResponse(gf.galaxy_downlink_json_url);
+
+        if (response.empty())
+        {
+            msgQueue.push(Message("Found nothing in " + gf.galaxy_downlink_json_url + ", skipping file", MSGTYPE_WARNING, msg_prefix));
+            continue;
+        }
+        jsonparser->parse(response, downlinkJson);
+
+        if (!downlinkJson.isMember("downlink"))
+        {
+            msgQueue.push(Message("Invalid JSON response, skipping file", MSGTYPE_WARNING, msg_prefix));
+            continue;
+        }
+
+        std::string xml;
+        if (gf.type & (GFTYPE_INSTALLER | GFTYPE_PATCH) && conf.dlConf.bRemoteXML)
+        {
+            std::string xml_url;
+            if (downlinkJson.isMember("checksum"))
+                if (!downlinkJson["checksum"].empty())
+                    xml_url = downlinkJson["checksum"].asString();
+
+            // Get XML data
+            if (conf.dlConf.bRemoteXML && !xml_url.empty())
+                xml = galaxy->getResponse(xml_url);
+
+            if (!xml.empty())
             {
-                if (!xml.empty())
+                std::string localHash = Util::getLocalFileHash(conf.sXMLDirectory, filepath.string(), gf.gamename);
+                // Do version check if local hash exists
+                if (!localHash.empty())
                 {
-                    std::string localHash = Util::getLocalFileHash(conf.sXMLDirectory, filepath.string(), gf.gamename);
-                    // Do version check if local hash exists
-                    if (!localHash.empty())
+                    tinyxml2::XMLDocument remote_xml;
+                    remote_xml.Parse(xml.c_str());
+                    tinyxml2::XMLElement *fileElem = remote_xml.FirstChildElement("file");
+                    if (fileElem)
                     {
-                        tinyxml2::XMLDocument remote_xml;
-                        remote_xml.Parse(xml.c_str());
-                        tinyxml2::XMLElement *fileElem = remote_xml.FirstChildElement("file");
-                        if (fileElem)
-                        {
-                            std::string remoteHash = fileElem->Attribute("md5");
-                            if (remoteHash != localHash)
-                                bSameVersion = false;
-                        }
+                        std::string remoteHash = fileElem->Attribute("md5");
+                        if (remoteHash != localHash)
+                            bSameVersion = false;
                     }
                 }
             }
@@ -2909,26 +2848,7 @@ void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
             }
         }
 
-        // Get download url
-        std::string url;
-        if (gf.type & GFTYPE_INSTALLER)
-            url = api->getInstallerLink(gf.gamename, gf.id);
-        else if (gf.type & GFTYPE_PATCH)
-            url = api->getPatchLink(gf.gamename, gf.id);
-        else if (gf.type & GFTYPE_LANGPACK)
-            url = api->getLanguagePackLink(gf.gamename, gf.id);
-        else if (gf.type & GFTYPE_EXTRA)
-            url = api->getExtraLink(gf.gamename, gf.id);
-        else
-            url = api->getExtraLink(gf.gamename, gf.id); // assume extra if type didn't match any of the others
-
-        if (api->getError())
-        {
-            msgQueue.push(Message(api->getErrorMessage(), MSGTYPE_ERROR, msg_prefix));
-            api->clearError();
-            continue;
-        }
-
+        std::string url = downlinkJson["downlink"].asString();
         curl_easy_setopt(dlhandle, CURLOPT_URL, url.c_str());
         do
         {
@@ -2988,6 +2908,15 @@ void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
         }
         if (result == CURLE_OK || result == CURLE_RANGE_ERROR || (result == CURLE_HTTP_RETURNED_ERROR && response_code == 416))
         {
+            // Set timestamp for downloaded file to same value as file on server
+            long filetime = -1;
+            CURLcode res = curl_easy_getinfo(dlhandle, CURLINFO_FILETIME, &filetime);
+            if (res == CURLE_OK && filetime >= 0)
+            {
+                std::time_t timestamp = (std::time_t)filetime;
+                boost::filesystem::last_write_time(filepath, timestamp);
+            }
+
             // Average download speed
             std::ostringstream dlrate_avg;
             std::string rate_unit;
@@ -3033,7 +2962,8 @@ void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
     }
 
     curl_easy_cleanup(dlhandle);
-    delete api;
+    delete jsonparser;
+    delete galaxy;
 
     vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
     msgQueue.push(Message("Finished all tasks", MSGTYPE_INFO, msg_prefix));
@@ -3102,7 +3032,7 @@ void Downloader::printProgress()
         dl_status = DLSTATUS_NOTSTARTED;
 
         // Print progress information once per 100ms
-        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+        std::this_thread::sleep_for(std::chrono::milliseconds(Globals::globalConfig.iProgressInterval));
         std::cout << "\033[J\r" << std::flush; // Clear screen from the current line down to the bottom of the screen
 
         // Print messages from message queue first
@@ -3243,25 +3173,23 @@ void Downloader::getGameDetailsThread(Config config, const unsigned int& tid)
 {
     std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
 
-    API* api = new API(config.apiConf.sToken, config.apiConf.sSecret);
-    api->curlSetOpt(CURLOPT_SSL_VERIFYPEER, config.curlConf.bVerifyPeer);
-    api->curlSetOpt(CURLOPT_CONNECTTIMEOUT, config.curlConf.iTimeout);
-    if (!config.curlConf.sCACertPath.empty())
-        api->curlSetOpt(CURLOPT_CAINFO, config.curlConf.sCACertPath.c_str());
-
-    if (!api->init())
+    galaxyAPI* galaxy = new galaxyAPI(Globals::globalConfig.curlConf);
+    if (!galaxy->init())
     {
-        delete api;
-        msgQueue.push(Message("API init failed", MSGTYPE_ERROR, msg_prefix));
-        vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
-        return;
+        if (!galaxy->refreshLogin())
+        {
+            delete galaxy;
+            msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+            vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+            return;
+        }
     }
 
     // Create new GOG website handle
     Website* website = new Website();
     if (!website->IsLoggedIn())
     {
-        delete api;
+        delete galaxy;
         delete website;
         msgQueue.push(Message("Website not logged in", MSGTYPE_ERROR, msg_prefix));
         vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
@@ -3276,7 +3204,6 @@ void Downloader::getGameDetailsThread(Config config, const unsigned int& tid)
     while (gameItemQueue.try_pop(game_item))
     {
         gameDetails game;
-        bool bHasDLC = !game_item.dlcnames.empty();
 
         gameSpecificConfig conf;
         conf.dlConf = config.dlConf;
@@ -3321,164 +3248,60 @@ void Downloader::getGameDetailsThread(Config config, const unsigned int& tid)
             }
         }
 
-        game = api->getGameDetails(game_item.name, conf.dlConf.iInstallerPlatform, conf.dlConf.iInstallerLanguage, conf.dlConf.bDuplicateHandler);
-        game.product_id = game_item.id;
-        if (!api->getError())
+        // Refresh Galaxy login if token is expired
+        if (galaxy->isTokenExpired())
         {
-            game.filterWithPriorities(conf);
-            Json::Value gameDetailsJSON;
-
-            if (!game_item.gamedetailsjson.empty())
-                gameDetailsJSON = game_item.gamedetailsjson;
-
-            if (game.extras.empty() && conf.dlConf.bExtras) // Try to get extras from account page if API didn't return any extras
-            {
-                if (gameDetailsJSON.empty())
-                    gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
-                game.extras = Downloader::getExtrasFromJSON(gameDetailsJSON, game_item.name, config);
-            }
-            if (conf.dlConf.bSaveSerials)
+            if (!galaxy->refreshLogin())
             {
-                if (gameDetailsJSON.empty())
-                    gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
-                game.serials = Downloader::getSerialsFromJSON(gameDetailsJSON);
-            }
-            if (conf.dlConf.bSaveChangelogs)
-            {
-                if (gameDetailsJSON.empty())
-                    gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
-                game.changelog = Downloader::getChangelogFromJSON(gameDetailsJSON);
-            }
-
-            // Ignore DLC count and try to get DLCs from JSON
-            if (game.dlcs.empty() && !bHasDLC && conf.dlConf.bDLC && conf.dlConf.bIgnoreDLCCount)
-            {
-                if (gameDetailsJSON.empty())
-                    gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
-
-                game_item.dlcnames = Util::getDLCNamesFromJSON(gameDetailsJSON["dlcs"]);
-                bHasDLC = !game_item.dlcnames.empty();
+                msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+                break;
             }
+        }
 
-            if (game.dlcs.empty() && bHasDLC && conf.dlConf.bDLC)
-            {
-                for (unsigned int j = 0; j < game_item.dlcnames.size(); ++j)
-                {
-                    gameDetails dlc;
-                    dlc = api->getGameDetails(game_item.dlcnames[j], conf.dlConf.iInstallerPlatform, conf.dlConf.iInstallerLanguage, conf.dlConf.bDuplicateHandler);
-                    dlc.filterWithPriorities(conf);
-                    if (dlc.extras.empty() && conf.dlConf.bExtras) // Try to get extras from account page if API didn't return any extras
-                    {
-                        if (gameDetailsJSON.empty())
-                            gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
-
-                        // Make sure we get extras for the right DLC
-                        for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k)
-                        {
-                            std::vector<std::string> urls;
-                            if (gameDetailsJSON["dlcs"][k].isMember("extras"))
-                                Util::getDownloaderUrlsFromJSON(gameDetailsJSON["dlcs"][k]["extras"], urls);
-
-                            if (!urls.empty())
-                            {
-                                if (urls[0].find("/" + game_item.dlcnames[j] + "/") != std::string::npos)
-                                {
-                                    dlc.extras = Downloader::getExtrasFromJSON(gameDetailsJSON["dlcs"][k], game_item.dlcnames[j], config);
-                                }
-                            }
-                        }
-                    }
-
-                    if (conf.dlConf.bSaveSerials)
-                    {
-                        if (gameDetailsJSON.empty())
-                            gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
-
-                        // Make sure we save serial for the right DLC
-                        for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k)
-                        {
-                            std::vector<std::string> urls;
-                            if (gameDetailsJSON["dlcs"][k].isMember("cdKey") && gameDetailsJSON["dlcs"][k].isMember("downloads"))
-                            {
-                                // Assuming that only DLC with installers can have serial
-                                Util::getDownloaderUrlsFromJSON(gameDetailsJSON["dlcs"][k]["downloads"], urls);
-                            }
+        Json::Value product_info = galaxy->getProductInfo(game_item.id);
+        game = galaxy->productInfoJsonToGameDetails(product_info, conf.dlConf);
+        game.filterWithPriorities(conf);
 
-                            if (!urls.empty())
-                            {
-                                if (urls[0].find("/" + game_item.dlcnames[j] + "/") != std::string::npos)
-                                {
-                                    dlc.serials = Downloader::getSerialsFromJSON(gameDetailsJSON["dlcs"][k]);
-                                }
-                            }
-                        }
-                    }
+        Json::Value gameDetailsJSON;
 
-                    if (conf.dlConf.bSaveChangelogs)
-                    {
-                        if (gameDetailsJSON.empty())
-                            gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+        if (!game_item.gamedetailsjson.empty())
+            gameDetailsJSON = game_item.gamedetailsjson;
 
-                        // Make sure we save changelog for the right DLC
-                        for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k)
-                        {
-                            std::vector<std::string> urls;
-                            if (gameDetailsJSON["dlcs"][k].isMember("changelog") && gameDetailsJSON["dlcs"][k].isMember("downloads"))
-                            {
-                                // Assuming that only DLC with installers can have changelog
-                                Util::getDownloaderUrlsFromJSON(gameDetailsJSON["dlcs"][k]["downloads"], urls);
-                            }
-
-                            if (!urls.empty())
-                            {
-                                if (urls[0].find("/" + game_item.dlcnames[j] + "/") != std::string::npos)
-                                {
-                                    dlc.changelog = Downloader::getChangelogFromJSON(gameDetailsJSON["dlcs"][k]);
-                                }
-                            }
-                        }
-                    }
+        if (conf.dlConf.bSaveSerials && game.serials.empty())
+        {
+            if (gameDetailsJSON.empty())
+                gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+            game.serials = Downloader::getSerialsFromJSON(gameDetailsJSON);
+        }
 
-                    // Add DLC type to all DLC files
-                    for (unsigned int a = 0; a < dlc.installers.size(); ++a)
-                        dlc.installers[a].type |= GFTYPE_DLC;
-                    for (unsigned int a = 0; a < dlc.extras.size(); ++a)
-                        dlc.extras[a].type |= GFTYPE_DLC;
-                    for (unsigned int a = 0; a < dlc.patches.size(); ++a)
-                        dlc.patches[a].type |= GFTYPE_DLC;
-                    for (unsigned int a = 0; a < dlc.languagepacks.size(); ++a)
-                        dlc.languagepacks[a].type |= GFTYPE_DLC;
-
-                    game.dlcs.push_back(dlc);
-                }
-            }
+        if (conf.dlConf.bSaveChangelogs && game.changelog.empty())
+        {
+            if (gameDetailsJSON.empty())
+                gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+            game.changelog = Downloader::getChangelogFromJSON(gameDetailsJSON);
+        }
 
-            game.makeFilepaths(conf.dirConf);
+        game.makeFilepaths(conf.dirConf);
 
-            if (!config.bUpdateCheck)
-                gameDetailsQueue.push(game);
-            else
-            { // Update check, only add games that have updated files
-                for (unsigned int j = 0; j < game.installers.size(); ++j)
+        if (!config.bUpdateCheck)
+            gameDetailsQueue.push(game);
+        else
+        { // Update check, only add games that have updated files
+            for (unsigned int j = 0; j < game.installers.size(); ++j)
+            {
+                if (game.installers[j].updated)
                 {
-                    if (game.installers[j].updated)
-                    {
-                        gameDetailsQueue.push(game);
-                        break; // add the game only once
-                    }
+                    gameDetailsQueue.push(game);
+                    break; // add the game only once
                 }
             }
         }
-        else
-        {
-            msgQueue.push(Message(api->getErrorMessage(), MSGTYPE_ERROR, msg_prefix));
-            api->clearError();
-            continue;
-        }
     }
+
     vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
-    delete api;
+    delete galaxy;
     delete website;
+
     return;
 }
 
@@ -3502,7 +3325,7 @@ void Downloader::saveGalaxyJSON()
     }
 }
 
-void Downloader::galaxyInstallGame(const std::string& product_id, int build_index)
+void Downloader::galaxyInstallGame(const std::string& product_id, int build_index, const unsigned int& iGalaxyArch)
 {
     if (build_index < 0)
         build_index = 0;
@@ -3527,6 +3350,16 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
         }
     }
 
+    std::string sGalaxyArch = "64";
+    for (unsigned int i = 0; i < GlobalConstants::GALAXY_ARCHS.size(); ++i)
+    {
+        if (GlobalConstants::GALAXY_ARCHS[i].id == iGalaxyArch)
+        {
+            sGalaxyArch = GlobalConstants::GALAXY_ARCHS[i].code;
+            break;
+        }
+    }
+
     Json::Value json = gogGalaxy->getProductBuilds(product_id, sPlatform);
 
     // JSON is empty and platform is Linux. Most likely cause is that Galaxy API doesn't have Linux support
@@ -3552,10 +3385,13 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
     if (install_directory.empty())
         install_directory = product_id;
 
+    std::string install_path = Globals::globalConfig.dirConf.sDirectory + install_directory;
+
     std::vector<galaxyDepotItem> items;
     for (unsigned int i = 0; i < json["depots"].size(); ++i)
     {
         bool bSelectedLanguage = false;
+        bool bSelectedArch = false;
         for (unsigned int j = 0; j < json["depots"][i]["languages"].size(); ++j)
         {
             std::string language = json["depots"][i]["languages"][j].asString();
@@ -3563,7 +3399,22 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
                 bSelectedLanguage = true;
         }
 
-        if (!bSelectedLanguage)
+        if (json["depots"][i].isMember("osBitness"))
+        {
+            for (unsigned int j = 0; j < json["depots"][i]["osBitness"].size(); ++j)
+            {
+                std::string osBitness = json["depots"][i]["osBitness"][j].asString();
+                if (osBitness == "*" || osBitness == sGalaxyArch)
+                    bSelectedArch = true;
+            }
+        }
+        else
+        {
+            // No osBitness found, assume that we want to download this depot
+            bSelectedArch = true;
+        }
+
+        if (!bSelectedLanguage || !bSelectedArch)
             continue;
 
         std::string depotHash = json["depots"][i]["manifest"].asString();
@@ -3595,12 +3446,12 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
 
     double totalSizeMB = static_cast<double>(totalSize)/1024/1024;
     std::cout << game_title << std::endl;
-    std::cout << "Files: " << items.size() - 1  << std::endl;
+    std::cout << "Files: " << items.size() << std::endl;
     std::cout << "Total size installed: " << totalSizeMB << " MB" << std::endl;
 
     for (unsigned int i = 0; i < items.size(); ++i)
     {
-        boost::filesystem::path path = Globals::globalConfig.dirConf.sDirectory + install_directory + "/" + items[i].path;
+        boost::filesystem::path path = install_path + "/" + items[i].path;
 
         // Check that directory exists and create it
         boost::filesystem::path directory = path.parent_path();
@@ -3731,12 +3582,24 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
             }
         }
 
+        std::time_t timestamp = -1;
         for (unsigned int j = start_chunk; j < items[i].chunks.size(); ++j)
         {
             ChunkMemoryStruct chunk;
             chunk.memory = (char *) malloc(1);
             chunk.size = 0;
 
+            // Refresh Galaxy login if token is expired
+            if (gogGalaxy->isTokenExpired())
+            {
+                if (!gogGalaxy->refreshLogin())
+                {
+                    std::cerr << "Galaxy API failed to refresh login" << std::endl;
+                    free(chunk.memory);
+                    return;
+                }
+            }
+
             json = gogGalaxy->getSecureLink(items[i].product_id, gogGalaxy->hashToGalaxyPath(items[i].chunks[j].md5_compressed));
 
             // Prefer edgecast urls
@@ -3771,6 +3634,7 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
             curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &chunk);
             curl_easy_setopt(curlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallback);
             curl_easy_setopt(curlhandle, CURLOPT_XFERINFODATA, this);
+            curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 1L);
 
             std::cout << path.string() << " (chunk " << (j + 1) << "/" << items[i].chunks.size() << ")" << std::endl;
 
@@ -3784,6 +3648,7 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
 
             curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
             curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0);
+            curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L);
 
             if (result != CURLE_OK)
             {
@@ -3799,11 +3664,16 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
                         std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl;
                 }
             }
+            else
+            {
+                // Get timestamp for downloaded file
+                long filetime = -1;
+                result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime);
+                if (result == CURLE_OK && filetime >= 0)
+                    timestamp = (std::time_t)filetime;
+            }
             std::cout << std::endl;
 
-            curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
-            curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0);
-
             std::ofstream ofs(path.string(), std::ofstream::out | std::ofstream::binary | std::ofstream::app);
             if (ofs)
             {
@@ -3817,7 +3687,17 @@ void Downloader::galaxyInstallGame(const std::string& product_id, int build_inde
 
             free(chunk.memory);
         }
+
+        // Set timestamp for downloaded file to same value as file on server
+        if (boost::filesystem::exists(path) && timestamp >= 0)
+            boost::filesystem::last_write_time(path, timestamp);
     }
+
+    std::cout << "Checking for orphaned files" << std::endl;
+    std::vector<std::string> orphans = this->galaxyGetOrphanedFiles(items, install_path);
+    std::cout << "\t" << orphans.size() << " orphaned files" << std::endl;
+    for (unsigned int i = 0; i < orphans.size(); ++i)
+        std::cout << "\t" << orphans[i] << std::endl;
 }
 
 void Downloader::galaxyShowBuilds(const std::string& product_id, int build_index)
@@ -3862,3 +3742,77 @@ void Downloader::galaxyShowBuilds(const std::string& product_id, int build_index
 
     std::cout << json << std::endl;
 }
+
+std::vector<std::string> Downloader::galaxyGetOrphanedFiles(const std::vector<galaxyDepotItem>& items, const std::string& install_path)
+{
+    std::vector<std::string> orphans;
+    std::vector<std::string> item_paths;
+    for (unsigned int i = 0; i < items.size(); ++i)
+        item_paths.push_back(install_path + "/" + items[i].path);
+
+    std::vector<boost::filesystem::path> filepath_vector;
+    try
+    {
+        std::size_t pathlen = Globals::globalConfig.dirConf.sDirectory.length();
+        if (boost::filesystem::exists(install_path))
+        {
+            if (boost::filesystem::is_directory(install_path))
+            {
+                // Recursively iterate over files in directory
+                boost::filesystem::recursive_directory_iterator end_iter;
+                boost::filesystem::recursive_directory_iterator dir_iter(install_path);
+                while (dir_iter != end_iter)
+                {
+                    if (boost::filesystem::is_regular_file(dir_iter->status()))
+                    {
+                        std::string filepath = dir_iter->path().string();
+                        if (Globals::globalConfig.ignorelist.isBlacklisted(filepath.substr(pathlen)))
+                        {
+                            if (Globals::globalConfig.bVerbose)
+                                std::cerr << "skipped ignorelisted file " << filepath << std::endl;
+                        }
+                        else
+                        {
+                            filepath_vector.push_back(dir_iter->path());
+                        }
+                    }
+                    dir_iter++;
+                }
+            }
+        }
+        else
+            std::cerr << install_path << " does not exist" << std::endl;
+    }
+    catch (const boost::filesystem::filesystem_error& ex)
+    {
+        std::cout << ex.what() << std::endl;
+    }
+
+    std::sort(item_paths.begin(), item_paths.end());
+    std::sort(filepath_vector.begin(), filepath_vector.end());
+
+    if (!filepath_vector.empty())
+    {
+        for (unsigned int i = 0; i < filepath_vector.size(); ++i)
+        {
+            bool bFileIsOrphaned = true;
+            for (std::vector<std::string>::iterator it = item_paths.begin(); it != item_paths.end(); it++)
+            {
+                boost::filesystem::path item_path = *it;
+                boost::filesystem::path file_path = filepath_vector[i].native();
+
+                if (item_path == file_path)
+                {
+                    bFileIsOrphaned = false;
+                    item_paths.erase(it);
+                    break;
+                }
+            }
+
+            if (bFileIsOrphaned)
+                orphans.push_back(filepath_vector[i].string());
+        }
+    }
+
+    return orphans;
+}
diff --git a/src/galaxyapi.cpp b/src/galaxyapi.cpp
index a06c6b0..b36b01c 100644
--- a/src/galaxyapi.cpp
+++ b/src/galaxyapi.cpp
@@ -95,7 +95,7 @@ bool galaxyAPI::refreshLogin()
 
 bool galaxyAPI::isTokenExpired()
 {
-    int res = false;
+    bool res = false;
 
     if (Globals::galaxyConf.isExpired())
         res = true;
@@ -254,3 +254,207 @@ std::vector<galaxyDepotItem> galaxyAPI::getDepotItemsVector(const std::string& h
 
     return items;
 }
+
+Json::Value galaxyAPI::getProductInfo(const std::string& product_id)
+{
+    Json::Value json;
+
+    std::string url = "https://api.gog.com/products/" + product_id + "?expand=downloads,expanded_dlcs,description,screenshots,videos,related_products,changelog&locale=en-US";
+    std::string response = this->getResponse(url);
+
+    Json::Reader *jsonparser = new Json::Reader;
+    jsonparser->parse(response, json);
+    delete jsonparser;
+
+    return json;
+}
+
+gameDetails galaxyAPI::productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf)
+{
+    gameDetails gamedetails;
+
+    gamedetails.gamename = json["slug"].asString();
+    gamedetails.product_id = json["id"].asString();
+    gamedetails.title = json["title"].asString();
+    gamedetails.icon = "https:" + json["images"]["icon"].asString();
+
+    if (json.isMember("changelog"))
+        gamedetails.changelog = json["changelog"].asString();
+
+    if (dlConf.bInstallers)
+    {
+        gamedetails.installers = this->installerJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["installers"], dlConf);
+    }
+
+    if (dlConf.bExtras)
+    {
+        gamedetails.extras = this->extraJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["bonus_content"]);
+    }
+
+    if (dlConf.bPatches)
+    {
+        gamedetails.patches = this->patchJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["patches"], dlConf);
+    }
+
+    if (dlConf.bLanguagePacks)
+    {
+        gamedetails.languagepacks = this->languagepackJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["language_packs"], dlConf);
+    }
+
+    if (dlConf.bDLC)
+    {
+        if (json.isMember("expanded_dlcs"))
+        {
+            for (unsigned int i = 0; i < json["expanded_dlcs"].size(); ++i)
+            {
+                gameDetails dlc_gamedetails = this->productInfoJsonToGameDetails(json["expanded_dlcs"][i], dlConf);
+
+                // Add DLC type to all DLC files
+                for (unsigned int j = 0; j < dlc_gamedetails.installers.size(); ++j)
+                    dlc_gamedetails.installers[j].type |= GFTYPE_DLC;
+                for (unsigned int j = 0; j < dlc_gamedetails.extras.size(); ++j)
+                    dlc_gamedetails.extras[j].type |= GFTYPE_DLC;
+                for (unsigned int j = 0; j < dlc_gamedetails.patches.size(); ++j)
+                    dlc_gamedetails.patches[j].type |= GFTYPE_DLC;
+                for (unsigned int j = 0; j < dlc_gamedetails.languagepacks.size(); ++j)
+                    dlc_gamedetails.languagepacks[j].type |= GFTYPE_DLC;
+
+                // Add DLC only if it has any files
+                if (!dlc_gamedetails.installers.empty() || !dlc_gamedetails.extras.empty() || !dlc_gamedetails.patches.empty() || !dlc_gamedetails.languagepacks.empty())
+                    gamedetails.dlcs.push_back(dlc_gamedetails);
+            }
+        }
+    }
+
+    return gamedetails;
+}
+
+std::vector<gameFile> galaxyAPI::installerJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf)
+{
+    return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_INSTALLER, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler);
+}
+
+std::vector<gameFile> galaxyAPI::patchJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf)
+{
+    return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_PATCH, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler);
+}
+
+std::vector<gameFile> galaxyAPI::languagepackJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf)
+{
+    return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_LANGPACK, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler);
+}
+
+std::vector<gameFile> galaxyAPI::extraJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json)
+{
+    return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_EXTRA);
+}
+
+std::vector<gameFile> galaxyAPI::fileJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const unsigned int& type, const unsigned int& platform, const unsigned int& lang, const bool& useDuplicateHandler)
+{
+    std::vector<gameFile> gamefiles;
+    unsigned int iInfoNodes = json.size();
+    for (unsigned int i = 0; i < iInfoNodes; ++i)
+    {
+        Json::Value infoNode = json[i];
+        unsigned int iFiles = infoNode["files"].size();
+        std::string name = infoNode["name"].asString();
+
+        unsigned int iPlatform = GlobalConstants::PLATFORM_WINDOWS;
+        unsigned int iLanguage = GlobalConstants::LANGUAGE_EN;
+        if (!(type & GFTYPE_EXTRA))
+        {
+            iPlatform = Util::getOptionValue(infoNode["os"].asString(), GlobalConstants::PLATFORMS);
+            iLanguage = Util::getOptionValue(infoNode["language"].asString(), GlobalConstants::LANGUAGES);
+
+            if (!(iPlatform & platform))
+                continue;
+
+            if (!(iLanguage & lang))
+                continue;
+        }
+
+        for (unsigned int j = 0; j < iFiles; ++j)
+        {
+            Json::Value fileNode = infoNode["files"][j];
+            std::string downlink = fileNode["downlink"].asString();
+
+            std::string downlinkResponse = this->getResponse(downlink);
+
+            if (downlinkResponse.empty())
+                continue;
+
+            Json::Value downlinkJson;
+            Json::Reader *jsonparser = new Json::Reader;
+            jsonparser->parse(downlinkResponse, downlinkJson);
+            delete jsonparser;
+
+            std::string downlink_url = downlinkJson["downlink"].asString();
+            std::string downlink_url_unescaped = (std::string)curl_easy_unescape(curlhandle, downlink_url.c_str(), downlink_url.size(), NULL);
+            std::string path;
+
+            // GOG has changed the url formatting few times between 2 different formats.
+            // Try to get proper file name in both cases.
+            size_t filename_end_pos;
+            if (downlink_url_unescaped.find("?path=") != std::string::npos)
+                filename_end_pos = downlink_url_unescaped.find_first_of("&");
+            else
+                filename_end_pos = downlink_url_unescaped.find_first_of("?");
+
+            if (downlink_url_unescaped.find("/" + gamename + "/") != std::string::npos)
+            {
+                path.assign(downlink_url_unescaped.begin()+downlink_url_unescaped.find("/" + gamename + "/"), downlink_url_unescaped.begin()+filename_end_pos);
+            }
+            else
+            {
+                path.assign(downlink_url_unescaped.begin()+downlink_url_unescaped.find_last_of("/")+1, downlink_url_unescaped.begin()+filename_end_pos);
+                path = "/" + gamename + "/" + path;
+            }
+
+            // Workaround for filename issue caused by different (currently unknown) url formatting scheme
+            // https://github.com/Sude-/lgogdownloader/issues/126
+            if (path.find("?") != std::string::npos)
+            {
+                if (path.find_last_of("?") > path.find_last_of("/"))
+                {
+                    path.assign(path.begin(), path.begin()+path.find_last_of("?"));
+                }
+            }
+
+            gameFile gf;
+            gf.gamename = gamename;
+            gf.type = type;
+            gf.id = fileNode["id"].asString();
+            gf.name = name;
+            gf.path = path;
+            gf.size = Util::getJsonUIntValueAsString(fileNode["size"]);
+            gf.updated = 0; // assume not updated
+            gf.galaxy_downlink_json_url = downlink;
+
+            if (!(type & GFTYPE_EXTRA))
+            {
+                gf.platform = iPlatform;
+                gf.language = iLanguage;
+
+                if (useDuplicateHandler)
+                {
+                    bool bDuplicate = false;
+                    for (unsigned int k = 0; k < gamefiles.size(); ++k)
+                    {
+                        if (gamefiles[k].path == gf.path)
+                        {
+                            gamefiles[k].language |= gf.language; // Add language code to installer
+                            bDuplicate = true;
+                            break;
+                        }
+                    }
+                    if (bDuplicate)
+                        continue;
+                }
+            }
+
+            gamefiles.push_back(gf);
+        }
+    }
+
+    return gamefiles;
+}
diff --git a/src/gamedetails.cpp b/src/gamedetails.cpp
index 8d60102..473e0ee 100644
--- a/src/gamedetails.cpp
+++ b/src/gamedetails.cpp
@@ -24,6 +24,12 @@ void gameDetails::filterWithPriorities(const gameSpecificConfig& config)
     filterListWithPriorities(installers, config);
     filterListWithPriorities(patches, config);
     filterListWithPriorities(languagepacks, config);
+    for (unsigned int i = 0; i < dlcs.size(); ++i)
+    {
+        filterListWithPriorities(dlcs[i].installers, config);
+        filterListWithPriorities(dlcs[i].patches, config);
+        filterListWithPriorities(dlcs[i].languagepacks, config);
+    }
 }
 
 void gameDetails::filterListWithPriorities(std::vector<gameFile>& list, const gameSpecificConfig& config)
diff --git a/src/gamefile.cpp b/src/gamefile.cpp
index d78623e..a8a1e54 100644
--- a/src/gamefile.cpp
+++ b/src/gamefile.cpp
@@ -43,6 +43,7 @@ Json::Value gameFile::getAsJson()
     json["silent"] = this->silent;
     json["gamename"] = this->gamename;
     json["type"] = this->type;
+    json["galaxy_downlink_json_url"] = this->galaxy_downlink_json_url;
 
     return json;
 }
diff --git a/src/util.cpp b/src/util.cpp
index 9f11554..f23a80c 100644
--- a/src/util.cpp
+++ b/src/util.cpp
@@ -482,7 +482,7 @@ std::vector<std::string> Util::tokenize(const std::string& str, const std::strin
     return tokens;
 }
 
-unsigned int Util::getOptionValue(const std::string& str, const std::vector<GlobalConstants::optionsStruct>& options)
+unsigned int Util::getOptionValue(const std::string& str, const std::vector<GlobalConstants::optionsStruct>& options, const bool& bAllowStringToIntConversion)
 {
     unsigned int value = 0;
     boost::regex expression("^[+-]?\\d+$", boost::regex::perl);
@@ -491,7 +491,7 @@ unsigned int Util::getOptionValue(const std::string& str, const std::vector<Glob
     {
         value = (1 << options.size()) - 1;
     }
-    else if (boost::regex_search(str, what, expression))
+    else if (boost::regex_search(str, what, expression) && bAllowStringToIntConversion)
     {
         value = std::stoi(str);
     }
@@ -590,3 +590,26 @@ void Util::shortenStringToTerminalWidth(std::string& str)
         str.replace(str.begin()+pos1, str.begin()+pos2, "...");
     }
 }
+
+std::string Util::getJsonUIntValueAsString(const Json::Value& json_value)
+{
+    std::string value;
+    try
+    {
+        value = json_value.asString();
+    }
+    catch (...)
+    {
+        try
+        {
+            uintmax_t value_uint = json_value.asLargestUInt();
+            value = std::to_string(value_uint);
+        }
+        catch (...)
+        {
+            value = "";
+        }
+    }
+
+    return value;
+}
diff --git a/src/website.cpp b/src/website.cpp
index 307e3cb..6fe6d5f 100644
--- a/src/website.cpp
+++ b/src/website.cpp
@@ -16,7 +16,7 @@ Website::Website()
 
     curlhandle = curl_easy_init();
     curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
-    curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, Globals::globalConfig.sVersionString.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, Globals::globalConfig.curlConf.sUserAgent.c_str());
     curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
     curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
     curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, Globals::globalConfig.curlConf.iTimeout);
@@ -331,7 +331,7 @@ int Website::Login(const std::string& email, const std::string& password)
     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 \"" << Globals::globalConfig.curlConf.sCookiePath << "\"" << std::endl;
+                    << "Try to login later" << std::endl;
         return res = 0;
     }
 
@@ -495,10 +495,7 @@ int Website::Login(const std::string& email, const std::string& password)
             res = 1; // Login was successful
     }
 
-    if (auth_code.empty())
-        res = 0;
-
-    if (res == 1)
+    if (res == 1 && !auth_code.empty())
     {
         std::string token_url = "https://auth.gog.com/token?client_id=" + Globals::galaxyConf.getClientId()
                             + "&client_secret=" + Globals::galaxyConf.getClientSecret()
@@ -506,28 +503,25 @@ int Website::Login(const std::string& email, const std::string& password)
                             + "&redirect_uri=" + (std::string)curl_easy_escape(curlhandle, Globals::galaxyConf.getRedirectUri().c_str(), Globals::galaxyConf.getRedirectUri().size());
 
         std::string json = this->getResponse(token_url);
-        if (json.empty())
-            res = 0;
-        else
+        if (!json.empty())
         {
             Json::Value token_json;
             Json::Reader *jsonparser = new Json::Reader;
             if (jsonparser->parse(json, token_json))
             {
                 Globals::galaxyConf.setJSON(token_json);
-                res = 1;
+                res = 2;
             }
             else
             {
                 std::cerr << "Failed to parse json" << std::endl << json << std::endl;
                 std::cerr << jsonparser->getFormattedErrorMessages() << std::endl;
-                res = 0;
             }
             delete jsonparser;
         }
     }
 
-    if (res == 1)
+    if (res >= 1)
         curl_easy_setopt(curlhandle, CURLOPT_COOKIELIST, "FLUSH"); // Write all known cookies to the file specified by CURLOPT_COOKIEJAR
 
     return res;
@@ -545,7 +539,7 @@ bool Website::IsLoggedIn()
 bool Website::IsLoggedInComplex(const std::string& email)
 {
     bool bIsLoggedIn = false;
-    std::string html = this->getResponse("https://www.gog.com/account/settings/personal");
+    std::string html = this->getResponse("https://www.gog.com/account/settings/security");
     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;

-- 
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