Import lgogdownloader_3.1.orig.tar.gz
authorStephen Kitt <skitt@debian.org>
Mon, 16 Jan 2017 18:35:28 +0000 (18:35 +0000)
committerStephen Kitt <skitt@debian.org>
Mon, 16 Jan 2017 18:35:28 +0000 (18:35 +0000)
[dgit import orig lgogdownloader_3.1.orig.tar.gz]

35 files changed:
.gitignore [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
README.md [new file with mode: 0644]
cmake/FindHtmlcxx.cmake [new file with mode: 0644]
cmake/FindJsoncpp.cmake [new file with mode: 0644]
cmake/FindLibcrypto.cmake [new file with mode: 0644]
cmake/FindOAuth.cmake [new file with mode: 0644]
cmake/FindRhash.cmake [new file with mode: 0644]
cmake/FindTinyxml2.cmake [new file with mode: 0644]
include/api.h [new file with mode: 0644]
include/blacklist.h [new file with mode: 0644]
include/config.h [new file with mode: 0644]
include/downloader.h [new file with mode: 0644]
include/downloadinfo.h [new file with mode: 0644]
include/gamedetails.h [new file with mode: 0644]
include/gamefile.h [new file with mode: 0644]
include/globalconstants.h [new file with mode: 0644]
include/message.h [new file with mode: 0644]
include/progressbar.h [new file with mode: 0644]
include/ssl_thread_setup.h [new file with mode: 0644]
include/threadsafequeue.h [new file with mode: 0644]
include/util.h [new file with mode: 0644]
include/website.h [new file with mode: 0644]
main.cpp [new file with mode: 0644]
man/CMakeLists.txt [new file with mode: 0644]
man/lgogdownloader.supplemental.groff [new file with mode: 0644]
src/api.cpp [new file with mode: 0644]
src/blacklist.cpp [new file with mode: 0644]
src/downloader.cpp [new file with mode: 0644]
src/gamedetails.cpp [new file with mode: 0644]
src/gamefile.cpp [new file with mode: 0644]
src/progressbar.cpp [new file with mode: 0644]
src/util.cpp [new file with mode: 0644]
src/website.cpp [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..461ec87
--- /dev/null
@@ -0,0 +1,13 @@
+*.layout
+*~
+*.[oa]
+bin/*
+obj/*
+*.1
+*.gz
+Makefile
+CMakeCache.txt
+CMakeFiles/
+cmake_install.cmake
+build/
+*.cbp
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..992cc90
--- /dev/null
@@ -0,0 +1,140 @@
+cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR)
+project (lgogdownloader LANGUAGES C CXX VERSION 3.1)
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG=1")
+set(LINK_LIBCRYPTO 0)
+
+find_program(READELF readelf DOC "Location of the readelf program")
+find_program(GREP grep DOC "Location of the grep program")
+find_package(Boost
+  REQUIRED
+  system
+  filesystem
+  regex
+  program_options
+  date_time
+  )
+find_package(CURL 7.32.0 REQUIRED)
+if(CURL_FOUND)
+  execute_process(
+    COMMAND ${READELF} -d ${CURL_LIBRARIES}
+    COMMAND ${GREP} -q "libssl\\|libcrypto"
+    RESULT_VARIABLE READELF_RESULT_VAR
+  )
+  if(READELF_RESULT_VAR EQUAL 0)
+    add_definitions(-DSSL_THREAD_SETUP_OPENSSL=1)
+    find_package(Libcrypto REQUIRED)
+    set(LINK_LIBCRYPTO 1)
+  endif(READELF_RESULT_VAR EQUAL 0)
+endif(CURL_FOUND)
+
+find_package(OAuth REQUIRED)
+find_package(Jsoncpp REQUIRED)
+find_package(Htmlcxx REQUIRED)
+find_package(Tinyxml2 REQUIRED)
+find_package(Rhash REQUIRED)
+find_package(Threads REQUIRED)
+
+file(GLOB SRC_FILES
+  main.cpp
+  src/api.cpp
+  src/website.cpp
+  src/downloader.cpp
+  src/progressbar.cpp
+  src/util.cpp
+  src/blacklist.cpp
+  src/gamefile.cpp
+  src/gamedetails.cpp
+  )
+
+set(GIT_CHECKOUT FALSE)
+if(EXISTS ${PROJECT_SOURCE_DIR}/.git)
+  if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow)
+    find_package(Git)
+    if(GIT_FOUND)
+      set(GIT_CHECKOUT TRUE)
+    else(GIT_FOUND)
+      message(WARNING "Git executable not found")
+    endif(GIT_FOUND)
+  else(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow)
+    message(STATUS "Shallow Git clone detected, not attempting to retrieve version info")
+  endif(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow)
+endif(EXISTS ${PROJECT_SOURCE_DIR}/.git)
+
+if(GIT_CHECKOUT)
+  execute_process(COMMAND ${GIT_EXECUTABLE} diff --shortstat
+    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+    OUTPUT_VARIABLE GIT_SHORTSTAT
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+  )
+  execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
+    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+    OUTPUT_VARIABLE GIT_REV_PARSE
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+  )
+  if(GIT_SHORTSTAT)
+    set(GIT_DIRTY ON)
+  endif(GIT_SHORTSTAT)
+
+  if(GIT_DIRTY)
+    set(PROJECT_VERSION_MINOR ${PROJECT_VERSION_MINOR}M)
+  endif(GIT_DIRTY)
+
+  set(PROJECT_VERSION_PATCH ${GIT_REV_PARSE})
+  set(PROJECT_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH})
+endif(GIT_CHECKOUT)
+
+set(VERSION_NUMBER ${PROJECT_VERSION})
+set(VERSION_STRING "LGOGDownloader ${VERSION_NUMBER}")
+
+add_definitions(-D_FILE_OFFSET_BITS=64 -DVERSION_NUMBER="${VERSION_NUMBER}" -DVERSION_STRING="${VERSION_STRING}")
+
+add_executable (${PROJECT_NAME} ${SRC_FILES})
+
+
+target_include_directories(${PROJECT_NAME}
+  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include
+  PRIVATE ${Boost_INCLUDE_DIRS}
+  PRIVATE ${CURL_INCLUDE_DIRS}
+  PRIVATE ${OAuth_INCLUDE_DIRS}
+  PRIVATE ${Jsoncpp_INCLUDE_DIRS}
+  PRIVATE ${Htmlcxx_INCLUDE_DIRS}
+  PRIVATE ${Tinyxml2_INCLUDE_DIRS}
+  PRIVATE ${Rhash_INCLUDE_DIRS}
+  )
+
+target_link_libraries(${PROJECT_NAME}
+  PRIVATE ${Boost_LIBRARIES}
+  PRIVATE ${CURL_LIBRARIES}
+  PRIVATE ${OAuth_LIBRARIES}
+  PRIVATE ${Jsoncpp_LIBRARIES}
+  PRIVATE ${Htmlcxx_LIBRARIES}
+  PRIVATE ${Tinyxml2_LIBRARIES}
+  PRIVATE ${Rhash_LIBRARIES}
+  PRIVATE ${CMAKE_THREAD_LIBS_INIT}
+  )
+
+if(LINK_LIBCRYPTO EQUAL 1)
+  target_link_libraries(${PROJECT_NAME}
+    PRIVATE ${Libcrypto_LIBRARIES}
+  )
+endif(LINK_LIBCRYPTO EQUAL 1)
+
+if(MSVC)
+  # Force to always compile with W4
+  if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
+    string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+  else()
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
+  endif()
+elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+  # Update if necessary
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wextra -Wno-long-long -fexceptions")
+endif()
+
+set(INSTALL_BIN_DIR bin CACHE PATH "Installation directory for executables")
+set(INSTALL_SHARE_DIR share CACHE PATH "Installation directory for resource files")
+
+install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}${CMAKE_EXECUTABLE_SUFFIX} DESTINATION ${INSTALL_BIN_DIR})
+add_subdirectory(man)
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..5a8e332
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,14 @@
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+                    Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. You just DO WHAT THE FUCK YOU WANT TO.
+
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..46b59ee
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+# GOG Downloader
+
+This repository contains the code of unofficial [GOG](http://www.gog.com/) downloader.
+
+## Dependencies
+
+* [libcurl](https://curl.haxx.se/libcurl/) >= 7.32.0
+* [liboauth](https://sourceforge.net/projects/liboauth/)
+* [librhash](https://github.com/rhash/RHash)
+* [jsoncpp](https://github.com/open-source-parsers/jsoncpp)
+* [htmlcxx](http://htmlcxx.sourceforge.net/)
+* [tinyxml2](https://github.com/leethomason/tinyxml2)
+* [boost](http://www.boost.org/) (regex, date-time, system, filesystem, program-options)
+* [libcrypto](https://www.openssl.org/) if libcurl is built with OpenSSL
+
+## Make dependencies
+* [cmake](https://cmake.org/) >= 3.0.0
+* [help2man](https://www.gnu.org/software/help2man/help2man.html) (optional, man page generation)
+* [grep](https://www.gnu.org/software/grep/)
+* [binutils](https://www.gnu.org/software/binutils/) (readelf)
+
+### Debian/Ubuntu
+
+    # apt install build-essential libcurl4-openssl-dev libboost-regex-dev \
+    libjsoncpp-dev liboauth-dev librhash-dev libtinyxml2-dev libhtmlcxx-dev \
+    libboost-system-dev libboost-filesystem-dev libboost-program-options-dev \
+    libboost-date-time-dev help2man cmake libssl-dev pkg-config
+
+## Build and install
+
+    $ mkdir build
+    $ cd build
+    $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release
+    $ make
+    # sudo make install
+
+## Use
+
+    man lgogdownloader
+
+## Links
+- [LGOGDownloader website](https://sites.google.com/site/gogdownloader/)
+- [GOG forum thread](https://www.gog.com/forum/general/lgogdownloader_gogdownloader_for_linux)
+- [LGOGDownloader @ AUR](https://aur.archlinux.org/packages/lgogdownloader/)
+- [LGOGDownloader @ AUR (git version)](https://aur.archlinux.org/packages/lgogdownloader-git/)
+- [LGOGDownloader @ Debian](https://tracker.debian.org/lgogdownloader)
+- [LGOGDownloader @ Ubuntu](https://launchpad.net/ubuntu/+source/lgogdownloader)
diff --git a/cmake/FindHtmlcxx.cmake b/cmake/FindHtmlcxx.cmake
new file mode 100644 (file)
index 0000000..6dbf4b1
--- /dev/null
@@ -0,0 +1,54 @@
+# - Try to find htmlcxx
+#
+# Once done this will define
+#  Htmlcxx_FOUND - System has htmlcxx
+#  Htmlcxx_INCLUDE_DIRS - The htmlcxx include directories
+#  Htmlcxx_LIBRARIES - The libraries needed to use htmlcxx
+
+find_package(PkgConfig)
+pkg_check_modules(PC_HTMLCXX REQUIRED htmlcxx)
+
+find_path(HTMLCXX_INCLUDE_DIR
+  NAMES
+    css/parser.h
+    html/tree.h
+  HINTS
+    ${PC_HTMLCXX_INCLUDEDIR}
+    ${PC_HTMLCXX_INCLUDE_DIRS}
+  PATH_SUFFIXES
+    htmlcxx
+  PATHS
+    ${PC_HTMLCXX_INCLUDE_DIRS}
+  )
+
+find_library(HTMLCXX_LIBRARY_HTMLCXX htmlcxx
+  HINTS
+    ${PC_HTMLCXX_LIBDIR}
+    ${PC_HTMLCXX_LIBRARY_DIRS}
+  PATHS
+    ${PC_HTMLCXX_LIBRARY_DIRS}
+  )
+
+find_library(HTMLCXX_LIBRARY_CSS_PARSER css_parser
+  HINTS
+    ${PC_HTMLCXX_LIBDIR}
+    ${PC_HTMLCXX_LIBRARY_DIRS}
+  PATHS
+    ${PC_HTMLCXX_LIBRARY_DIRS}
+  )
+
+find_library(HTMLCXX_LIBRARY_CSS_PARSER_PP css_parser_pp
+  HINTS
+    ${PC_HTMLCXX_LIBDIR}
+    ${PC_HTMLCXX_LIBRARY_DIRS}
+  PATHS
+    ${PC_HTMLCXX_LIBRARY_DIRS}
+  )
+
+mark_as_advanced(HTMLCXX_INCLUDE_DIR HTMLCXX_LIBRARY_HTMLCXX HTMLCXX_LIBRARY_CSS_PARSER HTMLCXX_LIBRARY_CSS_PARSER_PP)
+
+if(PC_HTMLCXX_FOUND)
+  set(Htmlcxx_FOUND ON)
+  set(Htmlcxx_INCLUDE_DIRS ${HTMLCXX_INCLUDE_DIR})
+  set(Htmlcxx_LIBRARIES ${HTMLCXX_LIBRARY_HTMLCXX} ${HTMLCXX_LIBRARY_CSS_PARSER} ${HTMLCXX_LIBRARY_CSS_PARSER_PP})
+endif(PC_HTMLCXX_FOUND)
diff --git a/cmake/FindJsoncpp.cmake b/cmake/FindJsoncpp.cmake
new file mode 100644 (file)
index 0000000..5731980
--- /dev/null
@@ -0,0 +1,34 @@
+# - Try to find Jsoncpp
+#
+# Once done, this will define
+#  Jsoncpp_FOUND - system has Jsoncpp
+#  Jsoncpp_INCLUDE_DIRS - the Jsoncpp include directories
+#  Jsoncpp_LIBRARIES - link these to use Jsoncpp
+
+find_package(PkgConfig)
+pkg_check_modules(PC_JSONCPP REQUIRED jsoncpp)
+
+find_path(JSONCPP_INCLUDE_DIR
+  NAMES
+    json/features.h
+  HINTS
+    ${PC_JSONCPP_INCLUDEDIR}
+    ${PC_JSONCPP_INCLUDEDIRS}
+  PATH_SUFFIXES
+    jsoncpp
+  PATHS
+    ${PC_JSONCPP_INCLUDE_DIRS}
+  )
+
+find_library(JSONCPP_LIBRARY jsoncpp
+  PATHS
+    ${PC_JSONCPP_LIBRARY_DIRS}
+  )
+
+mark_as_advanced(JSONCPP_INCLUDE_DIR JSONCPP_LIBRARY)
+
+if(PC_JSONCPP_FOUND)
+  set(Jsoncpp_FOUND ON)
+  set(Jsoncpp_INCLUDE_DIRS ${JSONCPP_INCLUDE_DIR})
+  set(Jsoncpp_LIBRARIES ${JSONCPP_LIBRARY})
+endif(PC_JSONCPP_FOUND)
diff --git a/cmake/FindLibcrypto.cmake b/cmake/FindLibcrypto.cmake
new file mode 100644 (file)
index 0000000..3aa00b3
--- /dev/null
@@ -0,0 +1,27 @@
+# - Try to find libcrypto
+#
+# Once done this will define
+#  Libcrypto_FOUND - System has libcrypto
+#  Libcrypto_INCLUDE_DIRS - The libcrypto include directories
+#  Libcrypto_LIBRARIES - The libraries needed to use libcrypto
+
+find_package(PkgConfig)
+pkg_check_modules(PC_LIBCRYPTO REQUIRED libcrypto)
+
+find_path(LIBCRYPTO_INCLUDE_DIR openssl/crypto.h
+  HINTS ${PC_LIBCRYPTO_INCLUDEDIR}
+  ${PC_LIBCRYPTO_INCLUDE_DIRS}
+  )
+
+find_library(LIBCRYPTO_LIBRARY NAMES crypto
+  HINTS ${PC_LIBCRYPTO_LIBDIR}
+  ${PC_LIBCRYPTO_LIBRARY_DIRS}
+  )
+
+mark_as_advanced(LIBCRYPTO_INCLUDE_DIR LIBCRYPTO_LIBRARY)
+
+if(PC_LIBCRYPTO_FOUND)
+  set(Libcrypto_FOUND ON)
+  set(Libcrypto_INCLUDE_DIRS ${LIBCRYPTO_INCLUDE_DIR})
+  set(Libcrypto_LIBRARIES ${LIBCRYPTO_LIBRARY})
+endif(PC_LIBCRYPTO_FOUND)
diff --git a/cmake/FindOAuth.cmake b/cmake/FindOAuth.cmake
new file mode 100644 (file)
index 0000000..55dbd63
--- /dev/null
@@ -0,0 +1,28 @@
+# - Try to find oauth
+#
+# Once done this will define
+#  OAuth_FOUND - System has oauth
+#  OAuth_INCLUDE_DIRS - The oauth include directories
+#  OAuth_LIBRARIES - The libraries needed to use oauth
+
+find_package(PkgConfig)
+pkg_check_modules(PC_OAUTH REQUIRED oauth)
+
+find_path(OAUTH_INCLUDE_DIR oauth.h
+  HINTS ${PC_OAUTH_INCLUDEDIR}
+  ${PC_OAUTH_INCLUDE_DIRS}
+  PATH_SUFFIXES oauth
+  )
+
+find_library(OAUTH_LIBRARY NAMES oauth
+  HINTS ${PC_OAUTH_LIBDIR}
+  ${PC_OAUTH_LIBRARY_DIRS}
+  )
+
+mark_as_advanced(OAUTH_INCLUDE_DIR OAUTH_LIBRARY)
+
+if(PC_OAUTH_FOUND)
+  set(OAuth_FOUND ON)
+  set(OAuth_INCLUDE_DIRS ${OAUTH_INCLUDE_DIR})
+  set(OAuth_LIBRARIES ${OAUTH_LIBRARY})
+endif(PC_OAUTH_FOUND)
diff --git a/cmake/FindRhash.cmake b/cmake/FindRhash.cmake
new file mode 100644 (file)
index 0000000..cdfecd3
--- /dev/null
@@ -0,0 +1,22 @@
+# - Try to find rhash
+#
+# Once done this will define
+#  Rhash_FOUND - System has rhash
+#  Rhash_INCLUDE_DIRS - The rhash include directories
+#  Rhash_LIBRARIES - The libraries needed to use rhash
+
+find_path(RHASH_INCLUDE_DIR rhash.h)
+find_library(RHASH_LIBRARY rhash)
+
+mark_as_advanced(RHASH_INCLUDE_DIR RHASH_LIBRARY)
+
+if(RHASH_LIBRARY AND RHASH_INCLUDE_DIR)
+  set(Rhash_FOUND ON)
+  set(Rhash_LIBRARIES ${RHASH_LIBRARY})
+  set(Rhash_INCLUDE_DIRS ${RHASH_INCLUDE_DIR})
+else()
+  set(Rhash_FOUND OFF)
+  if(Rhash_FIND_REQUIRED)
+    message(FATAL_ERROR "Could not find rhash")
+  endif(Rhash_FIND_REQUIRED)
+endif(RHASH_LIBRARY AND RHASH_INCLUDE_DIR)
diff --git a/cmake/FindTinyxml2.cmake b/cmake/FindTinyxml2.cmake
new file mode 100644 (file)
index 0000000..4952977
--- /dev/null
@@ -0,0 +1,33 @@
+# - Try to find tinyxml2
+#
+# Once done this will define
+#  Tinyxml2_FOUND - System has tinyxml2
+#  Tinyxml2_INCLUDE_DIRS - The tinyxml2 include directories
+#  Tinyxml2_LIBRARIES - The libraries needed to use tinyxml
+
+find_package(PkgConfig)
+pkg_check_modules(PC_TINYXML2 tinyxml2)
+
+find_path(TINYXML2_INCLUDE_DIR tinyxml2.h
+  HINTS
+    ${PC_TINYXML2_INCLUDEDIR}
+    ${PC_TINYXML2_INCLUDE_DIRS}
+  PATHS
+    ${PC_TINYXML2_INCLUDE_DIRS}
+  )
+
+find_library(TINYXML2_LIBRARY tinyxml2
+  HINTS
+    ${PC_TINYXML2_LIBDIR}
+    ${PC_TINYXML2_LIBRARY_DIRS}
+  PATHS
+    ${PC_TINYXML2_LIBRARY_DIRS}
+  )
+
+mark_as_advanced(TINYXML2_INCLUDE_DIR TINYXML2_LIBRARY)
+
+if(TINYXML2_INCLUDE_DIR)
+  set(Tinyxml2_FOUND ON)
+  set(Tinyxml2_INCLUDE_DIRS ${TINYXML2_INCLUDE_DIR})
+  set(Tinyxml2_LIBRARIES ${TINYXML2_LIBRARY})
+endif(TINYXML2_INCLUDE_DIR)
diff --git a/include/api.h b/include/api.h
new file mode 100644 (file)
index 0000000..93cfb47
--- /dev/null
@@ -0,0 +1,94 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef API_H
+#define API_H
+
+#include "globalconstants.h"
+#include "gamedetails.h"
+
+#include <iostream>
+#include <vector>
+#include <curl/curl.h>
+extern "C" {
+    #include <oauth.h>
+}
+#include <cstring>
+#include <sys/time.h>
+
+class userDetails {
+    public:
+        std::string avatar_small;
+        std::string avatar_big;
+        std::string username;
+        std::string email;
+        unsigned long long id;
+        int notifications_forum;
+        int notifications_games;
+        int notifications_messages;
+};
+
+class apiConfig {
+    public:
+        std::string oauth_authorize_temp_token;
+        std::string oauth_get_temp_token;
+        std::string oauth_get_token;
+        std::string get_user_games;
+        std::string get_user_details;
+        std::string get_installer_link;
+        std::string get_game_details;
+        std::string get_extra_link;
+        std::string set_app_status;
+        std::string oauth_token;
+        std::string oauth_secret;
+};
+
+size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
+
+class API
+{
+    public:
+        userDetails user;
+
+        API(const std::string& token,const std::string& secret);
+        int init();
+        bool isLoggedIn();
+        int login(const std::string& email, const std::string& password);
+        int getAPIConfig();
+        std::string getResponse(const std::string& url);
+        std::string getResponseOAuth(const std::string& url);
+        int getUserDetails();
+        int getGames();
+        gameDetails getGameDetails(const std::string& game_name, const unsigned int& platform = (GlobalConstants::PLATFORM_WINDOWS | GlobalConstants::PLATFORM_LINUX), const unsigned int& lang = GlobalConstants::LANGUAGE_EN, const bool& useDuplicateHandler = false);
+        std::string getInstallerLink(const std::string& game_name, const std::string& id);
+        std::string getExtraLink(const std::string& game_name, const std::string& id);
+        std::string getPatchLink(const std::string& game_name, const std::string& id);
+        std::string getLanguagePackLink(const std::string& game_name, const std::string& id);
+        std::string getXML(const std::string& game_name, const std::string& id);
+        void clearError();
+        bool getError() { return this->error; };
+        std::string getErrorMessage() { return this->error_message; };
+        std::string getToken() { return this->config.oauth_token; };
+        std::string getSecret() { return this->config.oauth_secret; };
+        template <typename T> CURLcode curlSetOpt(CURLoption option, T value) { return curl_easy_setopt(this->curlhandle, option, value); }
+        virtual ~API();
+    protected:
+    private:
+        apiConfig config;
+        CURL* curlhandle;
+        void setError(const std::string& err);
+        bool error;
+        std::string error_message;
+
+        // API constants
+        const std::string CONSUMER_KEY = "1f444d14ea8ec776585524a33f6ecc1c413ed4a5";
+        const std::string CONSUMER_SECRET = "20d175147f9db9a10fc0584aa128090217b9cf88";
+        const int OAUTH_VERIFIER_LENGTH = 14;
+        const int OAUTH_TOKEN_LENGTH = 11;
+        const int OAUTH_SECRET_LENGTH = 18;
+};
+
+#endif // API_H
diff --git a/include/blacklist.h b/include/blacklist.h
new file mode 100644 (file)
index 0000000..9cb49b0
--- /dev/null
@@ -0,0 +1,40 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef BLACKLIST_H__
+#define BLACKLIST_H__
+
+#include <boost/regex.hpp>
+#include <string>
+#include <vector>
+
+class Config;
+class gameFile;
+
+class BlacklistItem {
+    public:
+        unsigned int linenr; // where the blacklist item is defined in blacklist.txt
+        unsigned int flags;
+        std::string source; // source representation of the item
+        boost::regex regex;
+};
+
+class Blacklist
+{
+    public:
+        Blacklist() {};
+
+        void initialize(const std::vector<std::string>& lines);
+        bool isBlacklisted(const std::string& path);
+        bool isBlacklisted(const std::string& path, const std::string& gamename, std::string subdirectory = "");
+
+        std::vector<BlacklistItem>::size_type size() const { return blacklist_.size(); }
+        bool empty() { return blacklist_.empty(); }
+    private:
+        std::vector<BlacklistItem> blacklist_;
+};
+
+#endif // BLACKLIST_H_
diff --git a/include/config.h b/include/config.h
new file mode 100644 (file)
index 0000000..2881f3e
--- /dev/null
@@ -0,0 +1,101 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef CONFIG_H__
+#define CONFIG_H__
+
+#include <iostream>
+#include <curl/curl.h>
+
+#include "blacklist.h"
+
+class Config
+{
+    public:
+        Config() {};
+        virtual ~Config() {};
+        bool bVerbose;
+        bool bRemoteXML;
+        bool bCover;
+        bool bUpdateCheck;
+        bool bDownload;
+        bool bList;
+        bool bListDetails;
+        bool bLoginHTTP;
+        bool bLoginAPI;
+        bool bRepair;
+        bool bInstallers;
+        bool bExtras;
+        bool bPatches;
+        bool bLanguagePacks;
+        bool bDLC;
+        bool bUnicode; // use Unicode in console output
+        bool bColor;   // use colors
+        bool bVerifyPeer;
+        bool bCheckStatus;
+        bool bDuplicateHandler;
+        bool bSaveConfig;
+        bool bResetConfig;
+        bool bReport;
+        bool bSubDirectories;
+        bool bUseCache;
+        bool bUpdateCache;
+        bool bSaveSerials;
+        bool bPlatformDetection;
+        bool bShowWishlist;
+        bool bAutomaticXMLCreation;
+        bool bSaveChangelogs;
+        bool bRespectUmask;
+        std::string sGameRegex;
+        std::string sDirectory;
+        std::string sCacheDirectory;
+        std::string sXMLFile;
+        std::string sXMLDirectory;
+        std::string sToken;
+        std::string sSecret;
+        std::string sVersionString;
+        std::string sVersionNumber;
+        std::string sConfigDirectory;
+        std::string sCookiePath;
+        std::string sConfigFilePath;
+        std::string sBlacklistFilePath;
+        std::string sIgnorelistFilePath;
+        std::string sGameHasDLCListFilePath;
+        std::string sOrphanRegex;
+        std::string sCoverList;
+        std::string sGameHasDLCList;
+        std::string sReportFilePath;
+        std::string sInstallersSubdir;
+        std::string sExtrasSubdir;
+        std::string sPatchesSubdir;
+        std::string sLanguagePackSubdir;
+        std::string sDLCSubdir;
+        std::string sGameSubdir;
+        std::string sFileIdString;
+        std::string sOutputFilename;
+        std::string sLanguagePriority;
+        std::string sPlatformPriority;
+        std::string sIgnoreDLCCountRegex;
+        std::string sCACertPath;
+        std::vector<unsigned int> vLanguagePriority;
+        std::vector<unsigned int> vPlatformPriority;
+
+        unsigned int iInstallerPlatform;
+        unsigned int iInstallerLanguage;
+        unsigned int iInclude;
+        unsigned int iThreads;
+        int iRetries;
+        int iWait;
+        int iCacheValid;
+        size_t iChunkSize;
+        curl_off_t iDownloadRate;
+        long int iTimeout;
+        Blacklist blacklist;
+        Blacklist ignorelist;
+        Blacklist gamehasdlc;
+};
+
+#endif // CONFIG_H__
diff --git a/include/downloader.h b/include/downloader.h
new file mode 100644 (file)
index 0000000..cb0fa6b
--- /dev/null
@@ -0,0 +1,124 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef DOWNLOADER_H
+#define DOWNLOADER_H
+
+#if __GNUC__
+#   if !(__x86_64__ || __ppc64__ || __LP64__)
+#       ifndef _LARGEFILE_SOURCE
+#           define _LARGEFILE_SOURCE
+#       endif
+#       ifndef _LARGEFILE64_SOURCE
+#           define _LARGEFILE64_SOURCE
+#       endif
+#       if !defined(_FILE_OFFSET_BITS) || (_FILE_OFFSET_BITS == 32)
+#           define _FILE_OFFSET_BITS 64
+#       endif
+#   endif
+#endif
+
+#include "config.h"
+#include "api.h"
+#include "progressbar.h"
+#include "website.h"
+#include "threadsafequeue.h"
+#include <curl/curl.h>
+#include <json/json.h>
+#include <ctime>
+#include <fstream>
+#include <deque>
+
+class Timer
+{
+    public:
+        Timer() { this->reset(); };
+        void reset() { gettimeofday(&(this->last_update), NULL); };
+        double getTimeBetweenUpdates()
+        { // Returns time elapsed between updates in milliseconds
+            struct timeval time_now;
+            gettimeofday(&time_now, NULL);
+            double time_between = ( (time_now.tv_sec+(time_now.tv_usec/1000000.0))*1000.0 - (this->last_update.tv_sec+(this->last_update.tv_usec/1000000.0))*1000.0 );
+            return time_between;
+        };
+        ~Timer() {};
+    private:
+        struct timeval last_update;
+};
+
+struct xferInfo
+{
+    unsigned int tid;
+    CURL* curlhandle;
+    Timer timer;
+    std::deque< std::pair<time_t, uintmax_t> > TimeAndSize;
+    curl_off_t offset;
+};
+
+class Downloader
+{
+    public:
+        Downloader(Config &conf);
+        virtual ~Downloader();
+        bool isLoggedIn();
+        int init();
+        int login();
+        int listGames();
+        void updateCheck();
+        void repair();
+        void download();
+        void checkOrphans();
+        void checkStatus();
+        void updateCache();
+        int downloadFileWithId(const std::string& fileid_string, const std::string& output_filepath);
+        void showWishlist();
+        CURL* curlhandle;
+        Timer timer;
+        Config config;
+        ProgressBar* progressbar;
+        std::deque< std::pair<time_t, uintmax_t> > TimeAndSize;
+    protected:
+    private:
+        CURLcode downloadFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string());
+        int repairFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string());
+        int downloadCovers(const std::string& gamename, const std::string& directory, const std::string& cover_xml_data);
+        int getGameDetails();
+        void getGameList();
+        uintmax_t getResumePosition();
+        CURLcode beginDownload();
+        std::string getResponse(const std::string& url);
+        std::string getLocalFileHash(const std::string& filepath, const std::string& gamename = std::string());
+        std::string getRemoteFileHash(const std::string& gamename, const std::string& id);
+        int loadGameDetailsCache();
+        int saveGameDetailsCache();
+        std::vector<gameDetails> getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0);
+        static std::vector<gameFile> getExtrasFromJSON(const Json::Value& json, const std::string& gamename, const Config& config);
+        static std::string getSerialsFromJSON(const Json::Value& json);
+        void saveSerials(const std::string& serials, const std::string& filepath);
+        static std::string getChangelogFromJSON(const Json::Value& json);
+        void saveChangelog(const std::string& changelog, const std::string& filepath);
+        static void processDownloadQueue(Config conf, const unsigned int& tid);
+        static int progressCallbackForThread(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
+        void printProgress();
+        static void getGameDetailsThread(Config config, const unsigned int& tid);
+
+        static int progressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
+        static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
+        static size_t writeData(void *ptr, size_t size, size_t nmemb, FILE *stream);
+        static size_t readData(void *ptr, size_t size, size_t nmemb, FILE *stream);
+
+        Website *gogWebsite;
+        API *gogAPI;
+        std::vector<gameItem> gameItems;
+        std::vector<gameDetails> games;
+        std::string coverXML;
+
+        off_t resume_position;
+        int retries;
+        std::ofstream report_ofs;
+};
+
+#endif // DOWNLOADER_H
diff --git a/include/downloadinfo.h b/include/downloadinfo.h
new file mode 100644 (file)
index 0000000..6015f89
--- /dev/null
@@ -0,0 +1,95 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef DOWNLOADINFO_H
+#define DOWNLOADINFO_H
+
+#include <curl/curl.h>
+#include <mutex>
+
+const unsigned int DLSTATUS_NOTSTARTED = 0;
+const unsigned int DLSTATUS_STARTING   = 1 << 0;
+const unsigned int DLSTATUS_RUNNING    = 1 << 1;
+const unsigned int DLSTATUS_FINISHED   = 1 << 2;
+
+struct progressInfo
+{
+    curl_off_t dlnow;
+    curl_off_t dltotal;
+    double rate;
+    double rate_avg;
+};
+
+class DownloadInfo
+{
+    public:
+        void setFilename(const std::string& filename_)
+        {
+            std::unique_lock<std::mutex> lock(m);
+            filename = filename_;
+        }
+
+        std::string getFilename()
+        {
+            std::unique_lock<std::mutex> lock(m);
+            return filename;
+        }
+
+        void setStatus(const unsigned int& status_)
+        {
+            std::unique_lock<std::mutex> lock(m);
+            status = status_;
+        }
+
+        unsigned int getStatus()
+        {
+            std::unique_lock<std::mutex> lock(m);
+            return status;
+        }
+
+        void setProgressInfo(const progressInfo& info)
+        {
+            std::unique_lock<std::mutex> lock(m);
+            progress_info = info;
+        }
+
+        progressInfo getProgressInfo()
+        {
+            std::unique_lock<std::mutex> lock(m);
+            return progress_info;
+        }
+
+        DownloadInfo()=default;
+
+        DownloadInfo(const DownloadInfo& other)
+        {
+            std::lock_guard<std::mutex> guard(other.m);
+            filename = other.filename;
+            status = other.status;
+            progress_info = other.progress_info;
+        }
+
+        DownloadInfo& operator= (DownloadInfo& other)
+        {
+            if(&other == this)
+                return *this;
+
+            std::unique_lock<std::mutex> lock1(m, std::defer_lock);
+            std::unique_lock<std::mutex> lock2(other.m, std::defer_lock);
+            std::lock(lock1, lock2);
+            filename = other.filename;
+            status = other.status;
+            progress_info = other.progress_info;
+            return *this;
+        }
+    private:
+        std::string filename;
+        unsigned int status;
+        progressInfo progress_info;
+        mutable std::mutex m;
+};
+
+#endif // DOWNLOADINFO_H
diff --git a/include/gamedetails.h b/include/gamedetails.h
new file mode 100644 (file)
index 0000000..7009364
--- /dev/null
@@ -0,0 +1,47 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GAMEDETAILS_H
+#define GAMEDETAILS_H
+
+#include "globalconstants.h"
+#include "gamefile.h"
+#include "config.h"
+#include "util.h"
+
+#include <iostream>
+#include <vector>
+#include <json/json.h>
+
+class gameDetails
+{
+    public:
+        gameDetails();
+        std::vector<gameFile> extras;
+        std::vector<gameFile> installers;
+        std::vector<gameFile> patches;
+        std::vector<gameFile> languagepacks;
+        std::vector<gameDetails> dlcs;
+        std::string gamename;
+        std::string title;
+        std::string icon;
+        std::string serials;
+        std::string changelog;
+        void filterWithPriorities(const gameSpecificConfig& config);
+        void makeFilepaths(const gameSpecificDirectoryConfig& config);
+        std::string getSerialsFilepath();
+        std::string getChangelogFilepath();
+        Json::Value getDetailsAsJson();
+        std::vector<gameFile> getGameFileVector();
+        virtual ~gameDetails();
+    protected:
+        void filterListWithPriorities(std::vector<gameFile>& list, const gameSpecificConfig& config);
+    private:
+        std::string serialsFilepath;
+        std::string changelogFilepath;
+};
+
+#endif // GAMEDETAILS_H
diff --git a/include/gamefile.h b/include/gamefile.h
new file mode 100644 (file)
index 0000000..90d6004
--- /dev/null
@@ -0,0 +1,47 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GAMEFILE_H
+#define GAMEFILE_H
+
+#include "globalconstants.h"
+
+#include <iostream>
+#include <vector>
+#include <json/json.h>
+
+// Game file types
+const unsigned int GFTYPE_INSTALLER = 1 << 0;
+const unsigned int GFTYPE_EXTRA     = 1 << 1;
+const unsigned int GFTYPE_PATCH     = 1 << 2;
+const unsigned int GFTYPE_LANGPACK  = 1 << 3;
+const unsigned int GFTYPE_DLC       = 1 << 4;
+
+class gameFile
+{
+    public:
+        gameFile();
+        int updated;
+        std::string gamename;
+        std::string id;
+        std::string name;
+        std::string path;
+        std::string size;
+        unsigned int platform;
+        unsigned int language;
+        unsigned int type;
+        int score;
+        int silent;
+        void setFilepath(const std::string& path);
+        std::string getFilepath();
+        Json::Value getAsJson();
+        virtual ~gameFile();
+    protected:
+    private:
+        std::string filepath;
+};
+
+#endif // GAMEFILE_H
diff --git a/include/globalconstants.h b/include/globalconstants.h
new file mode 100644 (file)
index 0000000..52b9dc1
--- /dev/null
@@ -0,0 +1,83 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GLOBALCONSTANTS_H_INCLUDED
+#define GLOBALCONSTANTS_H_INCLUDED
+
+#include <iostream>
+#include <vector>
+
+namespace GlobalConstants
+{
+    const int GAMEDETAILS_CACHE_VERSION = 1;
+
+    struct optionsStruct {const unsigned int id; const std::string code; const std::string str; const std::string regexp;};
+    const std::string PROTOCOL_PREFIX = "gogdownloader://";
+
+    // Language constants
+    const unsigned int LANGUAGE_EN = 1 << 0;
+    const unsigned int LANGUAGE_DE = 1 << 1;
+    const unsigned int LANGUAGE_FR = 1 << 2;
+    const unsigned int LANGUAGE_PL = 1 << 3;
+    const unsigned int LANGUAGE_RU = 1 << 4;
+    const unsigned int LANGUAGE_CN = 1 << 5;
+    const unsigned int LANGUAGE_CZ = 1 << 6;
+    const unsigned int LANGUAGE_ES = 1 << 7;
+    const unsigned int LANGUAGE_HU = 1 << 8;
+    const unsigned int LANGUAGE_IT = 1 << 9;
+    const unsigned int LANGUAGE_JP = 1 << 10;
+    const unsigned int LANGUAGE_TR = 1 << 11;
+    const unsigned int LANGUAGE_PT = 1 << 12;
+    const unsigned int LANGUAGE_KO = 1 << 13;
+    const unsigned int LANGUAGE_NL = 1 << 14;
+    const unsigned int LANGUAGE_SV = 1 << 15;
+    const unsigned int LANGUAGE_NO = 1 << 16;
+    const unsigned int LANGUAGE_DA = 1 << 17;
+    const unsigned int LANGUAGE_FI = 1 << 18;
+    const unsigned int LANGUAGE_PT_BR = 1 << 19;
+    const unsigned int LANGUAGE_SK = 1 << 20;
+    const unsigned int LANGUAGE_BL = 1 << 21;
+
+    const std::vector<optionsStruct> LANGUAGES =
+    {
+        { LANGUAGE_EN, "en", "English"   , "en|eng|english"        },
+        { LANGUAGE_DE, "de", "German"    , "de|deu|ger|german"     },
+        { LANGUAGE_FR, "fr", "French"    , "fr|fra|fre|french"     },
+        { LANGUAGE_PL, "pl", "Polish"    , "pl|pol|polish"         },
+        { LANGUAGE_RU, "ru", "Russian"   , "ru|rus|russian"        },
+        { LANGUAGE_CN, "cn", "Chinese"   , "cn|zh|zho|chi|chinese" },
+        { LANGUAGE_CZ, "cz", "Czech"     , "cz|cs|ces|cze|czech"   },
+        { LANGUAGE_ES, "es", "Spanish"   , "es|spa|spanish"        },
+        { LANGUAGE_HU, "hu", "Hungarian" , "hu|hun|hungarian"      },
+        { LANGUAGE_IT, "it", "Italian"   , "it|ita|italian"        },
+        { LANGUAGE_JP, "jp", "Japanese"  , "jp|ja|jpn|japanese"    },
+        { LANGUAGE_TR, "tr", "Turkish"   , "tr|tur|turkish"        },
+        { LANGUAGE_PT, "pt", "Portuguese", "pt|por|portuguese"     },
+        { LANGUAGE_KO, "ko", "Korean"    , "ko|kor|korean"         },
+        { LANGUAGE_NL, "nl", "Dutch"     , "nl|nld|dut|dutch"      },
+        { LANGUAGE_SV, "sv", "Swedish"   , "sv|swe|swedish"        },
+        { LANGUAGE_NO, "no", "Norwegian" , "no|nor|norwegian"      },
+        { LANGUAGE_DA, "da", "Danish"    , "da|dan|danish"         },
+        { 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"   }
+    };
+
+    // Platform constants
+    const unsigned int PLATFORM_WINDOWS = 1 << 0;
+    const unsigned int PLATFORM_MAC     = 1 << 1;
+    const unsigned int PLATFORM_LINUX   = 1 << 2;
+
+    const std::vector<optionsStruct> PLATFORMS =
+    {
+        { PLATFORM_WINDOWS, "win",   "Windows" , "w|win|windows" },
+        { PLATFORM_MAC,     "mac",   "Mac"     , "m|mac|osx"     },
+        { PLATFORM_LINUX,   "linux", "Linux"   , "l|lin|linux"   }
+    };
+}
+
+#endif // GLOBALCONSTANTS_H_INCLUDED
diff --git a/include/message.h b/include/message.h
new file mode 100644 (file)
index 0000000..1704ccb
--- /dev/null
@@ -0,0 +1,108 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef MESSAGE_H
+#define MESSAGE_H
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+const unsigned int MSGTYPE_INFO    = 1 << 0;
+const unsigned int MSGTYPE_WARNING = 1 << 1;
+const unsigned int MSGTYPE_ERROR   = 1 << 2;
+const unsigned int MSGTYPE_SUCCESS = 1 << 3;
+
+class Message
+{
+    public:
+        Message() = default;
+        Message(std::string msg, const unsigned int& type = MSGTYPE_INFO, const std::string& prefix = std::string())
+        {
+            prefix_ = prefix;
+            msg_ = msg;
+            type_ = type;
+            timestamp_ = boost::posix_time::second_clock::local_time();
+        }
+
+        void setMessage(const std::string& msg)
+        {
+            msg_ = msg;
+        }
+
+        void setType(const unsigned int& type)
+        {
+            type_ = type;
+        }
+
+        void setTimestamp(const boost::posix_time::ptime& timestamp)
+        {
+            timestamp_ = timestamp;
+        }
+
+        void setPrefix(const std::string& prefix)
+        {
+            prefix_ = prefix;
+        }
+
+        std::string getMessage()
+        {
+            return msg_;
+        }
+
+        unsigned int getType()
+        {
+            return type_;
+        }
+
+        boost::posix_time::ptime getTimestamp()
+        {
+            return timestamp_;
+        }
+
+        std::string getTimestampString()
+        {
+            return boost::posix_time::to_simple_string(timestamp_);
+        }
+
+        std::string getPrefix()
+        {
+            return prefix_;
+        }
+
+        std::string getFormattedString(const bool& bColor = true, const bool& bPrefix = true)
+        {
+            std::string str;
+            std::string color_value = "\033[39m"; // Default foreground color
+            std::string color_reset = "\033[0m";
+
+            if (type_ == MSGTYPE_INFO)
+                color_value = "\033[39m"; // Default foreground color
+            else if (type_ == MSGTYPE_WARNING)
+                color_value = "\033[33m"; // Yellow
+            else if (type_ == MSGTYPE_ERROR)
+                color_value = "\033[31m"; // Red
+            else if (type_ == MSGTYPE_SUCCESS)
+                color_value = "\033[32m"; // Green
+
+            str = msg_;
+            if (!prefix_.empty() && bPrefix)
+                str = prefix_ + " " + str;
+
+            str = getTimestampString() + " " + str;
+
+            if (bColor)
+                str = color_value + str + color_reset;
+
+            return str;
+        }
+
+    private:
+        std::string msg_;
+        boost::posix_time::ptime timestamp_;
+        unsigned int type_;
+        std::string prefix_;
+};
+
+#endif // MESSAGE_H
diff --git a/include/progressbar.h b/include/progressbar.h
new file mode 100644 (file)
index 0000000..4d2f88d
--- /dev/null
@@ -0,0 +1,36 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef PROGRESSBAR_H
+#define PROGRESSBAR_H
+
+#include <iostream>
+#include <vector>
+
+class ProgressBar
+{
+    public:
+        ProgressBar(bool bUnicode, bool bColor);
+        virtual ~ProgressBar();
+        void draw(unsigned int length, double fraction);
+        std::string createBarString(unsigned int length, double fraction);
+    protected:
+    private:
+        std::vector<std::string> const m_bar_chars;
+        std::string const m_left_border;
+        std::string const m_right_border;
+        std::string const m_simple_left_border;
+        std::string const m_simple_right_border;
+        std::string const m_simple_empty_fill;
+        std::string const m_simple_bar_char;
+        std::string const m_bar_color;
+        std::string const m_border_color;
+        std::string const COLOR_RESET;
+        bool m_use_unicode;
+        bool m_use_color;
+};
+
+#endif // PROGRESSBAR_H
diff --git a/include/ssl_thread_setup.h b/include/ssl_thread_setup.h
new file mode 100644 (file)
index 0000000..6a5aee4
--- /dev/null
@@ -0,0 +1,60 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef SSL_THREAD_SETUP_H
+#define SSL_THREAD_SETUP_H
+
+#include <thread>
+#include <mutex>
+
+#if SSL_THREAD_SETUP_OPENSSL == 1
+    #include <openssl/crypto.h>
+
+    static std::mutex* ssl_mutex_array;
+
+    void thread_locking_callback(int mode, int n, const char* file, int line)
+    {
+        if(mode & CRYPTO_LOCK)
+            ssl_mutex_array[n].lock();
+        else
+            ssl_mutex_array[n].unlock();
+    }
+
+    unsigned long thread_id_callback()
+    {
+        return (unsigned long)std::hash<std::thread::id>() (std::this_thread::get_id());
+    }
+
+    int ssl_thread_setup()
+    {
+        ssl_mutex_array = new std::mutex[CRYPTO_num_locks()];
+        if(!ssl_mutex_array)
+            return 0;
+        else
+        {
+            CRYPTO_set_id_callback(thread_id_callback);
+            CRYPTO_set_locking_callback(thread_locking_callback);
+        }
+        return 1;
+    }
+
+    int ssl_thread_cleanup()
+    {
+        if(!ssl_mutex_array)
+            return 0;
+
+        CRYPTO_set_id_callback(NULL);
+        CRYPTO_set_locking_callback(NULL);
+        delete[] ssl_mutex_array;
+        ssl_mutex_array = NULL;
+        return 1;
+    }
+#else
+    #define ssl_thread_setup()
+    #define ssl_thread_cleanup()
+#endif
+
+#endif // SSL_THREAD_SETUP_H
diff --git a/include/threadsafequeue.h b/include/threadsafequeue.h
new file mode 100644 (file)
index 0000000..a456daa
--- /dev/null
@@ -0,0 +1,84 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef THREADSAFEQUEUE_H
+#define THREADSAFEQUEUE_H
+
+#include <queue>
+#include <mutex>
+#include <condition_variable>
+
+template<typename T>
+class ThreadSafeQueue
+{
+    public:
+        void push(const T& item)
+        {
+            std::unique_lock<std::mutex> lock(m);
+            q.push(item);
+            lock.unlock();
+            cvar.notify_one();
+        }
+
+        bool empty() const
+        {
+            std::unique_lock<std::mutex> lock(m);
+            return q.empty();
+        }
+
+        typename std::queue<T>::size_type size() const
+        {
+            std::unique_lock<std::mutex> lock(m);
+            return q.size();
+        }
+
+        bool try_pop(T& item)
+        {
+            std::unique_lock<std::mutex> lock(m);
+            if(q.empty())
+                return false;
+
+            item = q.front();
+            q.pop();
+            return true;
+        }
+
+        void wait_and_pop(T& item)
+        {
+            std::unique_lock<std::mutex> lock(m);
+            while(q.empty())
+                cvar.wait(lock);
+
+            item = q.front();
+            q.pop();
+        }
+
+        ThreadSafeQueue() = default;
+
+        ThreadSafeQueue(const ThreadSafeQueue& other)
+        {
+            std::lock_guard<std::mutex> guard(other.m);
+            q = other.q;
+        }
+
+        ThreadSafeQueue& operator= (ThreadSafeQueue& other)
+        {
+            if(&other == this)
+                return *this;
+
+            std::unique_lock<std::mutex> lock1(m, std::defer_lock);
+            std::unique_lock<std::mutex> lock2(other.m, std::defer_lock);
+            std::lock(lock1, lock2);
+            q = other.q;
+            return *this;
+        }
+    private:
+        std::queue<T> q;
+        mutable std::mutex m;
+        std::condition_variable cvar;
+};
+
+#endif // THREADSAFEQUEUE_H
diff --git a/include/util.h b/include/util.h
new file mode 100644 (file)
index 0000000..b30f12c
--- /dev/null
@@ -0,0 +1,105 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef UTIL_H
+#define UTIL_H
+
+#include "globalconstants.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <cerrno>
+#include <iostream>
+#include <sstream>
+#include <memory>
+#include <rhash.h>
+#include <boost/filesystem.hpp>
+#include <boost/regex.hpp>
+#include <json/json.h>
+
+struct gameSpecificDirectoryConfig
+{
+    bool bSubDirectories;
+    std::string sDirectory;
+    std::string sGameSubdir;
+    std::string sInstallersSubdir;
+    std::string sExtrasSubdir;
+    std::string sPatchesSubdir;
+    std::string sLanguagePackSubdir;
+    std::string sDLCSubdir;
+};
+
+struct gameSpecificConfig
+{
+    unsigned int iInstallerPlatform;
+    unsigned int iInstallerLanguage;
+    bool bDLC;
+    bool bIgnoreDLCCount;
+    gameSpecificDirectoryConfig dirConf;
+    std::vector<unsigned int> vLanguagePriority;
+    std::vector<unsigned int> vPlatformPriority;
+};
+
+struct gameItem
+{
+    std::string name;
+    std::string id;
+    std::vector<std::string> dlcnames;
+    Json::Value gamedetailsjson;
+    int updates = 0;
+};
+
+struct wishlistItem
+{
+    std::string title;
+    unsigned int platform;
+    std::vector<std::string> tags;
+    time_t release_date_time;
+    std::string currency;
+    std::string price;
+    std::string discount_percent;
+    std::string discount;
+    std::string store_credit;
+    std::string url;
+    bool bIsBonusStoreCreditIncluded;
+    bool bIsDiscounted;
+};
+
+namespace Util
+{
+    std::string makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename, std::string subdirectory = "", const unsigned int& platformId = 0, const std::string& dlcname = "");
+    std::string makeRelativeFilepath(const std::string& path, const std::string& gamename, std::string subdirectory = "");
+    std::string getFileHash(const std::string& filename, unsigned hash_id);
+    std::string getChunkHash(unsigned char* chunk, uintmax_t chunk_size, unsigned hash_id);
+    int createXML(std::string filepath, uintmax_t chunk_size, std::string xml_dir = std::string());
+    int getGameSpecificConfig(std::string gamename, gameSpecificConfig* conf, std::string directory = std::string());
+    int replaceString(std::string& str, const std::string& to_replace, const std::string& replace_with);
+    void filepathReplaceReservedStrings(std::string& str, const std::string& gamename, const unsigned int& platformId = 0, const std::string& dlcname = "");
+    void setFilePermissions(const boost::filesystem::path& path, const boost::filesystem::perms& permissions);
+    int getTerminalWidth();
+    void getDownloaderUrlsFromJSON(const Json::Value &root, std::vector<std::string> &urls);
+    std::vector<std::string> getDLCNamesFromJSON(const Json::Value &root);
+    std::string getHomeDir();
+    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);
+    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);
+
+    template<typename ... Args> std::string formattedString(const std::string& format, Args ... args)
+    {
+        std::size_t sz = std::snprintf(nullptr, 0, format.c_str(), args ...) + 1; // +1 for null terminator
+        std::unique_ptr<char[]> buf(new char[sz]);
+        std::snprintf(buf.get(), sz, format.c_str(), args ...);
+        return std::string(buf.get(), buf.get() + sz - 1); // -1 because we don't want the null terminator
+    }
+}
+
+#endif // UTIL_H
diff --git a/include/website.h b/include/website.h
new file mode 100644 (file)
index 0000000..0664784
--- /dev/null
@@ -0,0 +1,39 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef WEBSITE_H
+#define WEBSITE_H
+
+#include "config.h"
+#include "util.h"
+#include <curl/curl.h>
+#include <json/json.h>
+#include <fstream>
+
+class Website
+{
+    public:
+        Website(Config &conf);
+        int Login(const std::string& email, const std::string& password);
+        std::string getResponse(const std::string& url);
+        Json::Value getGameDetailsJSON(const std::string& gameid);
+        std::vector<gameItem> getGames();
+        std::vector<gameItem> getFreeGames();
+        std::vector<wishlistItem> getWishlistItems();
+        bool IsLoggedIn();
+        void setConfig(Config &conf);
+        virtual ~Website();
+    protected:
+    private:
+        static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
+        CURL* curlhandle;
+        Config config;
+        bool IsloggedInSimple();
+        bool IsLoggedInComplex(const std::string& email);
+        int retries;
+};
+
+#endif // WEBSITE_H
diff --git a/main.cpp b/main.cpp
new file mode 100644 (file)
index 0000000..23b13c5
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,675 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "downloader.h"
+#include "config.h"
+#include "util.h"
+#include "globalconstants.h"
+#include "ssl_thread_setup.h"
+
+#include <fstream>
+#include <boost/filesystem.hpp>
+#include <boost/program_options.hpp>
+
+namespace bpo = boost::program_options;
+
+template<typename T> void set_vm_value(std::map<std::string, bpo::variable_value>& vm, const std::string& option, const T& value)
+{
+    vm[option].value() = boost::any(value);
+}
+
+int main(int argc, char *argv[])
+{
+    // Constants for option selection with include/exclude
+    /* TODO: Add options to give better control for user
+             For example: option to select base game and DLC installers separately,
+             this requires some changes to Downloader class to implement */
+    const unsigned int OPTION_INSTALLERS = 1 << 0;
+    const unsigned int OPTION_EXTRAS     = 1 << 1;
+    const unsigned int OPTION_PATCHES    = 1 << 2;
+    const unsigned int OPTION_LANGPACKS  = 1 << 3;
+    const unsigned int OPTION_COVERS     = 1 << 4;
+    const unsigned int OPTION_DLCS       = 1 << 5;
+
+    const std::vector<GlobalConstants::optionsStruct> INCLUDE_OPTIONS =
+    {
+        { OPTION_INSTALLERS, "i", "Installers",     "i|installers"              },
+        { OPTION_EXTRAS,     "e", "Extras",         "e|extras"                  },
+        { OPTION_PATCHES,    "p", "Patches",        "p|patches"                 },
+        { OPTION_LANGPACKS,  "l", "Language packs", "l|languagepacks|langpacks" },
+        { OPTION_COVERS,     "c", "Covers",         "c|cover|covers"            },
+        { OPTION_DLCS,       "d", "DLCs",           "d|dlc|dlcs"                }
+    };
+
+    Config config;
+    config.sVersionString = VERSION_STRING;
+    config.sVersionNumber = VERSION_NUMBER;
+
+    config.sCacheDirectory = Util::getCacheHome() + "/lgogdownloader";
+    config.sXMLDirectory = config.sCacheDirectory + "/xml";
+
+    config.sConfigDirectory = Util::getConfigHome() + "/lgogdownloader";
+    config.sCookiePath = config.sConfigDirectory + "/cookies.txt";
+    config.sConfigFilePath = config.sConfigDirectory + "/config.cfg";
+    config.sBlacklistFilePath = config.sConfigDirectory + "/blacklist.txt";
+    config.sIgnorelistFilePath = config.sConfigDirectory + "/ignorelist.txt";
+    config.sGameHasDLCListFilePath = config.sConfigDirectory + "/game_has_dlc.txt";
+
+    std::string priority_help_text = "Set priority by separating values with \",\"\nCombine values by separating with \"+\"";
+    // Create help text for --platform option
+    std::string platform_text = "Select which installers are downloaded\n";
+    unsigned int platform_all = Util::getOptionValue("all", GlobalConstants::PLATFORMS);
+    for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+    {
+        platform_text += GlobalConstants::PLATFORMS[i].str + " = " + GlobalConstants::PLATFORMS[i].regexp + "|" + std::to_string(GlobalConstants::PLATFORMS[i].id) + "\n";
+    }
+    platform_text += "All = all|" + std::to_string(platform_all);
+    platform_text += "\n\n" + priority_help_text;
+    platform_text += "\nExample: Linux if available otherwise Windows and Mac: l,w+m";
+
+    // Create help text for --language option
+    std::string language_text = "Select which language installers are downloaded\n";
+    unsigned int language_all = Util::getOptionValue("all", GlobalConstants::LANGUAGES);
+    for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+    {
+        language_text +=  GlobalConstants::LANGUAGES[i].str + " = " + GlobalConstants::LANGUAGES[i].regexp + "|" + std::to_string(GlobalConstants::LANGUAGES[i].id) + "\n";
+    }
+    language_text += "Add the values to download multiple languages\nAll = all|" + std::to_string(language_all) + "\n"
+                    + "French + Polish = \"fr+pl\"|" + std::to_string(GlobalConstants::LANGUAGE_FR | GlobalConstants::LANGUAGE_PL) + " (" + std::to_string(GlobalConstants::LANGUAGE_FR) + "+" + std::to_string(GlobalConstants::LANGUAGE_PL) + "=" + std::to_string(GlobalConstants::LANGUAGE_FR | GlobalConstants::LANGUAGE_PL) + ")";
+    language_text += "\n\n" + priority_help_text;
+    language_text += "\nExample: German if available otherwise English and French: de,en+fr";
+
+    // 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 + "'";
+
+    // Help text for subdir options
+    std::string subdir_help_text = "\nTemplates:\n- %platform%\n- %gamename%\n- %dlcname%";
+
+    // Help text for include and exclude options
+    std::string include_options_text;
+    for (unsigned int i = 0; i < INCLUDE_OPTIONS.size(); ++i)
+    {
+        include_options_text +=  INCLUDE_OPTIONS[i].str + " = " + INCLUDE_OPTIONS[i].regexp + "|" + std::to_string(INCLUDE_OPTIONS[i].id) + "\n";
+    }
+    include_options_text += "Separate with \",\" to use multiple values";
+
+    std::vector<std::string> vFileIdStrings;
+    std::vector<std::string> unrecognized_options_cfg;
+    std::vector<std::string> unrecognized_options_cli;
+    bpo::variables_map vm;
+    bpo::options_description options_cli_all("Options");
+    bpo::options_description options_cli_no_cfg;
+    bpo::options_description options_cli_cfg;
+    bpo::options_description options_cfg_only;
+    bpo::options_description options_cfg_all("Configuration");
+    try
+    {
+        bool bInsecure = false;
+        bool bNoColor = false;
+        bool bNoUnicode = false;
+        bool bNoDuplicateHandler = false;
+        bool bNoRemoteXML = false;
+        bool bNoSubDirectories = false;
+        bool bNoPlatformDetection = false;
+        bool bLogin = false;
+        std::string sInstallerPlatform;
+        std::string sInstallerLanguage;
+        std::string sIncludeOptions;
+        std::string sExcludeOptions;
+        config.bReport = false;
+        // Commandline options (no config file)
+        options_cli_no_cfg.add_options()
+            ("help,h", "Print help message")
+            ("version", "Print version information")
+            ("login", bpo::value<bool>(&bLogin)->zero_tokens()->default_value(false), "Login")
+            ("list", bpo::value<bool>(&config.bList)->zero_tokens()->default_value(false), "List games")
+            ("list-details", bpo::value<bool>(&config.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info")
+            ("download", bpo::value<bool>(&config.bDownload)->zero_tokens()->default_value(false), "Download")
+            ("repair", bpo::value<bool>(&config.bRepair)->zero_tokens()->default_value(false), "Repair downloaded files\nUse --repair --download to redownload files when filesizes don't match (possibly different version). Redownload will rename the old file (appends .old to filename)")
+            ("game", bpo::value<std::string>(&config.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)\nAliases: \"all\", \"free\"\nAlias \"free\" doesn't work with cached details")
+            ("create-xml", bpo::value<std::string>(&config.sXMLFile)->implicit_value("automatic"), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation")
+            ("update-check", bpo::value<bool>(&config.bUpdateCheck)->zero_tokens()->default_value(false), "Check for update notifications")
+            ("check-orphans", bpo::value<std::string>(&config.sOrphanRegex)->implicit_value(""), check_orphans_text.c_str())
+            ("status", bpo::value<bool>(&config.bCheckStatus)->zero_tokens()->default_value(false), "Show status of files\n\nOutput format:\nstatuscode gamename filename filesize filehash\n\nStatus codes:\nOK - File is OK\nND - File is not downloaded\nMD5 - MD5 mismatch, different version\nFS - File size mismatch, incomplete download")
+            ("save-config", bpo::value<bool>(&config.bSaveConfig)->zero_tokens()->default_value(false), "Create config file with current settings")
+            ("reset-config", bpo::value<bool>(&config.bResetConfig)->zero_tokens()->default_value(false), "Reset config settings to default")
+            ("report", bpo::value<std::string>(&config.sReportFilePath)->implicit_value("lgogdownloader-report.log"), "Save report of downloaded/repaired files to specified file\nDefault filename: lgogdownloader-report.log")
+            ("update-cache", bpo::value<bool>(&config.bUpdateCache)->zero_tokens()->default_value(false), "Update game details cache")
+            ("no-platform-detection", bpo::value<bool>(&bNoPlatformDetection)->zero_tokens()->default_value(false), "Don't try to detect supported platforms from game shelf.\nSkips the initial fast platform detection and detects the supported platforms from game details which is slower but more accurate.\nUseful in case platform identifier is missing for some games in the game shelf.\nUsing --platform with --list doesn't work with this option.")
+            ("download-file", bpo::value<std::string>(&config.sFileIdString)->default_value(""), "Download files using fileid\n\nFormat:\n\"gamename/fileid\"\nor: \"gogdownloader://gamename/fileid\"\n\nMultiple files:\n\"gamename1/fileid1,gamename2/fileid2\"\nor: \"gogdownloader://gamename1/fileid1,gamename2/fileid2\"\n\nThis option ignores all subdir options. The files are downloaded to directory specified with --directory option.")
+            ("output-file,o", bpo::value<std::string>(&config.sOutputFilename)->default_value(""), "Set filename of file downloaded with --download-file.")
+            ("wishlist", bpo::value<bool>(&config.bShowWishlist)->zero_tokens()->default_value(false), "Show wishlist")
+            ("login-api", bpo::value<bool>(&config.bLoginAPI)->zero_tokens()->default_value(false), "Login (API only)")
+            ("login-website", bpo::value<bool>(&config.bLoginHTTP)->zero_tokens()->default_value(false), "Login (website only)")
+            ("cacert", bpo::value<std::string>(&config.sCACertPath)->default_value(""), "Path to CA certificate bundle in PEM format")
+            ("respect-umask", bpo::value<bool>(&config.bRespectUmask)->zero_tokens()->default_value(false), "Do not adjust permissions of sensitive files")
+        ;
+        // Commandline options (config file)
+        options_cli_cfg.add_options()
+            ("directory", bpo::value<std::string>(&config.sDirectory)->default_value("."), "Set download directory")
+            ("limit-rate", bpo::value<curl_off_t>(&config.iDownloadRate)->default_value(0), "Limit download rate to value in kB\n0 = unlimited")
+            ("xml-directory", bpo::value<std::string>(&config.sXMLDirectory), "Set directory for GOG XML files")
+            ("chunk-size", bpo::value<size_t>(&config.iChunkSize)->default_value(10), "Chunk size (in MB) when creating XML")
+            ("platform", bpo::value<std::string>(&sInstallerPlatform)->default_value("w+l"), platform_text.c_str())
+            ("language", bpo::value<std::string>(&sInstallerLanguage)->default_value("en"), language_text.c_str())
+            ("no-remote-xml", bpo::value<bool>(&bNoRemoteXML)->zero_tokens()->default_value(false), "Don't use remote XML for repair")
+            ("no-unicode", bpo::value<bool>(&bNoUnicode)->zero_tokens()->default_value(false), "Don't use Unicode in the progress bar")
+            ("no-color", bpo::value<bool>(&bNoColor)->zero_tokens()->default_value(false), "Don't use coloring in the progress bar or status messages")
+            ("no-duplicate-handling", bpo::value<bool>(&bNoDuplicateHandler)->zero_tokens()->default_value(false), "Don't use duplicate handler for installers\nDuplicate installers from different languages are handled separately")
+            ("no-subdirectories", bpo::value<bool>(&bNoSubDirectories)->zero_tokens()->default_value(false), "Don't create subdirectories for extras, patches and language packs")
+            ("verbose", bpo::value<bool>(&config.bVerbose)->zero_tokens()->default_value(false), "Print lots of information")
+            ("insecure", bpo::value<bool>(&bInsecure)->zero_tokens()->default_value(false), "Don't verify authenticity of SSL certificates")
+            ("timeout", bpo::value<long int>(&config.iTimeout)->default_value(10), "Set timeout for connection\nMaximum time in seconds that connection phase is allowed to take")
+            ("retries", bpo::value<int>(&config.iRetries)->default_value(3), "Set maximum number of retries on failed download")
+            ("wait", bpo::value<int>(&config.iWait)->default_value(0), "Time to wait between requests (milliseconds)")
+            ("cover-list", bpo::value<std::string>(&config.sCoverList)->default_value("https://raw.githubusercontent.com/Sude-/lgogdownloader-lists/master/covers.xml"), "Set URL for cover list")
+            ("subdir-installers", bpo::value<std::string>(&config.sInstallersSubdir)->default_value(""), ("Set subdirectory for extras" + subdir_help_text).c_str())
+            ("subdir-extras", bpo::value<std::string>(&config.sExtrasSubdir)->default_value("extras"), ("Set subdirectory for extras" + subdir_help_text).c_str())
+            ("subdir-patches", bpo::value<std::string>(&config.sPatchesSubdir)->default_value("patches"), ("Set subdirectory for patches" + subdir_help_text).c_str())
+            ("subdir-language-packs", bpo::value<std::string>(&config.sLanguagePackSubdir)->default_value("languagepacks"), ("Set subdirectory for language packs" + subdir_help_text).c_str())
+            ("subdir-dlc", bpo::value<std::string>(&config.sDLCSubdir)->default_value("dlc/%dlcname%"), ("Set subdirectory for dlc" + subdir_help_text).c_str())
+            ("subdir-game", bpo::value<std::string>(&config.sGameSubdir)->default_value("%gamename%"), ("Set subdirectory for game" + subdir_help_text).c_str())
+            ("use-cache", bpo::value<bool>(&config.bUseCache)->zero_tokens()->default_value(false), ("Use game details cache"))
+            ("cache-valid", bpo::value<int>(&config.iCacheValid)->default_value(2880), ("Set how long cached game details are valid (in minutes)\nDefault: 2880 minutes (48 hours)"))
+            ("save-serials", bpo::value<bool>(&config.bSaveSerials)->zero_tokens()->default_value(false), "Save serial numbers when downloading")
+            ("ignore-dlc-count", bpo::value<std::string>(&config.sIgnoreDLCCountRegex)->implicit_value(".*"), "Set regular expression filter for games to ignore DLC count information\nIgnoring DLC count information helps in situations where the account page doesn't provide accurate information about DLCs")
+            ("include", bpo::value<std::string>(&sIncludeOptions)->default_value("all"), ("Select what to download/list/repair\n" + include_options_text).c_str())
+            ("exclude", bpo::value<std::string>(&sExcludeOptions)->default_value("covers"), ("Select what not to download/list/repair\n" + include_options_text).c_str())
+            ("automatic-xml-creation", bpo::value<bool>(&config.bAutomaticXMLCreation)->zero_tokens()->default_value(false), "Automatically create XML data after download has completed")
+            ("save-changelogs", bpo::value<bool>(&config.bSaveChangelogs)->zero_tokens()->default_value(false), "Save changelogs when downloading")
+            ("threads", bpo::value<unsigned int>(&config.iThreads)->default_value(4), "Number of download threads")
+            ("dlc-list", bpo::value<std::string>(&config.sGameHasDLCList)->default_value("https://raw.githubusercontent.com/Sude-/lgogdownloader-lists/master/game_has_dlc.txt"), "Set URL for list of games that have DLC")
+        ;
+        // Options read from config file
+        options_cfg_only.add_options()
+            ("token", bpo::value<std::string>(&config.sToken)->default_value(""), "oauth token")
+            ("secret", bpo::value<std::string>(&config.sSecret)->default_value(""), "oauth secret")
+        ;
+
+        options_cli_all.add(options_cli_no_cfg).add(options_cli_cfg);
+        options_cfg_all.add(options_cfg_only).add(options_cli_cfg);
+
+        bpo::parsed_options parsed = bpo::parse_command_line(argc, argv, options_cli_all);
+        bpo::store(parsed, vm);
+        unrecognized_options_cli = bpo::collect_unrecognized(parsed.options, bpo::include_positional);
+        bpo::notify(vm);
+
+        if (vm.count("help"))
+        {
+            std::cout   << config.sVersionString << std::endl
+                        << options_cli_all << std::endl;
+            return 0;
+        }
+
+        if (vm.count("version"))
+        {
+            std::cout << VERSION_STRING << std::endl;
+            return 0;
+        }
+
+        // Create lgogdownloader directories
+        boost::filesystem::path path = config.sXMLDirectory;
+        if (!boost::filesystem::exists(path))
+        {
+            if (!boost::filesystem::create_directories(path))
+            {
+                std::cerr << "Failed to create directory: " << path << std::endl;
+                return 1;
+            }
+        }
+
+        path = config.sConfigDirectory;
+        if (!boost::filesystem::exists(path))
+        {
+            if (!boost::filesystem::create_directories(path))
+            {
+                std::cerr << "Failed to create directory: " << path << std::endl;
+                return 1;
+            }
+        }
+
+        path = config.sCacheDirectory;
+        if (!boost::filesystem::exists(path))
+        {
+            if (!boost::filesystem::create_directories(path))
+            {
+                std::cerr << "Failed to create directory: " << path << std::endl;
+                return 1;
+            }
+        }
+
+        if (boost::filesystem::exists(config.sConfigFilePath))
+        {
+            std::ifstream ifs(config.sConfigFilePath.c_str());
+            if (!ifs)
+            {
+                std::cerr << "Could not open config file: " << config.sConfigFilePath << std::endl;
+                return 1;
+            }
+            else
+            {
+                bpo::parsed_options parsed = bpo::parse_config_file(ifs, options_cfg_all, true);
+                bpo::store(parsed, vm);
+                bpo::notify(vm);
+                ifs.close();
+                unrecognized_options_cfg = bpo::collect_unrecognized(parsed.options, bpo::include_positional);
+            }
+        }
+        if (boost::filesystem::exists(config.sBlacklistFilePath))
+        {
+            std::ifstream ifs(config.sBlacklistFilePath.c_str());
+            if (!ifs)
+            {
+                std::cerr << "Could not open blacklist file: " << config.sBlacklistFilePath << std::endl;
+                return 1;
+            }
+            else
+            {
+                std::string line;
+                std::vector<std::string> lines;
+                while (!ifs.eof())
+                {
+                    std::getline(ifs, line);
+                    lines.push_back(std::move(line));
+                }
+                config.blacklist.initialize(lines);
+            }
+        }
+
+        if (boost::filesystem::exists(config.sIgnorelistFilePath))
+        {
+            std::ifstream ifs(config.sIgnorelistFilePath.c_str());
+            if (!ifs)
+            {
+                std::cerr << "Could not open ignorelist file: " << config.sIgnorelistFilePath << std::endl;
+                return 1;
+            }
+            else
+            {
+                std::string line;
+                std::vector<std::string> lines;
+                while (!ifs.eof())
+                {
+                    std::getline(ifs, line);
+                    lines.push_back(std::move(line));
+                }
+                config.ignorelist.initialize(lines);
+            }
+        }
+
+        if (config.sIgnoreDLCCountRegex.empty())
+        {
+            if (boost::filesystem::exists(config.sGameHasDLCListFilePath))
+            {
+                std::ifstream ifs(config.sGameHasDLCListFilePath.c_str());
+                if (!ifs)
+                {
+                    std::cerr << "Could not open list of games that have dlc: " << config.sGameHasDLCListFilePath << std::endl;
+                    return 1;
+                }
+                else
+                {
+                    std::string line;
+                    std::vector<std::string> lines;
+                    while (!ifs.eof())
+                    {
+                        std::getline(ifs, line);
+                        lines.push_back(std::move(line));
+                    }
+                    config.gamehasdlc.initialize(lines);
+                }
+            }
+        }
+
+        if (vm.count("chunk-size"))
+            config.iChunkSize <<= 20; // Convert chunk size from bytes to megabytes
+
+        if (vm.count("limit-rate"))
+            config.iDownloadRate <<= 10; // Convert download rate from bytes to kilobytes
+
+        if (vm.count("check-orphans"))
+            if (config.sOrphanRegex.empty())
+                config.sOrphanRegex = orphans_regex_default;
+
+        if (vm.count("report"))
+            config.bReport = true;
+
+        if (config.iWait > 0)
+            config.iWait *= 1000;
+
+        if (config.iThreads < 1)
+        {
+            config.iThreads = 1;
+            set_vm_value(vm, "threads", config.iThreads);
+        }
+
+        config.bVerifyPeer = !bInsecure;
+        config.bColor = !bNoColor;
+        config.bUnicode = !bNoUnicode;
+        config.bDuplicateHandler = !bNoDuplicateHandler;
+        config.bRemoteXML = !bNoRemoteXML;
+        config.bSubDirectories = !bNoSubDirectories;
+        config.bPlatformDetection = !bNoPlatformDetection;
+
+        for (auto i = unrecognized_options_cli.begin(); i != unrecognized_options_cli.end(); ++i)
+            if (i->compare(0, GlobalConstants::PROTOCOL_PREFIX.length(), GlobalConstants::PROTOCOL_PREFIX) == 0)
+                config.sFileIdString = *i;
+
+        if (!config.sFileIdString.empty())
+        {
+            if (config.sFileIdString.compare(0, GlobalConstants::PROTOCOL_PREFIX.length(), GlobalConstants::PROTOCOL_PREFIX) == 0)
+            {
+                config.sFileIdString.replace(0, GlobalConstants::PROTOCOL_PREFIX.length(), "");
+            }
+            vFileIdStrings = Util::tokenize(config.sFileIdString, ",");
+        }
+
+        if (!config.sOutputFilename.empty() && vFileIdStrings.size() > 1)
+        {
+            std::cerr << "Cannot specify an output file name when downloading multiple files." << std::endl;
+            return 1;
+        }
+
+        if (bLogin)
+        {
+            config.bLoginAPI = true;
+            config.bLoginHTTP = true;
+        }
+
+        if (config.sXMLFile == "automatic")
+            config.bAutomaticXMLCreation = true;
+
+        Util::parseOptionString(sInstallerLanguage, config.vLanguagePriority, config.iInstallerLanguage, GlobalConstants::LANGUAGES);
+        Util::parseOptionString(sInstallerPlatform, config.vPlatformPriority, config.iInstallerPlatform, GlobalConstants::PLATFORMS);
+
+        unsigned int include_value = 0;
+        unsigned int exclude_value = 0;
+        std::vector<std::string> vInclude = Util::tokenize(sIncludeOptions, ",");
+        std::vector<std::string> vExclude = Util::tokenize(sExcludeOptions, ",");
+        for (std::vector<std::string>::iterator it = vInclude.begin(); it != vInclude.end(); it++)
+        {
+            include_value |= Util::getOptionValue(*it, INCLUDE_OPTIONS);
+        }
+        for (std::vector<std::string>::iterator it = vExclude.begin(); it != vExclude.end(); it++)
+        {
+            exclude_value |= Util::getOptionValue(*it, INCLUDE_OPTIONS);
+        }
+        config.iInclude = include_value & ~exclude_value;
+
+        // Assign values
+        // TODO: Use config.iInclude in Downloader class directly and get rid of this value assignment
+        config.bCover = (config.iInclude & OPTION_COVERS);
+        config.bInstallers = (config.iInclude & OPTION_INSTALLERS);
+        config.bExtras = (config.iInclude & OPTION_EXTRAS);
+        config.bPatches = (config.iInclude & OPTION_PATCHES);
+        config.bLanguagePacks = (config.iInclude & OPTION_LANGPACKS);
+        config.bDLC = (config.iInclude & OPTION_DLCS);
+    }
+    catch (std::exception& e)
+    {
+        std::cerr << "Error: " << e.what() << std::endl;
+        return 1;
+    }
+    catch (...)
+    {
+        std::cerr << "Exception of unknown type!" << std::endl;
+        return 1;
+    }
+
+    if (config.iInstallerPlatform < GlobalConstants::PLATFORMS[0].id || config.iInstallerPlatform > platform_all)
+    {
+        std::cerr << "Invalid value for --platform" << std::endl;
+        return 1;
+    }
+
+    if (config.iInstallerLanguage < GlobalConstants::LANGUAGES[0].id || config.iInstallerLanguage > language_all)
+    {
+        std::cerr << "Invalid value for --language" << std::endl;
+        return 1;
+    }
+
+    if (!config.sXMLDirectory.empty())
+    {
+        // Make sure that xml directory doesn't have trailing slash
+        if (config.sXMLDirectory.at(config.sXMLDirectory.length()-1)=='/')
+            config.sXMLDirectory.assign(config.sXMLDirectory.begin(),config.sXMLDirectory.end()-1);
+    }
+
+    // Create GOG XML for a file
+    if (!config.sXMLFile.empty() && (config.sXMLFile != "automatic"))
+    {
+        Util::createXML(config.sXMLFile, config.iChunkSize, config.sXMLDirectory);
+        return 0;
+    }
+
+    // Make sure that directory has trailing slash
+    if (!config.sDirectory.empty())
+    {
+        if (config.sDirectory.at(config.sDirectory.length()-1)!='/')
+            config.sDirectory += "/";
+    }
+    else
+    {
+        config.sDirectory = "./"; // Directory wasn't specified, use current directory
+    }
+
+    // CA certificate bundle
+    if (config.sCACertPath.empty())
+    {
+        // Use CURL_CA_BUNDLE environment variable for CA certificate path if it is set
+        char *ca_bundle = getenv("CURL_CA_BUNDLE");
+        if (ca_bundle)
+            config.sCACertPath = (std::string)ca_bundle;
+    }
+
+    if (!unrecognized_options_cfg.empty() && (!config.bSaveConfig || !config.bResetConfig))
+    {
+        std::cerr << "Unrecognized options in " << config.sConfigFilePath << std::endl;
+        for (unsigned int i = 0; i < unrecognized_options_cfg.size(); i+=2)
+        {
+            std::cerr << unrecognized_options_cfg[i] << " = " << unrecognized_options_cfg[i+1] << std::endl;
+        }
+        std::cerr << std::endl;
+    }
+
+    // Init curl globally
+    ssl_thread_setup();
+    curl_global_init(CURL_GLOBAL_ALL);
+
+    if (config.bLoginAPI)
+    {
+        config.sToken = "";
+        config.sSecret = "";
+    }
+
+    Downloader downloader(config);
+
+    int iLoginTries = 0;
+    bool bLoginOK = false;
+
+    // Login because --login, --login-api or --login-website was used
+    if (config.bLoginAPI || config.bLoginHTTP)
+        bLoginOK = downloader.login();
+
+    bool bIsLoggedin = downloader.isLoggedIn();
+
+    // Login because we are not logged in
+    while (iLoginTries++ < config.iRetries && !bIsLoggedin)
+    {
+        bLoginOK = downloader.login();
+        if (bLoginOK)
+        {
+            bIsLoggedin = downloader.isLoggedIn();
+        }
+    }
+
+    // Login failed, cleanup
+    if (!bLoginOK && !bIsLoggedin)
+    {
+        curl_global_cleanup();
+        ssl_thread_cleanup();
+        return 1;
+    }
+
+    // Make sure that config file and cookie file are only readable/writable by owner
+    if (!config.bRespectUmask)
+    {
+        Util::setFilePermissions(config.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+        Util::setFilePermissions(config.sCookiePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+    }
+
+    if (config.bSaveConfig || bLoginOK)
+    {
+        if (bLoginOK)
+        {
+            set_vm_value(vm, "token", downloader.config.sToken);
+            set_vm_value(vm, "secret", downloader.config.sSecret);
+        }
+        std::ofstream ofs(config.sConfigFilePath.c_str());
+        if (ofs)
+        {
+            std::cerr << "Saving config: " << config.sConfigFilePath << std::endl;
+            for (bpo::variables_map::iterator it = vm.begin(); it != vm.end(); ++it)
+            {
+                std::string option = it->first;
+                std::string option_value_string;
+                const bpo::variable_value& option_value = it->second;
+
+                try
+                {
+                    if (options_cfg_all.find(option, false).long_name() == option)
+                    {
+                        if (!option_value.empty())
+                        {
+                            const std::type_info& type = option_value.value().type() ;
+                            if ( type == typeid(std::string) )
+                               option_value_string = option_value.as<std::string>();
+                            else if ( type == typeid(int) )
+                                 option_value_string = std::to_string(option_value.as<int>());
+                            else if ( type == typeid(size_t) )
+                                option_value_string = std::to_string(option_value.as<size_t>());
+                            else if ( type == typeid(unsigned int) )
+                                option_value_string = std::to_string(option_value.as<unsigned int>());
+                            else if ( type == typeid(long int) )
+                                option_value_string = std::to_string(option_value.as<long int>());
+                            else if ( type == typeid(bool) )
+                            {
+                                if (option_value.as<bool>() == true)
+                                    option_value_string = "true";
+                                else
+                                    option_value_string = "false";
+                            }
+                        }
+                    }
+                }
+                catch (...)
+                {
+                    continue;
+                }
+
+                if (!option_value_string.empty())
+                {
+                    ofs << option << " = " << option_value_string << std::endl;
+                }
+            }
+            ofs.close();
+            if (!config.bRespectUmask)
+                Util::setFilePermissions(config.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+            if (config.bSaveConfig)
+            {
+                curl_global_cleanup();
+                ssl_thread_cleanup();
+                return 0;
+            }
+        }
+        else
+        {
+            std::cerr << "Failed to create config: " << config.sConfigFilePath << std::endl;
+            curl_global_cleanup();
+            ssl_thread_cleanup();
+            return 1;
+        }
+    }
+    else if (config.bResetConfig)
+    {
+        std::ofstream ofs(config.sConfigFilePath.c_str());
+        if (ofs)
+        {
+            if (!config.sToken.empty() && !config.sSecret.empty())
+            {
+                ofs << "token = " << config.sToken << std::endl;
+                ofs << "secret = " << config.sSecret << std::endl;
+            }
+            ofs.close();
+            if (!config.bRespectUmask)
+                Util::setFilePermissions(config.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+
+            curl_global_cleanup();
+            ssl_thread_cleanup();
+            return 0;
+        }
+        else
+        {
+            std::cerr << "Failed to create config: " << config.sConfigFilePath << std::endl;
+            curl_global_cleanup();
+            ssl_thread_cleanup();
+            return 1;
+        }
+    }
+
+    bool bInitOK = downloader.init();
+    if (!bInitOK)
+    {
+        curl_global_cleanup();
+        ssl_thread_cleanup();
+        return 1;
+    }
+
+    int res = 0;
+
+    if (config.bShowWishlist)
+        downloader.showWishlist();
+    else if (config.bUpdateCache)
+        downloader.updateCache();
+    else if (config.bUpdateCheck) // Update check has priority over download and list
+        downloader.updateCheck();
+    else if (!vFileIdStrings.empty())
+    {
+        for (std::vector<std::string>::iterator it = vFileIdStrings.begin(); it != vFileIdStrings.end(); it++)
+        {
+            res |= downloader.downloadFileWithId(*it, config.sOutputFilename) ? 1 : 0;
+        }
+    }
+    else if (config.bRepair) // Repair file
+        downloader.repair();
+    else if (config.bDownload) // Download games
+        downloader.download();
+    else if (config.bListDetails || config.bList) // Detailed list of games/extras
+        res = downloader.listGames();
+    else if (!config.sOrphanRegex.empty()) // Check for orphaned files if regex for orphans is set
+        downloader.checkOrphans();
+    else if (config.bCheckStatus)
+        downloader.checkStatus();
+    else
+    {
+        if (!(config.bLoginAPI || config.bLoginHTTP))
+        {
+            // Show help message
+            std::cerr   << config.sVersionString << std::endl
+                        << options_cli_all << std::endl;
+        }
+    }
+
+    // Orphan check was called at the same time as download. Perform it after download has finished
+    if (!config.sOrphanRegex.empty() && config.bDownload)
+        downloader.checkOrphans();
+
+    curl_global_cleanup();
+    ssl_thread_cleanup();
+
+    return res;
+}
diff --git a/man/CMakeLists.txt b/man/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7f123b0
--- /dev/null
@@ -0,0 +1,22 @@
+find_program(HELP2MAN help2man DOC "Location of the help2man program")
+find_program(GZIP gzip DOC "Location of the gzip program")
+mark_as_advanced(HELP2MAN)
+mark_as_advanced(GZIP)
+
+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)
+  set(MAN_FILE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.1.gz)
+  add_custom_command(
+    OUTPUT ${MAN_FILE}
+    COMMAND ${HELP2MAN} -N -i ${H2M_FILE} -o ${MAN_PAGE} ${PROJECT_BINARY_DIR}/${PROJECT_NAME}${CMAKE_EXECUTABLE_SUFFIX}
+    COMMAND ${GZIP} -f -9 ${MAN_PAGE}
+    MAIN_DEPENDENCY ${H2M_FILE}
+       COMMENT "Building man page"
+       VERBATIM
+       )
+  add_custom_target(manpage ALL DEPENDS ${MAN_FILE} ${PROJECT_NAME})
+  install(FILES ${MAN_FILE} DESTINATION ${INSTALL_SHARE_DIR}/man/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
new file mode 100644 (file)
index 0000000..d3d4800
--- /dev/null
@@ -0,0 +1,123 @@
+[synopsis]
+.B lgogdownloader
+[\fIOPTION\fP]...
+
+[description]
+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
+/--no-installers/
+.fi
+
+/Status codes:/
+.nf
+
+[blacklist]
+.fi
+Allows user to specify individual files that should not be downloaded or mentioned as orphans.
+.sp 1
+Each line in the file specifies one blacklist expression, except for empty lines and lines starting with #.
+First few characters specify blacklist item type and flags.
+So far, only regular expression (perl variant) are supported, so each line must start with "Rp" characters.
+After a space comes the expression itself. Expressions are matched against file path relative to what was specified as \fI--directory\fP.
+
+\fIExample black list\fP
+.br
+# used to store manually downloaded mods/patches/maps/, don't mention it as orphans
+.br
+Rp ^[^/]*/manual/.*
+.br
+# included with every *divinity game, once is enough
+.br
+Rp beyond_divinity/extras/bd_ladymageknight\.zip
+.br
+Rp divinity_2_developers_cut/extras/divinity_2_ladymageknight\.zip
+.sp
+# extra 6GB is A LOT of space if you don't actually plan to mod your game
+.br
+Rp the_witcher_2/extras/the_witcher_2_redkit\.zip
+.br
+Rp the_witcher_2/extras/extras_pack_3_hu_pl_ru_tr_zh_\.zip
+.br
+Rp the_witcher_2/extras/extras_pack_2_fr_it_jp_\.zip
+
+[files]
+.fi
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/\fP
+Storage for configuration files and cookies
+.br
+If \fB$XDG_CONFIG_HOME\fP is not set, it will use \fI$HOME/.config/lgogdownloader/\fP.
+
+.TP
+\fI$XDG_CACHE_HOME/lgogdownloader/xml/\fP
+Storage for XML files
+.br
+If \fB$XDG_CACHE_HOME\fP is not set, it will use \fI$HOME/.cache/lgogdownloader/xml/\fP.
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/blacklist.txt\fP
+Allows user to specify individual files that should not be downloaded.
+.br
+It doesn't have to exist, but if it does exist, it must be readable to lgogdownloader.
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/ignorelist.txt\fP
+Allows user to specify individual files that should not be mentioned
+as orphans.  The file has the same format and interpretation as a
+blacklist.
+.br
+It doesn't have to exist, but if it does exist, it must be readable to lgogdownloader.
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/game_has_dlc.txt\fP
+Allows user to specify which games have dlc and should have their DLC count
+information ignored. The file has the same format and interpretation as a
+blacklist.
+.br
+It doesn't have to exist, but if it does exist, it must be readable to lgogdownloader.
+.br
+If the file exists lgogdownloader uses it instead of list specified with
+\fB--dlc-list\fP option
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/gamespecific/gamename.conf\fP
+JSON formatted file. Sets game specific settings for \fBgamename\fP.
+.br
+Allowed settings are \fBlanguage\fP, \fBplatform\fP, \fBdlc\fP, \fBignore-dlc-count\fP \fBsubdirectories\fP, \fBdirectory\fP, \fBsubdir-game\fP, \fBsubdir-installers\fP, \fBsubdir-extras\fP, \fBsubdir-patches\fP, \fBsubdir-language-packs\fP and \fBsubdir-dlc\fP.
+.br
+The \fBdlc\fP option is limited to disabling DLC for specific game. It can't enable DLC listing/downloading if \fB--no-dlc\fP option is used.
+.br
+Must be in the following format:
+.br
+{
+    "language" : <string>,
+    "platform" : <string>,
+    "dlc" : <bool>,
+    "ignore-dlc-count" : <bool>,
+    "subdirectories" : <bool>,
+    "directory" : <string>,
+    "subdir-game" : <string>,
+    "subdir-installers" : <string>,
+    "subdir-extras" : <string>,
+    "subdir-patches" : <string>,
+    "subdir-language-packs" : <string>,
+    "subdir-dlc" : <string>
+.br
+}
+
+[priorities]
+Separating values with "," when using \fBlanguage\fP and \fBplatform\fP switches enables a priority-based mode: only the first matching one will be downloaded.
+.PP
+For example, setting \fBlanguage\fP to \fBfr+en\fP means both French and English will be downloaded (if available) for all games. Setting \fBlanguage\fP to \fBfr,en\fP means that the French version (and only that one) will be downloaded if available, and if not, the English version will be downloaded.
+.PP
+You're allowed to "stack" codes in the priority string if needed. If you set \fBlanguage\fP to \fBes+fr,en\fP it means it'll download both Spanish (es) and French (fr) versions if they are available, and the English (en) one only if none of French and Spanish are available.
+
+[availability]
+The latest version of this distribution is available from \fIhttps://github.com/Sude-/lgogdownloader\fP
+
diff --git a/src/api.cpp b/src/api.cpp
new file mode 100644 (file)
index 0000000..f465ce1
--- /dev/null
@@ -0,0 +1,706 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "api.h"
+#include "gamefile.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <sstream>
+#include <json/json.h>
+
+#if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) >= 40900
+#   define _regex_namespace_ std
+#   include <regex>
+#else
+#   define _regex_namespace_ boost
+#   include <boost/regex.hpp>
+#endif
+
+size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) {
+    std::ostringstream *stream = (std::ostringstream*)userp;
+    std::streamsize count = (std::streamsize) size * nmemb;
+    stream->write(ptr, count);
+    return count;
+}
+
+API::API(const std::string& token, const std::string& secret)
+{
+    curlhandle = curl_easy_init();
+    curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_PROGRESSDATA, this);
+    curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+    curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
+
+    this->error = false;
+    this->config.oauth_token = token;
+    this->config.oauth_secret = secret;
+}
+
+/* Initialize the API
+    returns 0 if failed
+    returns 1 if successful
+*/
+int API::init()
+{
+    int res = 0;
+
+    this->getAPIConfig();
+
+    if (!this->getError())
+        res = 1;
+    else
+        this->clearError();
+
+    return res;
+}
+
+/* Login check
+    returns false if not logged in
+    returns true if logged in
+*/
+bool API::isLoggedIn()
+{
+    int res = 0;
+
+    // Check if we already have token and secret
+    if (!this->config.oauth_token.empty() && !this->config.oauth_secret.empty())
+    {
+        // Test authorization by getting user details
+        res = this->getUserDetails(); // res = 1 if successful
+    }
+
+    return res;
+}
+
+int API::getAPIConfig()
+{
+    std::string url = "https://api.gog.com/downloader2/status/stable/"; // Stable API
+    //std::string url = "https://api.gog.com/downloader2/status/beta/"; // Beta API
+    //std::string url = "https://api.gog.com/downloader2/status/e77989ed21758e78331b20e477fc5582/"; // Development API? Not sure because the downloader version number it reports is lower than beta.
+    int res = 0;
+
+    std::string json = this->getResponse(url);
+
+    if (!json.empty())
+    {
+        Json::Value root;
+        Json::Reader *jsonparser = new Json::Reader;
+        if (jsonparser->parse(json, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getAPIConfig)" << std::endl << root << std::endl;
+            #endif
+            this->config.oauth_authorize_temp_token = root["config"]["oauth_authorize_temp_token"].asString() + "/";
+            this->config.oauth_get_temp_token = root["config"]["oauth_get_temp_token"].asString() + "/";
+            this->config.oauth_get_token = root["config"]["oauth_get_token"].asString() + "/";
+            this->config.get_user_games = root["config"]["get_user_games"].asString() + "/";
+            this->config.get_user_details = root["config"]["get_user_details"].asString() + "/";
+            this->config.get_installer_link = root["config"]["get_installer_link"].asString() + "/";
+            this->config.get_game_details = root["config"]["get_game_details"].asString() + "/";
+            this->config.get_extra_link = root["config"]["get_extra_link"].asString() + "/";
+            this->config.set_app_status = root["config"]["set_app_status"].asString() + "/";
+            res = 1;
+        }
+        else
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getAPIConfig)" << std::endl << json << std::endl;
+            #endif
+            this->setError(jsonparser->getFormattedErrorMessages());
+            res = 0;
+        }
+        delete jsonparser;
+    }
+    else
+    {
+        this->setError("Found nothing in " + url);
+        res = 0;
+    }
+
+    return res;
+}
+
+int API::login(const std::string& email, const std::string& password)
+{
+    int res = 0;
+    std::string url;
+
+    std::string token, secret;
+
+    // Get temporary request token
+    url = oauth_sign_url2(this->config.oauth_get_temp_token.c_str(), NULL, OA_HMAC, NULL, CONSUMER_KEY.c_str(), CONSUMER_SECRET.c_str(), NULL /* token */, NULL /* secret */);
+
+    std::string request_token_resp = this->getResponse(url);
+
+    char **rv = NULL;
+    int rc = oauth_split_url_parameters(request_token_resp.c_str(), &rv);
+    qsort(rv, rc, sizeof(char *), oauth_cmpstringp);
+    if (rc == 3 && !strncmp(rv[1], "oauth_token=", OAUTH_TOKEN_LENGTH) && !strncmp(rv[2], "oauth_token_secret=", OAUTH_SECRET_LENGTH)) {
+        token = rv[1]+OAUTH_TOKEN_LENGTH+1;
+        secret = rv[2]+OAUTH_SECRET_LENGTH+1;
+        rv = NULL;
+    }
+    else
+    {
+        return res;
+    }
+
+    // Authorize temporary token and get verifier
+    url = this->config.oauth_authorize_temp_token + "?username=" + oauth_url_escape(email.c_str()) + "&password=" + oauth_url_escape(password.c_str());
+    url = oauth_sign_url2(url.c_str(), NULL, OA_HMAC, NULL, CONSUMER_KEY.c_str(), CONSUMER_SECRET.c_str(), token.c_str(), secret.c_str());
+    std::string authorize_resp = this->getResponse(url);
+
+    std::string verifier;
+    rc = oauth_split_url_parameters(authorize_resp.c_str(), &rv);
+    qsort(rv, rc, sizeof(char *), oauth_cmpstringp);
+    if (rc == 2 && !strncmp(rv[1], "oauth_verifier=", OAUTH_VERIFIER_LENGTH)) {
+        verifier = rv[1]+OAUTH_VERIFIER_LENGTH+1;
+        rv = NULL;
+    }
+    else
+    {
+        return res;
+    }
+
+    // Get final token and secret
+    url = this->config.oauth_get_token + "?oauth_verifier=" + verifier;
+    url = oauth_sign_url2(url.c_str(), NULL, OA_HMAC, NULL, CONSUMER_KEY.c_str(), CONSUMER_SECRET.c_str(), token.c_str(), secret.c_str());
+    std::string token_resp = this->getResponse(url);
+
+    rc = oauth_split_url_parameters(token_resp.c_str(), &rv);
+    qsort(rv, rc, sizeof(char *), oauth_cmpstringp);
+    if (rc == 2 && !strncmp(rv[0], "oauth_token=", OAUTH_TOKEN_LENGTH) && !strncmp(rv[1], "oauth_token_secret=", OAUTH_SECRET_LENGTH)) {
+        this->config.oauth_token = rv[0]+OAUTH_TOKEN_LENGTH+1;
+        this->config.oauth_secret = rv[1]+OAUTH_SECRET_LENGTH+1;
+        free(rv);
+        res = 1;
+    }
+
+    return res;
+}
+
+int API::getUserDetails()
+{
+    int res = 0;
+    std::string url;
+
+    url = this->config.get_user_details;
+    std::string json = this->getResponseOAuth(url);
+
+    if (!json.empty())
+    {
+        Json::Value root;
+        Json::Reader *jsonparser = new Json::Reader;
+        if (jsonparser->parse(json, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getUserDetails)" << std::endl << root << std::endl;
+            #endif
+            this->user.id = std::stoull(root["user"]["id"].asString());
+            this->user.username = root["user"]["xywka"].asString();
+            this->user.email = root["user"]["email"].asString();
+            this->user.avatar_big = root["user"]["avatar"]["big"].asString();
+            this->user.avatar_small = root["user"]["avatar"]["small"].asString();
+            this->user.notifications_forum = root["user"]["notifications"]["forum"].isInt() ? root["user"]["notifications"]["forum"].asInt() : std::stoi(root["user"]["notifications"]["forum"].asString());
+            this->user.notifications_games = root["user"]["notifications"]["games"].isInt() ? root["user"]["notifications"]["games"].asInt() : std::stoi(root["user"]["notifications"]["games"].asString());
+            this->user.notifications_messages = root["user"]["notifications"]["messages"].isInt() ? root["user"]["notifications"]["messages"].asInt() : std::stoi(root["user"]["notifications"]["messages"].asString());
+            res = 1;
+        }
+        else
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getUserDetails)" << std::endl << json << std::endl;
+            #endif
+            this->setError(jsonparser->getFormattedErrorMessages());
+            res = 0;
+        }
+        delete jsonparser;
+    }
+    else
+    {
+        this->setError("Found nothing in " + url);
+        res = 0;
+    }
+
+    return res;
+}
+
+
+int API::getGames()
+{
+    // Not implemented on the server side currently
+
+    //std::string json = this->getResponseOAuth(this->config.get_user_games);
+
+    return 0;
+}
+
+std::string API::getResponse(const std::string& url)
+{
+    #ifdef DEBUG
+        std::cerr << "DEBUG INFO (API::getResponse)" << std::endl << "URL: " << url << std::endl;
+    #endif
+    std::ostringstream memory;
+
+    curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+    CURLcode result = curl_easy_perform(curlhandle);
+    std::string response = memory.str();
+    memory.str(std::string());
+
+    if (result == CURLE_HTTP_RETURNED_ERROR)
+    {
+        long int response_code = 0;
+        result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+        if (result == CURLE_OK)
+            this->setError("HTTP ERROR: " + std::to_string(response_code) + " (" + url + ")");
+        else
+            this->setError("HTTP ERROR: failed to get error code: " + static_cast<std::string>(curl_easy_strerror(result)) + " (" + url + ")");
+
+        #ifdef DEBUG
+            curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, false);
+            result = curl_easy_perform(curlhandle);
+            std::string debug_response = memory.str();
+            memory.str(std::string());
+            std::cerr << "Response (CURLE_HTTP_RETURNED_ERROR):";
+            if (debug_response.empty())
+                std::cerr << " Response was empty" << std::endl;
+            else
+                std::cerr << std::endl << debug_response << std::endl;
+            curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+        #endif
+    }
+
+    return response;
+}
+
+std::string API::getResponseOAuth(const std::string& url)
+{
+    #ifdef DEBUG
+        std::cerr << "DEBUG INFO (API::getResponseOAuth)" << std::endl << "URL: " << url << std::endl;
+    #endif
+    std::string url_oauth = oauth_sign_url2(url.c_str(), NULL, OA_HMAC, NULL, CONSUMER_KEY.c_str(), CONSUMER_SECRET.c_str(), this->config.oauth_token.c_str(), this->config.oauth_secret.c_str());
+    std::string response = this->getResponse(url_oauth);
+
+    return response;
+}
+
+gameDetails API::getGameDetails(const std::string& game_name, const unsigned int& platform, const unsigned int& lang, const bool& useDuplicateHandler)
+{
+    std::string url;
+    gameDetails game;
+    struct gameFileInfo
+    {
+        Json::Value jsonNode;
+        unsigned int platform;
+        unsigned int language;
+    };
+
+    url = this->config.get_game_details + game_name + "/" + "installer_win_en"; // can't get game details without file id, any file id seems to return all details which is good for us
+    std::string json = this->getResponseOAuth(url);
+
+    if (!json.empty())
+    {
+        Json::Value root;
+        Json::Reader *jsonparser = new Json::Reader;
+        if (jsonparser->parse(json, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getGameDetails)" << std::endl << root << std::endl;
+            #endif
+            game.gamename = game_name;
+            game.title = root["game"]["title"].asString();
+            game.icon = root["game"]["icon"].asString();
+            std::vector<std::string> membernames = root["game"].getMemberNames();
+
+            // Installer details
+            // Create a list of installers from JSON
+            std::vector<gameFileInfo> installers;
+            for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+            {   // Check against the specified platforms
+                if (platform & GlobalConstants::PLATFORMS[i].id)
+                {
+                    std::string installer = "installer_" + GlobalConstants::PLATFORMS[i].code + "_";
+                    for (unsigned int j = 0; j < GlobalConstants::LANGUAGES.size(); ++j)
+                    {   // Check against the specified languages
+                        if (lang & GlobalConstants::LANGUAGES[j].id)
+                        {   // Make sure that the installer exists in the JSON
+                            if (root["game"].isMember(installer+GlobalConstants::LANGUAGES[j].code))
+                            {
+                                gameFileInfo installerInfo;
+                                installerInfo.jsonNode = root["game"][installer+GlobalConstants::LANGUAGES[j].code];
+                                installerInfo.platform = GlobalConstants::PLATFORMS[i].id;
+                                installerInfo.language = GlobalConstants::LANGUAGES[j].id;
+                                installers.push_back(installerInfo);
+                            }
+                        }
+                    }
+                }
+            }
+
+            for ( unsigned int i = 0; i < installers.size(); ++i )
+            {
+                for ( unsigned int index = 0; index < installers[i].jsonNode.size(); ++index )
+                {
+                    Json::Value installer = installers[i].jsonNode[index];
+                    unsigned int language = installers[i].language;
+
+                    // 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
+                    if (useDuplicateHandler)
+                    {
+                        bool bDuplicate = false;
+                        for (unsigned int j = 0; j < game.installers.size(); ++j)
+                        {
+                            if (game.installers[j].path == installer["link"].asString())
+                            {
+                                game.installers[j].language |= language; // Add language code to installer
+                                bDuplicate = true;
+                                break;
+                            }
+                        }
+                        if (bDuplicate)
+                            continue;
+                    }
+
+                    gameFile gf;
+                    gf.type = GFTYPE_INSTALLER;
+                    gf.gamename = game.gamename;
+                    gf.updated = installer["notificated"].isInt() ? installer["notificated"].asInt() : std::stoi(installer["notificated"].asString());
+                    gf.id = installer["id"].isInt() ? std::to_string(installer["id"].asInt()) : installer["id"].asString();
+                    gf.name = installer["name"].asString();
+                    gf.path = installer["link"].asString();
+                    gf.size = installer["size"].asString();
+                    gf.language = language;
+                    gf.platform = installers[i].platform;
+                    gf.silent = installer["silent"].isInt() ? installer["silent"].asInt() : std::stoi(installer["silent"].asString());
+
+                    game.installers.push_back(gf);
+                }
+            }
+
+            // Extra details
+            const Json::Value extras = root["game"]["extras"];
+            for ( unsigned int index = 0; index < extras.size(); ++index )
+            {
+                Json::Value extra = extras[index];
+
+                gameFile gf;
+                gf.type = GFTYPE_EXTRA;
+                gf.gamename = game.gamename;
+                gf.updated = false; // extras don't have "updated" flag
+                gf.id = extra["id"].isInt() ? std::to_string(extra["id"].asInt()) : extra["id"].asString();
+                gf.name = extra["name"].asString();
+                gf.path = extra["link"].asString();
+                gf.size = extra["size_mb"].asString();
+
+                game.extras.push_back(gf);
+            }
+
+            // Patch details
+            for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+            {   // Check against the specified languages
+                if (lang & GlobalConstants::LANGUAGES[i].id)
+                {
+                    // Try to find a patch
+                    _regex_namespace_::regex re(GlobalConstants::LANGUAGES[i].code + "\\d+patch\\d+", _regex_namespace_::regex_constants::icase); // regex for patch node names
+                    std::vector<gameFileInfo> patches;
+                    for (unsigned int j = 0; j < membernames.size(); ++j)
+                    {
+                        if (_regex_namespace_::regex_match(membernames[j], re))
+                        {   // Regex matches, we have a patch node
+                            gameFileInfo patchInfo;
+                            patchInfo.jsonNode = root["game"][membernames[j]];
+                            patchInfo.language = GlobalConstants::LANGUAGES[i].id;
+                            if (patchInfo.jsonNode["link"].asString().find("/mac/") != std::string::npos)
+                                patchInfo.platform = GlobalConstants::PLATFORM_MAC;
+                            else if (patchInfo.jsonNode["link"].asString().find("/linux/") != std::string::npos)
+                                patchInfo.platform = GlobalConstants::PLATFORM_LINUX;
+                            else
+                                patchInfo.platform = GlobalConstants::PLATFORM_WINDOWS;
+
+                            if (platform & patchInfo.platform)
+                                patches.push_back(patchInfo);
+                        }
+                    }
+
+                    if (!patches.empty()) // found at least one patch
+                    {
+                        for (unsigned int j = 0; j < patches.size(); ++j)
+                        {
+                            Json::Value patchnode = patches[j].jsonNode;
+                            if (patchnode.isArray()) // Patch has multiple files
+                            {
+                                for ( unsigned int index = 0; index < patchnode.size(); ++index )
+                                {
+                                    Json::Value patch = patchnode[index];
+
+                                    // 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 j = 0; j < game.patches.size(); ++j)
+                                        {
+                                            if (game.patches[j].path == patch["link"].asString())
+                                            {
+                                                game.patches[j].language |= GlobalConstants::LANGUAGES[i].id; // Add language code to patch
+                                                bDuplicate = true;
+                                                break;
+                                            }
+                                        }
+                                        if (bDuplicate)
+                                            continue;
+                                    }
+
+                                    gameFile gf;
+                                    gf.type = GFTYPE_PATCH;
+                                    gf.gamename = game.gamename;
+                                    gf.updated = patch["notificated"].isInt() ? patch["notificated"].asInt() : std::stoi(patch["notificated"].asString());
+                                    gf.id = patch["id"].isInt() ? std::to_string(patch["id"].asInt()) : patch["id"].asString();
+                                    gf.name = patch["name"].asString();
+                                    gf.path = patch["link"].asString();
+                                    gf.size = patch["size"].asString();
+                                    gf.language = GlobalConstants::LANGUAGES[i].id;
+                                    gf.platform = patches[j].platform;
+
+                                    game.patches.push_back(gf);
+                                }
+                            }
+                            else // Patch is a single file
+                            {
+                                // 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())
+                                        {
+                                            game.patches[k].language |= GlobalConstants::LANGUAGES[i].id; // Add language code to patch
+                                            bDuplicate = true;
+                                            break;
+                                        }
+                                    }
+                                    if (bDuplicate)
+                                        continue;
+                                }
+
+                                gameFile gf;
+                                gf.type = GFTYPE_PATCH;
+                                gf.gamename = game.gamename;
+                                gf.updated = patchnode["notificated"].isInt() ? patchnode["notificated"].asInt() : std::stoi(patchnode["notificated"].asString());
+                                gf.id = patchnode["id"].isInt() ? std::to_string(patchnode["id"].asInt()) : patchnode["id"].asString();
+                                gf.name = patchnode["name"].asString();
+                                gf.path = patchnode["link"].asString();
+                                gf.size = patchnode["size"].asString();
+                                gf.language = GlobalConstants::LANGUAGES[i].id;
+                                gf.platform = patches[j].platform;
+
+                                game.patches.push_back(gf);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Language pack details
+            for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+            {   // Check against the specified languages
+                if (lang & GlobalConstants::LANGUAGES[i].id)
+                {
+                    // Try to find a language pack
+                    _regex_namespace_::regex re(GlobalConstants::LANGUAGES[i].code + "\\d+langpack\\d+", _regex_namespace_::regex_constants::icase); // regex for language pack node names
+                    std::vector<std::string> langpacknames;
+                    for (unsigned int j = 0; j < membernames.size(); ++j)
+                    {
+                        if (_regex_namespace_::regex_match(membernames[j], re))
+                            langpacknames.push_back(membernames[j]);
+                    }
+
+                    if (!langpacknames.empty()) // found at least one language pack
+                    {
+                        for (unsigned int j = 0; j < langpacknames.size(); ++j)
+                        {
+                            Json::Value langpack = root["game"][langpacknames[j]];
+
+                            gameFile gf;
+                            gf.type = GFTYPE_LANGPACK;
+                            gf.gamename = game.gamename;
+                            gf.updated = false; // language packs don't have "updated" flag
+                            gf.id = langpack["id"].isInt() ? std::to_string(langpack["id"].asInt()) : langpack["id"].asString();
+                            gf.name = langpack["name"].asString();
+                            gf.path = langpack["link"].asString();
+                            gf.size = langpack["size"].asString();
+                            gf.language = GlobalConstants::LANGUAGES[i].id;
+
+                            game.languagepacks.push_back(gf);
+                        }
+                    }
+                }
+            }
+        }
+        else
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getGameDetails)" << std::endl << json << std::endl;
+            #endif
+            this->setError(jsonparser->getFormattedErrorMessages());
+        }
+        delete jsonparser;
+    }
+    else
+    {
+        this->setError("Found nothing in " + url);
+    }
+
+    return game;
+}
+
+
+std::string API::getInstallerLink(const std::string& game_name, const std::string& id)
+{
+    std::string url, link;
+    url = this->config.get_installer_link + game_name + "/" + id + "/";
+    std::string json = this->getResponseOAuth(url);
+
+    if (!json.empty())
+    {
+        Json::Value root;
+        Json::Reader *jsonparser = new Json::Reader;
+        if (jsonparser->parse(json, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getInstallerLink)" << std::endl << root << std::endl;
+            #endif
+            int available = root["file"]["available"].isInt() ? root["file"]["available"].asInt() : std::stoi(root["file"]["available"].asString());
+            if (available)
+                link = root["file"]["link"].asString();
+        }
+        else
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getInstallerLink)" << std::endl << json << std::endl;
+            #endif
+            this->setError(jsonparser->getFormattedErrorMessages());
+        }
+        delete jsonparser;
+    }
+    else
+    {
+        this->setError("Found nothing in " + url);
+    }
+
+    return link;
+}
+
+std::string API::getExtraLink(const std::string& game_name, const std::string& id)
+{
+    std::string url, link;
+    url = this->config.get_extra_link + game_name + "/" + id + "/";
+    std::string json = this->getResponseOAuth(url);
+
+    if (!json.empty())
+    {
+        Json::Value root;
+        Json::Reader *jsonparser = new Json::Reader;
+        if (jsonparser->parse(json, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getExtraLink)" << std::endl << root << std::endl;
+            #endif
+            int available = root["file"]["available"].isInt() ? root["file"]["available"].asInt() : std::stoi(root["file"]["available"].asString());
+            if (available)
+                link = root["file"]["link"].asString();
+        }
+        else
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getExtraLink)" << std::endl << json << std::endl;
+            #endif
+            this->setError(jsonparser->getFormattedErrorMessages());
+        }
+        delete jsonparser;
+    }
+    else
+    {
+        this->setError("Found nothing in " + url);
+    }
+
+    return link;
+}
+
+std::string API::getPatchLink(const std::string& game_name, const std::string& id)
+{
+    return this->getInstallerLink(game_name, id);
+}
+
+std::string API::getLanguagePackLink(const std::string& game_name, const std::string& id)
+{
+    return this->getInstallerLink(game_name, id);
+}
+
+std::string API::getXML(const std::string& game_name, const std::string& id)
+{
+    std::string url, XML;
+    url = this->config.get_installer_link + game_name + "/" + id + "/crc/";
+    std::string json = this->getResponseOAuth(url);
+
+    if (!json.empty())
+    {
+        Json::Value root;
+        Json::Reader *jsonparser = new Json::Reader;
+        if (jsonparser->parse(json, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getXML)" << std::endl << root << std::endl;
+            #endif
+            int available = root["file"]["available"].isInt() ? root["file"]["available"].asInt() : std::stoi(root["file"]["available"].asString());
+            if (available)
+            {
+                url = root["file"]["link"].asString();
+                XML = this->getResponse(url);
+            }
+        }
+        else
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (API::getXML)" << std::endl << json << std::endl;
+            #endif
+            this->setError(jsonparser->getFormattedErrorMessages());
+        }
+        delete jsonparser;
+    }
+    else
+    {
+        this->setError("Found nothing in " + url);
+    }
+
+    return XML;
+}
+
+void API::clearError()
+{
+    this->error = false;
+    this->error_message = "";
+}
+
+void API::setError(const std::string& err)
+{
+    this->error = true;
+    if (this->error_message.empty())
+        this->error_message = err;
+    else
+        this->error_message += "\n" + err;
+}
+
+API::~API()
+{
+    curl_easy_cleanup(curlhandle);
+}
diff --git a/src/blacklist.cpp b/src/blacklist.cpp
new file mode 100644 (file)
index 0000000..0b0b2b7
--- /dev/null
@@ -0,0 +1,80 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "blacklist.h"
+#include "config.h"
+#include "api.h"
+#include "util.h"
+
+#include <iostream>
+#include <utility>
+
+enum {
+    BLFLAG_RX = 1 << 0,
+    BLFLAG_PERL = 1 << 1
+};
+
+void Blacklist::initialize(const std::vector<std::string>& lines) {
+    int linenr = 1;
+    for (auto it = lines.begin(); it != lines.end(); ++it, ++linenr) {
+        BlacklistItem item;
+        const std::string& s = *it;
+
+        if (s.length() == 0 || s[0] == '#')
+          continue;
+
+        std::size_t i;
+        for (i = 0; i < s.length() && s[i] != '\x20'; ++i) {
+            switch (s[i]) {
+                case 'R':
+                    item.flags |= BLFLAG_RX;
+                    break;
+                case 'p':
+                    item.flags |= BLFLAG_PERL;
+                    break;
+                default:
+                    std::cout << "unknown flag '" << s[i] << "' in blacklist line " << linenr << std::endl;
+                    break;
+            }
+        }
+        ++i;
+        if (i == s.length()) {
+            std::cout << "empty expression in blacklist line " << linenr << std::endl;
+            continue;
+        }
+        if (item.flags & BLFLAG_RX) {
+            boost::regex::flag_type rx_flags = boost::regex::normal;
+
+            // we only support perl-like syntax for now, which is boost default (normal). Add further flag processing
+            // here if that changes.
+
+            rx_flags |= boost::regex::nosubs;
+
+            item.linenr = linenr;
+            item.source.assign(s.substr(i).c_str());
+            item.regex.assign(item.source, rx_flags);
+            blacklist_.push_back(std::move(item));
+        } else {
+            std::cout << "unknown expression type in blacklist line " << linenr << std::endl;
+        }
+    }
+}
+
+bool Blacklist::isBlacklisted(const std::string& path) {
+    for (auto it = blacklist_.begin(); it != blacklist_.end(); ++it) {
+        const BlacklistItem& item = *it;
+        if (item.flags & BLFLAG_RX && boost::regex_search(path, item.regex))
+            return true;
+    }
+    return false;
+}
+
+bool Blacklist::isBlacklisted(const std::string& path, const std::string& gamename, std::string subdirectory)
+{
+    std::string filepath = Util::makeRelativeFilepath(path, gamename, subdirectory);
+    return isBlacklisted(filepath);
+}
+
diff --git a/src/downloader.cpp b/src/downloader.cpp
new file mode 100644 (file)
index 0000000..d0019d0
--- /dev/null
@@ -0,0 +1,3405 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "downloader.h"
+#include "util.h"
+#include "globalconstants.h"
+#include "downloadinfo.h"
+#include "message.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <ctime>
+#include <iostream>
+#include <sstream>
+#include <unistd.h>
+#include <fstream>
+#include <iomanip>
+#include <boost/filesystem.hpp>
+#include <boost/regex.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <tinyxml2.h>
+#include <json/json.h>
+#include <htmlcxx/html/ParserDom.h>
+#include <htmlcxx/html/Uri.h>
+#include <termios.h>
+#include <algorithm>
+#include <thread>
+#include <mutex>
+
+namespace bptime = boost::posix_time;
+
+std::vector<DownloadInfo> vDownloadInfo;
+ThreadSafeQueue<gameFile> dlQueue;
+ThreadSafeQueue<Message> msgQueue;
+ThreadSafeQueue<gameFile> createXMLQueue;
+ThreadSafeQueue<gameItem> gameItemQueue;
+ThreadSafeQueue<gameDetails> gameDetailsQueue;
+std::mutex mtx_create_directories; // Mutex for creating directories in Downloader::processDownloadQueue
+
+Downloader::Downloader(Config &conf)
+{
+    this->config = conf;
+    if (config.bLoginHTTP && boost::filesystem::exists(config.sCookiePath))
+        if (!boost::filesystem::remove(config.sCookiePath))
+            std::cerr << "Failed to delete " << config.sCookiePath << std::endl;
+
+    this->resume_position = 0;
+    this->retries = 0;
+
+    // Initialize curl and set curl options
+    curlhandle = curl_easy_init();
+    curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, config.sVersionString.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0);
+    curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, config.iTimeout);
+    curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+    curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer);
+    curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, config.bVerbose);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+    curl_easy_setopt(curlhandle, CURLOPT_READFUNCTION, Downloader::readData);
+    curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate);
+    curl_easy_setopt(curlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallback);
+    curl_easy_setopt(curlhandle, CURLOPT_XFERINFODATA, this);
+
+    // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+    curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30);
+    curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200);
+
+    if (!config.sCACertPath.empty())
+        curl_easy_setopt(curlhandle, CURLOPT_CAINFO, config.sCACertPath.c_str());
+
+    // Create new GOG website handle
+    gogWebsite = new Website(config);
+
+    // Create new API handle and set curl options for the API
+    gogAPI = new API(config.sToken, config.sSecret);
+    gogAPI->curlSetOpt(CURLOPT_VERBOSE, config.bVerbose);
+    gogAPI->curlSetOpt(CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer);
+    gogAPI->curlSetOpt(CURLOPT_CONNECTTIMEOUT, config.iTimeout);
+    if (!config.sCACertPath.empty())
+        gogAPI->curlSetOpt(CURLOPT_CAINFO, config.sCACertPath.c_str());
+
+    gogAPI->init();
+
+    progressbar = new ProgressBar(config.bUnicode, config.bColor);
+}
+
+Downloader::~Downloader()
+{
+    if (config.bReport)
+        if (this->report_ofs)
+            this->report_ofs.close();
+    delete progressbar;
+    delete gogAPI;
+    delete gogWebsite;
+    curl_easy_cleanup(curlhandle);
+    // Make sure that cookie file is only readable/writable by owner
+    if (!config.bRespectUmask)
+        Util::setFilePermissions(config.sCookiePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+}
+
+/* Login check
+    returns false if not logged in
+    returns true if logged in
+*/
+bool Downloader::isLoggedIn()
+{
+    bool bIsLoggedIn = false;
+    config.bLoginAPI = false;
+    config.bLoginHTTP = false;
+
+    bool bWebsiteIsLoggedIn = gogWebsite->IsLoggedIn();
+    if (!bWebsiteIsLoggedIn)
+        config.bLoginHTTP = true;
+
+    bool bIsLoggedInAPI = gogAPI->isLoggedIn();
+    if (!bIsLoggedInAPI)
+        config.bLoginAPI = true;
+
+    if (bIsLoggedInAPI && bWebsiteIsLoggedIn)
+        bIsLoggedIn = true;
+
+    return bIsLoggedIn;
+}
+
+/* Initialize the downloader
+    returns 0 if failed
+    returns 1 if successful
+*/
+int Downloader::init()
+{
+    if (!config.sGameHasDLCList.empty())
+    {
+        if (config.gamehasdlc.empty())
+        {
+            std::string game_has_dlc_list = this->getResponse(config.sGameHasDLCList);
+            if (!game_has_dlc_list.empty())
+                config.gamehasdlc.initialize(Util::tokenize(game_has_dlc_list, "\n"));
+        }
+    }
+    gogWebsite->setConfig(config); // Update config for website handle
+
+    if (config.bReport && (config.bDownload || config.bRepair))
+    {
+        this->report_ofs.open(config.sReportFilePath);
+        if (!this->report_ofs)
+        {
+            config.bReport = false;
+            std::cerr << "Failed to create " << config.sReportFilePath << std::endl;
+            return 0;
+        }
+    }
+
+    return 1;
+}
+
+/* Login
+    returns 0 if login fails
+    returns 1 if successful
+*/
+int Downloader::login()
+{
+    std::string email;
+    if (!isatty(STDIN_FILENO)) {
+        std::cerr << "Unable to read email and password" << std::endl;
+        return 0;
+    }
+    std::cerr << "Email: ";
+    std::getline(std::cin,email);
+
+    std::string password;
+    std::cerr << "Password: ";
+    struct termios termios_old, termios_new;
+    tcgetattr(STDIN_FILENO, &termios_old); // Get current terminal attributes
+    termios_new = termios_old;
+    termios_new.c_lflag &= ~ECHO; // Set ECHO off
+    tcsetattr(STDIN_FILENO, TCSANOW, &termios_new); // Set terminal attributes
+    std::getline(std::cin, password);
+    tcsetattr(STDIN_FILENO, TCSANOW, &termios_old); // Restore old terminal attributes
+    std::cerr << std::endl;
+
+    if (email.empty() || password.empty())
+    {
+        std::cerr << "Email and/or password empty" << std::endl;
+        return 0;
+    }
+    else
+    {
+        // Login to website
+        if (config.bLoginHTTP)
+        {
+            // Delete old cookies
+            if (boost::filesystem::exists(config.sCookiePath))
+                if (!boost::filesystem::remove(config.sCookiePath))
+                    std::cerr << "Failed to delete " << config.sCookiePath << std::endl;
+
+            if (!gogWebsite->Login(email, password))
+            {
+                std::cerr << "HTTP: Login failed" << std::endl;
+                return 0;
+            }
+            else
+            {
+                std::cerr << "HTTP: Login successful" << std::endl;
+                if (!config.bLoginAPI)
+                    return 1;
+            }
+        }
+        // Login to API
+        if (config.bLoginAPI)
+        {
+            if (!gogAPI->login(email, password))
+            {
+                std::cerr << "API: Login failed" << std::endl;
+                return 0;
+            }
+            else
+            {
+                std::cerr << "API: Login successful" << std::endl;
+                config.sToken = gogAPI->getToken();
+                config.sSecret = gogAPI->getSecret();
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+void Downloader::updateCheck()
+{
+    std::cout << "New forum replies: " << gogAPI->user.notifications_forum << std::endl;
+    std::cout << "New private messages: " << gogAPI->user.notifications_messages << std::endl;
+    std::cout << "Updated games: " << gogAPI->user.notifications_games << std::endl;
+
+    if (gogAPI->user.notifications_games)
+    {
+        config.sGameRegex = ".*"; // Always check all games
+        gogWebsite->setConfig(config); // Make sure that website handle has updated config
+        if (config.bList || config.bListDetails || config.bDownload)
+        {
+            if (config.bList)
+                config.bListDetails = true; // Always list details
+            this->getGameList();
+            if (config.bDownload)
+                this->download();
+            else
+                this->listGames();
+        }
+    }
+}
+
+void Downloader::getGameList()
+{
+    if (config.sGameRegex == "free")
+    {
+        gameItems = gogWebsite->getFreeGames();
+    }
+    else
+    {
+        gameItems = gogWebsite->getGames();
+    }
+}
+
+/* Get detailed info about the games
+    returns 0 if successful
+    returns 1 if fails
+*/
+int Downloader::getGameDetails()
+{
+    // Set default game specific directory options to values from config
+    gameSpecificDirectoryConfig dirConfDefault;
+    dirConfDefault.sDirectory = config.sDirectory;
+    dirConfDefault.bSubDirectories = config.bSubDirectories;
+    dirConfDefault.sGameSubdir = config.sGameSubdir;
+    dirConfDefault.sInstallersSubdir = config.sInstallersSubdir;
+    dirConfDefault.sExtrasSubdir = config.sExtrasSubdir;
+    dirConfDefault.sLanguagePackSubdir = config.sLanguagePackSubdir;
+    dirConfDefault.sDLCSubdir = config.sDLCSubdir;
+    dirConfDefault.sPatchesSubdir = config.sPatchesSubdir;
+
+    if (config.bUseCache && !config.bUpdateCache)
+    {
+        // GameRegex filter alias for all games
+        if (config.sGameRegex == "all")
+            config.sGameRegex = ".*";
+        else if (config.sGameRegex == "free")
+            std::cerr << "Warning: regex alias \"free\" doesn't work with cached details" << std::endl;
+
+        int result = this->loadGameDetailsCache();
+        if (result == 0)
+        {
+            for (unsigned int i = 0; i < this->games.size(); ++i)
+            {
+                gameSpecificConfig conf;
+                conf.dirConf = dirConfDefault;
+                Util::getGameSpecificConfig(games[i].gamename, &conf);
+                this->games[i].makeFilepaths(conf.dirConf);
+            }
+            return 0;
+        }
+        else
+        {
+            if (result == 1)
+            {
+                std::cerr << "Cache doesn't exist." << std::endl;
+                std::cerr << "Create cache with --update-cache" << std::endl;
+            }
+            else if (result == 3)
+            {
+                std::cerr << "Cache is too old." << std::endl;
+                std::cerr << "Update cache with --update-cache or use bigger --cache-valid" << std::endl;
+            }
+            else if (result == 5)
+            {
+                std::cerr << "Cache version doesn't match current version." << std::endl;
+                std::cerr << "Update cache with --update-cache" << std::endl;
+            }
+            return 1;
+        }
+    }
+
+    if (gameItems.empty())
+        this->getGameList();
+
+    if (!gameItems.empty())
+    {
+        for (unsigned int i = 0; i < gameItems.size(); ++i)
+        {
+            gameItemQueue.push(gameItems[i]);
+        }
+
+        // Create threads
+        unsigned int threads = std::min(config.iThreads, static_cast<unsigned int>(gameItemQueue.size()));
+        std::vector<std::thread> vThreads;
+        for (unsigned int i = 0; i < threads; ++i)
+        {
+            DownloadInfo dlInfo;
+            dlInfo.setStatus(DLSTATUS_NOTSTARTED);
+            vDownloadInfo.push_back(dlInfo);
+            vThreads.push_back(std::thread(Downloader::getGameDetailsThread, this->config, i));
+        }
+
+        unsigned int dl_status = DLSTATUS_NOTSTARTED;
+        while (dl_status != DLSTATUS_FINISHED)
+        {
+            dl_status = DLSTATUS_NOTSTARTED;
+
+            // Print progress information once per 100ms
+            std::this_thread::sleep_for(std::chrono::milliseconds(100));
+            std::cerr << "\033[J\r" << std::flush; // Clear screen from the current line down to the bottom of the screen
+
+            // Print messages from message queue first
+            Message msg;
+            while (msgQueue.try_pop(msg))
+            {
+                std::cerr << msg.getFormattedString(config.bColor, true) << std::endl;
+                if (config.bReport)
+                {
+                    this->report_ofs << msg.getTimestampString() << ": " << msg.getMessage() << std::endl;
+                }
+            }
+
+            for (unsigned int i = 0; i < vDownloadInfo.size(); ++i)
+            {
+                unsigned int status = vDownloadInfo[i].getStatus();
+                dl_status |= status;
+            }
+
+            std::cerr << "Getting game info " << (gameItems.size() - gameItemQueue.size()) << " / " << gameItems.size() << std::endl;
+
+            if (dl_status != DLSTATUS_FINISHED)
+            {
+                std::cerr << "\033[1A\r" << std::flush; // Move cursor up by 1 row
+            }
+        }
+
+        // Join threads
+        for (unsigned int i = 0; i < vThreads.size(); ++i)
+            vThreads[i].join();
+
+        vThreads.clear();
+        vDownloadInfo.clear();
+
+        gameDetails details;
+        while (gameDetailsQueue.try_pop(details))
+        {
+            this->games.push_back(details);
+        }
+        std::sort(this->games.begin(), this->games.end(), [](const gameDetails& i, const gameDetails& j) -> bool { return i.gamename < j.gamename; });
+    }
+
+    return 0;
+}
+
+int Downloader::listGames()
+{
+    if (config.bListDetails) // Detailed list
+    {
+        if (this->games.empty()) {
+            int res = this->getGameDetails();
+            if (res > 0)
+                return res;
+        }
+
+        for (unsigned int i = 0; i < games.size(); ++i)
+        {
+            std::cout   << "gamename: " << games[i].gamename << std::endl
+                        << "title: " << games[i].title << std::endl
+                        << "icon: " << "http://static.gog.com" << games[i].icon << std::endl;
+            if (!games[i].serials.empty())
+                std::cout << "serials:" << std::endl << games[i].serials << std::endl;
+
+            // List installers
+            if (config.bInstallers)
+            {
+                std::cout << "installers: " << std::endl;
+                for (unsigned int j = 0; j < games[i].installers.size(); ++j)
+                {
+                    if (!config.bUpdateCheck || games[i].installers[j].updated) // Always list updated files
+                    {
+                        std::string filepath = games[i].installers[j].getFilepath();
+                        if (config.blacklist.isBlacklisted(filepath))
+                        {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        std::string languages = Util::getOptionNameString(games[i].installers[j].language, GlobalConstants::LANGUAGES);
+
+                        std::cout   << "\tid: " << games[i].installers[j].id << std::endl
+                                    << "\tname: " << games[i].installers[j].name << std::endl
+                                    << "\tpath: " << games[i].installers[j].path << std::endl
+                                    << "\tsize: " << games[i].installers[j].size << std::endl
+                                    << "\tupdated: " << (games[i].installers[j].updated ? "True" : "False") << std::endl
+                                    << "\tlanguage: " << languages << std::endl
+                                    << std::endl;
+                    }
+                }
+            }
+            // List extras
+            if (config.bExtras && !config.bUpdateCheck && !games[i].extras.empty())
+            {
+                std::cout << "extras: " << std::endl;
+                for (unsigned int j = 0; j < games[i].extras.size(); ++j)
+                {
+                    std::string filepath = games[i].extras[j].getFilepath();
+                    if (config.blacklist.isBlacklisted(filepath))
+                    {
+                        if (config.bVerbose)
+                            std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                        continue;
+                    }
+
+                    std::cout   << "\tid: " << games[i].extras[j].id << std::endl
+                                << "\tname: " << games[i].extras[j].name << std::endl
+                                << "\tpath: " << games[i].extras[j].path << std::endl
+                                << "\tsize: " << games[i].extras[j].size << std::endl
+                                << std::endl;
+                }
+            }
+            // List patches
+            if (config.bPatches && !config.bUpdateCheck && !games[i].patches.empty())
+            {
+                std::cout << "patches: " << std::endl;
+                for (unsigned int j = 0; j < games[i].patches.size(); ++j)
+                {
+                    std::string filepath = games[i].patches[j].getFilepath();
+                    if (config.blacklist.isBlacklisted(filepath))
+                    {
+                        if (config.bVerbose)
+                            std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                        continue;
+                    }
+
+                    std::string languages = Util::getOptionNameString(games[i].patches[j].language, GlobalConstants::LANGUAGES);
+
+                    std::cout   << "\tid: " << games[i].patches[j].id << std::endl
+                                << "\tname: " << games[i].patches[j].name << std::endl
+                                << "\tpath: " << games[i].patches[j].path << std::endl
+                                << "\tsize: " << games[i].patches[j].size << std::endl
+                                << "\tupdated: " << (games[i].patches[j].updated ? "True" : "False") << std::endl
+                                << "\tlanguage: " << languages << std::endl
+                                << std::endl;
+                }
+            }
+            // List language packs
+            if (config.bLanguagePacks && !config.bUpdateCheck && !games[i].languagepacks.empty())
+            {
+                std::cout << "language packs: " << std::endl;
+                for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j)
+                {
+                    std::string filepath = games[i].languagepacks[j].getFilepath();
+                    if (config.blacklist.isBlacklisted(filepath))
+                    {
+                        if (config.bVerbose)
+                            std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                        continue;
+                    }
+
+                    std::cout   << "\tid: " << games[i].languagepacks[j].id << std::endl
+                                << "\tname: " << games[i].languagepacks[j].name << std::endl
+                                << "\tpath: " << games[i].languagepacks[j].path << std::endl
+                                << "\tsize: " << games[i].languagepacks[j].size << std::endl
+                                << std::endl;
+                }
+            }
+            if (config.bDLC && !games[i].dlcs.empty())
+            {
+                std::cout << "DLCs: " << std::endl;
+                for (unsigned int j = 0; j < games[i].dlcs.size(); ++j)
+                {
+                    if (!games[i].dlcs[j].serials.empty())
+                    {
+                        std::cout   << "\tDLC gamename: " << games[i].dlcs[j].gamename << std::endl
+                                    << "\tserials:" << games[i].dlcs[j].serials << std::endl;
+                    }
+
+                    for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k)
+                    {
+                        std::string filepath = games[i].dlcs[j].installers[k].getFilepath();
+                        if (config.blacklist.isBlacklisted(filepath))
+                        {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << 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
+                                    << "\tsize: " << games[i].dlcs[j].installers[k].size << std::endl
+                                    << "\tupdated: " << (games[i].dlcs[j].installers[k].updated ? "True" : "False") << std::endl
+                                    << std::endl;
+                    }
+                    for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k)
+                    {
+                        std::string filepath = games[i].dlcs[j].patches[k].getFilepath();
+                        if (config.blacklist.isBlacklisted(filepath)) {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << 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
+                                    << "\tsize: " << games[i].dlcs[j].patches[k].size << std::endl
+                                    << std::endl;
+                    }
+                    for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k)
+                    {
+                        std::string filepath = games[i].dlcs[j].extras[k].getFilepath();
+                        if (config.blacklist.isBlacklisted(filepath)) {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << 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
+                                    << "\tsize: " << games[i].dlcs[j].extras[k].size << std::endl
+                                    << std::endl;
+                    }
+                    for (unsigned int k = 0; k < games[i].dlcs[j].languagepacks.size(); ++k)
+                    {
+                        std::string filepath = games[i].dlcs[j].languagepacks[k].getFilepath();
+                        if (config.blacklist.isBlacklisted(filepath)) {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        std::cout   << "\tgamename: " << games[i].dlcs[j].gamename << 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
+                                    << "\tsize: " << games[i].dlcs[j].languagepacks[k].size << std::endl
+                                    << std::endl;
+                    }
+                }
+            }
+        }
+    }
+    else
+    {   // List game names
+        if (gameItems.empty())
+            this->getGameList();
+
+        for (unsigned int i = 0; i < gameItems.size(); ++i)
+        {
+            std::string gamename = gameItems[i].name;
+            if (gameItems[i].updates > 0)
+            {
+                gamename += " [" + std::to_string(gameItems[i].updates) + "]";
+                if (config.bColor)
+                    gamename = "\033[32m" + gamename + "\033[0m";
+            }
+            std::cout << gamename << std::endl;
+            for (unsigned int j = 0; j < gameItems[i].dlcnames.size(); ++j)
+                std::cout << "+> " << gameItems[i].dlcnames[j] << std::endl;
+        }
+    }
+
+    return 0;
+}
+
+void Downloader::repair()
+{
+    if (this->games.empty())
+        this->getGameDetails();
+
+    for (unsigned int i = 0; i < games.size(); ++i)
+    {
+        // Installers (use remote or local file)
+        if (config.bInstallers)
+        {
+            for (unsigned int j = 0; j < games[i].installers.size(); ++j)
+            {
+                std::string filepath = games[i].installers[j].getFilepath();
+                if (config.blacklist.isBlacklisted(filepath))
+                {
+                    if (config.bVerbose)
+                        std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                    continue;
+                }
+
+                // Get XML data
+                std::string XML = "";
+                if (config.bRemoteXML)
+                {
+                    XML = gogAPI->getXML(games[i].gamename, games[i].installers[j].id);
+                    if (gogAPI->getError())
+                    {
+                        std::cerr << gogAPI->getErrorMessage() << std::endl;
+                        gogAPI->clearError();
+                        continue;
+                    }
+                }
+
+                // Repair
+                bool bUseLocalXML = !config.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;
+                }
+            }
+        }
+
+        // Extras (GOG doesn't provide XML data for extras, use local file)
+        if (config.bExtras)
+        {
+            for (unsigned int j = 0; j < games[i].extras.size(); ++j)
+            {
+                std::string filepath = games[i].extras[j].getFilepath();
+                if (config.blacklist.isBlacklisted(filepath))
+                {
+                    if (config.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;
+            }
+        }
+
+        // Patches (use remote or local file)
+        if (config.bPatches)
+        {
+            for (unsigned int j = 0; j < games[i].patches.size(); ++j)
+            {
+                std::string filepath = games[i].patches[j].getFilepath();
+                if (config.blacklist.isBlacklisted(filepath))
+                {
+                    if (config.bVerbose)
+                        std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                    continue;
+                }
+
+                // Get XML data
+                std::string XML = "";
+                if (config.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;
+            }
+        }
+
+        // Language packs (GOG doesn't provide XML data for language packs, use local file)
+        if (config.bLanguagePacks)
+        {
+            for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j)
+            {
+                std::string filepath = games[i].languagepacks[j].getFilepath();
+                if (config.blacklist.isBlacklisted(filepath))
+                {
+                    if (config.bVerbose)
+                        std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                    continue;
+                }
+
+                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 (config.bDLC && !games[i].dlcs.empty())
+        {
+            for (unsigned int j = 0; j < games[i].dlcs.size(); ++j)
+            {
+                if (config.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 (config.blacklist.isBlacklisted(filepath))
+                        {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        // Get XML data
+                        std::string XML = "";
+                        if (config.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;
+                            }
+                        }
+
+                        // Repair
+                        bool bUseLocalXML = !config.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 (config.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 (config.blacklist.isBlacklisted(filepath)) {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        // Get XML data
+                        std::string XML = "";
+                        if (config.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();
+                            }
+                        }
+
+                        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 (config.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 (config.blacklist.isBlacklisted(filepath)) {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        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 (config.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 (config.blacklist.isBlacklisted(filepath)) {
+                            if (config.bVerbose)
+                                std::cerr << "skipped blacklisted file " << filepath << std::endl;
+                            continue;
+                        }
+
+                        // Get XML data
+                        std::string XML = "";
+                        if (config.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();
+                            }
+                        }
+
+                        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;
+                    }
+                }
+            }
+        }
+    }
+}
+
+void Downloader::download()
+{
+    if (this->games.empty())
+        this->getGameDetails();
+
+    if (config.bCover && !config.bUpdateCheck)
+        coverXML = this->getResponse(config.sCoverList);
+
+    for (unsigned int i = 0; i < games.size(); ++i)
+    {
+        if (config.bSaveSerials && !games[i].serials.empty())
+        {
+            std::string filepath = games[i].getSerialsFilepath();
+            this->saveSerials(games[i].serials, filepath);
+        }
+
+        if (config.bSaveChangelogs && !games[i].changelog.empty())
+        {
+            std::string filepath = games[i].getChangelogFilepath();
+            this->saveChangelog(games[i].changelog, filepath);
+        }
+
+        // Download covers
+        if (config.bCover && !config.bUpdateCheck)
+        {
+            if (!games[i].installers.empty())
+            {
+                // Take path from installer path because for some games the base directory for installer/extra path is not "gamename"
+                boost::filesystem::path filepath = boost::filesystem::absolute(games[i].installers[0].getFilepath(), boost::filesystem::current_path());
+
+                // Get base directory from filepath
+                std::string directory = filepath.parent_path().string();
+
+                this->downloadCovers(games[i].gamename, directory, coverXML);
+            }
+        }
+
+        if (config.bInstallers)
+        {
+            for (unsigned int j = 0; j < games[i].installers.size(); ++j)
+            {
+                dlQueue.push(games[i].installers[j]);
+            }
+        }
+        if (config.bPatches)
+        {
+            for (unsigned int j = 0; j < games[i].patches.size(); ++j)
+            {
+                dlQueue.push(games[i].patches[j]);
+            }
+        }
+        if (config.bExtras)
+        {
+            for (unsigned int j = 0; j < games[i].extras.size(); ++j)
+            {
+                dlQueue.push(games[i].extras[j]);
+            }
+        }
+        if (config.bLanguagePacks)
+        {
+            for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j)
+            {
+                dlQueue.push(games[i].languagepacks[j]);
+            }
+        }
+        if (config.bDLC && !games[i].dlcs.empty())
+        {
+            for (unsigned int j = 0; j < games[i].dlcs.size(); ++j)
+            {
+                if (config.bSaveSerials && !games[i].dlcs[j].serials.empty())
+                {
+                    std::string filepath = games[i].dlcs[j].getSerialsFilepath();
+                    this->saveSerials(games[i].dlcs[j].serials, filepath);
+                }
+                if (config.bSaveChangelogs && !games[i].dlcs[j].changelog.empty())
+                {
+                    std::string filepath = games[i].dlcs[j].getChangelogFilepath();
+                    this->saveChangelog(games[i].dlcs[j].changelog, filepath);
+                }
+
+                if (config.bInstallers)
+                {
+                    for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k)
+                    {
+                        dlQueue.push(games[i].dlcs[j].installers[k]);
+                    }
+                }
+                if (config.bPatches)
+                {
+                    for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k)
+                    {
+                        dlQueue.push(games[i].dlcs[j].patches[k]);
+                    }
+                }
+                if (config.bExtras)
+                {
+                    for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k)
+                    {
+                        dlQueue.push(games[i].dlcs[j].extras[k]);
+                    }
+                }
+                if (config.bLanguagePacks)
+                {
+                    for (unsigned int k = 0; k < games[i].dlcs[j].languagepacks.size(); ++k)
+                    {
+                        dlQueue.push(games[i].dlcs[j].languagepacks[k]);
+                    }
+                }
+            }
+        }
+    }
+
+    if (!dlQueue.empty())
+    {
+        // Limit thread count to number of items in download queue
+        unsigned int iThreads = std::min(config.iThreads, static_cast<unsigned int>(dlQueue.size()));
+
+        // Create download threads
+        std::vector<std::thread> vThreads;
+        for (unsigned int i = 0; i < iThreads; ++i)
+        {
+            DownloadInfo dlInfo;
+            dlInfo.setStatus(DLSTATUS_NOTSTARTED);
+            vDownloadInfo.push_back(dlInfo);
+            vThreads.push_back(std::thread(Downloader::processDownloadQueue, this->config, i));
+        }
+
+        this->printProgress();
+
+        // Join threads
+        for (unsigned int i = 0; i < vThreads.size(); ++i)
+            vThreads[i].join();
+
+        vThreads.clear();
+        vDownloadInfo.clear();
+    }
+
+    // Create xml data for all files in the queue
+    if (!createXMLQueue.empty())
+    {
+        std::cout << "Starting XML creation" << std::endl;
+        gameFile gf;
+        while (createXMLQueue.try_pop(gf))
+        {
+            std::string xml_directory = config.sXMLDirectory + "/" + gf.gamename;
+            Util::createXML(gf.getFilepath(), config.iChunkSize, xml_directory);
+        }
+    }
+}
+
+// Download a file, resume if possible
+CURLcode Downloader::downloadFile(const std::string& url, const std::string& filepath, const std::string& xml_data, const std::string& gamename)
+{
+    CURLcode res = CURLE_RECV_ERROR; // assume network error
+    bool bResume = false;
+    FILE *outfile;
+    off_t offset=0;
+
+    // Get directory from filepath
+    boost::filesystem::path pathname = filepath;
+    pathname = boost::filesystem::absolute(pathname, boost::filesystem::current_path());
+    std::string directory = pathname.parent_path().string();
+    std::string filenameXML = pathname.filename().string() + ".xml";
+    std::string xml_directory;
+    if (!gamename.empty())
+        xml_directory = config.sXMLDirectory + "/" + gamename;
+    else
+        xml_directory = config.sXMLDirectory;
+
+    // Using local XML data for version check before resuming
+    boost::filesystem::path local_xml_file;
+    local_xml_file = xml_directory + "/" + filenameXML;
+
+    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
+
+    if (!xml_data.empty())
+    {
+        std::string localHash = this->getLocalFileHash(filepath, gamename);
+        // Do version check if local hash exists
+        if (!localHash.empty())
+        {
+            tinyxml2::XMLDocument remote_xml;
+            remote_xml.Parse(xml_data.c_str());
+            tinyxml2::XMLElement *fileElemRemote = remote_xml.FirstChildElement("file");
+            if (fileElemRemote)
+            {
+                std::string remoteHash = fileElemRemote->Attribute("md5");
+                if (remoteHash != localHash)
+                    bSameVersion = false;
+            }
+        }
+    }
+
+    // Check that directory exists and create subdirectories
+    boost::filesystem::path path = directory;
+    if (boost::filesystem::exists(path))
+    {
+        if (!boost::filesystem::is_directory(path))
+        {
+            std::cerr << path << " is not directory" << std::endl;
+            return res;
+        }
+    }
+    else
+    {
+        if (!boost::filesystem::create_directories(path))
+        {
+            std::cerr << "Failed to create directory: " << path << std::endl;
+            return res;
+        }
+    }
+
+    // Check if file exists
+    if ((outfile=fopen(filepath.c_str(), "r"))!=NULL)
+    {
+        if (bSameVersion)
+        {
+            // File exists, resume
+            if ((outfile = freopen(filepath.c_str(), "r+", outfile))!=NULL )
+            {
+                bResume = true;
+                fseek(outfile, 0, SEEK_END);
+                // use ftello to support large files on 32 bit platforms
+                offset = ftello(outfile);
+                curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM_LARGE, offset);
+                this->resume_position = offset;
+            }
+            else
+            {
+                std::cerr << "Failed to reopen " << filepath << std::endl;
+                return res;
+            }
+        }
+        else
+        {   // File exists but is not the same version
+            fclose(outfile);
+            std::cerr << "Remote file is different, renaming local file" << std::endl;
+            std::string date_old = "." + bptime::to_iso_string(bptime::second_clock::local_time()) + ".old";
+            boost::filesystem::path new_name = filepath + date_old; // Rename old file by appending date and ".old" to filename
+            boost::system::error_code ec;
+            boost::filesystem::rename(pathname, new_name, ec); // Rename the file
+            if (ec)
+            {
+                std::cerr << "Failed to rename " << filepath << " to " << new_name.string() << std::endl;
+                std::cerr << "Skipping file" << std::endl;
+                return res;
+            }
+            else
+            {
+                // Create new file
+                if ((outfile=fopen(filepath.c_str(), "w"))!=NULL)
+                {
+                    curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM, 0); // start downloading from the beginning of file
+                    this->resume_position = 0;
+                }
+                else
+                {
+                    std::cerr << "Failed to create " << filepath << std::endl;
+                    return res;
+                }
+            }
+        }
+    }
+    else
+    {
+        // File doesn't exist, create new file
+        if ((outfile=fopen(filepath.c_str(), "w"))!=NULL)
+        {
+            curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM, 0); // start downloading from the beginning of file
+            this->resume_position = 0;
+        }
+        else
+        {
+            std::cerr << "Failed to create " << filepath << std::endl;
+            return res;
+        }
+    }
+
+    // Save remote XML
+    if (!xml_data.empty())
+    {
+        if ((bLocalXMLExists && (!bSameVersion || config.bRepair)) || !bLocalXMLExists)
+        {
+            // Check that directory exists and create subdirectories
+            boost::filesystem::path path = xml_directory;
+            if (boost::filesystem::exists(path))
+            {
+                if (!boost::filesystem::is_directory(path))
+                {
+                    std::cerr << path << " is not directory" << std::endl;
+                }
+            }
+            else
+            {
+                if (!boost::filesystem::create_directories(path))
+                {
+                    std::cerr << "Failed to create directory: " << path << std::endl;
+                }
+            }
+            std::ofstream ofs(local_xml_file.string().c_str());
+            if (ofs)
+            {
+                ofs << xml_data;
+                ofs.close();
+            }
+            else
+            {
+                std::cerr << "Can't create " << local_xml_file.string() << std::endl;
+            }
+        }
+    }
+
+    curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, outfile);
+    res = this->beginDownload();
+
+    fclose(outfile);
+
+    // Download failed and was not a resume attempt so delete the file
+    if (res != CURLE_OK && res != CURLE_PARTIAL_FILE && !bResume && res != CURLE_OPERATION_TIMEDOUT)
+    {
+        boost::filesystem::path path = filepath;
+        if (boost::filesystem::exists(path))
+            if (!boost::filesystem::remove(path))
+                std::cerr << "Failed to delete " << path << std::endl;
+    }
+
+    if (config.bReport)
+    {
+        std::string status = static_cast<std::string>(curl_easy_strerror(res));
+        if (bResume && res == CURLE_RANGE_ERROR) // CURLE_RANGE_ERROR on resume attempts is not an error that user needs to know about
+            status = "No error";
+        std::string report_line = "Downloaded [" + status + "] " + filepath;
+        this->report_ofs << report_line << std::endl;
+    }
+
+    // Retry partially downloaded file
+    // Retry if we aborted the transfer due to low speed limit
+    if ((res == CURLE_PARTIAL_FILE || res == CURLE_OPERATION_TIMEDOUT) && (this->retries < config.iRetries) )
+    {
+        this->retries++;
+
+        std::cerr << std::endl << "Retry " << this->retries << "/" << config.iRetries;
+        if (res == CURLE_PARTIAL_FILE)
+            std::cerr << " (partial download)";
+        else if (res == CURLE_OPERATION_TIMEDOUT)
+            std::cerr << " (timeout)";
+        std::cerr << std::endl;
+
+        res = this->downloadFile(url, filepath, xml_data, gamename);
+    }
+    else
+    {
+        this->retries = 0; // Reset retries counter
+    }
+
+    return res;
+}
+
+// Repair file
+int Downloader::repairFile(const std::string& url, const std::string& filepath, const std::string& xml_data, const std::string& gamename)
+{
+    int res = 0;
+    FILE *outfile;
+    off_t offset=0, from_offset, to_offset, filesize;
+    std::string filehash;
+    int chunks;
+    std::vector<off_t> chunk_from, chunk_to;
+    std::vector<std::string> chunk_hash;
+    bool bParsingFailed = false;
+
+    // Get filename
+    boost::filesystem::path pathname = filepath;
+    std::string filename = pathname.filename().string();
+    std::string xml_directory;
+    if (!gamename.empty())
+        xml_directory = config.sXMLDirectory + "/" + gamename;
+    else
+        xml_directory = config.sXMLDirectory;
+    std::string xml_file = xml_directory + "/" + filename + ".xml";
+    bool bFileExists = boost::filesystem::exists(pathname);
+    bool bLocalXMLExists = boost::filesystem::exists(xml_file);
+
+    tinyxml2::XMLDocument xml;
+    if (!xml_data.empty()) // Parse remote XML data
+    {
+        std::cout << "XML: Using remote file" << std::endl;
+        xml.Parse(xml_data.c_str());
+    }
+    else
+    {   // Parse local XML data
+        std::cout << "XML: Using local file" << std::endl;
+        if (!bLocalXMLExists)
+            std::cout << "XML: File doesn't exist (" << xml_file << ")" << std::endl;
+        xml.LoadFile(xml_file.c_str());
+    }
+
+    // Check if file node exists in XML data
+    tinyxml2::XMLElement *fileElem = xml.FirstChildElement("file");
+    if (!fileElem)
+    {   // File node doesn't exist
+        std::cout << "XML: Parsing failed / not valid XML" << std::endl;
+        if (config.bDownload)
+            bParsingFailed = true;
+        else
+            return res;
+    }
+    else
+    {   // File node exists --> valid XML
+        std::cout << "XML: Valid XML" << std::endl;
+        filename = fileElem->Attribute("name");
+        filehash = fileElem->Attribute("md5");
+        std::stringstream(fileElem->Attribute("chunks")) >> chunks;
+        std::stringstream(fileElem->Attribute("total_size")) >> filesize;
+
+        //Iterate through all chunk nodes
+        tinyxml2::XMLElement *chunkElem = fileElem->FirstChildElement("chunk");
+        while (chunkElem)
+        {
+            std::stringstream(chunkElem->Attribute("from")) >> from_offset;
+            std::stringstream(chunkElem->Attribute("to")) >> to_offset;
+            chunk_from.push_back(from_offset);
+            chunk_to.push_back(to_offset);
+            chunk_hash.push_back(chunkElem->GetText());
+            chunkElem = chunkElem->NextSiblingElement("chunk");
+        }
+
+        std::cout   << "XML: Parsing finished" << std::endl << std::endl
+                    << filename << std::endl
+                    << "\tMD5:\t" << filehash << std::endl
+                    << "\tChunks:\t" << chunks << std::endl
+                    << "\tSize:\t" << filesize << " bytes" << std::endl << std::endl;
+    }
+
+    // No local XML file and parsing failed.
+    if (bParsingFailed && !bLocalXMLExists)
+    {
+        if (this->config.bDownload)
+        {
+            std::cout << "Downloading: " << filepath << std::endl;
+            CURLcode result = this->downloadFile(url, filepath, xml_data, gamename);
+            std::cout << std::endl;
+            if  (
+                    (!bFileExists && result == CURLE_OK) || /* File doesn't exist so only accept if everything was OK */
+                    (bFileExists && (result == CURLE_OK || result == CURLE_RANGE_ERROR ))   /* File exists so accept also CURLE_RANGE_ERROR because curl will return CURLE_RANGE_ERROR */
+                )                                                                           /* if the file is already fully downloaded and we want to resume it */
+            {
+                bLocalXMLExists = boost::filesystem::exists(xml_file); // Check to see if downloadFile saved XML data
+
+                if (config.bAutomaticXMLCreation && !bLocalXMLExists)
+                {
+                    std::cout << "Starting automatic XML creation" << std::endl;
+                    Util::createXML(filepath, config.iChunkSize, xml_directory);
+                }
+                res = 1;
+            }
+        }
+        else
+        {
+            std::cout << "Can't repair file." << std::endl;
+        }
+        return res;
+    }
+
+    // Check if file exists
+    if (bFileExists)
+    {
+        // File exists
+        if ((outfile = fopen(filepath.c_str(), "r+"))!=NULL )
+        {
+            fseek(outfile, 0, SEEK_END);
+            // use ftello to support large files on 32 bit platforms
+            offset = ftello(outfile);
+        }
+        else
+        {
+            std::cout << "Failed to open " << filepath << std::endl;
+            return res;
+        }
+    }
+    else
+    {
+        std::cout << "File doesn't exist " << filepath << std::endl;
+        if (this->config.bDownload)
+        {
+            std::cout << "Downloading: " << filepath << std::endl;
+            CURLcode result = this->downloadFile(url, filepath, xml_data, gamename);
+            std::cout << std::endl;
+            if (result == CURLE_OK)
+            {
+                if (config.bAutomaticXMLCreation && bParsingFailed)
+                {
+                    std::cout << "Starting automatic XML creation" << std::endl;
+                    Util::createXML(filepath, config.iChunkSize, xml_directory);
+                }
+                res = 1;
+            }
+        }
+        return res;
+    }
+
+    // check if file sizes match
+    if (offset != filesize)
+    {
+        std::cout   << "Filesizes don't match" << std::endl
+                    << "Incomplete download or different version" << std::endl;
+        fclose(outfile);
+        if (this->config.bDownload)
+        {
+            std::cout << "Redownloading file" << std::endl;
+
+            std::string date_old = "." + bptime::to_iso_string(bptime::second_clock::local_time()) + ".old";
+            boost::filesystem::path new_name = filepath + date_old; // Rename old file by appending date and ".old" to filename
+            std::cout << "Renaming old file to " << new_name.string() << std::endl;
+            boost::system::error_code ec;
+            boost::filesystem::rename(pathname, new_name, ec); // Rename the file
+            if (ec)
+            {
+                std::cout << "Failed to rename " << filepath << " to " << new_name.string() << std::endl;
+                std::cout << "Skipping file" << std::endl;
+                res = 0;
+            }
+            else
+            {
+                if (bLocalXMLExists)
+                {
+                    std::cout << "Deleting old XML data" << std::endl;
+                    boost::filesystem::remove(xml_file, ec); // Delete old XML data
+                    if (ec)
+                    {
+                        std::cout << "Failed to delete " << xml_file << std::endl;
+                    }
+                }
+
+                CURLcode result = this->downloadFile(url, filepath, xml_data, gamename);
+                std::cout << std::endl;
+                if (result == CURLE_OK)
+                {
+                    bLocalXMLExists = boost::filesystem::exists(xml_file); // Check to see if downloadFile saved XML data
+                    if (!bLocalXMLExists)
+                    {
+                        std::cout << "Starting automatic XML creation" << std::endl;
+                        Util::createXML(filepath, config.iChunkSize, xml_directory);
+                    }
+                    res = 1;
+                }
+                else
+                {
+                    res = 0;
+                }
+            }
+        }
+        return res;
+    }
+
+    // Check all chunks
+    int iChunksRepaired = 0;
+    for (int i=0; i<chunks; i++)
+    {
+        off_t chunk_begin = chunk_from.at(i);
+        off_t chunk_end = chunk_to.at(i);
+        off_t size=0, chunk_size = chunk_end - chunk_begin + 1;
+        std::string range = std::to_string(chunk_begin) + "-" + std::to_string(chunk_end); // Download range string for curl
+
+        std::cout << "\033[0K\rChunk " << i << " (" << chunk_size << " bytes): ";
+        // use fseeko to support large files on 32 bit platforms
+        fseeko(outfile, chunk_begin, SEEK_SET);
+        unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *));
+        if (chunk == NULL)
+        {
+            std::cout << "Memory error" << std::endl;
+            fclose(outfile);
+            return res;
+        }
+        size = fread(chunk, 1, chunk_size, outfile);
+        if (size != chunk_size)
+        {
+            std::cout << "Read error" << std::endl;
+            free(chunk);
+            fclose(outfile);
+            return res;
+        }
+        std::string hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5);
+        if (hash != chunk_hash.at(i))
+        {
+            std::cout << "Failed - downloading chunk" << 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
+            this->beginDownload(); //begin chunk download
+            std::cout << std::endl;
+            if (config.bReport)
+                iChunksRepaired++;
+            i--; //verify downloaded chunk
+        }
+        else
+        {
+            std::cout << "OK\r" << std::flush;
+        }
+        free(chunk);
+        res = 1;
+    }
+    std::cout << std::endl;
+    fclose(outfile);
+
+    if (config.bReport)
+    {
+        std::string report_line = "Repaired [" + std::to_string(iChunksRepaired) + "/" + std::to_string(chunks) + "] " + filepath;
+        this->report_ofs << report_line << std::endl;
+    }
+
+    return res;
+}
+
+// Download cover images
+int Downloader::downloadCovers(const std::string& gamename, const std::string& directory, const std::string& cover_xml_data)
+{
+    int res = 0;
+    tinyxml2::XMLDocument xml;
+
+    // Check that directory exists and create subdirectories
+    boost::filesystem::path path = directory;
+    if (boost::filesystem::exists(path))
+    {
+        if (!boost::filesystem::is_directory(path))
+        {
+            std::cout << path << " is not directory" << std::endl;
+            return res;
+        }
+
+    }
+    else
+    {
+        if (!boost::filesystem::create_directories(path))
+        {
+            std::cout << "Failed to create directory: " << path << std::endl;
+            return res;
+        }
+    }
+
+    xml.Parse(cover_xml_data.c_str());
+    tinyxml2::XMLElement *rootNode = xml.RootElement();
+    if (!rootNode)
+    {
+        std::cout << "Not valid XML" << std::endl;
+        return res;
+    }
+    else
+    {
+        tinyxml2::XMLNode *gameNode = rootNode->FirstChild();
+        while (gameNode)
+        {
+            tinyxml2::XMLElement *gameElem = gameNode->ToElement();
+            std::string game_name = gameElem->Attribute("name");
+
+            if (game_name == gamename)
+            {
+                boost::match_results<std::string::const_iterator> what;
+                tinyxml2::XMLNode *coverNode = gameNode->FirstChild();
+                while (coverNode)
+                {
+                    tinyxml2::XMLElement *coverElem = coverNode->ToElement();
+                    std::string cover_url = coverElem->GetText();
+                    // Get file extension for the image
+                    boost::regex e1(".*(\\.\\w+)$", boost::regex::perl | boost::regex::icase);
+                    boost::regex_search(cover_url, what, e1);
+                    std::string file_extension = what[1];
+                    std::string cover_name = std::string("cover_") + coverElem->Attribute("id") + file_extension;
+                    std::string filepath = directory + "/" + cover_name;
+
+                    std::cout << "Downloading cover " << filepath << std::endl;
+                    CURLcode result = this->downloadFile(cover_url, filepath);
+                    std::cout << std::endl;
+                    if (result == CURLE_OK)
+                        res = 1;
+                    else
+                        res = 0;
+
+                    if (result == CURLE_HTTP_RETURNED_ERROR)
+                    {
+                        long int response_code = 0;
+                        result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+                        std::cout << "HTTP ERROR: ";
+                        if (result == CURLE_OK)
+                            std::cout << response_code << " (" << cover_url << ")" << std::endl;
+                        else
+                            std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << cover_url << ")" << std::endl;
+                    }
+
+                    coverNode = coverNode->NextSibling();
+                }
+                break; // Found cover for game, no need to go through rest of the game nodes
+            }
+            gameNode = gameNode->NextSibling();
+        }
+    }
+
+    return res;
+}
+
+CURLcode Downloader::beginDownload()
+{
+    this->TimeAndSize.clear();
+    this->timer.reset();
+    CURLcode result = curl_easy_perform(curlhandle);
+    this->resume_position = 0;
+    return result;
+}
+
+std::string Downloader::getResponse(const std::string& url)
+{
+    std::ostringstream memory;
+    std::string response;
+
+    curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+
+    CURLcode result;
+    do
+    {
+        if (config.iWait > 0)
+            usleep(config.iWait); // Delay the request by specified time
+        result = curl_easy_perform(curlhandle);
+        response = memory.str();
+        memory.str(std::string());
+    }
+    while ((result != CURLE_OK) && response.empty() && (this->retries++ < config.iRetries));
+    this->retries = 0; // reset retries counter
+
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0);
+
+    if (result != CURLE_OK)
+    {
+        std::cout << curl_easy_strerror(result) << std::endl;
+        if (result == CURLE_HTTP_RETURNED_ERROR)
+        {
+            long int response_code = 0;
+            result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+            std::cout << "HTTP ERROR: ";
+            if (result == CURLE_OK)
+                std::cout << response_code << " (" << url << ")" << std::endl;
+            else
+                std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl;
+        }
+    }
+
+    return response;
+}
+
+int Downloader::progressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+    // unused so lets prevent warnings and be more pedantic
+    (void) ulnow;
+    (void) ultotal;
+
+    // on entry: dltotal - how much remains to download till the end of the file (bytes)
+    //           dlnow   - how much was downloaded from the start of the program (bytes)
+    int bar_length      = 26;
+    int min_bar_length  = 5;
+    Downloader* downloader = static_cast<Downloader*>(clientp);
+
+    double rate; //  average download speed in B/s
+    // trying to get rate and setting to NaN if it fails
+    if (CURLE_OK != curl_easy_getinfo(downloader->curlhandle, CURLINFO_SPEED_DOWNLOAD, &rate))
+       rate = std::numeric_limits<double>::quiet_NaN();
+
+    // (Shmerl): this flag is needed to catch the case before anything was downloaded on resume,
+    // and there is no way to calculate the fraction, so we set to 0 (otherwise it'd be 1).
+    // This is to prevent the progress bar from jumping to 100% and then to lower value.
+    // It's visually better to jump from 0% to higher one.
+    bool starting = ((0 == dlnow) && (0 == dltotal));
+
+    // (Shmerl): DEBUG: strange thing - when resuming a file which is already downloaded, dlnow is correctly 0.0
+    // but dltotal is 389.0! This messes things up in the progress bar not showing the very last bar as full.
+    // enable this debug line to test the problem:
+    //
+    //   printf("\r\033[0K dlnow: %0.2f, dltotal: %0.2f\r", dlnow, dltotal); fflush(stdout); return 0;
+    //
+    // For now making a quirky workaround and setting dltotal to 0.0 in that case.
+    // It's probably better to find a real fix.
+    if ((0 == dlnow) && (389 == dltotal)) dltotal = 0;
+
+    // setting full dlwnow and dltotal
+    curl_off_t offset = static_cast<curl_off_t>(downloader->getResumePosition());
+    if (offset>0)
+    {
+        dlnow   += offset;
+        dltotal += offset;
+    }
+
+    // Update progress bar every 100ms
+    if (downloader->timer.getTimeBetweenUpdates()>=100 || dlnow == dltotal)
+    {
+        downloader->timer.reset();
+        int iTermWidth = Util::getTerminalWidth();
+
+        // 10 second average download speed
+        // Don't use static value of 10 seconds because update interval depends on when and how often progress callback is called
+        downloader->TimeAndSize.push_back(std::make_pair(time(NULL), static_cast<uintmax_t>(dlnow)));
+        if (downloader->TimeAndSize.size() > 100) // 100 * 100ms = 10s
+        {
+            downloader->TimeAndSize.pop_front();
+            time_t time_first = downloader->TimeAndSize.front().first;
+            uintmax_t size_first = downloader->TimeAndSize.front().second;
+            time_t time_last = downloader->TimeAndSize.back().first;
+            uintmax_t size_last = downloader->TimeAndSize.back().second;
+            rate = (size_last - size_first) / static_cast<double>((time_last - time_first));
+        }
+
+        bptime::time_duration eta(bptime::seconds((long)((dltotal - dlnow) / rate)));
+        std::stringstream eta_ss;
+        if (eta.hours() > 23)
+        {
+           eta_ss << eta.hours() / 24 << "d " <<
+                     std::setfill('0') << std::setw(2) << eta.hours() % 24 << "h " <<
+                     std::setfill('0') << std::setw(2) << eta.minutes() << "m " <<
+                     std::setfill('0') << std::setw(2) << eta.seconds() << "s";
+        }
+        else if (eta.hours() > 0)
+        {
+           eta_ss << eta.hours() << "h " <<
+                     std::setfill('0') << std::setw(2) << eta.minutes() << "m " <<
+                     std::setfill('0') << std::setw(2) << eta.seconds() << "s";
+        }
+        else if (eta.minutes() > 0)
+        {
+           eta_ss << eta.minutes() << "m " <<
+                     std::setfill('0') << std::setw(2) << eta.seconds() << "s";
+        }
+        else
+        {
+           eta_ss << eta.seconds() << "s";
+        }
+
+        // Create progressbar
+        double fraction = starting ? 0.0 : static_cast<double>(dlnow) / static_cast<double>(dltotal);
+
+        std::cout << Util::formattedString("\033[0K\r%3.0f%% ", fraction * 100);
+
+        // Download rate unit conversion
+        std::string rate_unit;
+        if (rate > 1048576) // 1 MB
+        {
+            rate /= 1048576;
+            rate_unit = "MB/s";
+        }
+        else
+        {
+            rate /= 1024;
+            rate_unit = "kB/s";
+        }
+        std::string status_text = Util::formattedString(" %0.2f/%0.2fMB @ %0.2f%s ETA: %s\r", static_cast<double>(dlnow)/1024/1024, static_cast<double>(dltotal)/1024/1024, rate, rate_unit.c_str(), eta_ss.str().c_str());
+        int status_text_length = status_text.length() + 6;
+
+        if ((status_text_length + bar_length) > iTermWidth)
+            bar_length -= (status_text_length + bar_length) - iTermWidth;
+
+        // Don't draw progressbar if length is less than min_bar_length
+        if (bar_length >= min_bar_length)
+            downloader->progressbar->draw(bar_length, fraction);
+
+        std::cout << status_text << std::flush;
+    }
+
+    return 0;
+}
+
+size_t Downloader::writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) {
+    std::ostringstream *stream = (std::ostringstream*)userp;
+    size_t count = size * nmemb;
+    stream->write(ptr, count);
+    return count;
+}
+
+size_t Downloader::writeData(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+    return fwrite(ptr, size, nmemb, stream);
+}
+
+size_t Downloader::readData(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+    return fread(ptr, size, nmemb, stream);
+}
+
+uintmax_t Downloader::getResumePosition()
+{
+    return this->resume_position;
+}
+
+std::vector<gameFile> Downloader::getExtrasFromJSON(const Json::Value& json, const std::string& gamename, const Config& config)
+{
+    std::vector<gameFile> extras;
+
+    // Create new API handle and set curl options for the API
+    API* api = new API(config.sToken, config.sSecret);
+    api->curlSetOpt(CURLOPT_VERBOSE, config.bVerbose);
+    api->curlSetOpt(CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer);
+    api->curlSetOpt(CURLOPT_CONNECTTIMEOUT, config.iTimeout);
+    if (!config.sCACertPath.empty())
+        api->curlSetOpt(CURLOPT_CAINFO, config.sCACertPath.c_str());
+
+    if (!api->init())
+    {
+        delete api;
+        return extras;
+    }
+
+    for (unsigned int i = 0; i < json["extras"].size(); ++i)
+    {
+        std::string id, name, path, downloaderUrl;
+        name = json["extras"][i]["name"].asString();
+        downloaderUrl = json["extras"][i]["downloaderUrl"].asString();
+        id.assign(downloaderUrl.begin()+downloaderUrl.find_last_of("/")+1, downloaderUrl.end());
+
+        // Get path from download link
+        std::string url = api->getExtraLink(gamename, id);
+        if (api->getError())
+        {
+            api->clearError();
+            continue;
+        }
+        url = htmlcxx::Uri::decode(url);
+        if (url.find("/extras/") != std::string::npos)
+        {
+            path.assign(url.begin()+url.find("/extras/"), url.begin()+url.find_first_of("?"));
+            path = "/" + gamename + path;
+        }
+        else
+        {
+            path.assign(url.begin()+url.find_last_of("/")+1, url.begin()+url.find_first_of("?"));
+            path = "/" + gamename + "/extras/" + path;
+        }
+
+        // Get filename
+        std::string filename;
+        filename.assign(path.begin()+path.find_last_of("/")+1,path.end());
+
+        // Use filename if name was not specified
+        if (name.empty())
+            name = filename;
+
+        if (name.empty())
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (getExtrasFromJSON)" << std::endl;
+                std::cerr << "Skipped file without a name (game: " << gamename << ", fileid: " << id << ")" << std::endl;
+            #endif
+            continue;
+        }
+
+        if (filename.empty())
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (getExtrasFromJSON)" << std::endl;
+                std::cerr << "Skipped file without a filename (game: " << gamename << ", fileid: " << id << ", name: " << name << ")" << std::endl;
+            #endif
+            continue;
+        }
+
+        gameFile gf;
+        gf.type = GFTYPE_EXTRA;
+        gf.gamename = gamename;
+        gf.updated = false;
+        gf.id = id;
+        gf.name = name;
+        gf.path = path;
+
+        extras.push_back(gf);
+    }
+    delete api;
+
+    return extras;
+}
+
+std::string Downloader::getSerialsFromJSON(const Json::Value& json)
+{
+    std::ostringstream serials;
+
+    if (!json.isMember("cdKey"))
+        return std::string();
+
+    std::string cdkey = json["cdKey"].asString();
+
+    if (cdkey.empty())
+        return std::string();
+
+    if (cdkey.find("<span>") == std::string::npos)
+    {
+        serials << cdkey << std::endl;
+    }
+    else
+    {
+        htmlcxx::HTML::ParserDom parser;
+        tree<htmlcxx::HTML::Node> dom = parser.parseTree(cdkey);
+        tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+        tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+        for (; it != end; ++it)
+        {
+            std::string tag_text;
+            if (it->tagName() == "span")
+            {
+                for (unsigned int j = 0; j < dom.number_of_children(it); ++j)
+                {
+                    tree<htmlcxx::HTML::Node>::iterator span_it = dom.child(it, j);
+                    if (!span_it->isTag() && !span_it->isComment())
+                        tag_text = span_it->text();
+                }
+            }
+
+            if (!tag_text.empty())
+            {
+                boost::regex expression("^\\h+|\\h+$");
+                std::string text = boost::regex_replace(tag_text, expression, "");
+                if (!text.empty())
+                    serials << text << std::endl;
+            }
+        }
+    }
+
+    return serials.str();
+}
+
+std::string Downloader::getChangelogFromJSON(const Json::Value& json)
+{
+    std::string changelog;
+    std::string title = "Changelog";
+
+    if (!json.isMember("changelog"))
+        return std::string();
+
+    changelog = json["changelog"].asString();
+
+    if (changelog.empty())
+        return std::string();
+
+    if (json.isMember("title"))
+        title = title + ": " + json["title"].asString();
+
+    changelog = "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>" + title + "</title>\n</head>\n<body>" + changelog + "</body>\n</html>";
+
+    return changelog;
+}
+
+// Linear search.  Good thing computers are fast and lists are small.
+static int isPresent(std::vector<gameFile>& list, const boost::filesystem::path& path, Blacklist& blacklist)
+{
+    if(blacklist.isBlacklisted(path.native()))
+       return false;
+    for (unsigned int k = 0; k < list.size(); ++k)
+       if (list[k].getFilepath() == path.native())
+           return true;
+    return false;
+}
+
+void Downloader::checkOrphans()
+{
+    // Always check everything when checking for orphaned files
+    config.bInstallers = true;
+    config.bExtras = true;
+    config.bPatches = true;
+    config.bLanguagePacks = true;
+
+    if (this->games.empty())
+        this->getGameDetails();
+
+    std::vector<std::string> orphans;
+    for (unsigned int i = 0; i < games.size(); ++i)
+    {
+        std::cerr << "Checking for orphaned files " << i+1 << " / " << games.size() << "\r" << std::flush;
+        std::vector<boost::filesystem::path> filepath_vector;
+
+        try
+        {
+            std::vector<boost::filesystem::path> paths;
+            std::vector<unsigned int> platformIds;
+            platformIds.push_back(0);
+            for (unsigned int j = 0; j < GlobalConstants::PLATFORMS.size(); ++j)
+            {
+                platformIds.push_back(GlobalConstants::PLATFORMS[j].id);
+            }
+            for (unsigned int j = 0; j < platformIds.size(); ++j)
+            {
+                std::string directory = config.sDirectory + "/" + config.sGameSubdir + "/";
+                Util::filepathReplaceReservedStrings(directory, games[i].gamename, platformIds[j]);
+                boost::filesystem::path path (directory);
+                if (boost::filesystem::exists(path))
+                {
+                    bool bDuplicate = false;
+                    for (unsigned int k = 0; k < paths.size(); ++k)
+                    {
+                        if (path == paths[k])
+                        {
+                            bDuplicate = true;
+                            break;
+                        }
+                    }
+                    if (!bDuplicate)
+                        paths.push_back(path);
+                }
+            }
+
+            for (unsigned int j = 0; j < paths.size(); ++j)
+            {
+                std::size_t pathlen = config.sDirectory.length();
+                if (boost::filesystem::exists(paths[j]))
+                {
+                    if (boost::filesystem::is_directory(paths[j]))
+                    {
+                        // Recursively iterate over files in directory
+                        boost::filesystem::recursive_directory_iterator end_iter;
+                        boost::filesystem::recursive_directory_iterator dir_iter(paths[j]);
+                        while (dir_iter != end_iter)
+                        {
+                            if (boost::filesystem::is_regular_file(dir_iter->status()))
+                            {
+                                std::string filepath = dir_iter->path().string();
+                                if (config.ignorelist.isBlacklisted(filepath.substr(pathlen))) {
+                                    if (config.bVerbose)
+                                        std::cerr << "skipped ignorelisted file " << filepath << std::endl;
+                                } else {
+                                    boost::regex expression(config.sOrphanRegex); // Limit to files matching the regex
+                                    boost::match_results<std::string::const_iterator> what;
+                                    if (boost::regex_search(filepath, what, expression))
+                                        filepath_vector.push_back(dir_iter->path());
+                                }
+                            }
+                            dir_iter++;
+                        }
+                    }
+                }
+                else
+                    std::cerr << paths[j] << " does not exist" << std::endl;
+            }
+        }
+        catch (const boost::filesystem::filesystem_error& ex)
+        {
+            std::cout << ex.what() << std::endl;
+        }
+
+        if (!filepath_vector.empty())
+        {
+            for (unsigned int j = 0; j < filepath_vector.size(); ++j)
+            {
+                bool bFoundFile = isPresent(games[i].installers, filepath_vector[j], config.blacklist)
+                              || isPresent(games[i].extras, filepath_vector[j], config.blacklist)
+                              || isPresent(games[i].patches, filepath_vector[j], config.blacklist)
+                              || isPresent(games[i].languagepacks, filepath_vector[j], config.blacklist);
+
+                if (!bFoundFile)
+                {   // Check dlcs
+                    for (unsigned int k = 0; k < games[i].dlcs.size(); ++k)
+                    {
+                        bFoundFile = isPresent(games[i].dlcs[k].installers, filepath_vector[j], config.blacklist)
+                            || isPresent(games[i].dlcs[k].extras, filepath_vector[j], config.blacklist)
+                            || isPresent(games[i].dlcs[k].patches, filepath_vector[j], config.blacklist)
+                            || isPresent(games[i].dlcs[k].languagepacks, filepath_vector[j], config.blacklist);
+                        if(bFoundFile)
+                            break;
+                    }
+                }
+                if (!bFoundFile)
+                    orphans.push_back(filepath_vector[j].string());
+            }
+        }
+    }
+    std::cout << std::endl;
+
+    if (!orphans.empty())
+    {
+        for (unsigned int i = 0; i < orphans.size(); ++i)
+        {
+            std::cout << orphans[i] << std::endl;
+        }
+    }
+    else
+    {
+        std::cout << "No orphaned files" << std::endl;
+    }
+
+    return;
+}
+
+// Check status of files
+void Downloader::checkStatus()
+{
+    if (this->games.empty())
+        this->getGameDetails();
+
+    // Create a vector containing all game files
+    std::vector<gameFile> vGameFiles;
+    for (unsigned int i = 0; i < games.size(); ++i)
+    {
+        std::vector<gameFile> vec = games[i].getGameFileVector();
+        vGameFiles.insert(std::end(vGameFiles), std::begin(vec), std::end(vec));
+    }
+
+    for (unsigned int i = 0; i < vGameFiles.size(); ++i)
+    {
+        unsigned int type = vGameFiles[i].type;
+        if (!config.bDLC && (type & GFTYPE_DLC))
+            continue;
+        if (!config.bInstallers && (type & GFTYPE_INSTALLER))
+            continue;
+        if (!config.bExtras && (type & GFTYPE_EXTRA))
+            continue;
+        if (!config.bPatches && (type & GFTYPE_PATCH))
+            continue;
+        if (!config.bLanguagePacks && (type & GFTYPE_LANGPACK))
+            continue;
+
+        boost::filesystem::path filepath = vGameFiles[i].getFilepath();
+
+        if (config.blacklist.isBlacklisted(filepath.native()))
+            continue;
+
+        std::string gamename = vGameFiles[i].gamename;
+        std::string id = vGameFiles[i].id;
+
+        if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+        {
+            std::string remoteHash;
+            bool bHashOK = true; // assume hash OK
+            uintmax_t filesize = boost::filesystem::file_size(filepath);
+
+            // GOG only provides xml data for installers, patches and language packs
+            if (type & (GFTYPE_INSTALLER | GFTYPE_PATCH | GFTYPE_LANGPACK))
+                remoteHash = this->getRemoteFileHash(gamename, id);
+            std::string localHash = this->getLocalFileHash(filepath.string(), gamename);
+
+            if (!remoteHash.empty())
+            {
+                if (remoteHash != localHash)
+                    bHashOK = false;
+                else
+                {
+                    // Check for incomplete file by comparing the filesizes
+                    // Remote hash was saved but download was incomplete and therefore getLocalFileHash returned the same as getRemoteFileHash
+                    uintmax_t filesize_xml = 0;
+                    boost::filesystem::path path = filepath;
+                    boost::filesystem::path local_xml_file;
+                    if (!gamename.empty())
+                        local_xml_file = config.sXMLDirectory + "/" + gamename + "/" + path.filename().string() + ".xml";
+                    else
+                        local_xml_file = config.sXMLDirectory + "/" + path.filename().string() + ".xml";
+
+                    if (boost::filesystem::exists(local_xml_file))
+                    {
+                        tinyxml2::XMLDocument local_xml;
+                        local_xml.LoadFile(local_xml_file.string().c_str());
+                        tinyxml2::XMLElement *fileElemLocal = local_xml.FirstChildElement("file");
+                        if (fileElemLocal)
+                        {
+                            std::string filesize_xml_str = fileElemLocal->Attribute("total_size");
+                            filesize_xml = std::stoull(filesize_xml_str);
+                        }
+                    }
+
+                    if (filesize_xml > 0 && filesize_xml != filesize)
+                    {
+                        localHash = Util::getFileHash(path.string(), RHASH_MD5);
+                        std::cout << "FS " << gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl;
+                        continue;
+                    }
+                }
+            }
+            std::cout << (bHashOK ? "OK " : "MD5 ") << gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl;
+        }
+        else
+        {
+            std::cout << "ND " << gamename << " " << filepath.filename().string() << std::endl;
+        }
+    }
+
+    return;
+}
+
+std::string Downloader::getLocalFileHash(const std::string& filepath, const std::string& gamename)
+{
+    std::string localHash;
+    boost::filesystem::path path = filepath;
+    boost::filesystem::path local_xml_file;
+    if (!gamename.empty())
+        local_xml_file = config.sXMLDirectory + "/" + gamename + "/" + path.filename().string() + ".xml";
+    else
+        local_xml_file = config.sXMLDirectory + "/" + path.filename().string() + ".xml";
+
+    if (config.bAutomaticXMLCreation && !boost::filesystem::exists(local_xml_file) && boost::filesystem::exists(path))
+    {
+        std::string xml_directory = config.sXMLDirectory + "/" + gamename;
+        Util::createXML(filepath, config.iChunkSize, xml_directory);
+    }
+
+    localHash = Util::getLocalFileHash(config.sXMLDirectory, filepath, gamename);
+
+    return localHash;
+}
+
+std::string Downloader::getRemoteFileHash(const std::string& gamename, const std::string& id)
+{
+    std::string remoteHash;
+    std::string xml_data = gogAPI->getXML(gamename, id);
+    if (gogAPI->getError())
+    {
+        std::cout << gogAPI->getErrorMessage() << std::endl;
+        gogAPI->clearError();
+    }
+    if (!xml_data.empty())
+    {
+        tinyxml2::XMLDocument remote_xml;
+        remote_xml.Parse(xml_data.c_str());
+        tinyxml2::XMLElement *fileElemRemote = remote_xml.FirstChildElement("file");
+        if (fileElemRemote)
+        {
+            remoteHash = fileElemRemote->Attribute("md5");
+        }
+    }
+    return remoteHash;
+}
+
+/* Load game details from cache file
+    returns 0 if successful
+    returns 1 if cache file doesn't exist
+    returns 2 if JSON parsing failed
+    returns 3 if cache is too old
+    returns 4 if JSON doesn't contain "games" node
+    returns 5 if cache version doesn't match
+*/
+int Downloader::loadGameDetailsCache()
+{
+    int res = 0;
+    std::string cachepath = config.sCacheDirectory + "/gamedetails.json";
+
+    // Make sure file exists
+    boost::filesystem::path path = cachepath;
+    if (!boost::filesystem::exists(path)) {
+        return res = 1;
+    }
+
+    bptime::ptime now = bptime::second_clock::local_time();
+    bptime::ptime cachedate;
+
+    std::ifstream json(cachepath, std::ifstream::binary);
+    Json::Value root;
+    Json::Reader *jsonparser = new Json::Reader;
+    if (jsonparser->parse(json, root))
+    {
+        if (root.isMember("date"))
+        {
+            cachedate = bptime::from_iso_string(root["date"].asString());
+            if ((now - cachedate) > bptime::minutes(config.iCacheValid))
+            {
+                // cache is too old
+                delete jsonparser;
+                json.close();
+                return res = 3;
+            }
+        }
+
+        int iCacheVersion = 0;
+        if (root.isMember("gamedetails-cache-version"))
+            iCacheVersion = root["gamedetails-cache-version"].asInt();
+
+        if (iCacheVersion != GlobalConstants::GAMEDETAILS_CACHE_VERSION)
+        {
+                res = 5;
+        }
+        else
+        {
+            if (root.isMember("games"))
+            {
+                this->games = getGameDetailsFromJsonNode(root["games"]);
+                res = 0;
+            }
+            else
+            {
+                res = 4;
+            }
+        }
+    }
+    else
+    {
+        res = 2;
+        std::cout << "Failed to parse cache" << std::endl;
+        std::cout << jsonparser->getFormattedErrorMessages() << std::endl;
+    }
+    delete jsonparser;
+    if (json)
+        json.close();
+
+    return res;
+}
+/* Save game details to cache file
+    returns 0 if successful
+    returns 1 if fails
+*/
+int Downloader::saveGameDetailsCache()
+{
+    int res = 0;
+
+    // Don't try to save cache if we don't have any game details
+    if (this->games.empty())
+    {
+        return 1;
+    }
+
+    std::string cachepath = config.sCacheDirectory + "/gamedetails.json";
+
+    Json::Value json;
+
+    json["gamedetails-cache-version"] = GlobalConstants::GAMEDETAILS_CACHE_VERSION;
+    json["version-string"] = config.sVersionString;
+    json["version-number"] = config.sVersionNumber;
+    json["date"] = bptime::to_iso_string(bptime::second_clock::local_time());
+
+    for (unsigned int i = 0; i < this->games.size(); ++i)
+        json["games"].append(this->games[i].getDetailsAsJson());
+
+    std::ofstream ofs(cachepath);
+    if (!ofs)
+    {
+        res = 1;
+    }
+    else
+    {
+        Json::StyledStreamWriter jsonwriter;
+        jsonwriter.write(ofs, json);
+        ofs.close();
+    }
+    return res;
+}
+
+std::vector<gameDetails> Downloader::getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level)
+{
+    std::vector<gameDetails> details;
+
+    // If root node is not array and we use root.size() it will return the number of nodes --> limit to 1 "array" node to make sure it is handled properly
+    for (unsigned int i = 0; i < (root.isArray() ? root.size() : 1); ++i)
+    {
+        Json::Value gameDetailsNode = (root.isArray() ? root[i] : root); // This json node can be array or non-array so take that into account
+        gameDetails game;
+        game.gamename = gameDetailsNode["gamename"].asString();
+
+        // DLCs are handled as part of the game so make sure that filtering is done with base game name
+        if (recursion_level == 0) // recursion level is 0 when handling base game
+        {
+            boost::regex expression(config.sGameRegex);
+            boost::match_results<std::string::const_iterator> what;
+            if (!boost::regex_search(game.gamename, what, expression)) // Check if name matches the specified regex
+                continue;
+        }
+        game.title = gameDetailsNode["title"].asString();
+        game.icon = gameDetailsNode["icon"].asString();
+        game.serials = gameDetailsNode["serials"].asString();
+        game.changelog = gameDetailsNode["changelog"].asString();
+
+        // Make a vector of valid node names to make things easier
+        std::vector<std::string> nodes;
+        nodes.push_back("extras");
+        nodes.push_back("installers");
+        nodes.push_back("patches");
+        nodes.push_back("languagepacks");
+        nodes.push_back("dlcs");
+
+        gameSpecificConfig conf;
+        conf.bDLC = config.bDLC;
+        conf.iInstallerLanguage = config.iInstallerLanguage;
+        conf.iInstallerPlatform = config.iInstallerPlatform;
+        conf.vLanguagePriority = config.vLanguagePriority;
+        conf.vPlatformPriority = config.vPlatformPriority;
+        if (Util::getGameSpecificConfig(game.gamename, &conf) > 0)
+            std::cerr << game.gamename << " - Language: " << conf.iInstallerLanguage << ", Platform: " << conf.iInstallerPlatform << ", DLC: " << (conf.bDLC ? "true" : "false") << std::endl;
+
+        for (unsigned int j = 0; j < nodes.size(); ++j)
+        {
+            std::string nodeName = nodes[j];
+            if (gameDetailsNode.isMember(nodeName))
+            {
+                Json::Value fileDetailsNodeVector = gameDetailsNode[nodeName];
+                for (unsigned int index = 0; index < fileDetailsNodeVector.size(); ++index)
+                {
+                    Json::Value fileDetailsNode = fileDetailsNodeVector[index];
+                    gameFile fileDetails;
+
+                    if (nodeName != "dlcs")
+                    {
+                        fileDetails.updated = fileDetailsNode["updated"].asInt();
+                        fileDetails.id = fileDetailsNode["id"].asString();
+                        fileDetails.name = fileDetailsNode["name"].asString();
+                        fileDetails.path = fileDetailsNode["path"].asString();
+                        fileDetails.size = fileDetailsNode["size"].asString();
+                        fileDetails.platform = fileDetailsNode["platform"].asUInt();
+                        fileDetails.language = fileDetailsNode["language"].asUInt();
+                        fileDetails.silent = fileDetailsNode["silent"].asInt();
+                        fileDetails.gamename = fileDetailsNode["gamename"].asString();
+                        fileDetails.type = fileDetailsNode["type"].asUInt();
+
+                        if (nodeName != "extras" && !(fileDetails.platform & conf.iInstallerPlatform))
+                            continue;
+                        if (nodeName != "extras" && !(fileDetails.language & conf.iInstallerLanguage))
+                            continue;
+                    }
+
+                    if (nodeName == "extras" && config.bExtras)
+                        game.extras.push_back(fileDetails);
+                    else if (nodeName == "installers" && config.bInstallers)
+                        game.installers.push_back(fileDetails);
+                    else if (nodeName == "patches" && config.bPatches)
+                        game.patches.push_back(fileDetails);
+                    else if (nodeName == "languagepacks" && config.bLanguagePacks)
+                        game.languagepacks.push_back(fileDetails);
+                    else if (nodeName == "dlcs" && conf.bDLC)
+                    {
+                        std::vector<gameDetails> dlcs = this->getGameDetailsFromJsonNode(fileDetailsNode, recursion_level + 1);
+                        game.dlcs.insert(game.dlcs.end(), dlcs.begin(), dlcs.end());
+                    }
+                }
+            }
+        }
+        if (!game.extras.empty() || !game.installers.empty() || !game.patches.empty() || !game.languagepacks.empty() || !game.dlcs.empty())
+            {
+                game.filterWithPriorities(conf);
+                details.push_back(game);
+            }
+    }
+    return details;
+}
+
+void Downloader::updateCache()
+{
+    // Make sure that all details get cached
+    config.bExtras = true;
+    config.bInstallers = true;
+    config.bPatches = true;
+    config.bLanguagePacks = true;
+    config.bDLC = true;
+    config.sGameRegex = ".*";
+    config.iInstallerLanguage = Util::getOptionValue("all", GlobalConstants::LANGUAGES);
+    config.iInstallerPlatform = Util::getOptionValue("all", GlobalConstants::PLATFORMS);
+    config.vLanguagePriority.clear();
+    config.vPlatformPriority.clear();
+    config.sIgnoreDLCCountRegex = ".*"; // Ignore DLC count for all games because GOG doesn't report DLC count correctly
+    gogWebsite->setConfig(config); // Make sure that website handle has updated config
+
+    this->getGameList();
+    this->getGameDetails();
+    if (this->saveGameDetailsCache())
+        std::cout << "Failed to save cache" << std::endl;
+
+    return;
+}
+
+// Save serials to file
+void Downloader::saveSerials(const std::string& serials, const std::string& filepath)
+{
+    bool bFileExists = boost::filesystem::exists(filepath);
+
+    if (bFileExists)
+        return;
+
+    // Get directory from filepath
+    boost::filesystem::path pathname = filepath;
+    std::string directory = pathname.parent_path().string();
+
+    // Check that directory exists and create subdirectories
+    boost::filesystem::path path = directory;
+    if (boost::filesystem::exists(path))
+    {
+        if (!boost::filesystem::is_directory(path))
+        {
+            std::cout << path << " is not directory" << std::endl;
+            return;
+        }
+    }
+    else
+    {
+        if (!boost::filesystem::create_directories(path))
+        {
+            std::cout << "Failed to create directory: " << path << std::endl;
+            return;
+        }
+    }
+
+    std::ofstream ofs(filepath);
+    if (ofs)
+    {
+        std::cout << "Saving serials: " << filepath << std::endl;
+        ofs << serials;
+        ofs.close();
+    }
+    else
+    {
+        std::cout << "Failed to create file: " << filepath << std::endl;
+    }
+
+    return;
+}
+
+// Save changelog to file
+void Downloader::saveChangelog(const std::string& changelog, const std::string& filepath)
+{
+    // Get directory from filepath
+    boost::filesystem::path pathname = filepath;
+    std::string directory = pathname.parent_path().string();
+
+    // Check that directory exists and create subdirectories
+    boost::filesystem::path path = directory;
+    if (boost::filesystem::exists(path))
+    {
+        if (!boost::filesystem::is_directory(path))
+        {
+            std::cout << path << " is not directory" << std::endl;
+            return;
+        }
+    }
+    else
+    {
+        if (!boost::filesystem::create_directories(path))
+        {
+            std::cout << "Failed to create directory: " << path << std::endl;
+            return;
+        }
+    }
+
+    std::ofstream ofs(filepath);
+    if (ofs)
+    {
+        std::cout << "Saving changelog: " << filepath << std::endl;
+        ofs << changelog;
+        ofs.close();
+    }
+    else
+    {
+        std::cout << "Failed to create file: " << filepath << std::endl;
+    }
+
+    return;
+}
+
+int Downloader::downloadFileWithId(const std::string& fileid_string, const std::string& output_filepath)
+{
+    int res = 1;
+    size_t pos = fileid_string.find("/");
+    if (pos == std::string::npos)
+    {
+        std::cout << "Invalid file id " << fileid_string << ": could not find separator \"/\"" << std::endl;
+    }
+    else if (!output_filepath.empty() && boost::filesystem::is_directory(output_filepath))
+    {
+        std::cout << "Failed to create the file " << output_filepath << ": Is a directory" << std::endl;
+    }
+    else
+    {
+        std::string gamename, fileid, url;
+        gamename.assign(fileid_string.begin(), fileid_string.begin()+pos);
+        fileid.assign(fileid_string.begin()+pos+1, fileid_string.end());
+
+        if (fileid.find("installer") != std::string::npos)
+            url = gogAPI->getInstallerLink(gamename, fileid);
+        else if (fileid.find("patch") != std::string::npos)
+            url = gogAPI->getPatchLink(gamename, fileid);
+        else if (fileid.find("langpack") != std::string::npos)
+            url = gogAPI->getLanguagePackLink(gamename, fileid);
+        else
+            url = gogAPI->getExtraLink(gamename, fileid);
+
+        if (!gogAPI->getError())
+        {
+            std::string filename, filepath;
+            filename.assign(url.begin()+url.find_last_of("/")+1, url.begin()+url.find_first_of("?"));
+            if (output_filepath.empty())
+                filepath = Util::makeFilepath(config.sDirectory, filename, gamename);
+            else
+                filepath = output_filepath;
+            std::cout << "Downloading: " << filepath << std::endl;
+            res = this->downloadFile(url, filepath, std::string(), gamename);
+            std::cout << std::endl;
+        }
+        else
+        {
+            std::cout << gogAPI->getErrorMessage() << std::endl;
+            gogAPI->clearError();
+        }
+    }
+
+    return res;
+}
+
+void Downloader::showWishlist()
+{
+    std::vector<wishlistItem> wishlistItems = gogWebsite->getWishlistItems();
+    for (unsigned int i = 0; i < wishlistItems.size(); ++i)
+    {
+        wishlistItem item = wishlistItems[i];
+        std::string platforms_text = Util::getOptionNameString(item.platform, GlobalConstants::PLATFORMS);
+        std::string tags_text;
+        for (unsigned int j = 0; j < item.tags.size(); ++j)
+        {
+            tags_text += (tags_text.empty() ? "" : ", ")+item.tags[j];
+        }
+        if (!tags_text.empty())
+            tags_text = "[" + tags_text + "]";
+
+        std::string price_text = item.price;
+        if (item.bIsDiscounted)
+            price_text += " (-" + item.discount_percent + " | -" + item.discount + ")";
+
+        std::cout << item.title;
+        if (!tags_text.empty())
+            std::cout << " " << tags_text;
+        std::cout << std::endl;
+        std::cout << "\t" << item.url << std::endl;
+        if (item.platform != 0)
+            std::cout << "\tPlatforms: " << platforms_text << std::endl;
+        if (item.release_date_time != 0)
+            std::cout << "\tRelease date: " << bptime::to_simple_string(bptime::from_time_t(item.release_date_time)) << std::endl;
+        std::cout << "\tPrice: " << price_text << std::endl;
+        if (item.bIsBonusStoreCreditIncluded)
+            std::cout << "\tStore credit: " << item.store_credit << std::endl;
+        std::cout << std::endl;
+    }
+
+    return;
+}
+
+void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
+{
+    std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
+
+    API* api = new API(conf.sToken, conf.sSecret);
+    api->curlSetOpt(CURLOPT_SSL_VERIFYPEER, conf.bVerifyPeer);
+    api->curlSetOpt(CURLOPT_CONNECTTIMEOUT, conf.iTimeout);
+    if (!conf.sCACertPath.empty())
+        api->curlSetOpt(CURLOPT_CAINFO, conf.sCACertPath.c_str());
+
+    if (!api->init())
+    {
+        delete api;
+        msgQueue.push(Message("API init failed", MSGTYPE_ERROR, msg_prefix));
+        vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+        return;
+    }
+
+    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_NOPROGRESS, 0);
+    curl_easy_setopt(dlhandle, CURLOPT_NOSIGNAL, 1);
+
+    curl_easy_setopt(dlhandle, CURLOPT_CONNECTTIMEOUT, conf.iTimeout);
+    curl_easy_setopt(dlhandle, CURLOPT_FAILONERROR, true);
+    curl_easy_setopt(dlhandle, CURLOPT_SSL_VERIFYPEER, conf.bVerifyPeer);
+    curl_easy_setopt(dlhandle, CURLOPT_VERBOSE, conf.bVerbose);
+    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.iDownloadRate);
+
+    // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+    curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_TIME, 30);
+    curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_LIMIT, 200);
+
+    if (!conf.sCACertPath.empty())
+        curl_easy_setopt(dlhandle, CURLOPT_CAINFO, conf.sCACertPath.c_str());
+
+    xferInfo xferinfo;
+    xferinfo.tid = tid;
+    xferinfo.curlhandle = dlhandle;
+
+    curl_easy_setopt(dlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallbackForThread);
+    curl_easy_setopt(dlhandle, CURLOPT_XFERINFODATA, &xferinfo);
+
+    gameFile gf;
+    while (dlQueue.try_pop(gf))
+    {
+        CURLcode result = CURLE_RECV_ERROR; // assume network error
+        int iRetryCount = 0;
+        off_t iResumePosition = 0;
+
+        vDownloadInfo[tid].setStatus(DLSTATUS_STARTING);
+
+        // Get directory from filepath
+        boost::filesystem::path filepath = gf.getFilepath();
+        filepath = boost::filesystem::absolute(filepath, boost::filesystem::current_path());
+        boost::filesystem::path directory = filepath.parent_path();
+
+        // Skip blacklisted files
+        if (conf.blacklist.isBlacklisted(filepath.string()))
+        {
+            msgQueue.push(Message("Blacklisted file: " + filepath.string(), MSGTYPE_INFO, msg_prefix));
+            continue;
+        }
+
+        std::string filenameXML = filepath.filename().string() + ".xml";
+        std::string xml_directory = conf.sXMLDirectory + "/" + gf.gamename;
+        boost::filesystem::path local_xml_file = xml_directory + "/" + filenameXML;
+
+        vDownloadInfo[tid].setFilename(filepath.filename().string());
+        msgQueue.push(Message("Begin download: " + filepath.filename().string(), MSGTYPE_INFO, msg_prefix));
+
+        // Check that directory exists and create subdirectories
+        mtx_create_directories.lock(); // Use mutex to avoid possible race conditions
+        if (boost::filesystem::exists(directory))
+        {
+            if (!boost::filesystem::is_directory(directory))
+            {
+                mtx_create_directories.unlock();
+                msgQueue.push(Message(directory.string() + " is not directory, skipping file (" + filepath.filename().string() + ")", MSGTYPE_WARNING, msg_prefix));
+                continue;
+            }
+            else
+            {
+                mtx_create_directories.unlock();
+            }
+        }
+        else
+        {
+            if (!boost::filesystem::create_directories(directory))
+            {
+                mtx_create_directories.unlock();
+                msgQueue.push(Message("Failed to create directory (" + directory.string() + "), skipping file (" + filepath.filename().string() + ")", MSGTYPE_ERROR, msg_prefix));
+                continue;
+            }
+            else
+            {
+                mtx_create_directories.unlock();
+            }
+        }
+
+        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.bRemoteXML)
+        {
+            xml = api->getXML(gf.gamename, gf.id);
+            if (api->getError())
+            {
+                msgQueue.push(Message(api->getErrorMessage(), MSGTYPE_ERROR, msg_prefix));
+                api->clearError();
+            }
+            else
+            {
+                if (!xml.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)
+                        {
+                            std::string remoteHash = fileElem->Attribute("md5");
+                            if (remoteHash != localHash)
+                                bSameVersion = false;
+                        }
+                    }
+                }
+            }
+        }
+
+        bool bResume = false;
+        if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+        {
+            if (bSameVersion)
+            {
+                bResume = true;
+            }
+            else
+            {
+                msgQueue.push(Message("Remote file is different, renaming local file", MSGTYPE_INFO, msg_prefix));
+                std::string date_old = "." + bptime::to_iso_string(bptime::second_clock::local_time()) + ".old";
+                boost::filesystem::path new_name = filepath.string() + date_old; // Rename old file by appending date and ".old" to filename
+                boost::system::error_code ec;
+                boost::filesystem::rename(filepath, new_name, ec); // Rename the file
+                if (ec)
+                {
+                    msgQueue.push(Message("Failed to rename " + filepath.string() + " to " + new_name.string() + " - Skipping file", MSGTYPE_WARNING, msg_prefix));
+                    continue;
+                }
+            }
+        }
+
+        // Save remote XML
+        if (!xml.empty())
+        {
+            if ((bLocalXMLExists && !bSameVersion) || !bLocalXMLExists)
+            {
+                // Check that directory exists and create subdirectories
+                boost::filesystem::path path = xml_directory;
+                mtx_create_directories.lock(); // Use mutex to avoid race conditions
+                if (boost::filesystem::exists(path))
+                {
+                    if (!boost::filesystem::is_directory(path))
+                    {
+                        msgQueue.push(Message(path.string() + " is not directory", MSGTYPE_WARNING, msg_prefix));
+                    }
+                }
+                else
+                {
+                    if (!boost::filesystem::create_directories(path))
+                    {
+                        msgQueue.push(Message("Failed to create directory: " + path.string(), MSGTYPE_ERROR, msg_prefix));
+                    }
+                }
+                mtx_create_directories.unlock();
+                std::ofstream ofs(local_xml_file.string().c_str());
+                if (ofs)
+                {
+                    ofs << xml;
+                    ofs.close();
+                }
+                else
+                {
+                    msgQueue.push(Message("Can't create " + local_xml_file.string(), MSGTYPE_ERROR, msg_prefix));
+                }
+            }
+        }
+
+        // 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;
+        }
+
+        curl_easy_setopt(dlhandle, CURLOPT_URL, url.c_str());
+        do
+        {
+            if (iRetryCount != 0)
+                msgQueue.push(Message("Retry " + std::to_string(iRetryCount) + "/" + std::to_string(conf.iRetries) + ": " + filepath.filename().string(), MSGTYPE_INFO, msg_prefix));
+
+            FILE* outfile;
+            // File exists, resume
+            if (bResume)
+            {
+                iResumePosition = boost::filesystem::file_size(filepath);
+                if ((outfile=fopen(filepath.string().c_str(), "r+"))!=NULL)
+                {
+                    fseek(outfile, 0, SEEK_END);
+                    curl_easy_setopt(dlhandle, CURLOPT_RESUME_FROM_LARGE, iResumePosition);
+                    curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
+                }
+                else
+                {
+                    msgQueue.push(Message("Failed to open " + filepath.string(), MSGTYPE_ERROR, msg_prefix));
+                    break;
+                }
+            }
+            else // File doesn't exist, create new file
+            {
+                if ((outfile=fopen(filepath.string().c_str(), "w"))!=NULL)
+                {
+                    curl_easy_setopt(dlhandle, CURLOPT_RESUME_FROM_LARGE, 0); // start downloading from the beginning of file
+                    curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
+                }
+                else
+                {
+                    msgQueue.push(Message("Failed to create " + filepath.string(), MSGTYPE_ERROR, msg_prefix));
+                    break;
+                }
+            }
+
+            xferinfo.offset = iResumePosition;
+            xferinfo.timer.reset();
+            xferinfo.TimeAndSize.clear();
+            result = curl_easy_perform(dlhandle);
+            fclose(outfile);
+
+            if (result == CURLE_PARTIAL_FILE || result == CURLE_OPERATION_TIMEDOUT)
+            {
+                iRetryCount++;
+                if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+                    bResume = true;
+            }
+
+        } while ((result == CURLE_PARTIAL_FILE || result == CURLE_OPERATION_TIMEDOUT) && (iRetryCount <= conf.iRetries));
+
+        long int response_code = 0;
+        if (result == CURLE_HTTP_RETURNED_ERROR)
+        {
+            curl_easy_getinfo(dlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+        }
+        if (result == CURLE_OK || result == CURLE_RANGE_ERROR || (result == CURLE_HTTP_RETURNED_ERROR && response_code == 416))
+        {
+            // Average download speed
+            std::ostringstream dlrate_avg;
+            std::string rate_unit;
+            progressInfo progress_info = vDownloadInfo[tid].getProgressInfo();
+            if (progress_info.rate_avg > 1048576) // 1 MB
+            {
+                progress_info.rate_avg /= 1048576;
+                rate_unit = "MB/s";
+            }
+            else
+            {
+                progress_info.rate_avg /= 1024;
+                rate_unit = "kB/s";
+            }
+            dlrate_avg << std::setprecision(2) << std::fixed << progress_info.rate_avg << rate_unit;
+
+            msgQueue.push(Message("Download complete: " + filepath.filename().string() + " (@ " + dlrate_avg.str() + ")", MSGTYPE_SUCCESS, msg_prefix));
+        }
+        else
+        {
+            msgQueue.push(Message("Download complete (" + static_cast<std::string>(curl_easy_strerror(result)) + "): " + filepath.filename().string(), MSGTYPE_WARNING, msg_prefix));
+
+            // Delete the file if download failed and was not a resume attempt or the result is zero length file
+            if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+            {
+                if ((result != CURLE_PARTIAL_FILE && !bResume && result != CURLE_OPERATION_TIMEDOUT) || boost::filesystem::file_size(filepath) == 0)
+                {
+                    if (!boost::filesystem::remove(filepath))
+                        msgQueue.push(Message("Failed to delete " + filepath.filename().string(), MSGTYPE_ERROR, msg_prefix));
+                }
+            }
+        }
+
+        // Automatic xml creation
+        if (conf.bAutomaticXMLCreation)
+        {
+            if (result == CURLE_OK)
+            {
+                if ((gf.type & GFTYPE_EXTRA) || (conf.bRemoteXML && !bLocalXMLExists && xml.empty()))
+                    createXMLQueue.push(gf);
+            }
+        }
+    }
+
+    curl_easy_cleanup(dlhandle);
+    delete api;
+
+    vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+    msgQueue.push(Message("Finished all tasks", MSGTYPE_INFO, msg_prefix));
+
+    return;
+}
+
+int Downloader::progressCallbackForThread(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+    // unused so lets prevent warnings and be more pedantic
+    (void) ulnow;
+    (void) ultotal;
+
+    xferInfo* xferinfo = static_cast<xferInfo*>(clientp);
+
+    // Update progress info every 100ms
+    if (xferinfo->timer.getTimeBetweenUpdates()>=100 || dlnow == dltotal)
+    {
+        xferinfo->timer.reset();
+        progressInfo info;
+        info.dlnow = dlnow;
+        info.dltotal = dltotal;
+
+        // trying to get rate and setting to NaN if it fails
+        if (CURLE_OK != curl_easy_getinfo(xferinfo->curlhandle, CURLINFO_SPEED_DOWNLOAD, &info.rate_avg))
+            info.rate_avg = std::numeric_limits<double>::quiet_NaN();
+
+        // setting full dlwnow and dltotal
+        if (xferinfo->offset > 0)
+        {
+            info.dlnow   += xferinfo->offset;
+            info.dltotal += xferinfo->offset;
+        }
+
+        // 10 second average download speed
+        // Don't use static value of 10 seconds because update interval depends on when and how often progress callback is called
+        xferinfo->TimeAndSize.push_back(std::make_pair(time(NULL), static_cast<uintmax_t>(info.dlnow)));
+        if (xferinfo->TimeAndSize.size() > 100) // 100 * 100ms = 10s
+        {
+            xferinfo->TimeAndSize.pop_front();
+            time_t time_first = xferinfo->TimeAndSize.front().first;
+            uintmax_t size_first = xferinfo->TimeAndSize.front().second;
+            time_t time_last = xferinfo->TimeAndSize.back().first;
+            uintmax_t size_last = xferinfo->TimeAndSize.back().second;
+            info.rate = (size_last - size_first) / static_cast<double>((time_last - time_first));
+        }
+        else
+        {
+            info.rate = info.rate_avg;
+        }
+
+        vDownloadInfo[xferinfo->tid].setProgressInfo(info);
+        vDownloadInfo[xferinfo->tid].setStatus(DLSTATUS_RUNNING);
+    }
+
+    return 0;
+}
+
+void Downloader::printProgress()
+{
+    // Print progress information until all threads have finished their tasks
+    ProgressBar bar(config.bUnicode, config.bColor);
+    unsigned int dl_status = DLSTATUS_NOTSTARTED;
+    while (dl_status != DLSTATUS_FINISHED)
+    {
+        dl_status = DLSTATUS_NOTSTARTED;
+
+        // Print progress information once per 100ms
+        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+        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
+        Message msg;
+        while (msgQueue.try_pop(msg))
+        {
+            std::cout << msg.getFormattedString(config.bColor, true) << std::endl;
+            if (config.bReport)
+            {
+                this->report_ofs << msg.getTimestampString() << ": " << msg.getMessage() << std::endl;
+            }
+        }
+
+        int iTermWidth = Util::getTerminalWidth();
+        double total_rate = 0;
+
+        // Create progress info text for all download threads
+        std::vector<std::string> vProgressText;
+        for (unsigned int i = 0; i < vDownloadInfo.size(); ++i)
+        {
+            std::string progress_text;
+            int bar_length     = 26;
+            int min_bar_length = 5;
+
+            unsigned int status = vDownloadInfo[i].getStatus();
+            dl_status |= status;
+
+            if (status == DLSTATUS_FINISHED)
+            {
+                vProgressText.push_back("#" + std::to_string(i) + ": Finished");
+                continue;
+            }
+
+            std::string filename = vDownloadInfo[i].getFilename();
+            progressInfo progress_info = vDownloadInfo[i].getProgressInfo();
+            total_rate += progress_info.rate;
+
+            bool starting = ((0 == progress_info.dlnow) && (0 == progress_info.dltotal));
+            double fraction = starting ? 0.0 : static_cast<double>(progress_info.dlnow) / static_cast<double>(progress_info.dltotal);
+
+            std::string progress_percentage_text = Util::formattedString("%3.0f%% ", fraction * 100);
+            int progress_percentage_text_length = progress_percentage_text.length() + 1;
+
+            bptime::time_duration eta(bptime::seconds((long)((progress_info.dltotal - progress_info.dlnow) / progress_info.rate)));
+            std::stringstream eta_ss;
+            if (eta.hours() > 23)
+            {
+               eta_ss << eta.hours() / 24 << "d " <<
+                         std::setfill('0') << std::setw(2) << eta.hours() % 24 << "h " <<
+                         std::setfill('0') << std::setw(2) << eta.minutes() << "m " <<
+                         std::setfill('0') << std::setw(2) << eta.seconds() << "s";
+            }
+            else if (eta.hours() > 0)
+            {
+               eta_ss << eta.hours() << "h " <<
+                         std::setfill('0') << std::setw(2) << eta.minutes() << "m " <<
+                         std::setfill('0') << std::setw(2) << eta.seconds() << "s";
+            }
+            else if (eta.minutes() > 0)
+            {
+               eta_ss << eta.minutes() << "m " <<
+                         std::setfill('0') << std::setw(2) << eta.seconds() << "s";
+            }
+            else
+            {
+               eta_ss << eta.seconds() << "s";
+            }
+
+            std::string rate_unit;
+            if (progress_info.rate > 1048576) // 1 MB
+            {
+                progress_info.rate /= 1048576;
+                rate_unit = "MB/s";
+            }
+            else
+            {
+                progress_info.rate /= 1024;
+                rate_unit = "kB/s";
+            }
+
+            std::string progress_status_text = Util::formattedString(" %0.2f/%0.2fMB @ %0.2f%s ETA: %s", static_cast<double>(progress_info.dlnow)/1024/1024, static_cast<double>(progress_info.dltotal)/1024/1024, progress_info.rate, rate_unit.c_str(), eta_ss.str().c_str());
+            int status_text_length = progress_status_text.length() + 1;
+
+            if ((status_text_length + progress_percentage_text_length + bar_length) > iTermWidth)
+                bar_length -= (status_text_length + progress_percentage_text_length + bar_length) - iTermWidth;
+
+            // Don't draw progressbar if length is less than min_bar_length
+            std::string progress_bar_text;
+            if (bar_length >= min_bar_length)
+                progress_bar_text = bar.createBarString(bar_length, fraction);
+
+            progress_text = progress_percentage_text + progress_bar_text + progress_status_text;
+            std::string filename_text = "#" + std::to_string(i) + " " + filename;
+            Util::shortenStringToTerminalWidth(filename_text);
+
+            vProgressText.push_back(filename_text);
+            vProgressText.push_back(progress_text);
+        }
+
+        // Total download speed and number of remaining tasks in download queue
+        if (dl_status != DLSTATUS_FINISHED)
+        {
+            std::ostringstream ss;
+            if (config.iThreads > 1)
+            {
+                std::string rate_unit;
+                if (total_rate > 1048576) // 1 MB
+                {
+                    total_rate /= 1048576;
+                    rate_unit = "MB/s";
+                }
+                else
+                {
+                    total_rate /= 1024;
+                    rate_unit = "kB/s";
+                }
+                ss << "Total: " << std::setprecision(2) << std::fixed << total_rate << rate_unit << " | ";
+            }
+            ss << "Remaining: " << dlQueue.size();
+            vProgressText.push_back(ss.str());
+        }
+
+        // Print progress info
+        for (unsigned int i = 0; i < vProgressText.size(); ++i)
+        {
+            std::cout << vProgressText[i] << std::endl;
+        }
+
+        // Move cursor up by vProgressText.size() rows
+        if (dl_status != DLSTATUS_FINISHED)
+        {
+            std::cout << "\033[" << vProgressText.size() << "A\r" << std::flush;
+        }
+    }
+}
+
+void Downloader::getGameDetailsThread(Config config, const unsigned int& tid)
+{
+    std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
+
+    API* api = new API(config.sToken, config.sSecret);
+    api->curlSetOpt(CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer);
+    api->curlSetOpt(CURLOPT_CONNECTTIMEOUT, config.iTimeout);
+    if (!config.sCACertPath.empty())
+        api->curlSetOpt(CURLOPT_CAINFO, config.sCACertPath.c_str());
+
+    if (!api->init())
+    {
+        delete api;
+        msgQueue.push(Message("API init failed", MSGTYPE_ERROR, msg_prefix));
+        vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+        return;
+    }
+
+    // Create new GOG website handle
+    Website* website = new Website(config);
+    if (!website->IsLoggedIn())
+    {
+        delete api;
+        delete website;
+        msgQueue.push(Message("Website not logged in", MSGTYPE_ERROR, msg_prefix));
+        vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+        return;
+    }
+
+    // Set default game specific directory options to values from config
+    gameSpecificDirectoryConfig dirConfDefault;
+    dirConfDefault.sDirectory = config.sDirectory;
+    dirConfDefault.bSubDirectories = config.bSubDirectories;
+    dirConfDefault.sGameSubdir = config.sGameSubdir;
+    dirConfDefault.sInstallersSubdir = config.sInstallersSubdir;
+    dirConfDefault.sExtrasSubdir = config.sExtrasSubdir;
+    dirConfDefault.sLanguagePackSubdir = config.sLanguagePackSubdir;
+    dirConfDefault.sDLCSubdir = config.sDLCSubdir;
+    dirConfDefault.sPatchesSubdir = config.sPatchesSubdir;
+
+    gameItem game_item;
+    while (gameItemQueue.try_pop(game_item))
+    {
+        gameDetails game;
+        bool bHasDLC = !game_item.dlcnames.empty();
+
+        gameSpecificConfig conf;
+        conf.bDLC = config.bDLC;
+        conf.bIgnoreDLCCount = false;
+        conf.iInstallerLanguage = config.iInstallerLanguage;
+        conf.iInstallerPlatform = config.iInstallerPlatform;
+        conf.dirConf = dirConfDefault;
+        conf.vLanguagePriority = config.vLanguagePriority;
+        conf.vPlatformPriority = config.vPlatformPriority;
+        if (!config.bUpdateCache) // Disable game specific config files for cache update
+        {
+            int iOptionsOverridden = Util::getGameSpecificConfig(game_item.name, &conf);
+            if (iOptionsOverridden > 0)
+            {
+                std::ostringstream ss;
+                ss << game_item.name << " - " << iOptionsOverridden << " options overridden with game specific options" << std::endl;
+                if (config.bVerbose)
+                {
+                    if (conf.bIgnoreDLCCount)
+                        ss << "\tIgnore DLC count" << std::endl;
+                    if (conf.bDLC != config.bDLC)
+                        ss << "\tDLC: " << (conf.bDLC ? "true" : "false") << std::endl;
+                    if (conf.iInstallerLanguage != config.iInstallerLanguage)
+                        ss << "\tLanguage: " << Util::getOptionNameString(conf.iInstallerLanguage, GlobalConstants::LANGUAGES) << std::endl;
+                    if (conf.vLanguagePriority != config.vLanguagePriority)
+                    {
+                        ss << "\tLanguage priority:" << std::endl;
+                        for (unsigned int j = 0; j < conf.vLanguagePriority.size(); ++j)
+                        {
+                            ss << "\t  " << j << ": " << Util::getOptionNameString(conf.vLanguagePriority[j], GlobalConstants::LANGUAGES) << std::endl;
+                        }
+                    }
+                    if (conf.iInstallerPlatform != config.iInstallerPlatform)
+                        ss << "\tPlatform: " << Util::getOptionNameString(conf.iInstallerPlatform, GlobalConstants::PLATFORMS) << std::endl;
+                    if (conf.vPlatformPriority != config.vPlatformPriority)
+                    {
+                        ss << "\tPlatform priority:" << std::endl;
+                        for (unsigned int j = 0; j < conf.vPlatformPriority.size(); ++j)
+                        {
+                            ss << "\t  " << j << ": " << Util::getOptionNameString(conf.vPlatformPriority[j], GlobalConstants::PLATFORMS) << std::endl;
+                        }
+                    }
+                }
+                msgQueue.push(Message(ss.str(), MSGTYPE_INFO, msg_prefix));
+            }
+        }
+
+        game = api->getGameDetails(game_item.name, conf.iInstallerPlatform, conf.iInstallerLanguage, config.bDuplicateHandler);
+        if (!api->getError())
+        {
+            game.filterWithPriorities(conf);
+            Json::Value gameDetailsJSON;
+
+            if (!game_item.gamedetailsjson.empty())
+                gameDetailsJSON = game_item.gamedetailsjson;
+
+            if (game.extras.empty() && config.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 (config.bSaveSerials)
+            {
+                if (gameDetailsJSON.empty())
+                    gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+                game.serials = Downloader::getSerialsFromJSON(gameDetailsJSON);
+            }
+            if (config.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.bDLC && conf.bIgnoreDLCCount)
+            {
+                if (gameDetailsJSON.empty())
+                    gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+
+                game_item.dlcnames = Util::getDLCNamesFromJSON(gameDetailsJSON["dlcs"]);
+                bHasDLC = !game_item.dlcnames.empty();
+            }
+
+            if (game.dlcs.empty() && bHasDLC && conf.bDLC)
+            {
+                for (unsigned int j = 0; j < game_item.dlcnames.size(); ++j)
+                {
+                    gameDetails dlc;
+                    dlc = api->getGameDetails(game_item.dlcnames[j], conf.iInstallerPlatform, conf.iInstallerLanguage, config.bDuplicateHandler);
+                    dlc.filterWithPriorities(conf);
+                    if (dlc.extras.empty() && config.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 (config.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);
+                            }
+
+                            if (!urls.empty())
+                            {
+                                if (urls[0].find("/" + game_item.dlcnames[j] + "/") != std::string::npos)
+                                {
+                                    dlc.serials = Downloader::getSerialsFromJSON(gameDetailsJSON["dlcs"][k]);
+                                }
+                            }
+                        }
+                    }
+
+                    if (config.bSaveChangelogs)
+                    {
+                        if (gameDetailsJSON.empty())
+                            gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+
+                        // 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]);
+                                }
+                            }
+                        }
+                    }
+
+                    // 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);
+                }
+            }
+
+            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 (game.installers[j].updated)
+                    {
+                        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 website;
+    return;
+}
diff --git a/src/gamedetails.cpp b/src/gamedetails.cpp
new file mode 100644 (file)
index 0000000..794ca70
--- /dev/null
@@ -0,0 +1,205 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "gamedetails.h"
+
+gameDetails::gameDetails()
+{
+    //ctor
+}
+
+gameDetails::~gameDetails()
+{
+    //dtor
+}
+
+void gameDetails::filterWithPriorities(const gameSpecificConfig& config)
+{
+    if (config.vPlatformPriority.empty() && config.vLanguagePriority.empty())
+        return;
+
+    filterListWithPriorities(installers, config);
+    filterListWithPriorities(patches, config);
+    filterListWithPriorities(languagepacks, config);
+}
+
+void gameDetails::filterListWithPriorities(std::vector<gameFile>& list, const gameSpecificConfig& config)
+{
+    /*
+      Compute the score of each item - we use a scoring mechanism and we keep all ties
+      Like if someone asked French then English and Linux then Windows, but there are
+      only Windows French, Windows English and Linux English versions, we'll get the
+      Windows French and Linux English ones.
+      Score is inverted: lower is better.
+    */
+    int bestscore = -1;
+
+    for (std::vector<gameFile>::iterator fileDetails = list.begin(); fileDetails != list.end(); fileDetails++)
+        {
+            fileDetails->score = 0;
+            if (!config.vPlatformPriority.empty())
+                {
+                    for (size_t i = 0; i != config.vPlatformPriority.size(); i++)
+                        if (fileDetails->platform & config.vPlatformPriority[i])
+                            {
+                                fileDetails->score += i;
+                                break;
+                            }
+                }
+            if (!config.vLanguagePriority.empty())
+                {
+                    for (size_t i = 0; i != config.vLanguagePriority.size(); i++)
+                        if (fileDetails->language & config.vLanguagePriority[i])
+                            {
+                                fileDetails->score += i;
+                                break;
+                            }
+                }
+            if ((fileDetails->score < bestscore) or (bestscore < 0))
+                bestscore = fileDetails->score;
+        }
+
+    for (std::vector<gameFile>::iterator fileDetails = list.begin(); fileDetails != list.end(); )
+        {
+            if (fileDetails->score > bestscore)
+                fileDetails = list.erase(fileDetails);
+            else
+                fileDetails++;
+        }
+}
+
+void gameDetails::makeFilepaths(const gameSpecificDirectoryConfig& config)
+{
+    std::string filepath;
+    std::string directory = config.sDirectory + "/" + config.sGameSubdir + "/";
+    std::string subdir;
+    this->serialsFilepath = Util::makeFilepath(directory, "serials.txt", this->gamename, subdir, 0);
+    this->changelogFilepath = Util::makeFilepath(directory, "changelog_" + gamename + ".html", this->gamename, subdir, 0);
+
+    for (unsigned int i = 0; i < this->installers.size(); ++i)
+    {
+        subdir = config.bSubDirectories ? config.sInstallersSubdir : "";
+        filepath = Util::makeFilepath(directory, this->installers[i].path, this->gamename, subdir, this->installers[i].platform);
+        this->installers[i].setFilepath(filepath);
+    }
+
+    for (unsigned int i = 0; i < this->extras.size(); ++i)
+    {
+        subdir = config.bSubDirectories ? config.sExtrasSubdir : "";
+        filepath = Util::makeFilepath(directory, this->extras[i].path, this->gamename, subdir, 0);
+        this->extras[i].setFilepath(filepath);
+    }
+
+    for (unsigned int i = 0; i < this->patches.size(); ++i)
+    {
+        subdir = config.bSubDirectories ? config.sPatchesSubdir : "";
+        filepath = Util::makeFilepath(directory, this->patches[i].path, this->gamename, subdir, this->patches[i].platform);
+        this->patches[i].setFilepath(filepath);
+    }
+
+    for (unsigned int i = 0; i < this->languagepacks.size(); ++i)
+    {
+        subdir = config.bSubDirectories ? config.sLanguagePackSubdir : "";
+        filepath = Util::makeFilepath(directory, this->languagepacks[i].path, this->gamename, subdir, 0);
+        this->languagepacks[i].setFilepath(filepath);
+    }
+
+    for (unsigned int i = 0; i < this->dlcs.size(); ++i)
+    {
+        subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sInstallersSubdir : "";
+        this->dlcs[i].serialsFilepath = Util::makeFilepath(directory, "serials.txt", this->gamename, subdir, 0);
+        this->dlcs[i].changelogFilepath = Util::makeFilepath(directory, "changelog_" + this->dlcs[i].gamename + ".html", this->gamename, subdir, 0);
+        for (unsigned int j = 0; j < this->dlcs[i].installers.size(); ++j)
+        {
+            subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sInstallersSubdir : "";
+            filepath = Util::makeFilepath(directory, this->dlcs[i].installers[j].path, this->gamename, subdir, this->dlcs[i].installers[j].platform, this->dlcs[i].gamename);
+            this->dlcs[i].installers[j].setFilepath(filepath);
+        }
+
+        for (unsigned int j = 0; j < this->dlcs[i].patches.size(); ++j)
+        {
+            subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sPatchesSubdir : "";
+            filepath = Util::makeFilepath(directory, this->dlcs[i].patches[j].path, this->gamename, subdir, this->dlcs[i].patches[j].platform, this->dlcs[i].gamename);
+            this->dlcs[i].patches[j].setFilepath(filepath);
+        }
+
+        for (unsigned int j = 0; j < this->dlcs[i].extras.size(); ++j)
+        {
+            subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sExtrasSubdir : "";
+            filepath = Util::makeFilepath(directory, this->dlcs[i].extras[j].path, this->gamename, subdir, 0, this->dlcs[i].gamename);
+            this->dlcs[i].extras[j].setFilepath(filepath);
+        }
+
+        for (unsigned int j = 0; j < this->dlcs[i].languagepacks.size(); ++j)
+        {
+            subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sLanguagePackSubdir : "";
+            filepath = Util::makeFilepath(directory, this->dlcs[i].languagepacks[j].path, this->gamename, subdir, 0, this->dlcs[i].gamename);
+            this->dlcs[i].languagepacks[j].setFilepath(filepath);
+        }
+    }
+}
+
+Json::Value gameDetails::getDetailsAsJson()
+{
+    Json::Value json;
+
+    json["gamename"] = this->gamename;
+    json["title"] = this->title;
+    json["icon"] = this->icon;
+    json["serials"] = this->serials;
+    json["changelog"] = this->changelog;
+
+    for (unsigned int i = 0; i < this->extras.size(); ++i)
+        json["extras"].append(this->extras[i].getAsJson());
+    for (unsigned int i = 0; i < this->installers.size(); ++i)
+        json["installers"].append(this->installers[i].getAsJson());
+    for (unsigned int i = 0; i < this->patches.size(); ++i)
+        json["patches"].append(this->patches[i].getAsJson());
+    for (unsigned int i = 0; i < this->languagepacks.size(); ++i)
+        json["languagepacks"].append(this->languagepacks[i].getAsJson());
+
+    if (!this->dlcs.empty())
+    {
+        for (unsigned int i = 0; i < this->dlcs.size(); ++i)
+        {
+            json["dlcs"].append(this->dlcs[i].getDetailsAsJson());
+        }
+    }
+
+    return json;
+}
+
+std::string gameDetails::getSerialsFilepath()
+{
+    return this->serialsFilepath;
+}
+
+std::string gameDetails::getChangelogFilepath()
+{
+    return this->changelogFilepath;
+}
+
+// Return vector containing all game files
+std::vector<gameFile> gameDetails::getGameFileVector()
+{
+    std::vector<gameFile> vGameFiles;
+
+    vGameFiles.insert(std::end(vGameFiles), std::begin(installers), std::end(installers));
+    vGameFiles.insert(std::end(vGameFiles), std::begin(patches), std::end(patches));
+    vGameFiles.insert(std::end(vGameFiles), std::begin(extras), std::end(extras));
+    vGameFiles.insert(std::end(vGameFiles), std::begin(languagepacks), std::end(languagepacks));
+
+    if (!dlcs.empty())
+    {
+        for (unsigned int i = 0; i < dlcs.size(); ++i)
+        {
+            std::vector<gameFile> vGameFilesDLC = dlcs[i].getGameFileVector();
+            vGameFiles.insert(std::end(vGameFiles), std::begin(vGameFilesDLC), std::end(vGameFilesDLC));
+        }
+    }
+
+    return vGameFiles;
+}
diff --git a/src/gamefile.cpp b/src/gamefile.cpp
new file mode 100644 (file)
index 0000000..d78623e
--- /dev/null
@@ -0,0 +1,48 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "gamefile.h"
+
+gameFile::gameFile()
+{
+    this->platform = GlobalConstants::PLATFORM_WINDOWS;
+    this->language = GlobalConstants::LANGUAGE_EN;
+    this->silent = 0;
+    this->type = 0;
+}
+
+gameFile::~gameFile()
+{
+    //dtor
+}
+
+void gameFile::setFilepath(const std::string& path)
+{
+    this->filepath = path;
+}
+
+std::string gameFile::getFilepath()
+{
+    return this->filepath;
+}
+
+Json::Value gameFile::getAsJson()
+{
+    Json::Value json;
+
+    json["updated"] = this->updated;
+    json["id"] = this->id;
+    json["name"] = this->name;
+    json["path"] = this->path;
+    json["size"] = this->size;
+    json["platform"] = this->platform;
+    json["language"] = this->language;
+    json["silent"] = this->silent;
+    json["gamename"] = this->gamename;
+    json["type"] = this->type;
+
+    return json;
+}
diff --git a/src/progressbar.cpp b/src/progressbar.cpp
new file mode 100644 (file)
index 0000000..1baf3e2
--- /dev/null
@@ -0,0 +1,94 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "progressbar.h"
+#include <cmath>
+#include <sstream>
+
+ProgressBar::ProgressBar(bool bUnicode, bool bColor)
+:
+    // Based on block characters.
+    // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Block_elements
+    // u8"\u2591" - you can try using this ("light shade") instead of space, but it looks worse,
+    //              since partial bar has no shade behind it.
+    m_bar_chars
+    {
+        " ",        // 0/8
+        u8"\u258F", // 1/8
+        u8"\u258E", // 2/8
+        u8"\u258D", // 3/8
+        u8"\u258C", // 4/8
+        u8"\u258B", // 5/8
+        u8"\u258A", // 6/8
+        u8"\u2589", // 7/8
+        u8"\u2588"  /* 8/8 */
+    },
+    m_left_border(u8"\u2595"),  // right 1/8th
+    m_right_border(u8"\u258F"), // left  1/8th
+    m_simple_left_border("["),
+    m_simple_right_border("]"),
+    m_simple_empty_fill(" "),
+    m_simple_bar_char("="),
+    // using vt100 escape sequences for colors... See http://ascii-table.com/ansi-escape-sequences.php
+    m_bar_color("\033[1;34m"),
+    m_border_color("\033[1;37m"),
+    COLOR_RESET("\033[0m"),
+    m_use_unicode(bUnicode),
+    m_use_color(bColor)
+{ }
+
+ProgressBar::~ProgressBar()
+{
+    //dtor
+}
+
+void ProgressBar::draw(unsigned int length, double fraction)
+{
+    std::cout << createBarString(length, fraction);
+}
+
+std::string ProgressBar::createBarString(unsigned int length, double fraction)
+{
+    std::ostringstream ss;
+    // validation
+    if (!std::isnormal(fraction) || (fraction < 0.0)) fraction = 0.0;
+    else if (fraction > 1.0) fraction = 1.0;
+
+    double bar_part                = fraction * length;
+    double whole_bar_chars         = std::floor(bar_part);
+    unsigned int whole_bar_chars_i = (unsigned int) whole_bar_chars;
+    // The bar uses symbols graded with 1/8
+    unsigned int partial_bar_char_index = (unsigned int) std::floor((bar_part - whole_bar_chars) * 8.0);
+
+    // left border
+    if (m_use_color) ss << m_border_color;
+    ss << (m_use_unicode ? m_left_border : m_simple_left_border);
+
+    // whole completed bars
+    if (m_use_color) ss << m_bar_color;
+    unsigned int i = 0;
+    for (; i < whole_bar_chars_i; i++)
+    {
+        ss << (m_use_unicode ? m_bar_chars[8] : m_simple_bar_char);
+    }
+
+    // partial completed bar
+    if (i < length) ss << (m_use_unicode ? m_bar_chars[partial_bar_char_index] : m_simple_empty_fill);
+
+    // whole unfinished bars
+    if (m_use_color) ss << COLOR_RESET;
+    for (i = whole_bar_chars_i + 1; i < length; i++)
+    {  // first entry in m_bar_chars is assumed to be the empty bar
+        ss << (m_use_unicode ? m_bar_chars[0] : m_simple_empty_fill);
+    }
+
+    // right border
+    if (m_use_color) ss << m_border_color;
+    ss << (m_use_unicode ? m_right_border : m_simple_right_border);
+    if (m_use_color) ss << COLOR_RESET;
+
+    return ss.str();
+}
diff --git a/src/util.cpp b/src/util.cpp
new file mode 100644 (file)
index 0000000..e90392d
--- /dev/null
@@ -0,0 +1,592 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "util.h"
+
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <tinyxml2.h>
+#include <json/json.h>
+#include <fstream>
+#include <sys/ioctl.h>
+
+/*
+    Create filepath from specified directory and path
+    Remove the leading slash from path if needed
+    Use gamename as base directory if specified
+*/
+std::string Util::makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename, std::string subdirectory, const unsigned int& platformId, const std::string& dlcname)
+{
+    std::string dir = directory + makeRelativeFilepath(path, gamename, subdirectory);
+    Util::filepathReplaceReservedStrings(dir, gamename, platformId, dlcname);
+    return dir;
+}
+
+/* Create filepath relative to download base directory specified in config.
+ */
+std::string Util::makeRelativeFilepath(const std::string& path, const std::string& gamename, std::string subdirectory)
+{
+    std::string filepath;
+
+    if (gamename.empty())
+    {
+        if (path.at(0)=='/')
+        {
+            std::string tmp_path = path.substr(1,path.length());
+            filepath = tmp_path;
+        }
+        else
+        {
+            filepath = path;
+        }
+    }
+    else
+    {
+        std::string filename = path.substr(path.find_last_of("/")+1, path.length());
+        if (!subdirectory.empty())
+        {
+            subdirectory = "/" + subdirectory;
+        }
+        filepath = subdirectory + "/" + filename;
+    }
+
+    return filepath;
+}
+
+std::string Util::getFileHash(const std::string& filename, unsigned hash_id)
+{
+    unsigned char digest[rhash_get_digest_size(hash_id)];
+    char result[rhash_get_hash_length(hash_id)];
+
+    rhash_library_init();
+    int i = rhash_file(hash_id, filename.c_str(), digest);
+    if (i < 0)
+        std::cerr << "LibRHash error: " << strerror(errno) << std::endl;
+    else
+        rhash_print_bytes(result, digest, rhash_get_digest_size(hash_id), RHPR_HEX);
+
+    return result;
+}
+
+std::string Util::getChunkHash(unsigned char *chunk, uintmax_t chunk_size, unsigned hash_id)
+{
+    unsigned char digest[rhash_get_digest_size(hash_id)];
+    char result[rhash_get_hash_length(hash_id)];
+
+    rhash_library_init();
+    int i = rhash_msg(hash_id, chunk, chunk_size, digest);
+    if (i < 0)
+        std::cerr << "LibRHash error: " << strerror(errno) << std::endl;
+    else
+        rhash_print_bytes(result, digest, rhash_get_digest_size(hash_id), RHPR_HEX);
+
+    return result;
+}
+
+// Create GOG XML
+int Util::createXML(std::string filepath, uintmax_t chunk_size, std::string xml_dir)
+{
+    int res = 0;
+    FILE *infile;
+    FILE *xmlfile;
+    uintmax_t filesize, size;
+    int chunks, i;
+
+    if (xml_dir.empty())
+    {
+        xml_dir = Util::getCacheHome() + "/lgogdownloader/xml";
+    }
+
+    // Make sure directory exists
+    boost::filesystem::path path = xml_dir;
+    if (!boost::filesystem::exists(path)) {
+        if (!boost::filesystem::create_directories(path)) {
+            std::cerr << "Failed to create directory: " << path << std::endl;
+            return res;
+        }
+    }
+
+    if ((infile=fopen(filepath.c_str(), "r"))!=NULL) {
+        //File exists
+        fseek(infile, 0, SEEK_END);
+        filesize = ftell(infile);
+        rewind(infile);
+    } else {
+        std::cerr << filepath << " doesn't exist" << std::endl;
+        return res;
+    }
+
+    // Get filename
+    boost::filesystem::path pathname = filepath;
+    std::string filename = pathname.filename().string();
+    std::string filenameXML = xml_dir + "/" + filename + ".xml";
+
+    std::cout << filename << std::endl;
+    //Determine number of chunks
+    int remaining = filesize % chunk_size;
+    chunks = (remaining == 0) ? filesize/chunk_size : (filesize/chunk_size)+1;
+    std::cout   << "Filesize: " << filesize << " bytes" << std::endl
+                << "Chunks: " << chunks << std::endl
+                << "Chunk size: " << (chunk_size >> 20) << " MB" << std::endl;
+
+    tinyxml2::XMLDocument xml;
+    tinyxml2::XMLElement *fileElem = xml.NewElement("file");
+    fileElem->SetAttribute("name", filename.c_str());
+    fileElem->SetAttribute("chunks", chunks);
+    fileElem->SetAttribute("total_size", std::to_string(filesize).c_str());
+
+    std::cout << "Getting MD5 for chunks" << std::endl;
+
+    rhash rhash_context;
+    rhash_library_init();
+    rhash_context = rhash_init(RHASH_MD5);
+    if(!rhash_context)
+    {
+        std::cerr << "error: couldn't initialize rhash context" << std::endl;
+        return res;
+    }
+    char rhash_result[rhash_get_hash_length(RHASH_MD5)];
+
+    for (i = 0; i < chunks; i++) {
+        uintmax_t range_begin = i*chunk_size;
+        fseek(infile, range_begin, SEEK_SET);
+        if ((i == chunks-1) && (remaining != 0))
+            chunk_size = remaining;
+        uintmax_t range_end = range_begin + chunk_size - 1;
+        unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *));
+        if (chunk == NULL)
+        {
+            std::cerr << "Memory error" << std::endl;
+            return res;
+        }
+        size = fread(chunk, 1, chunk_size, infile);
+        if (size != chunk_size)
+        {
+            std::cerr << "Read error" << std::endl;
+            free(chunk);
+            return res;
+        }
+
+        std::string hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5);
+        rhash_update(rhash_context, chunk, chunk_size); // Update hash for the whole file
+
+        free(chunk);
+
+        tinyxml2::XMLElement *chunkElem = xml.NewElement("chunk");
+        chunkElem->SetAttribute("id", i);
+        chunkElem->SetAttribute("from", std::to_string(range_begin).c_str());
+        chunkElem->SetAttribute("to", std::to_string(range_end).c_str());
+        chunkElem->SetAttribute("method", "md5");
+        tinyxml2::XMLText *text = xml.NewText(hash.c_str());
+        chunkElem->LinkEndChild(text);
+        fileElem->LinkEndChild(chunkElem);
+
+        std::cout << "Chunks hashed " << (i+1) << " / " << chunks << "\r" << std::flush;
+    }
+    fclose(infile);
+
+    rhash_final(rhash_context, NULL);
+    rhash_print(rhash_result, rhash_context, RHASH_MD5, RHPR_HEX);
+    rhash_free(rhash_context);
+
+    std::cout << std::endl << "MD5: " << rhash_result << std::endl;
+    fileElem->SetAttribute("md5", rhash_result);
+
+    xml.LinkEndChild(fileElem);
+
+    std::cout << "Writing XML: " << filenameXML << std::endl;
+    if ((xmlfile=fopen(filenameXML.c_str(), "w"))!=NULL) {
+        tinyxml2::XMLPrinter printer(xmlfile);
+        xml.Print(&printer);
+        fclose(xmlfile);
+        res = 1;
+    } else {
+        std::cerr << "Can't create " << filenameXML << std::endl;
+        return res;
+    }
+
+    return res;
+}
+
+/*
+    Overrides global settings with game specific settings
+    returns 0 if fails
+    returns number of changed settings if succesful
+*/
+int Util::getGameSpecificConfig(std::string gamename, gameSpecificConfig* conf, std::string directory)
+{
+    int res = 0;
+
+    if (directory.empty())
+    {
+        directory = Util::getConfigHome() + "/lgogdownloader/gamespecific";
+    }
+
+    std::string filepath = directory + "/" + gamename + ".conf";
+
+    // Make sure file exists
+    boost::filesystem::path path = filepath;
+    if (!boost::filesystem::exists(path)) {
+        return res;
+    }
+
+    std::ifstream json(filepath, std::ifstream::binary);
+    Json::Value root;
+    Json::Reader *jsonparser = new Json::Reader;
+    if (jsonparser->parse(json, root))
+    {
+        if (root.isMember("language"))
+        {
+            if (root["language"].isInt())
+                conf->iInstallerLanguage = root["language"].asUInt();
+            else
+            {
+                Util::parseOptionString(root["language"].asString(), conf->vLanguagePriority, conf->iInstallerLanguage, GlobalConstants::LANGUAGES);
+            }
+            res++;
+        }
+        if (root.isMember("platform"))
+        {
+            if (root["platform"].isInt())
+                conf->iInstallerPlatform = root["platform"].asUInt();
+            else
+            {
+                Util::parseOptionString(root["platform"].asString(), conf->vPlatformPriority, conf->iInstallerPlatform, GlobalConstants::PLATFORMS);
+            }
+            res++;
+        }
+        if (root.isMember("dlc"))
+        {
+            conf->bDLC = root["dlc"].asBool();
+            res++;
+        }
+        if (root.isMember("ignore-dlc-count"))
+        {
+            conf->bIgnoreDLCCount = root["ignore-dlc-count"].asBool();
+            res++;
+        }
+        if (root.isMember("subdirectories"))
+        {
+            conf->dirConf.bSubDirectories = root["subdirectories"].asBool();
+            res++;
+        }
+        if (root.isMember("directory"))
+        {
+            conf->dirConf.sDirectory = root["directory"].asString();
+            res++;
+        }
+        if (root.isMember("subdir-game"))
+        {
+            conf->dirConf.sGameSubdir = root["subdir-game"].asString();
+            res++;
+        }
+        if (root.isMember("subdir-installers"))
+        {
+            conf->dirConf.sInstallersSubdir = root["subdir-installers"].asString();
+            res++;
+        }
+        if (root.isMember("subdir-extras"))
+        {
+            conf->dirConf.sExtrasSubdir = root["subdir-extras"].asString();
+            res++;
+        }
+        if (root.isMember("subdir-patches"))
+        {
+            conf->dirConf.sPatchesSubdir = root["subdir-patches"].asString();
+            res++;
+        }
+        if (root.isMember("subdir-language-packs"))
+        {
+            conf->dirConf.sLanguagePackSubdir = root["subdir-language-packs"].asString();
+            res++;
+        }
+        if (root.isMember("subdir-dlc"))
+        {
+            conf->dirConf.sDLCSubdir = root["subdir-dlc"].asString();
+            res++;
+        }
+    }
+    else
+    {
+        std::cerr << "Failed to parse game specific config " << filepath << std::endl;
+        std::cerr << jsonparser->getFormattedErrorMessages() << std::endl;
+    }
+    delete jsonparser;
+    if (json)
+        json.close();
+
+    return res;
+}
+
+int Util::replaceString(std::string& str, const std::string& to_replace, const std::string& replace_with)
+{
+    size_t pos = str.find(to_replace);
+    if (pos == std::string::npos)
+    {
+        return 0;
+    }
+    str.replace(str.begin()+pos, str.begin()+pos+to_replace.length(), replace_with);
+    return 1;
+}
+
+void Util::filepathReplaceReservedStrings(std::string& str, const std::string& gamename, const unsigned int& platformId, const std::string& dlcname)
+{
+    std::string platform;
+    for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+    {
+        if ((platformId & GlobalConstants::PLATFORMS[i].id) == GlobalConstants::PLATFORMS[i].id)
+        {
+            platform = boost::algorithm::to_lower_copy(GlobalConstants::PLATFORMS[i].str);
+            break;
+        }
+    }
+    if (platform.empty())
+    {
+        if (str.find("%gamename%/%platform%") != std::string::npos)
+            platform = "";
+        else
+            platform = "no_platform";
+    }
+
+    while (Util::replaceString(str, "%gamename%", gamename));
+    while (Util::replaceString(str, "%dlcname%", dlcname));
+    while (Util::replaceString(str, "%platform%", platform));
+    while (Util::replaceString(str, "//", "/")); // Replace any double slashes with single slash
+}
+
+void Util::setFilePermissions(const boost::filesystem::path& path, const boost::filesystem::perms& permissions)
+{
+    if (boost::filesystem::exists(path))
+    {
+        if (boost::filesystem::is_regular_file(path))
+        {
+            boost::filesystem::file_status s = boost::filesystem::status(path);
+            if (s.permissions() != permissions)
+            {
+                boost::system::error_code ec;
+                boost::filesystem::permissions(path, permissions, ec);
+                if (ec)
+                {
+                    std::cerr << "Failed to set file permissions for " << path.string() << std::endl;
+                }
+            }
+        }
+    }
+}
+
+int Util::getTerminalWidth()
+{
+    int width;
+    if(isatty(STDOUT_FILENO))
+    {
+        struct winsize w;
+        ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
+        width = static_cast<int>(w.ws_col);
+    }
+    else
+        width = 10000;//Something sufficiently big
+    return width;
+}
+
+
+void Util::getDownloaderUrlsFromJSON(const Json::Value &root, std::vector<std::string> &urls)
+{
+    if(root.size() > 0) {
+        for(Json::ValueConstIterator it = root.begin() ; it != root.end() ; ++it)
+        {
+            if (it.key() == "downloaderUrl")
+            {
+                Json::Value url = *it;
+                urls.push_back(url.asString());
+            }
+            else
+                getDownloaderUrlsFromJSON(*it, urls);
+        }
+    }
+    return;
+}
+
+std::vector<std::string> Util::getDLCNamesFromJSON(const Json::Value &root)
+{
+    std::vector<std::string> urls, dlcnames;
+    getDownloaderUrlsFromJSON(root, urls);
+
+    for (unsigned int i = 0; i < urls.size(); ++i)
+    {
+        std::string gamename;
+        if (urls[i].find(GlobalConstants::PROTOCOL_PREFIX) == std::string::npos)
+            continue;
+
+        gamename.assign(urls[i].begin()+urls[i].find(GlobalConstants::PROTOCOL_PREFIX)+GlobalConstants::PROTOCOL_PREFIX.length(), urls[i].begin()+urls[i].find_last_of("/"));
+        bool bDuplicate = false;
+        for (unsigned int j = 0; j < dlcnames.size(); ++j)
+        {
+            if (gamename == dlcnames[j])
+            {
+                bDuplicate = true;
+                break;
+            }
+        }
+        if (!bDuplicate)
+            dlcnames.push_back(gamename);
+    }
+    return dlcnames;
+}
+
+std::string Util::getHomeDir()
+{
+    return (std::string)getenv("HOME");
+}
+
+std::string Util::getConfigHome()
+{
+    std::string configHome;
+    char *xdgconfig = getenv("XDG_CONFIG_HOME");
+    if (xdgconfig)
+        configHome = (std::string)xdgconfig;
+    else
+        configHome = Util::getHomeDir() + "/.config";
+    return configHome;
+}
+
+std::string Util::getCacheHome()
+{
+    std::string cacheHome;
+    char *xdgcache = getenv("XDG_CACHE_HOME");
+    if (xdgcache)
+        cacheHome = (std::string)xdgcache;
+    else
+        cacheHome = Util::getHomeDir() + "/.cache";
+    return cacheHome;
+}
+
+std::vector<std::string> Util::tokenize(const std::string& str, const std::string& separator)
+{
+    std::vector<std::string> tokens;
+    std::string token;
+    size_t idx = 0, found;
+    while ((found = str.find(separator, idx)) != std::string::npos)
+    {
+        token = str.substr(idx, found - idx);
+        if (!token.empty())
+            tokens.push_back(token);
+        idx = found + separator.length();
+    }
+    token = str.substr(idx);
+    if (!token.empty())
+        tokens.push_back(token);
+
+    return tokens;
+}
+
+unsigned int Util::getOptionValue(const std::string& str, const std::vector<GlobalConstants::optionsStruct>& options)
+{
+    unsigned int value = 0;
+    boost::regex expression("^[+-]?\\d+$", boost::regex::perl);
+    boost::match_results<std::string::const_iterator> what;
+    if (str == "all")
+    {
+        value = (1 << options.size()) - 1;
+    }
+    else if (boost::regex_search(str, what, expression))
+    {
+        value = std::stoi(str);
+    }
+    else
+    {
+        for (unsigned int i = 0; i < options.size(); ++i)
+        {
+            if (!options[i].regexp.empty())
+            {
+                boost::regex expr("^(" + options[i].regexp + ")$", boost::regex::perl | boost::regex::icase);
+                if (boost::regex_search(str, what, expr))
+                {
+                    value = options[i].id;
+                    break;
+                }
+            }
+            else if (str == options[i].code)
+            {
+                value = options[i].id;
+                break;
+            }
+        }
+    }
+    return value;
+}
+
+std::string Util::getOptionNameString(const unsigned int& value, const std::vector<GlobalConstants::optionsStruct>& options)
+{
+    std::string str;
+    for (unsigned int i = 0; i < options.size(); ++i)
+    {
+        if (value & options[i].id)
+            str += (str.empty() ? "" : ", ")+options[i].str;
+    }
+    return str;
+}
+
+// Parse the options string
+void Util::parseOptionString(const std::string &option_string, std::vector<unsigned int> &priority, unsigned int &type, const std::vector<GlobalConstants::optionsStruct>& options)
+{
+    type = 0;
+    priority.clear();
+    std::vector<std::string> tokens_priority = Util::tokenize(option_string, ",");
+    for (std::vector<std::string>::iterator it_priority = tokens_priority.begin(); it_priority != tokens_priority.end(); it_priority++)
+    {
+        unsigned int value = 0;
+        std::vector<std::string> tokens_value = Util::tokenize(*it_priority, "+");
+        for (std::vector<std::string>::iterator it_value = tokens_value.begin(); it_value != tokens_value.end(); it_value++)
+        {
+            value |= Util::getOptionValue(*it_value, options);
+        }
+        priority.push_back(value);
+        type |= value;
+    }
+}
+
+std::string Util::getLocalFileHash(const std::string& xml_dir, const std::string& filepath, const std::string& gamename)
+{
+    std::string localHash;
+    boost::filesystem::path path = filepath;
+    boost::filesystem::path local_xml_file;
+    if (!gamename.empty())
+        local_xml_file = xml_dir + "/" + gamename + "/" + path.filename().string() + ".xml";
+    else
+        local_xml_file = xml_dir + "/" + path.filename().string() + ".xml";
+
+    if (boost::filesystem::exists(local_xml_file))
+    {
+        tinyxml2::XMLDocument local_xml;
+        local_xml.LoadFile(local_xml_file.string().c_str());
+        tinyxml2::XMLElement *fileElem = local_xml.FirstChildElement("file");
+
+        if (fileElem)
+        {
+            localHash = fileElem->Attribute("md5");
+        }
+    }
+    else if (boost::filesystem::exists(path) && boost::filesystem::is_regular_file(path))
+    {
+        localHash = Util::getFileHash(path.string(), RHASH_MD5);
+    }
+
+    return localHash;
+}
+
+void Util::shortenStringToTerminalWidth(std::string& str)
+{
+    int iStrLen = static_cast<int>(str.length());
+    int iTermWidth = Util::getTerminalWidth();
+    if (iStrLen >= iTermWidth)
+    {
+        size_t chars_to_remove = (iStrLen - iTermWidth) + 4;
+        size_t middle = iStrLen / 2;
+        size_t pos1 = middle - (chars_to_remove / 2);
+        size_t pos2 = middle + (chars_to_remove / 2);
+        str.replace(str.begin()+pos1, str.begin()+pos2, "...");
+    }
+}
diff --git a/src/website.cpp b/src/website.cpp
new file mode 100644 (file)
index 0000000..0803432
--- /dev/null
@@ -0,0 +1,724 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "website.h"
+#include "globalconstants.h"
+
+#include <htmlcxx/html/ParserDom.h>
+#include <boost/algorithm/string/case_conv.hpp>
+
+Website::Website(Config &conf)
+{
+    this->config = conf;
+    this->retries = 0;
+
+    curlhandle = curl_easy_init();
+    curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, config.sVersionString.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, config.iTimeout);
+    curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+    curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, config.sCookiePath.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, config.sCookiePath.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer);
+    curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, config.bVerbose);
+    curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate);
+
+    // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+    curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30);
+    curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200);
+
+    if (!config.sCACertPath.empty())
+        curl_easy_setopt(curlhandle, CURLOPT_CAINFO, config.sCACertPath.c_str());
+}
+
+Website::~Website()
+{
+    curl_easy_cleanup(curlhandle);
+}
+
+size_t Website::writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp)
+{
+    std::ostringstream *stream = (std::ostringstream*)userp;
+    size_t count = size * nmemb;
+    stream->write(ptr, count);
+    return count;
+}
+
+std::string Website::getResponse(const std::string& url)
+{
+    std::ostringstream memory;
+    std::string response;
+
+    curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+
+    CURLcode result;
+    do
+    {
+        if (config.iWait > 0)
+            usleep(config.iWait); // Delay the request by specified time
+        result = curl_easy_perform(curlhandle);
+        response = memory.str();
+        memory.str(std::string());
+    }
+    while ((result != CURLE_OK) && response.empty() && (this->retries++ < config.iRetries));
+    this->retries = 0; // reset retries counter
+
+    if (result != CURLE_OK)
+    {
+        std::cout << curl_easy_strerror(result) << std::endl;
+        if (result == CURLE_HTTP_RETURNED_ERROR)
+        {
+            long int response_code = 0;
+            result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+            std::cout << "HTTP ERROR: ";
+            if (result == CURLE_OK)
+                std::cout << response_code << " (" << url << ")" << std::endl;
+            else
+                std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl;
+        }
+        else if (result == CURLE_SSL_CACERT)
+        {
+            std::cout << "Try using CA certificate bundle from cURL: https://curl.haxx.se/ca/cacert.pem" << std::endl;
+            std::cout << "Use --cacert to set the path for CA certificate bundle" << std::endl;
+        }
+    }
+
+    return response;
+}
+
+Json::Value Website::getGameDetailsJSON(const std::string& gameid)
+{
+    std::string gameDataUrl = "https://www.gog.com/account/gameDetails/" + gameid + ".json";
+    std::string json = this->getResponse(gameDataUrl);
+
+    // Parse JSON
+    Json::Value root;
+    Json::Reader *jsonparser = new Json::Reader;
+    if (!jsonparser->parse(json, root))
+    {
+        #ifdef DEBUG
+            std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << json << std::endl;
+        #endif
+        std::cout << jsonparser->getFormattedErrorMessages();
+        delete jsonparser;
+        exit(1);
+    }
+    #ifdef DEBUG
+        std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << root << std::endl;
+    #endif
+    delete jsonparser;
+
+    return root;
+}
+
+// Get list of games from account page
+std::vector<gameItem> Website::getGames()
+{
+    std::vector<gameItem> games;
+    Json::Value root;
+    Json::Reader *jsonparser = new Json::Reader;
+    int i = 1;
+    bool bAllPagesParsed = false;
+
+    do
+    {
+        std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=1&sortBy=title&system=&page=" + std::to_string(i));
+
+        // Parse JSON
+        if (!jsonparser->parse(response, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << response << std::endl;
+            #endif
+            std::cout << jsonparser->getFormattedErrorMessages();
+            delete jsonparser;
+            if (!response.empty())
+            {
+                if(response[0] != '{')
+                {
+                    // Response was not JSON. Assume that cookies have expired.
+                    std::cerr << "Response was not JSON. Cookies have most likely expired. Try --login first." << std::endl;
+                }
+            }
+            exit(1);
+        }
+        #ifdef DEBUG
+            std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << root << std::endl;
+        #endif
+        if (root["page"].asInt() == root["totalPages"].asInt())
+            bAllPagesParsed = true;
+        if (root["products"].isArray())
+        {
+            for (unsigned int i = 0; i < root["products"].size(); ++i)
+            {
+                std::cerr << "\033[KGetting game names " << "(" << root["page"].asInt() << "/" << root["totalPages"].asInt() << ") " << i+1 << " / " << root["products"].size() << "\r" << std::flush;
+                Json::Value product = root["products"][i];
+                gameItem game;
+                game.name = product["slug"].asString();
+                game.id = product["id"].isInt() ? std::to_string(product["id"].asInt()) : product["id"].asString();
+                game.updates = product["updates"].isInt() ? product["updates"].asInt() : std::stoi(product["updates"].asString());
+
+                unsigned int platform = 0;
+                if (product["worksOn"]["Windows"].asBool())
+                    platform |= GlobalConstants::PLATFORM_WINDOWS;
+                if (product["worksOn"]["Mac"].asBool())
+                    platform |= GlobalConstants::PLATFORM_MAC;
+                if (product["worksOn"]["Linux"].asBool())
+                    platform |= GlobalConstants::PLATFORM_LINUX;
+
+                // Skip if platform doesn't match
+                if (config.bPlatformDetection && !(platform & config.iInstallerPlatform))
+                    continue;
+
+                // Filter the game list
+                if (!config.sGameRegex.empty())
+                {
+                    // GameRegex filter aliases
+                    if (config.sGameRegex == "all")
+                        config.sGameRegex = ".*";
+
+                    boost::regex expression(config.sGameRegex);
+                    boost::match_results<std::string::const_iterator> what;
+                    if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
+                        continue;
+                }
+
+                if (config.bDLC)
+                {
+                    int dlcCount = product["dlcCount"].asInt();
+
+                    bool bDownloadDLCInfo = (dlcCount != 0);
+
+                    if (!bDownloadDLCInfo && !config.sIgnoreDLCCountRegex.empty())
+                    {
+                        boost::regex expression(config.sIgnoreDLCCountRegex);
+                        boost::match_results<std::string::const_iterator> what;
+                        if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
+                        {
+                            bDownloadDLCInfo = true;
+                        }
+                    }
+
+                    if (!bDownloadDLCInfo && !config.gamehasdlc.empty())
+                    {
+                        if (config.gamehasdlc.isBlacklisted(game.name))
+                            bDownloadDLCInfo = true;
+                    }
+
+                    // Check game specific config
+                    if (!config.bUpdateCache) // Disable game specific config files for cache update
+                    {
+                        gameSpecificConfig conf;
+                        conf.bIgnoreDLCCount = bDownloadDLCInfo;
+                        Util::getGameSpecificConfig(game.name, &conf);
+                        bDownloadDLCInfo = conf.bIgnoreDLCCount;
+                    }
+
+                    if (bDownloadDLCInfo && !config.sGameRegex.empty())
+                    {
+                        // don't download unnecessary info if user is only interested in a subset of his account
+                        boost::regex expression(config.sGameRegex);
+                        boost::match_results<std::string::const_iterator> what;
+                        if (!boost::regex_search(game.name, what, expression))
+                        {
+                            bDownloadDLCInfo = false;
+                        }
+                    }
+
+                    if (bDownloadDLCInfo)
+                    {
+                        game.gamedetailsjson = this->getGameDetailsJSON(game.id);
+                        if (!game.gamedetailsjson.empty())
+                            game.dlcnames = Util::getDLCNamesFromJSON(game.gamedetailsjson["dlcs"]);
+                    }
+                }
+                games.push_back(game);
+            }
+        }
+        i++;
+    } while (!bAllPagesParsed);
+    std::cerr << std::endl;
+
+    delete jsonparser;
+
+    return games;
+}
+
+// Get list of free games
+std::vector<gameItem> Website::getFreeGames()
+{
+    Json::Value root;
+    Json::Reader *jsonparser = new Json::Reader;
+    std::vector<gameItem> games;
+    std::string json = this->getResponse("https://www.gog.com/games/ajax/filtered?mediaType=game&page=1&price=free&sort=title");
+
+    // Parse JSON
+    if (!jsonparser->parse(json, root))
+    {
+        #ifdef DEBUG
+            std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << json << std::endl;
+        #endif
+        std::cout << jsonparser->getFormattedErrorMessages();
+        delete jsonparser;
+        exit(1);
+    }
+    #ifdef DEBUG
+        std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << root << std::endl;
+    #endif
+
+    Json::Value products = root["products"];
+    for (unsigned int i = 0; i < products.size(); ++i)
+    {
+        gameItem game;
+        game.name = products[i]["slug"].asString();
+        game.id = products[i]["id"].isInt() ? std::to_string(products[i]["id"].asInt()) : products[i]["id"].asString();
+        games.push_back(game);
+    }
+    delete jsonparser;
+
+    return games;
+}
+
+// Login to GOG website
+int Website::Login(const std::string& email, const std::string& password)
+{
+    int res = 0;
+    std::string postdata;
+    std::ostringstream memory;
+    std::string token;
+    std::string tagname_username;
+    std::string tagname_password;
+    std::string tagname_login;
+    std::string tagname_token;
+
+    // Get login token
+    std::string html = this->getResponse("https://www.gog.com/");
+    htmlcxx::HTML::ParserDom parser;
+    tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
+    tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+    tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+    // Find auth_url
+    bool bFoundAuthUrl = false;
+    for (; it != end; ++it)
+    {
+        if (it->tagName()=="script")
+        {
+            std::string auth_url;
+            for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
+            {
+                tree<htmlcxx::HTML::Node>::iterator script_it = dom.child(it, i);
+                if (!script_it->isTag() && !script_it->isComment())
+                {
+                    if (script_it->text().find("GalaxyAccounts") != std::string::npos)
+                    {
+                        boost::match_results<std::string::const_iterator> what;
+                        boost::regex expression(".*'(https://auth.gog.com/.*?)'.*");
+                        boost::regex_match(script_it->text(), what, expression);
+                        auth_url = what[1];
+                        break;
+                    }
+                }
+            }
+
+            if (!auth_url.empty())
+            {   // Found auth_url, get the necessary info for login
+                bFoundAuthUrl = true;
+                std::string login_form_html = this->getResponse(auth_url);
+                #ifdef DEBUG
+                    std::cerr << "DEBUG INFO (Website::Login)" << std::endl;
+                    std::cerr << login_form_html << std::endl;
+                #endif
+                if (login_form_html.find("google.com/recaptcha") != std::string::npos)
+                {
+                    std::cout   << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl
+                                << "Login with browser and export cookies to \"" << config.sCookiePath << "\"" << std::endl;
+                    return res = 0;
+                }
+
+                tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html);
+                tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin();
+                tree<htmlcxx::HTML::Node>::iterator login_it_end = login_dom.end();
+                for (; login_it != login_it_end; ++login_it)
+                {
+                    if (login_it->tagName()=="input")
+                    {
+                        login_it->parseAttributes();
+                        std::string id_login = login_it->attribute("id").second;
+                        if (id_login == "login_username")
+                        {
+                            tagname_username = login_it->attribute("name").second;
+                        }
+                        else if (id_login == "login_password")
+                        {
+                            tagname_password = login_it->attribute("name").second;
+                        }
+                        else if (id_login == "login__token")
+                        {
+                            token = login_it->attribute("value").second; // login token
+                            tagname_token = login_it->attribute("name").second;
+                        }
+                    }
+                    else if (login_it->tagName()=="button")
+                    {
+                        login_it->parseAttributes();
+                        std::string id_login = login_it->attribute("id").second;
+                        if (id_login == "login_login")
+                        {
+                            tagname_login = login_it->attribute("name").second;
+                        }
+                    }
+                }
+                break;
+            }
+        }
+    }
+
+    if (!bFoundAuthUrl)
+    {
+        std::cout << "Failed to find url for login form" << std::endl;
+    }
+
+    if (token.empty())
+    {
+        std::cout << "Failed to get login token" << std::endl;
+        return res = 0;
+    }
+
+    //Create postdata - escape characters in email/password to support special characters
+    postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size())
+            + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size())
+            + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "="
+            + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size());
+    curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check");
+    curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
+    curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
+
+    // Don't follow to redirect location because we need to check it for two step authorization.
+    curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
+    CURLcode result = curl_easy_perform(curlhandle);
+    memory.str(std::string());
+
+    if (result != CURLE_OK)
+    {
+        // Expected to hit maximum amount of redirects so don't print error on it
+        if (result != CURLE_TOO_MANY_REDIRECTS)
+            std::cout << curl_easy_strerror(result) << std::endl;
+    }
+
+    // Get redirect url
+    char *redirect_url;
+    curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
+
+    // Handle two step authorization
+    if (std::string(redirect_url).find("two_step") != std::string::npos)
+    {
+        std::string security_code, tagname_two_step_send, tagname_two_step_auth_letter_1, tagname_two_step_auth_letter_2, tagname_two_step_auth_letter_3, tagname_two_step_auth_letter_4, tagname_two_step_token, token_two_step;
+        std::string two_step_html = this->getResponse(redirect_url);
+        redirect_url = NULL;
+
+        tree<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html);
+        tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin();
+        tree<htmlcxx::HTML::Node>::iterator two_step_it_end = two_step_dom.end();
+        for (; two_step_it != two_step_it_end; ++two_step_it)
+        {
+            if (two_step_it->tagName()=="input")
+            {
+                two_step_it->parseAttributes();
+                std::string id_two_step = two_step_it->attribute("id").second;
+                if (id_two_step == "second_step_authentication_token_letter_1")
+                {
+                    tagname_two_step_auth_letter_1 = two_step_it->attribute("name").second;
+                }
+                else if (id_two_step == "second_step_authentication_token_letter_2")
+                {
+                    tagname_two_step_auth_letter_2 = two_step_it->attribute("name").second;
+                }
+                else if (id_two_step == "second_step_authentication_token_letter_3")
+                {
+                    tagname_two_step_auth_letter_3 = two_step_it->attribute("name").second;
+                }
+                else if (id_two_step == "second_step_authentication_token_letter_4")
+                {
+                    tagname_two_step_auth_letter_4 = two_step_it->attribute("name").second;
+                }
+                else if (id_two_step == "second_step_authentication__token")
+                {
+                    token_two_step = two_step_it->attribute("value").second; // two step token
+                    tagname_two_step_token = two_step_it->attribute("name").second;
+                }
+            }
+            else if (two_step_it->tagName()=="button")
+            {
+                two_step_it->parseAttributes();
+                std::string id_two_step = two_step_it->attribute("id").second;
+                if (id_two_step == "second_step_authentication_send")
+                {
+                    tagname_two_step_send = two_step_it->attribute("name").second;
+                }
+            }
+        }
+        std::cerr << "Security code: ";
+        std::getline(std::cin,security_code);
+        if (security_code.size() != 4)
+        {
+            std::cerr << "Security code must be 4 characters long" << std::endl;
+            exit(1);
+        }
+        postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0]
+                + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1]
+                + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2]
+                + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3]
+                + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "="
+                + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size());
+
+        curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step");
+        curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
+        curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
+        curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+        curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+        curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+        curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
+        curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
+
+        // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first.
+        curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
+        result = curl_easy_perform(curlhandle);
+        memory.str(std::string());
+        curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
+    }
+
+    curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url);
+    curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1);
+    curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1);
+    curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+    result = curl_easy_perform(curlhandle);
+
+    if (result != CURLE_OK)
+    {
+        std::cout << curl_easy_strerror(result) << std::endl;
+    }
+
+    if (this->IsLoggedInComplex(email))
+    {
+        res = 1; // Login was successful
+    }
+    else
+    {
+        if (this->IsloggedInSimple())
+            res = 1; // Login was successful
+    }
+
+    if (res == 1)
+    {
+        curl_easy_setopt(curlhandle, CURLOPT_COOKIELIST, "FLUSH"); // Write all known cookies to the file specified by CURLOPT_COOKIEJAR
+    }
+
+    return res;
+}
+
+bool Website::IsLoggedIn()
+{
+    return this->IsloggedInSimple();
+}
+
+/* Complex login check. Check login by checking email address on the account settings page.
+    returns true if we are logged in
+    returns false if we are not logged in
+*/
+bool Website::IsLoggedInComplex(const std::string& email)
+{
+    bool bIsLoggedIn = false;
+    std::string html = this->getResponse("https://www.gog.com/account/settings/personal");
+    std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it
+
+    htmlcxx::HTML::ParserDom parser;
+    tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
+    tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+    tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+    dom = parser.parseTree(html);
+    it = dom.begin();
+    end = dom.end();
+    for (; it != end; ++it)
+    {
+        if (it->tagName()=="strong")
+        {
+            it->parseAttributes();
+            if (it->attribute("class").second == "settings-item__value settings-item__section")
+            {
+                for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
+                {
+                    tree<htmlcxx::HTML::Node>::iterator tag_it = dom.child(it, i);
+                    if (!tag_it->isTag() && !tag_it->isComment())
+                    {
+                        std::string tag_text = boost::algorithm::to_lower_copy(tag_it->text());
+                        if (tag_text == email_lowercase)
+                        {
+                            bIsLoggedIn = true; // We are logged in
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        if (bIsLoggedIn) // We are logged in so no need to go through the remaining tags
+            break;
+    }
+
+    return bIsLoggedIn;
+}
+
+/* Simple login check. Check login by trying to get account page. If response code isn't 200 then login failed.
+    returns true if we are logged in
+    returns false if we are not logged in
+*/
+bool Website::IsloggedInSimple()
+{
+    bool bIsLoggedIn = false;
+    std::ostringstream memory;
+    std::string url = "https://www.gog.com/account";
+    long int response_code = 0;
+
+    curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+    curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+    curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+    curl_easy_perform(curlhandle);
+    memory.str(std::string());
+
+    curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+    curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+    if (response_code == 200)
+        bIsLoggedIn = true; // We are logged in
+
+    return bIsLoggedIn;
+}
+
+std::vector<wishlistItem> Website::getWishlistItems()
+{
+    Json::Value root;
+    Json::Reader *jsonparser = new Json::Reader;
+    int i = 1;
+    bool bAllPagesParsed = false;
+    std::vector<wishlistItem> wishlistItems;
+
+    do
+    {
+        std::string response = this->getResponse("https://www.gog.com/account/wishlist/search?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=0&sortBy=title&system=&page=" + std::to_string(i));
+
+        // Parse JSON
+        if (!jsonparser->parse(response, root))
+        {
+            #ifdef DEBUG
+                std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << response << std::endl;
+            #endif
+            std::cout << jsonparser->getFormattedErrorMessages();
+            delete jsonparser;
+            exit(1);
+        }
+        #ifdef DEBUG
+            std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << root << std::endl;
+        #endif
+        if (root["page"].asInt() >= root["totalPages"].asInt())
+            bAllPagesParsed = true;
+        if (root["products"].isArray())
+        {
+            for (unsigned int i = 0; i < root["products"].size(); ++i)
+            {
+                wishlistItem item;
+                Json::Value product = root["products"][i];
+
+                item.platform = 0;
+                std::string platforms_text;
+                bool bIsMovie = product["isMovie"].asBool();
+                if (!bIsMovie)
+                {
+                    if (product["worksOn"]["Windows"].asBool())
+                        item.platform |= GlobalConstants::PLATFORM_WINDOWS;
+                    if (product["worksOn"]["Mac"].asBool())
+                        item.platform |= GlobalConstants::PLATFORM_MAC;
+                    if (product["worksOn"]["Linux"].asBool())
+                        item.platform |= GlobalConstants::PLATFORM_LINUX;
+
+                    // Skip if platform doesn't match
+                    if (config.bPlatformDetection && !(item.platform & config.iInstallerPlatform))
+                        continue;
+                }
+
+                if (product["isComingSoon"].asBool())
+                    item.tags.push_back("Coming soon");
+                if (product["isDiscounted"].asBool())
+                    item.tags.push_back("Discount");
+                if (bIsMovie)
+                    item.tags.push_back("Movie");
+
+                item.release_date_time = 0;
+                if (product.isMember("releaseDate") && product["isComingSoon"].asBool())
+                {
+                    if (!product["releaseDate"].empty())
+                    {
+                        if (product["releaseDate"].isInt())
+                        {
+                            item.release_date_time = product["releaseDate"].asInt();
+                        }
+                        else
+                        {
+                            std::string release_date_time_string = product["releaseDate"].asString();
+                            if (!release_date_time_string.empty())
+                            {
+                                try
+                                {
+                                    item.release_date_time = std::stoi(release_date_time_string);
+                                }
+                                catch (std::invalid_argument& e)
+                                {
+                                    item.release_date_time = 0;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                item.currency = product["price"]["symbol"].asString();
+                item.price = product["price"]["finalAmount"].isDouble() ? std::to_string(product["price"]["finalAmount"].asDouble()) + item.currency : product["price"]["finalAmount"].asString() + item.currency;
+                item.discount_percent = product["price"]["discountPercentage"].isInt() ? std::to_string(product["price"]["discountPercentage"].asInt()) + "%" : product["price"]["discountPercentage"].asString() + "%";
+                item.discount = product["price"]["discountDifference"].isDouble() ? std::to_string(product["price"]["discountDifference"].asDouble()) + item.currency : product["price"]["discountDifference"].asString() + item.currency;
+                item.store_credit = product["price"]["bonusStoreCreditAmount"].isDouble() ? std::to_string(product["price"]["bonusStoreCreditAmount"].asDouble()) + item.currency : product["price"]["bonusStoreCreditAmount"].asString() + item.currency;
+
+                item.url = product["url"].asString();
+                if (item.url.find("/game/") == 0)
+                    item.url = "https://www.gog.com" + item.url;
+                else if (item.url.find("/movie/") == 0)
+                    item.url = "https://www.gog.com" + item.url;
+
+                item.title = product["title"].asString();
+                item.bIsBonusStoreCreditIncluded = product["price"]["isBonusStoreCreditIncluded"].asBool();
+                item.bIsDiscounted = product["isDiscounted"].asBool();
+
+                wishlistItems.push_back(item);
+            }
+        }
+        i++;
+    } while (!bAllPagesParsed);
+
+    delete jsonparser;
+
+    return wishlistItems;
+}
+
+void Website::setConfig(Config &conf)
+{
+    this->config = conf;
+}