pax_global_header00006660000000000000000000000064131770757250014530gustar00rootroot0000000000000052 comment=8cb0b99008ade93104e9edbab0d7121466c46eb3 lgogdownloader-3.3/000077500000000000000000000000001317707572500144045ustar00rootroot00000000000000lgogdownloader-3.3/.gitignore000066400000000000000000000001551317707572500163750ustar00rootroot00000000000000*.layout *~ *.[oa] bin/* obj/* *.1 *.gz Makefile CMakeCache.txt CMakeFiles/ cmake_install.cmake build/ *.cbp lgogdownloader-3.3/CMakeLists.txt000066400000000000000000000102111317707572500171370ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR) project (lgogdownloader LANGUAGES C CXX VERSION 3.3) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") 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 iostreams ) 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 src/galaxyapi.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) lgogdownloader-3.3/COPYING000066400000000000000000000007441317707572500154440ustar00rootroot00000000000000 DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar 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. lgogdownloader-3.3/README.md000066400000000000000000000035761317707572500156760ustar00rootroot00000000000000# 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, iostreams) * [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 libboost-iostreams-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) lgogdownloader-3.3/cmake/000077500000000000000000000000001317707572500154645ustar00rootroot00000000000000lgogdownloader-3.3/cmake/FindHtmlcxx.cmake000066400000000000000000000024561317707572500207250ustar00rootroot00000000000000# - 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) lgogdownloader-3.3/cmake/FindJsoncpp.cmake000066400000000000000000000014031317707572500207010ustar00rootroot00000000000000# - 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) lgogdownloader-3.3/cmake/FindLibcrypto.cmake000066400000000000000000000014011317707572500212320ustar00rootroot00000000000000# - 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) lgogdownloader-3.3/cmake/FindOAuth.cmake000066400000000000000000000012551317707572500203120ustar00rootroot00000000000000# - 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) lgogdownloader-3.3/cmake/FindRhash.cmake000066400000000000000000000012051317707572500203320ustar00rootroot00000000000000# - 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) lgogdownloader-3.3/cmake/FindTinyxml2.cmake000066400000000000000000000014671317707572500210250ustar00rootroot00000000000000# - 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) lgogdownloader-3.3/include/000077500000000000000000000000001317707572500160275ustar00rootroot00000000000000lgogdownloader-3.3/include/api.h000066400000000000000000000066741317707572500167660ustar00rootroot00000000000000/* 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 "globals.h" #include #include #include extern "C" { #include } #include #include 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 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 lgogdownloader-3.3/include/blacklist.h000066400000000000000000000022771317707572500201600ustar00rootroot00000000000000/* 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 #include #include 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& lines); bool isBlacklisted(const std::string& path); bool isBlacklisted(const std::string& path, const std::string& gamename, std::string subdirectory = ""); std::vector::size_type size() const { return blacklist_.size(); } bool empty() { return blacklist_.empty(); } private: std::vector blacklist_; }; #endif // BLACKLIST_H_ lgogdownloader-3.3/include/config.h000066400000000000000000000167461317707572500174630ustar00rootroot00000000000000/* 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 #include #include #include #include #include "blacklist.h" struct DirectoryConfig { bool bSubDirectories; std::string sDirectory; std::string sGameSubdir; std::string sInstallersSubdir; std::string sExtrasSubdir; std::string sPatchesSubdir; std::string sLanguagePackSubdir; std::string sDLCSubdir; }; struct DownloadConfig { unsigned int iInstallerPlatform; unsigned int iInstallerLanguage; std::vector vPlatformPriority; std::vector vLanguagePriority; unsigned int iInclude; unsigned int iGalaxyPlatform; unsigned int iGalaxyLanguage; unsigned int iGalaxyArch; bool bRemoteXML; bool bCover; bool bSaveChangelogs; bool bSaveSerials; bool bAutomaticXMLCreation; bool bInstallers; bool bExtras; bool bPatches; bool bLanguagePacks; bool bDLC; bool bIgnoreDLCCount; bool bDuplicateHandler; }; struct gameSpecificConfig { DownloadConfig dlConf; DirectoryConfig dirConf; }; class GalaxyConfig { public: bool isExpired() { std::unique_lock lock(m); bool bExpired = true; // assume that token is expired intmax_t time_now = time(NULL); if (this->token_json.isMember("expires_at")) bExpired = (time_now > this->token_json["expires_at"].asLargestInt()); return bExpired; } std::string getAccessToken() { std:: string access_token; std::unique_lock lock(m); if (this->token_json.isMember("access_token")) access_token = this->token_json["access_token"].asString(); return access_token; } std::string getRefreshToken() { std::string refresh_token; std::unique_lock lock(m); if (this->token_json.isMember("refresh_token")) refresh_token = this->token_json["refresh_token"].asString(); return refresh_token; } Json::Value getJSON() { std::unique_lock lock(m); return this->token_json; } void setJSON(Json::Value json) { std::unique_lock lock(m); if (!json.isMember("expires_at")) { intmax_t time_now = time(NULL); Json::Value::LargestInt expires_in = 3600; if (json.isMember("expires_in")) if (!json["expires_in"].isNull()) expires_in = json["expires_in"].asLargestInt(); Json::Value::LargestInt expires_at = time_now + expires_in; json["expires_at"] = expires_at; } this->token_json = json; } void setFilepath(const std::string& path) { std::unique_lock lock(m); this->filepath = path; } std::string getFilepath() { std::unique_lock lock(m); return this->filepath; } std::string getClientId() { std::unique_lock lock(m); return this->client_id; } std::string getClientSecret() { std::unique_lock lock(m); return this->client_secret; } std::string getRedirectUri() { std::unique_lock lock(m); return this->redirect_uri; } GalaxyConfig() = default; GalaxyConfig(const GalaxyConfig& other) { std::lock_guard guard(other.m); client_id = other.client_id; client_secret = other.client_secret; redirect_uri = other.redirect_uri; filepath = other.filepath; token_json = other.token_json; } GalaxyConfig& operator= (GalaxyConfig& other) { if(&other == this) return *this; std::unique_lock lock1(m, std::defer_lock); std::unique_lock lock2(other.m, std::defer_lock); std::lock(lock1, lock2); client_id = other.client_id; client_secret = other.client_secret; redirect_uri = other.redirect_uri; filepath = other.filepath; token_json = other.token_json; return *this; } protected: private: std::string client_id = "46899977096215655"; std::string client_secret = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"; std::string redirect_uri = "https://embed.gog.com/on_login_success?origin=client"; std::string filepath; Json::Value token_json; mutable std::mutex m; }; struct CurlConfig { bool bVerifyPeer; bool bVerbose; std::string sCACertPath; std::string sCookiePath; std::string sUserAgent; long int iTimeout; curl_off_t iDownloadRate; }; struct GogAPIConfig { std::string sToken; std::string sSecret; }; class Config { public: Config() {}; virtual ~Config() {}; // Booleans bool bLoginHTTP; bool bLoginAPI; bool bSaveConfig; bool bResetConfig; bool bDownload; bool bRepair; bool bUpdateCheck; bool bList; bool bListDetails; bool bCheckStatus; bool bShowWishlist; bool bVerbose; bool bUnicode; // use Unicode in console output bool bColor; // use colors bool bReport; bool bRespectUmask; bool bPlatformDetection; // Cache bool bUseCache; bool bUpdateCache; int iCacheValid; // Download with file id options std::string sFileIdString; std::string sOutputFilename; // Curl CurlConfig curlConf; // Download DownloadConfig dlConf; // Directories DirectoryConfig dirConf; std::string sCacheDirectory; std::string sXMLDirectory; std::string sConfigDirectory; // File paths std::string sConfigFilePath; std::string sBlacklistFilePath; std::string sIgnorelistFilePath; std::string sGameHasDLCListFilePath; std::string sReportFilePath; std::string sXMLFile; // Regex std::string sGameRegex; std::string sOrphanRegex; std::string sIgnoreDLCCountRegex; // Priorities std::string sPlatformPriority; std::string sLanguagePriority; // General strings std::string sVersionString; std::string sVersionNumber; std::string sEmail; std::string sPassword; GogAPIConfig apiConf; // Lists Blacklist blacklist; Blacklist ignorelist; Blacklist gamehasdlc; std::string sCoverList; std::string sGameHasDLCList; // Integers int iRetries; unsigned int iThreads; int iWait; size_t iChunkSize; int iProgressInterval; }; #endif // CONFIG_H__ lgogdownloader-3.3/include/downloader.h000066400000000000000000000123761317707572500203470ustar00rootroot00000000000000/* 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 "galaxyapi.h" #include "globals.h" #include #include #include #include #include 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 > TimeAndSize; curl_off_t offset; }; struct ChunkMemoryStruct { char *memory; curl_off_t size; }; class Downloader { public: Downloader(); 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; ProgressBar* progressbar; std::deque< std::pair > TimeAndSize; void saveGalaxyJSON(); void galaxyInstallGame(const std::string& product_id, int build_index = -1, const unsigned int& iGalaxyArch = GlobalConstants::ARCH_X64); void galaxyShowBuilds(const std::string& product_id, int build_index = -1); protected: private: 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 getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0); static std::vector 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); std::vector galaxyGetOrphanedFiles(const std::vector& items, const std::string& install_path); Website *gogWebsite; API *gogAPI; galaxyAPI *gogGalaxy; std::vector gameItems; std::vector games; std::string coverXML; off_t resume_position; int retries; std::ofstream report_ofs; }; #endif // DOWNLOADER_H lgogdownloader-3.3/include/downloadinfo.h000066400000000000000000000047461317707572500206760ustar00rootroot00000000000000/* 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 #include 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 lock(m); filename = filename_; } std::string getFilename() { std::unique_lock lock(m); return filename; } void setStatus(const unsigned int& status_) { std::unique_lock lock(m); status = status_; } unsigned int getStatus() { std::unique_lock lock(m); return status; } void setProgressInfo(const progressInfo& info) { std::unique_lock lock(m); progress_info = info; } progressInfo getProgressInfo() { std::unique_lock lock(m); return progress_info; } DownloadInfo()=default; DownloadInfo(const DownloadInfo& other) { std::lock_guard 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 lock1(m, std::defer_lock); std::unique_lock 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 lgogdownloader-3.3/include/galaxyapi.h000066400000000000000000000062241317707572500201630ustar00rootroot00000000000000/* 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 GALAXYAPI_H #define GALAXYAPI_H #include "globalconstants.h" #include "globals.h" #include "config.h" #include "util.h" #include "gamedetails.h" #include #include #include #include #include struct galaxyDepotItemChunk { std::string md5_compressed; std::string md5_uncompressed; uintmax_t size_compressed; uintmax_t size_uncompressed; uintmax_t offset_compressed; uintmax_t offset_uncompressed; }; struct galaxyDepotItem { std::string path; std::vector chunks; uintmax_t totalSizeCompressed; uintmax_t totalSizeUncompressed; std::string md5; std::string product_id; }; class galaxyAPI { public: galaxyAPI(CurlConfig& conf); virtual ~galaxyAPI(); int init(); bool isTokenExpired(); bool refreshLogin(); Json::Value getProductBuilds(const std::string& product_id, const std::string& platform = "windows", const std::string& generation = "2"); Json::Value getManifestV1(const std::string& product_id, const std::string& build_id, const std::string& manifest_id = "repository", const std::string& platform = "windows"); Json::Value getManifestV2(std::string manifest_hash); Json::Value getSecureLink(const std::string& product_id, const std::string& path); std::string getResponse(const std::string& url, const bool& zlib_decompress = false); std::string hashToGalaxyPath(const std::string& hash); std::vector getDepotItemsVector(const std::string& hash); Json::Value getProductInfo(const std::string& product_id); gameDetails productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf); protected: private: CurlConfig curlConf; static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp); CURL* curlhandle; std::vector installerJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf); std::vector patchJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf); std::vector languagepackJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf); std::vector extraJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json); std::vector fileJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const unsigned int& type = GFTYPE_INSTALLER, const unsigned int& platform = (GlobalConstants::PLATFORM_WINDOWS | GlobalConstants::PLATFORM_LINUX), const unsigned int& lang = GlobalConstants::LANGUAGE_EN, const bool& useDuplicateHandler = false); }; #endif // GALAXYAPI_H lgogdownloader-3.3/include/gamedetails.h000066400000000000000000000030031317707572500204530ustar00rootroot00000000000000/* 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 "globals.h" #include "gamefile.h" #include "config.h" #include "util.h" #include #include #include class gameDetails { public: gameDetails(); std::vector extras; std::vector installers; std::vector patches; std::vector languagepacks; std::vector dlcs; std::string gamename; std::string product_id; std::string title; std::string icon; std::string serials; std::string changelog; void filterWithPriorities(const gameSpecificConfig& config); void makeFilepaths(const DirectoryConfig& config); std::string getSerialsFilepath(); std::string getChangelogFilepath(); Json::Value getDetailsAsJson(); std::vector getGameFileVector(); virtual ~gameDetails(); protected: void filterListWithPriorities(std::vector& list, const gameSpecificConfig& config); private: std::string serialsFilepath; std::string changelogFilepath; }; #endif // GAMEDETAILS_H lgogdownloader-3.3/include/gamefile.h000066400000000000000000000024561317707572500177600ustar00rootroot00000000000000/* 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 "globals.h" #include #include #include // 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; std::string galaxy_downlink_json_url; 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 lgogdownloader-3.3/include/globalconstants.h000066400000000000000000000105451317707572500214020ustar00rootroot00000000000000/* 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 #include namespace GlobalConstants { const int GAMEDETAILS_CACHE_VERSION = 3; const int ZLIB_WINDOW_SIZE = 15; struct optionsStruct {const unsigned int id; const std::string code; const std::string str; const std::string regexp;}; 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 unsigned int LANGUAGE_UK = 1 << 22; const unsigned int LANGUAGE_ES_419 = 1 << 23; const std::vector 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" }, { LANGUAGE_UK, "uk", "Ukrainian" , "uk|ukr|ukrainian" }, { LANGUAGE_ES_419, "es_mx", "Spanish (Latin American)", "es_mx|es-mx|esmx|es-419|spanish_latin_american" } }; // Platform constants const unsigned int PLATFORM_WINDOWS = 1 << 0; const unsigned int PLATFORM_MAC = 1 << 1; const unsigned int PLATFORM_LINUX = 1 << 2; const std::vector PLATFORMS = { { PLATFORM_WINDOWS, "win", "Windows" , "w|win|windows" }, { PLATFORM_MAC, "mac", "Mac" , "m|mac|osx" }, { PLATFORM_LINUX, "linux", "Linux" , "l|lin|linux" } }; // Galaxy platform arch const unsigned int ARCH_X86 = 1 << 0; const unsigned int ARCH_X64 = 1 << 1; const std::vector GALAXY_ARCHS = { { ARCH_X86, "32", "32-bit", "32|x86|32bit|32-bit" }, { ARCH_X64, "64", "64-bit", "64|x64|64bit|64-bit" } }; } #endif // GLOBALCONSTANTS_H_INCLUDED lgogdownloader-3.3/include/globals.h000066400000000000000000000010501317707572500176170ustar00rootroot00000000000000/* 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 GLOBALS_H_INCLUDED #define GLOBALS_H_INCLUDED #include "config.h" #include #include namespace Globals { extern GalaxyConfig galaxyConf; extern Config globalConfig; } #endif // GLOBALS_H_INCLUDED lgogdownloader-3.3/include/message.h000066400000000000000000000055561317707572500176370ustar00rootroot00000000000000/* 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 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 lgogdownloader-3.3/include/progressbar.h000066400000000000000000000022601317707572500205310ustar00rootroot00000000000000/* 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 #include 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 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 lgogdownloader-3.3/include/ssl_thread_setup.h000066400000000000000000000030331317707572500215470ustar00rootroot00000000000000/* 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 #include #if SSL_THREAD_SETUP_OPENSSL == 1 #include 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::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 lgogdownloader-3.3/include/threadsafequeue.h000066400000000000000000000041111317707572500213500ustar00rootroot00000000000000/* 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 #include #include template class ThreadSafeQueue { public: void push(const T& item) { std::unique_lock lock(m); q.push(item); lock.unlock(); cvar.notify_one(); } bool empty() const { std::unique_lock lock(m); return q.empty(); } typename std::queue::size_type size() const { std::unique_lock lock(m); return q.size(); } bool try_pop(T& item) { std::unique_lock lock(m); if(q.empty()) return false; item = q.front(); q.pop(); return true; } void wait_and_pop(T& item) { std::unique_lock lock(m); while(q.empty()) cvar.wait(lock); item = q.front(); q.pop(); } ThreadSafeQueue() = default; ThreadSafeQueue(const ThreadSafeQueue& other) { std::lock_guard guard(other.m); q = other.q; } ThreadSafeQueue& operator= (ThreadSafeQueue& other) { if(&other == this) return *this; std::unique_lock lock1(m, std::defer_lock); std::unique_lock lock2(other.m, std::defer_lock); std::lock(lock1, lock2); q = other.q; return *this; } private: std::queue q; mutable std::mutex m; std::condition_variable cvar; }; #endif // THREADSAFEQUEUE_H lgogdownloader-3.3/include/util.h000066400000000000000000000074341317707572500171650ustar00rootroot00000000000000/* 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 "config.h" #include "globals.h" #include #include #include #include #include #include #include #include #include #include #include struct gameItem { std::string name; std::string id; std::vector dlcnames; Json::Value gamedetailsjson; int updates = 0; }; struct wishlistItem { std::string title; unsigned int platform; std::vector 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 &urls); std::vector getDLCNamesFromJSON(const Json::Value &root); std::string getHomeDir(); std::string getConfigHome(); std::string getCacheHome(); std::vector tokenize(const std::string& str, const std::string& separator = ","); unsigned int getOptionValue(const std::string& str, const std::vector& options, const bool& bAllowStringToIntConversion = true); std::string getOptionNameString(const unsigned int& value, const std::vector& options); void parseOptionString(const std::string &option_string, std::vector &priority, unsigned int &type, const std::vector& options); std::string getLocalFileHash(const std::string& xml_dir, const std::string& filepath, const std::string& gamename = std::string()); void shortenStringToTerminalWidth(std::string& str); std::string getJsonUIntValueAsString(const Json::Value& json); template 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 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 lgogdownloader-3.3/include/website.h000066400000000000000000000023141317707572500176420ustar00rootroot00000000000000/* 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 "globals.h" #include #include #include class Website { public: Website(); 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 getGames(); std::vector getFreeGames(); std::vector getWishlistItems(); bool IsLoggedIn(); 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 lgogdownloader-3.3/main.cpp000066400000000000000000001236551317707572500160500ustar00rootroot00000000000000/* 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 "galaxyapi.h" #include "globals.h" #include #include #include namespace bpo = boost::program_options; Config Globals::globalConfig; template void set_vm_value(std::map& 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 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" } }; Globals::globalConfig.sVersionString = VERSION_STRING; Globals::globalConfig.sVersionNumber = VERSION_NUMBER; Globals::globalConfig.sCacheDirectory = Util::getCacheHome() + "/lgogdownloader"; Globals::globalConfig.sXMLDirectory = Globals::globalConfig.sCacheDirectory + "/xml"; Globals::globalConfig.sConfigDirectory = Util::getConfigHome() + "/lgogdownloader"; Globals::globalConfig.curlConf.sCookiePath = Globals::globalConfig.sConfigDirectory + "/cookies.txt"; Globals::globalConfig.sConfigFilePath = Globals::globalConfig.sConfigDirectory + "/config.cfg"; Globals::globalConfig.sBlacklistFilePath = Globals::globalConfig.sConfigDirectory + "/blacklist.txt"; Globals::globalConfig.sIgnorelistFilePath = Globals::globalConfig.sConfigDirectory + "/ignorelist.txt"; Globals::globalConfig.sGameHasDLCListFilePath = Globals::globalConfig.sConfigDirectory + "/game_has_dlc.txt"; Globals::galaxyConf.setFilepath(Globals::globalConfig.sConfigDirectory + "/galaxy_tokens.json"); 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 --galaxy-platform option std::string galaxy_platform_text = "Select platform\n"; for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i) { galaxy_platform_text += GlobalConstants::PLATFORMS[i].str + " = " + GlobalConstants::PLATFORMS[i].regexp + "|" + std::to_string(GlobalConstants::PLATFORMS[i].id) + "\n"; } // 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 --galaxy-language option std::string galaxy_language_text = "Select language\n"; for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i) { galaxy_language_text += GlobalConstants::LANGUAGES[i].str + " = " + GlobalConstants::LANGUAGES[i].regexp + "|" + std::to_string(GlobalConstants::LANGUAGES[i].id) + "\n"; } // Create help text for --galaxy-arch option std::string galaxy_arch_text = "Select architecture\n"; for (unsigned int i = 0; i < GlobalConstants::GALAXY_ARCHS.size(); ++i) { galaxy_arch_text += GlobalConstants::GALAXY_ARCHS[i].str + " = " + GlobalConstants::GALAXY_ARCHS[i].regexp + "\n"; } // Create help text for --check-orphans std::string orphans_regex_default = ".*\\.(zip|exe|bin|dmg|old|deb|tar\\.gz|pkg|sh)$"; // Limit to files with these extensions (".old" is for renamed older version files) std::string check_orphans_text = "Check for orphaned files (files found on local filesystem that are not found on GOG servers). Sets regular expression filter (Perl syntax) for files to check. If no argument is given then the regex defaults to '" + orphans_regex_default + "'"; // 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::string galaxy_product_id_install; std::string galaxy_product_id_show_builds; std::vector vFileIdStrings; std::vector unrecognized_options_cfg; std::vector 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_no_cfg_hidden; bpo::options_description options_cli_all_include_hidden; bpo::options_description options_cli_experimental("Experimental"); bpo::options_description options_cli_cfg; bpo::options_description options_cfg_only; bpo::options_description options_cfg_all("Configuration"); 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; std::string sGalaxyPlatform; std::string sGalaxyLanguage; std::string sGalaxyArch; Globals::globalConfig.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(&bLogin)->zero_tokens()->default_value(false), "Login") ("list", bpo::value(&Globals::globalConfig.bList)->zero_tokens()->default_value(false), "List games") ("list-details", bpo::value(&Globals::globalConfig.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info") ("download", bpo::value(&Globals::globalConfig.bDownload)->zero_tokens()->default_value(false), "Download") ("repair", bpo::value(&Globals::globalConfig.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(&Globals::globalConfig.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(&Globals::globalConfig.sXMLFile)->implicit_value("automatic"), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation") ("update-check", bpo::value(&Globals::globalConfig.bUpdateCheck)->zero_tokens()->default_value(false), "Check for update notifications") ("check-orphans", bpo::value(&Globals::globalConfig.sOrphanRegex)->implicit_value(""), check_orphans_text.c_str()) ("status", bpo::value(&Globals::globalConfig.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(&Globals::globalConfig.bSaveConfig)->zero_tokens()->default_value(false), "Create config file with current settings") ("reset-config", bpo::value(&Globals::globalConfig.bResetConfig)->zero_tokens()->default_value(false), "Reset config settings to default") ("report", bpo::value(&Globals::globalConfig.sReportFilePath)->implicit_value("lgogdownloader-report.log"), "Save report of downloaded/repaired files to specified file\nDefault filename: lgogdownloader-report.log") ("update-cache", bpo::value(&Globals::globalConfig.bUpdateCache)->zero_tokens()->default_value(false), "Update game details cache") ("no-platform-detection", bpo::value(&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(&Globals::globalConfig.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(&Globals::globalConfig.sOutputFilename)->default_value(""), "Set filename of file downloaded with --download-file.") ("wishlist", bpo::value(&Globals::globalConfig.bShowWishlist)->zero_tokens()->default_value(false), "Show wishlist") ("login-api", bpo::value(&Globals::globalConfig.bLoginAPI)->zero_tokens()->default_value(false), "Login (API only)") ("login-website", bpo::value(&Globals::globalConfig.bLoginHTTP)->zero_tokens()->default_value(false), "Login (website only)") ("cacert", bpo::value(&Globals::globalConfig.curlConf.sCACertPath)->default_value(""), "Path to CA certificate bundle in PEM format") ("respect-umask", bpo::value(&Globals::globalConfig.bRespectUmask)->zero_tokens()->default_value(false), "Do not adjust permissions of sensitive files") ("user-agent", bpo::value(&Globals::globalConfig.curlConf.sUserAgent)->default_value(Globals::globalConfig.sVersionString), "Set user agent") ; // Commandline options (config file) options_cli_cfg.add_options() ("directory", bpo::value(&Globals::globalConfig.dirConf.sDirectory)->default_value("."), "Set download directory") ("limit-rate", bpo::value(&Globals::globalConfig.curlConf.iDownloadRate)->default_value(0), "Limit download rate to value in kB\n0 = unlimited") ("xml-directory", bpo::value(&Globals::globalConfig.sXMLDirectory), "Set directory for GOG XML files") ("chunk-size", bpo::value(&Globals::globalConfig.iChunkSize)->default_value(10), "Chunk size (in MB) when creating XML") ("platform", bpo::value(&sInstallerPlatform)->default_value("w+l"), platform_text.c_str()) ("language", bpo::value(&sInstallerLanguage)->default_value("en"), language_text.c_str()) ("no-remote-xml", bpo::value(&bNoRemoteXML)->zero_tokens()->default_value(false), "Don't use remote XML for repair") ("no-unicode", bpo::value(&bNoUnicode)->zero_tokens()->default_value(false), "Don't use Unicode in the progress bar") ("no-color", bpo::value(&bNoColor)->zero_tokens()->default_value(false), "Don't use coloring in the progress bar or status messages") ("no-duplicate-handling", bpo::value(&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(&bNoSubDirectories)->zero_tokens()->default_value(false), "Don't create subdirectories for extras, patches and language packs") ("verbose", bpo::value(&Globals::globalConfig.bVerbose)->zero_tokens()->default_value(false), "Print lots of information") ("insecure", bpo::value(&bInsecure)->zero_tokens()->default_value(false), "Don't verify authenticity of SSL certificates") ("timeout", bpo::value(&Globals::globalConfig.curlConf.iTimeout)->default_value(10), "Set timeout for connection\nMaximum time in seconds that connection phase is allowed to take") ("retries", bpo::value(&Globals::globalConfig.iRetries)->default_value(3), "Set maximum number of retries on failed download") ("wait", bpo::value(&Globals::globalConfig.iWait)->default_value(0), "Time to wait between requests (milliseconds)") ("cover-list", bpo::value(&Globals::globalConfig.sCoverList)->default_value("https://raw.githubusercontent.com/Sude-/lgogdownloader-lists/master/covers.xml"), "Set URL for cover list") ("subdir-installers", bpo::value(&Globals::globalConfig.dirConf.sInstallersSubdir)->default_value(""), ("Set subdirectory for installers" + subdir_help_text).c_str()) ("subdir-extras", bpo::value(&Globals::globalConfig.dirConf.sExtrasSubdir)->default_value("extras"), ("Set subdirectory for extras" + subdir_help_text).c_str()) ("subdir-patches", bpo::value(&Globals::globalConfig.dirConf.sPatchesSubdir)->default_value("patches"), ("Set subdirectory for patches" + subdir_help_text).c_str()) ("subdir-language-packs", bpo::value(&Globals::globalConfig.dirConf.sLanguagePackSubdir)->default_value("languagepacks"), ("Set subdirectory for language packs" + subdir_help_text).c_str()) ("subdir-dlc", bpo::value(&Globals::globalConfig.dirConf.sDLCSubdir)->default_value("dlc/%dlcname%"), ("Set subdirectory for dlc" + subdir_help_text).c_str()) ("subdir-game", bpo::value(&Globals::globalConfig.dirConf.sGameSubdir)->default_value("%gamename%"), ("Set subdirectory for game" + subdir_help_text).c_str()) ("use-cache", bpo::value(&Globals::globalConfig.bUseCache)->zero_tokens()->default_value(false), ("Use game details cache")) ("cache-valid", bpo::value(&Globals::globalConfig.iCacheValid)->default_value(2880), ("Set how long cached game details are valid (in minutes)\nDefault: 2880 minutes (48 hours)")) ("save-serials", bpo::value(&Globals::globalConfig.dlConf.bSaveSerials)->zero_tokens()->default_value(false), "Save serial numbers when downloading") ("ignore-dlc-count", bpo::value(&Globals::globalConfig.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(&sIncludeOptions)->default_value("all"), ("Select what to download/list/repair\n" + include_options_text).c_str()) ("exclude", bpo::value(&sExcludeOptions)->default_value("covers"), ("Select what not to download/list/repair\n" + include_options_text).c_str()) ("automatic-xml-creation", bpo::value(&Globals::globalConfig.dlConf.bAutomaticXMLCreation)->zero_tokens()->default_value(false), "Automatically create XML data after download has completed") ("save-changelogs", bpo::value(&Globals::globalConfig.dlConf.bSaveChangelogs)->zero_tokens()->default_value(false), "Save changelogs when downloading") ("threads", bpo::value(&Globals::globalConfig.iThreads)->default_value(4), "Number of download threads") ("dlc-list", bpo::value(&Globals::globalConfig.sGameHasDLCList)->default_value("https://raw.githubusercontent.com/Sude-/lgogdownloader-lists/master/game_has_dlc.txt"), "Set URL for list of games that have DLC") ("progress-interval", bpo::value(&Globals::globalConfig.iProgressInterval)->default_value(100), "Set interval for progress bar update (milliseconds)\nValue must be between 1 and 10000") ; // Options read from config file options_cfg_only.add_options() ("token", bpo::value(&Globals::globalConfig.apiConf.sToken)->default_value(""), "oauth token") ("secret", bpo::value(&Globals::globalConfig.apiConf.sSecret)->default_value(""), "oauth secret") ; options_cli_no_cfg_hidden.add_options() ("login-email", bpo::value(&Globals::globalConfig.sEmail)->default_value(""), "login email") ("login-password", bpo::value(&Globals::globalConfig.sPassword)->default_value(""), "login password") ; options_cli_experimental.add_options() ("galaxy-install", bpo::value(&galaxy_product_id_install)->default_value(""), "Install game using product id") ("galaxy-show-builds", bpo::value(&galaxy_product_id_show_builds)->default_value(""), "Show game builds using product id") ("galaxy-platform", bpo::value(&sGalaxyPlatform)->default_value("w"), galaxy_platform_text.c_str()) ("galaxy-language", bpo::value(&sGalaxyLanguage)->default_value("en"), galaxy_language_text.c_str()) ("galaxy-arch", bpo::value(&sGalaxyArch)->default_value("x64"), galaxy_arch_text.c_str()) ; options_cli_all.add(options_cli_no_cfg).add(options_cli_cfg).add(options_cli_experimental); options_cfg_all.add(options_cfg_only).add(options_cli_cfg); options_cli_all_include_hidden.add(options_cli_all).add(options_cli_no_cfg_hidden); bpo::parsed_options parsed = bpo::parse_command_line(argc, argv, options_cli_all_include_hidden); bpo::store(parsed, vm); unrecognized_options_cli = bpo::collect_unrecognized(parsed.options, bpo::include_positional); bpo::notify(vm); if (vm.count("help")) { std::cout << Globals::globalConfig.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 = Globals::globalConfig.sXMLDirectory; if (!boost::filesystem::exists(path)) { if (!boost::filesystem::create_directories(path)) { std::cerr << "Failed to create directory: " << path << std::endl; return 1; } } path = Globals::globalConfig.sConfigDirectory; if (!boost::filesystem::exists(path)) { if (!boost::filesystem::create_directories(path)) { std::cerr << "Failed to create directory: " << path << std::endl; return 1; } } path = Globals::globalConfig.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(Globals::globalConfig.sConfigFilePath)) { std::ifstream ifs(Globals::globalConfig.sConfigFilePath.c_str()); if (!ifs) { std::cerr << "Could not open config file: " << Globals::globalConfig.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(Globals::globalConfig.sBlacklistFilePath)) { std::ifstream ifs(Globals::globalConfig.sBlacklistFilePath.c_str()); if (!ifs) { std::cerr << "Could not open blacklist file: " << Globals::globalConfig.sBlacklistFilePath << std::endl; return 1; } else { std::string line; std::vector lines; while (!ifs.eof()) { std::getline(ifs, line); lines.push_back(std::move(line)); } Globals::globalConfig.blacklist.initialize(lines); } } if (boost::filesystem::exists(Globals::globalConfig.sIgnorelistFilePath)) { std::ifstream ifs(Globals::globalConfig.sIgnorelistFilePath.c_str()); if (!ifs) { std::cerr << "Could not open ignorelist file: " << Globals::globalConfig.sIgnorelistFilePath << std::endl; return 1; } else { std::string line; std::vector lines; while (!ifs.eof()) { std::getline(ifs, line); lines.push_back(std::move(line)); } Globals::globalConfig.ignorelist.initialize(lines); } } if (Globals::globalConfig.sIgnoreDLCCountRegex.empty()) { if (boost::filesystem::exists(Globals::globalConfig.sGameHasDLCListFilePath)) { std::ifstream ifs(Globals::globalConfig.sGameHasDLCListFilePath.c_str()); if (!ifs) { std::cerr << "Could not open list of games that have dlc: " << Globals::globalConfig.sGameHasDLCListFilePath << std::endl; return 1; } else { std::string line; std::vector lines; while (!ifs.eof()) { std::getline(ifs, line); lines.push_back(std::move(line)); } Globals::globalConfig.gamehasdlc.initialize(lines); } } } if (bLogin || Globals::globalConfig.bLoginAPI || Globals::globalConfig.bLoginHTTP) { std::string login_conf = Globals::globalConfig.sConfigDirectory + "/login.txt"; if (boost::filesystem::exists(login_conf)) { std::ifstream ifs(login_conf); if (!ifs) { std::cerr << "Could not open login conf: " << login_conf << std::endl; return 1; } else { std::string line; std::vector lines; while (!ifs.eof()) { std::getline(ifs, line); lines.push_back(std::move(line)); } Globals::globalConfig.sEmail = lines[0]; Globals::globalConfig.sPassword = lines[1]; } } } if (vm.count("chunk-size")) Globals::globalConfig.iChunkSize <<= 20; // Convert chunk size from bytes to megabytes if (vm.count("limit-rate")) Globals::globalConfig.curlConf.iDownloadRate <<= 10; // Convert download rate from bytes to kilobytes if (vm.count("check-orphans")) if (Globals::globalConfig.sOrphanRegex.empty()) Globals::globalConfig.sOrphanRegex = orphans_regex_default; if (vm.count("report")) Globals::globalConfig.bReport = true; if (Globals::globalConfig.iWait > 0) Globals::globalConfig.iWait *= 1000; if (Globals::globalConfig.iProgressInterval < 1) Globals::globalConfig.iProgressInterval = 1; else if (Globals::globalConfig.iProgressInterval > 10000) Globals::globalConfig.iProgressInterval = 10000; if (Globals::globalConfig.iThreads < 1) { Globals::globalConfig.iThreads = 1; set_vm_value(vm, "threads", Globals::globalConfig.iThreads); } Globals::globalConfig.curlConf.bVerbose = Globals::globalConfig.bVerbose; Globals::globalConfig.curlConf.bVerifyPeer = !bInsecure; Globals::globalConfig.bColor = !bNoColor; Globals::globalConfig.bUnicode = !bNoUnicode; Globals::globalConfig.dlConf.bDuplicateHandler = !bNoDuplicateHandler; Globals::globalConfig.dlConf.bRemoteXML = !bNoRemoteXML; Globals::globalConfig.dirConf.bSubDirectories = !bNoSubDirectories; Globals::globalConfig.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) Globals::globalConfig.sFileIdString = *i; if (!Globals::globalConfig.sFileIdString.empty()) { if (Globals::globalConfig.sFileIdString.compare(0, GlobalConstants::PROTOCOL_PREFIX.length(), GlobalConstants::PROTOCOL_PREFIX) == 0) { Globals::globalConfig.sFileIdString.replace(0, GlobalConstants::PROTOCOL_PREFIX.length(), ""); } vFileIdStrings = Util::tokenize(Globals::globalConfig.sFileIdString, ","); } if (!Globals::globalConfig.sOutputFilename.empty() && vFileIdStrings.size() > 1) { std::cerr << "Cannot specify an output file name when downloading multiple files." << std::endl; return 1; } if (bLogin) { Globals::globalConfig.bLoginAPI = true; Globals::globalConfig.bLoginHTTP = true; } if (Globals::globalConfig.sXMLFile == "automatic") Globals::globalConfig.dlConf.bAutomaticXMLCreation = true; Util::parseOptionString(sInstallerLanguage, Globals::globalConfig.dlConf.vLanguagePriority, Globals::globalConfig.dlConf.iInstallerLanguage, GlobalConstants::LANGUAGES); Util::parseOptionString(sInstallerPlatform, Globals::globalConfig.dlConf.vPlatformPriority, Globals::globalConfig.dlConf.iInstallerPlatform, GlobalConstants::PLATFORMS); Globals::globalConfig.dlConf.iGalaxyPlatform = Util::getOptionValue(sGalaxyPlatform, GlobalConstants::PLATFORMS); Globals::globalConfig.dlConf.iGalaxyLanguage = Util::getOptionValue(sGalaxyLanguage, GlobalConstants::LANGUAGES); Globals::globalConfig.dlConf.iGalaxyArch = Util::getOptionValue(sGalaxyArch, GlobalConstants::GALAXY_ARCHS, false); if (Globals::globalConfig.dlConf.iGalaxyArch == 0 || Globals::globalConfig.dlConf.iGalaxyArch == Util::getOptionValue("all", GlobalConstants::GALAXY_ARCHS, false)) Globals::globalConfig.dlConf.iGalaxyArch = GlobalConstants::ARCH_X64; unsigned int include_value = 0; unsigned int exclude_value = 0; std::vector vInclude = Util::tokenize(sIncludeOptions, ","); std::vector vExclude = Util::tokenize(sExcludeOptions, ","); for (std::vector::iterator it = vInclude.begin(); it != vInclude.end(); it++) { include_value |= Util::getOptionValue(*it, INCLUDE_OPTIONS); } for (std::vector::iterator it = vExclude.begin(); it != vExclude.end(); it++) { exclude_value |= Util::getOptionValue(*it, INCLUDE_OPTIONS); } Globals::globalConfig.dlConf.iInclude = include_value & ~exclude_value; // Assign values // TODO: Use config.iInclude in Downloader class directly and get rid of this value assignment Globals::globalConfig.dlConf.bCover = (Globals::globalConfig.dlConf.iInclude & OPTION_COVERS); Globals::globalConfig.dlConf.bInstallers = (Globals::globalConfig.dlConf.iInclude & OPTION_INSTALLERS); Globals::globalConfig.dlConf.bExtras = (Globals::globalConfig.dlConf.iInclude & OPTION_EXTRAS); Globals::globalConfig.dlConf.bPatches = (Globals::globalConfig.dlConf.iInclude & OPTION_PATCHES); Globals::globalConfig.dlConf.bLanguagePacks = (Globals::globalConfig.dlConf.iInclude & OPTION_LANGPACKS); Globals::globalConfig.dlConf.bDLC = (Globals::globalConfig.dlConf.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 (Globals::globalConfig.dlConf.iInstallerPlatform < GlobalConstants::PLATFORMS[0].id || Globals::globalConfig.dlConf.iInstallerPlatform > platform_all) { std::cerr << "Invalid value for --platform" << std::endl; return 1; } if (Globals::globalConfig.dlConf.iInstallerLanguage < GlobalConstants::LANGUAGES[0].id || Globals::globalConfig.dlConf.iInstallerLanguage > language_all) { std::cerr << "Invalid value for --language" << std::endl; return 1; } if (!Globals::globalConfig.sXMLDirectory.empty()) { // Make sure that xml directory doesn't have trailing slash if (Globals::globalConfig.sXMLDirectory.at(Globals::globalConfig.sXMLDirectory.length()-1)=='/') Globals::globalConfig.sXMLDirectory.assign(Globals::globalConfig.sXMLDirectory.begin(), Globals::globalConfig.sXMLDirectory.end()-1); } // Create GOG XML for a file if (!Globals::globalConfig.sXMLFile.empty() && (Globals::globalConfig.sXMLFile != "automatic")) { Util::createXML(Globals::globalConfig.sXMLFile, Globals::globalConfig.iChunkSize, Globals::globalConfig.sXMLDirectory); return 0; } // Make sure that directory has trailing slash if (!Globals::globalConfig.dirConf.sDirectory.empty()) { if (Globals::globalConfig.dirConf.sDirectory.at(Globals::globalConfig.dirConf.sDirectory.length()-1)!='/') Globals::globalConfig.dirConf.sDirectory += "/"; } else { Globals::globalConfig.dirConf.sDirectory = "./"; // Directory wasn't specified, use current directory } // CA certificate bundle if (Globals::globalConfig.curlConf.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) Globals::globalConfig.curlConf.sCACertPath = (std::string)ca_bundle; } if (!unrecognized_options_cfg.empty() && (!Globals::globalConfig.bSaveConfig || !Globals::globalConfig.bResetConfig)) { std::cerr << "Unrecognized options in " << Globals::globalConfig.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 (Globals::globalConfig.bLoginAPI) { Globals::globalConfig.apiConf.sToken = ""; Globals::globalConfig.apiConf.sSecret = ""; } Downloader downloader; int iLoginTries = 0; bool bLoginOK = false; // Login because --login, --login-api or --login-website was used if (Globals::globalConfig.bLoginAPI || Globals::globalConfig.bLoginHTTP) bLoginOK = downloader.login(); bool bIsLoggedin = downloader.isLoggedIn(); // Login because we are not logged in while (iLoginTries++ < Globals::globalConfig.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 (!Globals::globalConfig.bRespectUmask) { Util::setFilePermissions(Globals::globalConfig.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write); Util::setFilePermissions(Globals::globalConfig.curlConf.sCookiePath, boost::filesystem::owner_read | boost::filesystem::owner_write); Util::setFilePermissions(Globals::galaxyConf.getFilepath(), boost::filesystem::owner_read | boost::filesystem::owner_write); } if (Globals::globalConfig.bSaveConfig || bLoginOK) { if (bLoginOK) { set_vm_value(vm, "token", Globals::globalConfig.apiConf.sToken); set_vm_value(vm, "secret", Globals::globalConfig.apiConf.sSecret); } std::ofstream ofs(Globals::globalConfig.sConfigFilePath.c_str()); if (ofs) { std::cerr << "Saving config: " << Globals::globalConfig.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(); else if ( type == typeid(int) ) option_value_string = std::to_string(option_value.as()); else if ( type == typeid(size_t) ) option_value_string = std::to_string(option_value.as()); else if ( type == typeid(unsigned int) ) option_value_string = std::to_string(option_value.as()); else if ( type == typeid(long int) ) option_value_string = std::to_string(option_value.as()); else if ( type == typeid(bool) ) { if (option_value.as() == 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 (!Globals::globalConfig.bRespectUmask) Util::setFilePermissions(Globals::globalConfig.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write); if (Globals::globalConfig.bSaveConfig) { curl_global_cleanup(); ssl_thread_cleanup(); return 0; } } else { std::cerr << "Failed to create config: " << Globals::globalConfig.sConfigFilePath << std::endl; curl_global_cleanup(); ssl_thread_cleanup(); return 1; } } else if (Globals::globalConfig.bResetConfig) { std::ofstream ofs(Globals::globalConfig.sConfigFilePath.c_str()); if (ofs) { if (!Globals::globalConfig.apiConf.sToken.empty() && !Globals::globalConfig.apiConf.sSecret.empty()) { ofs << "token = " << Globals::globalConfig.apiConf.sToken << std::endl; ofs << "secret = " << Globals::globalConfig.apiConf.sSecret << std::endl; } ofs.close(); if (!Globals::globalConfig.bRespectUmask) Util::setFilePermissions(Globals::globalConfig.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write); curl_global_cleanup(); ssl_thread_cleanup(); return 0; } else { std::cerr << "Failed to create config: " << Globals::globalConfig.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 (Globals::globalConfig.bShowWishlist) downloader.showWishlist(); else if (Globals::globalConfig.bUpdateCache) downloader.updateCache(); else if (Globals::globalConfig.bUpdateCheck) // Update check has priority over download and list downloader.updateCheck(); else if (!vFileIdStrings.empty()) { for (std::vector::iterator it = vFileIdStrings.begin(); it != vFileIdStrings.end(); it++) { res |= downloader.downloadFileWithId(*it, Globals::globalConfig.sOutputFilename) ? 1 : 0; } } else if (Globals::globalConfig.bRepair) // Repair file downloader.repair(); else if (Globals::globalConfig.bDownload) // Download games downloader.download(); else if (Globals::globalConfig.bListDetails || Globals::globalConfig.bList) // Detailed list of games/extras res = downloader.listGames(); else if (!Globals::globalConfig.sOrphanRegex.empty()) // Check for orphaned files if regex for orphans is set downloader.checkOrphans(); else if (Globals::globalConfig.bCheckStatus) downloader.checkStatus(); else if (!galaxy_product_id_show_builds.empty()) { int build_index = -1; std::vector tokens = Util::tokenize(galaxy_product_id_show_builds, "/"); std::string product_id = tokens[0]; if (tokens.size() == 2) { build_index = std::stoi(tokens[1]); } downloader.galaxyShowBuilds(product_id, build_index); } else if (!galaxy_product_id_install.empty()) { int build_index = -1; std::vector tokens = Util::tokenize(galaxy_product_id_install, "/"); std::string product_id = tokens[0]; if (tokens.size() == 2) { build_index = std::stoi(tokens[1]); } downloader.galaxyInstallGame(product_id, build_index, Globals::globalConfig.dlConf.iGalaxyArch); } else { if (!(Globals::globalConfig.bLoginAPI || Globals::globalConfig.bLoginHTTP)) { // Show help message std::cerr << Globals::globalConfig.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 (!Globals::globalConfig.sOrphanRegex.empty() && Globals::globalConfig.bDownload) downloader.checkOrphans(); curl_global_cleanup(); ssl_thread_cleanup(); return res; } lgogdownloader-3.3/man/000077500000000000000000000000001317707572500151575ustar00rootroot00000000000000lgogdownloader-3.3/man/CMakeLists.txt000066400000000000000000000017531317707572500177250ustar00rootroot00000000000000find_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) include(GNUInstallDirs) if(HELP2MAN AND GZIP) set(H2M_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.supplemental.groff) set(MAN_PAGE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.1) 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 ${CMAKE_INSTALL_MANDIR}/man1) else(HELP2MAN AND GZIP) message("WARNING: One of the following is missing: help2man, gzip; man page will not be generated") endif(HELP2MAN AND GZIP) lgogdownloader-3.3/man/lgogdownloader.supplemental.groff000066400000000000000000000111411317707572500237210ustar00rootroot00000000000000[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. /--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" : , "platform" : , "dlc" : , "ignore-dlc-count" : , "subdirectories" : , "directory" : , "subdir-game" : , "subdir-installers" : , "subdir-extras" : , "subdir-patches" : , "subdir-language-packs" : , "subdir-dlc" : .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 lgogdownloader-3.3/src/000077500000000000000000000000001317707572500151735ustar00rootroot00000000000000lgogdownloader-3.3/src/api.cpp000066400000000000000000000721721317707572500164610ustar00rootroot00000000000000/* 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 #include #include #include #include #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) >= 40900 # define _regex_namespace_ std # include #else # define _regex_namespace_ boost # include #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; } usleep(500); // Wait to avoid "429 Too Many Requests" // 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; } usleep(500); // Wait to avoid "429 Too Many Requests" // 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(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 membernames = root["game"].getMemberNames(); // Installer details // Create a list of installers from JSON std::vector 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; std::string path = installer["link"].asString(); path = (std::string)curl_easy_unescape(curlhandle, path.c_str(), path.size(), NULL); // Check for duplicate installers in different languages and add languageId of duplicate installer to the original installer // https://secure.gog.com/forum/general/introducing_the_beta_release_of_the_new_gogcom_downloader/post1483 if (useDuplicateHandler) { bool bDuplicate = false; for (unsigned int j = 0; j < game.installers.size(); ++j) { if (game.installers[j].path == path) { 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 = path; 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.path = (std::string)curl_easy_unescape(curlhandle, gf.path.c_str(), gf.path.size(), NULL); gf.size = extra["size_mb"].asString(); game.extras.push_back(gf); } // 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 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]; std::string path = patch["link"].asString(); path = (std::string)curl_easy_unescape(curlhandle, path.c_str(), path.size(), NULL); // Check for duplicate patches in different languages and add languageId of duplicate patch to the original patch if (useDuplicateHandler) { bool bDuplicate = false; for (unsigned int j = 0; j < game.patches.size(); ++j) { if (game.patches[j].path == path) { 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 = path; 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 { std::string path = patchnode["link"].asString(); path = (std::string)curl_easy_unescape(curlhandle, path.c_str(), path.size(), NULL); // Check for duplicate patches in different languages and add languageId of duplicate patch to the original patch if (useDuplicateHandler) { bool bDuplicate = false; for (unsigned int k = 0; k < game.patches.size(); ++k) { if (game.patches[k].path == path) { 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 = path; 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 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.path = (std::string)curl_easy_unescape(curlhandle, gf.path.c_str(), gf.path.size(), NULL); gf.size = langpack["size"].asString(); gf.language = GlobalConstants::LANGUAGES[i].id; 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); } lgogdownloader-3.3/src/blacklist.cpp000066400000000000000000000050611317707572500176510ustar00rootroot00000000000000/* 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 #include enum { BLFLAG_RX = 1 << 0, BLFLAG_PERL = 1 << 1 }; void Blacklist::initialize(const std::vector& 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); } lgogdownloader-3.3/src/downloader.cpp000066400000000000000000004423101317707572500200410ustar00rootroot00000000000000/* 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 "globals.h" #include "downloadinfo.h" #include "message.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace bptime = boost::posix_time; std::vector vDownloadInfo; ThreadSafeQueue dlQueue; ThreadSafeQueue msgQueue; ThreadSafeQueue createXMLQueue; ThreadSafeQueue gameItemQueue; ThreadSafeQueue gameDetailsQueue; std::mutex mtx_create_directories; // Mutex for creating directories in Downloader::processDownloadQueue static curl_off_t WriteChunkMemoryCallback(void *contents, curl_off_t size, curl_off_t nmemb, void *userp) { curl_off_t realsize = size * nmemb; struct ChunkMemoryStruct *mem = (struct ChunkMemoryStruct *)userp; mem->memory = (char *) realloc(mem->memory, mem->size + realsize + 1); if(mem->memory == NULL) { std::cout << "Not enough memory (realloc returned NULL)" << std::endl; return 0; } memcpy(&(mem->memory[mem->size]), contents, realsize); mem->size += realsize; mem->memory[mem->size] = 0; return realsize; } Downloader::Downloader() { if (Globals::globalConfig.bLoginHTTP) { if (boost::filesystem::exists(Globals::globalConfig.curlConf.sCookiePath)) if (!boost::filesystem::remove(Globals::globalConfig.curlConf.sCookiePath)) std::cerr << "Failed to delete " << Globals::globalConfig.curlConf.sCookiePath << std::endl; if (boost::filesystem::exists(Globals::galaxyConf.getFilepath())) if (!boost::filesystem::remove(Globals::galaxyConf.getFilepath())) std::cerr << "Failed to delete " << Globals::galaxyConf.getFilepath() << 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, Globals::globalConfig.curlConf.sUserAgent.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, Globals::globalConfig.curlConf.iTimeout); curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true); curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, Globals::globalConfig.curlConf.bVerifyPeer); curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, Globals::globalConfig.curlConf.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, Globals::globalConfig.curlConf.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 (!Globals::globalConfig.curlConf.sCACertPath.empty()) curl_easy_setopt(curlhandle, CURLOPT_CAINFO, Globals::globalConfig.curlConf.sCACertPath.c_str()); // Create new GOG website handle gogWebsite = new Website(); // Create new API handle and set curl options for the API gogAPI = new API(Globals::globalConfig.apiConf.sToken, Globals::globalConfig.apiConf.sSecret); gogAPI->curlSetOpt(CURLOPT_VERBOSE, Globals::globalConfig.curlConf.bVerbose); gogAPI->curlSetOpt(CURLOPT_SSL_VERIFYPEER, Globals::globalConfig.curlConf.bVerifyPeer); gogAPI->curlSetOpt(CURLOPT_CONNECTTIMEOUT, Globals::globalConfig.curlConf.iTimeout); if (!Globals::globalConfig.curlConf.sCACertPath.empty()) gogAPI->curlSetOpt(CURLOPT_CAINFO, Globals::globalConfig.curlConf.sCACertPath.c_str()); gogAPI->init(); progressbar = new ProgressBar(Globals::globalConfig.bUnicode, Globals::globalConfig.bColor); if (boost::filesystem::exists(Globals::galaxyConf.getFilepath())) { std::ifstream ifs(Globals::galaxyConf.getFilepath(), std::ifstream::binary); Json::Value json; Json::Reader *jsonparser = new Json::Reader; if (jsonparser->parse(ifs, json)) { if (!json.isMember("expires_at")) { std::time_t last_modified = boost::filesystem::last_write_time(Globals::galaxyConf.getFilepath()); Json::Value::LargestInt expires_in = json["expires_in"].asLargestInt(); json["expires_at"] = expires_in + last_modified; } Globals::galaxyConf.setJSON(json); } else { std::cerr << "Failed to parse " << Globals::galaxyConf.getFilepath() << std::endl; std::cerr << jsonparser->getFormattedErrorMessages() << std::endl; } delete jsonparser; if (ifs) ifs.close(); } gogGalaxy = new galaxyAPI(Globals::globalConfig.curlConf); } Downloader::~Downloader() { if (Globals::globalConfig.bReport) if (this->report_ofs) this->report_ofs.close(); if (!gogGalaxy->isTokenExpired()) this->saveGalaxyJSON(); delete progressbar; delete gogGalaxy; delete gogAPI; delete gogWebsite; curl_easy_cleanup(curlhandle); // Make sure that cookie file is only readable/writable by owner if (!Globals::globalConfig.bRespectUmask) { Util::setFilePermissions(Globals::globalConfig.curlConf.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; Globals::globalConfig.bLoginAPI = false; Globals::globalConfig.bLoginHTTP = false; bool bWebsiteIsLoggedIn = gogWebsite->IsLoggedIn(); if (!bWebsiteIsLoggedIn) Globals::globalConfig.bLoginHTTP = true; bool bGalaxyIsLoggedIn = !gogGalaxy->isTokenExpired(); if (!bGalaxyIsLoggedIn) { if (gogGalaxy->refreshLogin()) bGalaxyIsLoggedIn = true; else Globals::globalConfig.bLoginHTTP = true; } bool bIsLoggedInAPI = gogAPI->isLoggedIn(); if (!bIsLoggedInAPI) Globals::globalConfig.bLoginAPI = true; /* Check that website and Galaxy API are logged in. Allows users to use most of the functionality without having valid API login credentials. Globals::globalConfig.bLoginAPI can still be set to true at this point which means that if website is not logged in we still try to login to API. */ if (bWebsiteIsLoggedIn && bGalaxyIsLoggedIn) bIsLoggedIn = true; return bIsLoggedIn; } /* Initialize the downloader returns 0 if failed returns 1 if successful */ int Downloader::init() { if (!Globals::globalConfig.sGameHasDLCList.empty()) { if (Globals::globalConfig.gamehasdlc.empty()) { std::string game_has_dlc_list = this->getResponse(Globals::globalConfig.sGameHasDLCList); if (!game_has_dlc_list.empty()) Globals::globalConfig.gamehasdlc.initialize(Util::tokenize(game_has_dlc_list, "\n")); } } if (!gogGalaxy->init()) { if (gogGalaxy->refreshLogin()) { this->saveGalaxyJSON(); } else return 0; } if (!Globals::galaxyConf.getJSON().empty()) { if (Globals::galaxyConf.isExpired()) { // Access token has expired, refresh if (gogGalaxy->refreshLogin()) { this->saveGalaxyJSON(); } } } if (Globals::globalConfig.bReport && (Globals::globalConfig.bDownload || Globals::globalConfig.bRepair)) { this->report_ofs.open(Globals::globalConfig.sReportFilePath); if (!this->report_ofs) { Globals::globalConfig.bReport = false; std::cerr << "Failed to create " << Globals::globalConfig.sReportFilePath << std::endl; return 0; } } return 1; } /* Login returns 0 if login fails returns 1 if successful */ int Downloader::login() { std::string email; std::string password; if (!Globals::globalConfig.sEmail.empty() && !Globals::globalConfig.sPassword.empty()) { email = Globals::globalConfig.sEmail; password = Globals::globalConfig.sPassword; } else { 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::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 (Globals::globalConfig.bLoginHTTP) { // Delete old cookies if (boost::filesystem::exists(Globals::globalConfig.curlConf.sCookiePath)) if (!boost::filesystem::remove(Globals::globalConfig.curlConf.sCookiePath)) std::cerr << "Failed to delete " << Globals::globalConfig.curlConf.sCookiePath << std::endl; int iWebsiteLoginResult = gogWebsite->Login(email, password); if (iWebsiteLoginResult < 1) { std::cerr << "HTTP: Login failed" << std::endl; return 0; } else { std::cerr << "HTTP: Login successful" << std::endl; } if (iWebsiteLoginResult < 2) { std::cerr << "Galaxy: Login failed" << std::endl; return 0; } else { std::cerr << "Galaxy: Login successful" << std::endl; if (!Globals::galaxyConf.getJSON().empty()) { this->saveGalaxyJSON(); } if (!Globals::globalConfig.bLoginAPI) return 1; } } // Login to API if (Globals::globalConfig.bLoginAPI) { if (!gogAPI->login(email, password)) { std::cerr << "API: Login failed (some features may not work)" << std::endl; return 0; } else { std::cerr << "API: Login successful" << std::endl; Globals::globalConfig.apiConf.sToken = gogAPI->getToken(); Globals::globalConfig.apiConf.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) { Globals::globalConfig.sGameRegex = ".*"; // Always check all games if (Globals::globalConfig.bList || Globals::globalConfig.bListDetails || Globals::globalConfig.bDownload) { if (Globals::globalConfig.bList) Globals::globalConfig.bListDetails = true; // Always list details this->getGameList(); if (Globals::globalConfig.bDownload) this->download(); else this->listGames(); } } } void Downloader::getGameList() { if (Globals::globalConfig.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 DirectoryConfig dirConfDefault = Globals::globalConfig.dirConf; if (Globals::globalConfig.bUseCache && !Globals::globalConfig.bUpdateCache) { // GameRegex filter alias for all games if (Globals::globalConfig.sGameRegex == "all") Globals::globalConfig.sGameRegex = ".*"; else if (Globals::globalConfig.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(Globals::globalConfig.iThreads, static_cast(gameItemQueue.size())); std::vector 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, Globals::globalConfig, 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(Globals::globalConfig.bColor, true) << std::endl; if (Globals::globalConfig.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 (Globals::globalConfig.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 << "product id: " << games[i].product_id << std::endl << "title: " << games[i].title << std::endl << "icon: " << games[i].icon << std::endl; if (!games[i].serials.empty()) std::cout << "serials:" << std::endl << games[i].serials << std::endl; // List installers if (Globals::globalConfig.dlConf.bInstallers) { std::cout << "installers: " << std::endl; for (unsigned int j = 0; j < games[i].installers.size(); ++j) { if (!Globals::globalConfig.bUpdateCheck || games[i].installers[j].updated) // Always list updated files { std::string filepath = games[i].installers[j].getFilepath(); if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.bVerbose) std::cerr << "skipped blacklisted file " << filepath << std::endl; continue; } std::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 (Globals::globalConfig.dlConf.bExtras && !Globals::globalConfig.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 (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.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 (Globals::globalConfig.dlConf.bPatches && !Globals::globalConfig.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 (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.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 (Globals::globalConfig.dlConf.bLanguagePacks && !Globals::globalConfig.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 (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.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 (Globals::globalConfig.dlConf.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 (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.bVerbose) std::cerr << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl << "\tid: " << games[i].dlcs[j].installers[k].id << std::endl << "\tname: " << games[i].dlcs[j].installers[k].name << std::endl << "\tpath: " << games[i].dlcs[j].installers[k].path << std::endl << "\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 (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.bVerbose) std::cerr << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl << "\tid: " << games[i].dlcs[j].patches[k].id << std::endl << "\tname: " << games[i].dlcs[j].patches[k].name << std::endl << "\tpath: " << games[i].dlcs[j].patches[k].path << std::endl << "\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 (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.bVerbose) std::cerr << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl << "\tid: " << games[i].dlcs[j].extras[k].id << std::endl << "\tname: " << games[i].dlcs[j].extras[k].name << std::endl << "\tpath: " << games[i].dlcs[j].extras[k].path << std::endl << "\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 (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.bVerbose) std::cerr << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl << "\tid: " << games[i].dlcs[j].languagepacks[k].id << std::endl << "\tname: " << games[i].dlcs[j].languagepacks[k].name << std::endl << "\tpath: " << games[i].dlcs[j].languagepacks[k].path << std::endl << "\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 (Globals::globalConfig.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(); Json::Reader *jsonparser = new Json::Reader; // Create a vector containing all game files std::vector vGameFiles; for (unsigned int i = 0; i < games.size(); ++i) { std::vector vec = games[i].getGameFileVector(); vGameFiles.insert(std::end(vGameFiles), std::begin(vec), std::end(vec)); } for (unsigned int i = 0; i < vGameFiles.size(); ++i) { gameSpecificConfig conf; conf.dlConf = Globals::globalConfig.dlConf; conf.dirConf = Globals::globalConfig.dirConf; unsigned int type = vGameFiles[i].type; if (!conf.dlConf.bDLC && (type & GFTYPE_DLC)) continue; if (!conf.dlConf.bInstallers && (type & GFTYPE_INSTALLER)) continue; if (!conf.dlConf.bExtras && (type & GFTYPE_EXTRA)) continue; if (!conf.dlConf.bPatches && (type & GFTYPE_PATCH)) continue; if (!conf.dlConf.bLanguagePacks && (type & GFTYPE_LANGPACK)) continue; std::string filepath = vGameFiles[i].getFilepath(); if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { if (Globals::globalConfig.bVerbose) std::cerr << "skipped blacklisted file " << filepath << std::endl; continue; } // Refresh Galaxy login if token is expired if (gogGalaxy->isTokenExpired()) { if (!gogGalaxy->refreshLogin()) { std::cerr << "Galaxy API failed to refresh login" << std::endl; break; } } Json::Value downlinkJson; std::string response = gogGalaxy->getResponse(vGameFiles[i].galaxy_downlink_json_url); if (response.empty()) { std::cerr << "Found nothing in " << vGameFiles[i].galaxy_downlink_json_url << ", skipping file" << std::endl; continue; } jsonparser->parse(response, downlinkJson); if (!downlinkJson.isMember("downlink")) { std::cerr << "Invalid JSON response, skipping file" << std::endl; continue; } std::string xml_url; if (downlinkJson.isMember("checksum")) if (!downlinkJson["checksum"].empty()) xml_url = downlinkJson["checksum"].asString(); // Get XML data std::string XML = ""; if (conf.dlConf.bRemoteXML && !xml_url.empty()) XML = gogGalaxy->getResponse(xml_url); // Repair bool bUseLocalXML = !conf.dlConf.bRemoteXML; // Use local XML data for extras if (XML.empty() && (type & GFTYPE_EXTRA)) bUseLocalXML = true; if (!XML.empty() || bUseLocalXML) { std::string url = downlinkJson["downlink"].asString(); std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, XML, vGameFiles[i].gamename); std::cout << std::endl; } } delete jsonparser; } void Downloader::download() { if (this->games.empty()) this->getGameDetails(); if (Globals::globalConfig.dlConf.bCover && !Globals::globalConfig.bUpdateCheck) coverXML = this->getResponse(Globals::globalConfig.sCoverList); for (unsigned int i = 0; i < games.size(); ++i) { gameSpecificConfig conf; conf.dlConf = Globals::globalConfig.dlConf; conf.dirConf = Globals::globalConfig.dirConf; if (conf.dlConf.bSaveSerials && !games[i].serials.empty()) { std::string filepath = games[i].getSerialsFilepath(); this->saveSerials(games[i].serials, filepath); } if (conf.dlConf.bSaveChangelogs && !games[i].changelog.empty()) { std::string filepath = games[i].getChangelogFilepath(); this->saveChangelog(games[i].changelog, filepath); } // Download covers if (conf.dlConf.bCover && !Globals::globalConfig.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 (conf.dlConf.bInstallers) { for (unsigned int j = 0; j < games[i].installers.size(); ++j) { dlQueue.push(games[i].installers[j]); } } if (conf.dlConf.bPatches) { for (unsigned int j = 0; j < games[i].patches.size(); ++j) { dlQueue.push(games[i].patches[j]); } } if (conf.dlConf.bExtras) { for (unsigned int j = 0; j < games[i].extras.size(); ++j) { dlQueue.push(games[i].extras[j]); } } if (conf.dlConf.bLanguagePacks) { for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j) { dlQueue.push(games[i].languagepacks[j]); } } if (conf.dlConf.bDLC && !games[i].dlcs.empty()) { for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) { if (conf.dlConf.bSaveSerials && !games[i].dlcs[j].serials.empty()) { std::string filepath = games[i].dlcs[j].getSerialsFilepath(); this->saveSerials(games[i].dlcs[j].serials, filepath); } if (conf.dlConf.bSaveChangelogs && !games[i].dlcs[j].changelog.empty()) { std::string filepath = games[i].dlcs[j].getChangelogFilepath(); this->saveChangelog(games[i].dlcs[j].changelog, filepath); } if (conf.dlConf.bInstallers) { for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) { dlQueue.push(games[i].dlcs[j].installers[k]); } } if (conf.dlConf.bPatches) { for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k) { dlQueue.push(games[i].dlcs[j].patches[k]); } } if (conf.dlConf.bExtras) { for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k) { dlQueue.push(games[i].dlcs[j].extras[k]); } } if (conf.dlConf.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(Globals::globalConfig.iThreads, static_cast(dlQueue.size())); // Create download threads std::vector 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, Globals::globalConfig, 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 = Globals::globalConfig.sXMLDirectory + "/" + gf.gamename; Util::createXML(gf.getFilepath(), Globals::globalConfig.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 = Globals::globalConfig.sXMLDirectory + "/" + gamename; else xml_directory = Globals::globalConfig.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 || Globals::globalConfig.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); curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 1L); 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 (Globals::globalConfig.bReport) { std::string status = static_cast(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 < Globals::globalConfig.iRetries) ) { this->retries++; std::cerr << std::endl << "Retry " << this->retries << "/" << Globals::globalConfig.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 // Set timestamp for downloaded file to same value as file on server long filetime = -1; CURLcode result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime); if (result == CURLE_OK && filetime >= 0) { std::time_t timestamp = (std::time_t)filetime; boost::filesystem::last_write_time(filepath, timestamp); } } curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L); return res; } // 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 chunk_from, chunk_to; std::vector 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 = Globals::globalConfig.sXMLDirectory + "/" + gamename; else xml_directory = Globals::globalConfig.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 (Globals::globalConfig.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 (Globals::globalConfig.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 (Globals::globalConfig.dlConf.bAutomaticXMLCreation && !bLocalXMLExists) { std::cout << "Starting automatic XML creation" << std::endl; Util::createXML(filepath, Globals::globalConfig.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 (Globals::globalConfig.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 (Globals::globalConfig.dlConf.bAutomaticXMLCreation && bParsingFailed) { std::cout << "Starting automatic XML creation" << std::endl; Util::createXML(filepath, Globals::globalConfig.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 (Globals::globalConfig.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, Globals::globalConfig.iChunkSize, xml_directory); } res = 1; } else { res = 0; } } } return res; } // Check all chunks int iChunksRepaired = 0; int iChunkRetryCount = 0; int iChunkRetryLimit = 3; bool bChunkRetryLimitReached = false; for (int i=0; ibeginDownload(); //begin chunk download std::cout << std::endl; if (Globals::globalConfig.bReport) iChunksRepaired++; i--; //verify downloaded chunk iChunkRetryCount++; if (iChunkRetryCount >= iChunkRetryLimit) { bChunkRetryLimitReached = true; } } else { std::cout << "OK\r" << std::flush; iChunkRetryCount = 0; // reset retry count bChunkRetryLimitReached = false; } free(chunk); res = 1; } std::cout << std::endl; fclose(outfile); if (Globals::globalConfig.bReport) { std::string report_line; if (bChunkRetryLimitReached) report_line = "Repair failed: " + filepath; else report_line = "Repaired [" + std::to_string(iChunksRepaired) + "/" + std::to_string(chunks) + "] " + filepath; this->report_ofs << report_line << std::endl; } if (bChunkRetryLimitReached) return res; // Set timestamp for downloaded file to same value as file on server long filetime = -1; CURLcode result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime); if (result == CURLE_OK && filetime >= 0) { std::time_t timestamp = (std::time_t)filetime; boost::filesystem::last_write_time(filepath, timestamp); } curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L); return res; } // 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 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 (Globals::globalConfig.iWait > 0) usleep(Globals::globalConfig.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++ < Globals::globalConfig.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(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::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(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(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((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(dlnow) / static_cast(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(dlnow)/1024/1024, static_cast(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 Downloader::getExtrasFromJSON(const Json::Value& json, const std::string& gamename, const Config& config) { std::vector extras; // Create new API handle and set curl options for the API API* api = new API(config.apiConf.sToken, config.apiConf.sSecret); api->curlSetOpt(CURLOPT_VERBOSE, config.curlConf.bVerbose); api->curlSetOpt(CURLOPT_SSL_VERIFYPEER, config.curlConf.bVerifyPeer); api->curlSetOpt(CURLOPT_CONNECTTIMEOUT, config.curlConf.iTimeout); if (!config.curlConf.sCACertPath.empty()) api->curlSetOpt(CURLOPT_CAINFO, config.curlConf.sCACertPath.c_str()); if (!api->init()) { 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("") == std::string::npos) { boost::regex expression(""); std::string text = boost::regex_replace(cdkey, expression, "\n"); serials << text << std::endl; } else { htmlcxx::HTML::ParserDom parser; tree dom = parser.parseTree(cdkey); tree::iterator it = dom.begin(); tree::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::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 = "\n\n\n\n" + title + "\n\n" + changelog + "\n"; return changelog; } // Linear search. Good thing computers are fast and lists are small. static int isPresent(std::vector& 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 config = Globals::globalConfig; config.dlConf.bInstallers = true; config.dlConf.bExtras = true; config.dlConf.bPatches = true; config.dlConf.bLanguagePacks = true; if (this->games.empty()) this->getGameDetails(); std::vector 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 filepath_vector; try { std::vector paths; std::vector 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.dirConf.sDirectory + "/" + config.dirConf.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.dirConf.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 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 vGameFiles; for (unsigned int i = 0; i < games.size(); ++i) { std::vector 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 (!Globals::globalConfig.dlConf.bDLC && (type & GFTYPE_DLC)) continue; if (!Globals::globalConfig.dlConf.bInstallers && (type & GFTYPE_INSTALLER)) continue; if (!Globals::globalConfig.dlConf.bExtras && (type & GFTYPE_EXTRA)) continue; if (!Globals::globalConfig.dlConf.bPatches && (type & GFTYPE_PATCH)) continue; if (!Globals::globalConfig.dlConf.bLanguagePacks && (type & GFTYPE_LANGPACK)) continue; boost::filesystem::path filepath = vGameFiles[i].getFilepath(); if (Globals::globalConfig.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 = Globals::globalConfig.sXMLDirectory + "/" + gamename + "/" + path.filename().string() + ".xml"; else local_xml_file = Globals::globalConfig.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 = Globals::globalConfig.sXMLDirectory + "/" + gamename + "/" + path.filename().string() + ".xml"; else local_xml_file = Globals::globalConfig.sXMLDirectory + "/" + path.filename().string() + ".xml"; if (Globals::globalConfig.dlConf.bAutomaticXMLCreation && !boost::filesystem::exists(local_xml_file) && boost::filesystem::exists(path)) { std::string xml_directory = Globals::globalConfig.sXMLDirectory + "/" + gamename; Util::createXML(filepath, Globals::globalConfig.iChunkSize, xml_directory); } localHash = Util::getLocalFileHash(Globals::globalConfig.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 = Globals::globalConfig.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(Globals::globalConfig.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 = Globals::globalConfig.sCacheDirectory + "/gamedetails.json"; Json::Value json; json["gamedetails-cache-version"] = GlobalConstants::GAMEDETAILS_CACHE_VERSION; json["version-string"] = Globals::globalConfig.sVersionString; json["version-number"] = Globals::globalConfig.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 Downloader::getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level) { std::vector 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(Globals::globalConfig.sGameRegex); boost::match_results 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(); game.product_id = gameDetailsNode["product_id"].asString(); // Make a vector of valid node names to make things easier std::vector nodes; nodes.push_back("extras"); nodes.push_back("installers"); nodes.push_back("patches"); nodes.push_back("languagepacks"); nodes.push_back("dlcs"); gameSpecificConfig conf; conf.dlConf = Globals::globalConfig.dlConf; if (Util::getGameSpecificConfig(game.gamename, &conf) > 0) std::cerr << game.gamename << " - Language: " << conf.dlConf.iInstallerLanguage << ", Platform: " << conf.dlConf.iInstallerPlatform << ", DLC: " << (conf.dlConf.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(); fileDetails.galaxy_downlink_json_url = fileDetailsNode["galaxy_downlink_json_url"].asString(); if (nodeName != "extras" && !(fileDetails.platform & conf.dlConf.iInstallerPlatform)) continue; if (nodeName != "extras" && !(fileDetails.language & conf.dlConf.iInstallerLanguage)) continue; } if (nodeName == "extras" && conf.dlConf.bExtras) game.extras.push_back(fileDetails); else if (nodeName == "installers" && conf.dlConf.bInstallers) game.installers.push_back(fileDetails); else if (nodeName == "patches" && conf.dlConf.bPatches) game.patches.push_back(fileDetails); else if (nodeName == "languagepacks" && conf.dlConf.bLanguagePacks) game.languagepacks.push_back(fileDetails); else if (nodeName == "dlcs" && conf.dlConf.bDLC) { std::vector 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 Globals::globalConfig.dlConf.bExtras = true; Globals::globalConfig.dlConf.bInstallers = true; Globals::globalConfig.dlConf.bPatches = true; Globals::globalConfig.dlConf.bLanguagePacks = true; Globals::globalConfig.dlConf.bDLC = true; Globals::globalConfig.sGameRegex = ".*"; Globals::globalConfig.dlConf.iInstallerLanguage = Util::getOptionValue("all", GlobalConstants::LANGUAGES); Globals::globalConfig.dlConf.iInstallerPlatform = Util::getOptionValue("all", GlobalConstants::PLATFORMS); Globals::globalConfig.dlConf.vLanguagePriority.clear(); Globals::globalConfig.dlConf.vPlatformPriority.clear(); Globals::globalConfig.sIgnoreDLCCountRegex = ".*"; // Ignore DLC count for all games because GOG doesn't report DLC count correctly 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) { if (!gogAPI->isLoggedIn()) { std::cout << "API not logged in. This feature doesn't work without valid API login." << std::endl; std::cout << "Try to login with --login-api" << std::endl; exit(1); } int res = 1; size_t pos = fileid_string.find("/"); if (pos == std::string::npos) { 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(Globals::globalConfig.dirConf.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 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) + "]"; galaxyAPI* galaxy = new galaxyAPI(Globals::globalConfig.curlConf); if (!galaxy->init()) { if (!galaxy->refreshLogin()) { delete galaxy; msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix)); vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED); return; } } Json::Reader *jsonparser = new Json::Reader; CURL* dlhandle = curl_easy_init(); curl_easy_setopt(dlhandle, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(dlhandle, CURLOPT_USERAGENT, conf.curlConf.sUserAgent.c_str()); curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(dlhandle, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(dlhandle, CURLOPT_CONNECTTIMEOUT, conf.curlConf.iTimeout); curl_easy_setopt(dlhandle, CURLOPT_FAILONERROR, true); curl_easy_setopt(dlhandle, CURLOPT_SSL_VERIFYPEER, conf.curlConf.bVerifyPeer); curl_easy_setopt(dlhandle, CURLOPT_VERBOSE, conf.curlConf.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.curlConf.iDownloadRate); curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 1L); // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_TIME, 30); curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_LIMIT, 200); if (!conf.curlConf.sCACertPath.empty()) curl_easy_setopt(dlhandle, CURLOPT_CAINFO, conf.curlConf.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 // Refresh Galaxy login if token is expired if (galaxy->isTokenExpired()) { if (!galaxy->refreshLogin()) { msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix)); vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED); delete jsonparser; delete galaxy; return; } } // Get downlink JSON from Galaxy API Json::Value downlinkJson; std::string response = galaxy->getResponse(gf.galaxy_downlink_json_url); if (response.empty()) { msgQueue.push(Message("Found nothing in " + gf.galaxy_downlink_json_url + ", skipping file", MSGTYPE_WARNING, msg_prefix)); continue; } jsonparser->parse(response, downlinkJson); if (!downlinkJson.isMember("downlink")) { msgQueue.push(Message("Invalid JSON response, skipping file", MSGTYPE_WARNING, msg_prefix)); continue; } std::string xml; if (gf.type & (GFTYPE_INSTALLER | GFTYPE_PATCH) && conf.dlConf.bRemoteXML) { std::string xml_url; if (downlinkJson.isMember("checksum")) if (!downlinkJson["checksum"].empty()) xml_url = downlinkJson["checksum"].asString(); // Get XML data if (conf.dlConf.bRemoteXML && !xml_url.empty()) xml = galaxy->getResponse(xml_url); if (!xml.empty()) { 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)); } } } std::string url = downlinkJson["downlink"].asString(); 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)) { // Set timestamp for downloaded file to same value as file on server long filetime = -1; CURLcode res = curl_easy_getinfo(dlhandle, CURLINFO_FILETIME, &filetime); if (res == CURLE_OK && filetime >= 0) { std::time_t timestamp = (std::time_t)filetime; boost::filesystem::last_write_time(filepath, timestamp); } // Average download speed std::ostringstream dlrate_avg; std::string rate_unit; 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(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.dlConf.bAutomaticXMLCreation) { if (result == CURLE_OK) { if ((gf.type & GFTYPE_EXTRA) || (conf.dlConf.bRemoteXML && !bLocalXMLExists && xml.empty())) createXMLQueue.push(gf); } } } curl_easy_cleanup(dlhandle); delete jsonparser; delete galaxy; 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(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::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(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((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(Globals::globalConfig.bUnicode, Globals::globalConfig.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(Globals::globalConfig.iProgressInterval)); std::cout << "\033[J\r" << std::flush; // Clear screen from the current line down to the bottom of the screen // Print messages from message queue first Message msg; while (msgQueue.try_pop(msg)) { std::cout << msg.getFormattedString(Globals::globalConfig.bColor, true) << std::endl; if (Globals::globalConfig.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 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(progress_info.dlnow) / static_cast(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(progress_info.dlnow)/1024/1024, static_cast(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 (Globals::globalConfig.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) + "]"; galaxyAPI* galaxy = new galaxyAPI(Globals::globalConfig.curlConf); if (!galaxy->init()) { if (!galaxy->refreshLogin()) { delete galaxy; msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix)); vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED); return; } } // Create new GOG website handle Website* website = new Website(); if (!website->IsLoggedIn()) { delete galaxy; 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 DirectoryConfig dirConfDefault; dirConfDefault = config.dirConf; gameItem game_item; while (gameItemQueue.try_pop(game_item)) { gameDetails game; gameSpecificConfig conf; conf.dlConf = config.dlConf; conf.dirConf = dirConfDefault; conf.dlConf.bIgnoreDLCCount = false; 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.dlConf.bIgnoreDLCCount) ss << "\tIgnore DLC count" << std::endl; if (conf.dlConf.bDLC != config.dlConf.bDLC) ss << "\tDLC: " << (conf.dlConf.bDLC ? "true" : "false") << std::endl; if (conf.dlConf.iInstallerLanguage != config.dlConf.iInstallerLanguage) ss << "\tLanguage: " << Util::getOptionNameString(conf.dlConf.iInstallerLanguage, GlobalConstants::LANGUAGES) << std::endl; if (conf.dlConf.vLanguagePriority != config.dlConf.vLanguagePriority) { ss << "\tLanguage priority:" << std::endl; for (unsigned int j = 0; j < conf.dlConf.vLanguagePriority.size(); ++j) { ss << "\t " << j << ": " << Util::getOptionNameString(conf.dlConf.vLanguagePriority[j], GlobalConstants::LANGUAGES) << std::endl; } } if (conf.dlConf.iInstallerPlatform != config.dlConf.iInstallerPlatform) ss << "\tPlatform: " << Util::getOptionNameString(conf.dlConf.iInstallerPlatform, GlobalConstants::PLATFORMS) << std::endl; if (conf.dlConf.vPlatformPriority != config.dlConf.vPlatformPriority) { ss << "\tPlatform priority:" << std::endl; for (unsigned int j = 0; j < conf.dlConf.vPlatformPriority.size(); ++j) { ss << "\t " << j << ": " << Util::getOptionNameString(conf.dlConf.vPlatformPriority[j], GlobalConstants::PLATFORMS) << std::endl; } } } msgQueue.push(Message(ss.str(), MSGTYPE_INFO, msg_prefix)); } } // Refresh Galaxy login if token is expired if (galaxy->isTokenExpired()) { if (!galaxy->refreshLogin()) { msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix)); break; } } Json::Value product_info = galaxy->getProductInfo(game_item.id); game = galaxy->productInfoJsonToGameDetails(product_info, conf.dlConf); game.filterWithPriorities(conf); Json::Value gameDetailsJSON; if (!game_item.gamedetailsjson.empty()) gameDetailsJSON = game_item.gamedetailsjson; if (conf.dlConf.bSaveSerials && game.serials.empty()) { if (gameDetailsJSON.empty()) gameDetailsJSON = website->getGameDetailsJSON(game_item.id); game.serials = Downloader::getSerialsFromJSON(gameDetailsJSON); } if (conf.dlConf.bSaveChangelogs && game.changelog.empty()) { if (gameDetailsJSON.empty()) gameDetailsJSON = website->getGameDetailsJSON(game_item.id); game.changelog = Downloader::getChangelogFromJSON(gameDetailsJSON); } game.makeFilepaths(conf.dirConf); 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 } } } } vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED); delete galaxy; delete website; return; } void Downloader::saveGalaxyJSON() { if (!Globals::galaxyConf.getJSON().empty()) { std::ofstream ofs(Globals::galaxyConf.getFilepath()); if (!ofs) { std::cerr << "Failed to write " << Globals::galaxyConf.getFilepath() << std::endl; } else { Json::StyledStreamWriter jsonwriter; jsonwriter.write(ofs, Globals::galaxyConf.getJSON()); ofs.close(); } if (!Globals::globalConfig.bRespectUmask) Util::setFilePermissions(Globals::galaxyConf.getFilepath(), boost::filesystem::owner_read | boost::filesystem::owner_write); } } void Downloader::galaxyInstallGame(const std::string& product_id, int build_index, const unsigned int& iGalaxyArch) { if (build_index < 0) build_index = 0; std::string sPlatform; unsigned int iPlatform = Globals::globalConfig.dlConf.iGalaxyPlatform; if (iPlatform == GlobalConstants::PLATFORM_LINUX) sPlatform = "linux"; else if (iPlatform == GlobalConstants::PLATFORM_MAC) sPlatform = "osx"; else sPlatform = "windows"; std::string sLanguage = "en"; unsigned int iLanguage = Globals::globalConfig.dlConf.iGalaxyLanguage; for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i) { if (GlobalConstants::LANGUAGES[i].id == iLanguage) { sLanguage = GlobalConstants::LANGUAGES[i].code; break; } } std::string sGalaxyArch = "64"; for (unsigned int i = 0; i < GlobalConstants::GALAXY_ARCHS.size(); ++i) { if (GlobalConstants::GALAXY_ARCHS[i].id == iGalaxyArch) { sGalaxyArch = GlobalConstants::GALAXY_ARCHS[i].code; break; } } Json::Value json = gogGalaxy->getProductBuilds(product_id, sPlatform); // JSON is empty and platform is Linux. Most likely cause is that Galaxy API doesn't have Linux support if (json.empty() && iPlatform == GlobalConstants::PLATFORM_LINUX) { std::cout << "Galaxy API doesn't have Linux support" << std::endl; return; } if (json["items"][build_index]["generation"].asInt() != 2) { std::cout << "Only generation 2 builds are supported currently" << std::endl; return; } std::string link = json["items"][build_index]["link"].asString(); std::string buildHash; buildHash.assign(link.begin()+link.find_last_of("/")+1, link.end()); json = gogGalaxy->getManifestV2(buildHash); std::string game_title = json["products"][0]["name"].asString(); std::string install_directory = json["installDirectory"].asString(); if (install_directory.empty()) install_directory = product_id; std::string install_path = Globals::globalConfig.dirConf.sDirectory + install_directory; std::vector items; for (unsigned int i = 0; i < json["depots"].size(); ++i) { bool bSelectedLanguage = false; bool bSelectedArch = false; for (unsigned int j = 0; j < json["depots"][i]["languages"].size(); ++j) { std::string language = json["depots"][i]["languages"][j].asString(); if (language == "*" || language == sLanguage) bSelectedLanguage = true; } if (json["depots"][i].isMember("osBitness")) { for (unsigned int j = 0; j < json["depots"][i]["osBitness"].size(); ++j) { std::string osBitness = json["depots"][i]["osBitness"][j].asString(); if (osBitness == "*" || osBitness == sGalaxyArch) bSelectedArch = true; } } else { // No osBitness found, assume that we want to download this depot bSelectedArch = true; } if (!bSelectedLanguage || !bSelectedArch) continue; std::string depotHash = json["depots"][i]["manifest"].asString(); std::string depot_product_id = json["depots"][i]["productId"].asString(); if (depot_product_id.empty()) depot_product_id = product_id; std::vector vec = gogGalaxy->getDepotItemsVector(depotHash); // Set product id for items for (auto it = vec.begin(); it != vec.end(); ++it) it->product_id = depot_product_id; items.insert(std::end(items), std::begin(vec), std::end(vec)); } uintmax_t totalSize = 0; for (unsigned int i = 0; i < items.size(); ++i) { if (Globals::globalConfig.bVerbose) { std::cout << items[i].path << std::endl; std::cout << "\tChunks: " << items[i].chunks.size() << std::endl; std::cout << "\tmd5: " << items[i].md5 << std::endl; } totalSize += items[i].totalSizeUncompressed; } double totalSizeMB = static_cast(totalSize)/1024/1024; std::cout << game_title << std::endl; std::cout << "Files: " << items.size() << std::endl; std::cout << "Total size installed: " << totalSizeMB << " MB" << std::endl; for (unsigned int i = 0; i < items.size(); ++i) { boost::filesystem::path path = install_path + "/" + items[i].path; // Check that directory exists and create it boost::filesystem::path directory = path.parent_path(); if (boost::filesystem::exists(directory)) { if (!boost::filesystem::is_directory(directory)) { std::cerr << directory << " is not directory" << std::endl; return; } } else { if (!boost::filesystem::create_directories(directory)) { std::cerr << "Failed to create directory: " << directory << std::endl; return; } } unsigned int start_chunk = 0; if (boost::filesystem::exists(path)) { std::cout << "File already exists: " << path.string() << std::endl; unsigned int resume_chunk = 0; uintmax_t filesize = boost::filesystem::file_size(path); if (filesize == items[i].totalSizeUncompressed) { // File is same size if (Util::getFileHash(path.string(), RHASH_MD5) == items[i].md5) { std::cout << "\tOK" << std::endl; continue; } else { std::cout << "\tMD5 mismatch" << std::endl; if (!boost::filesystem::remove(path)) { std::cerr << "\tFailed to delete " << path << std::endl; continue; } } } else if (filesize > items[i].totalSizeUncompressed) { // File is bigger than on server, delete old file and start from beginning std::cout << "\tFile is bigger than expected. Deleting old file and starting from beginning." << std::endl; if (!boost::filesystem::remove(path)) { std::cerr << "\tFailed to delete " << path << std::endl; continue; } } else { // File is smaller than on server, resume for (unsigned int j = 0; j < items[i].chunks.size(); ++j) { if (items[i].chunks[j].offset_uncompressed == filesize) { resume_chunk = j; break; } } if (resume_chunk > 0) { std::cout << "\tResume from chunk " << resume_chunk << std::endl; // Get chunk hash for previous chunk FILE* f = fopen(path.string().c_str(), "r"); if (!f) { std::cerr << "\tFailed to open: " << path << std::endl; continue; } unsigned int previous_chunk = resume_chunk - 1; uintmax_t chunk_size = items[i].chunks[previous_chunk].size_uncompressed; // use fseeko to support large files on 32 bit platforms fseeko(f, items[i].chunks[previous_chunk].offset_uncompressed, SEEK_SET); unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *)); if (chunk == NULL) { std::cerr << "Memory error" << std::endl; fclose(f); continue; } uintmax_t fread_size = fread(chunk, 1, chunk_size, f); fclose(f); if (fread_size != chunk_size) { std::cerr << "Read error" << std::endl; free(chunk); continue; } std::string chunk_hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5); free(chunk); if (chunk_hash == items[i].chunks[previous_chunk].md5_uncompressed) { // Hash for previous chunk matches, resume at this position start_chunk = resume_chunk; } else { // Hash for previous chunk is different, delete old file and start from beginning std::cout << "\tChunk hash is different. Deleting old file and starting from beginning." << std::endl; if (!boost::filesystem::remove(path)) { std::cerr << "\tFailed to delete " << path << std::endl; continue; } } } else { std::cout << "\tFailed to find valid resume position. Deleting old file and starting from beginning." << std::endl; if (!boost::filesystem::remove(path)) { std::cerr << "\tFailed to delete " << path << std::endl; continue; } } } } std::time_t timestamp = -1; for (unsigned int j = start_chunk; j < items[i].chunks.size(); ++j) { ChunkMemoryStruct chunk; chunk.memory = (char *) malloc(1); chunk.size = 0; // Refresh Galaxy login if token is expired if (gogGalaxy->isTokenExpired()) { if (!gogGalaxy->refreshLogin()) { std::cerr << "Galaxy API failed to refresh login" << std::endl; free(chunk.memory); return; } } json = gogGalaxy->getSecureLink(items[i].product_id, gogGalaxy->hashToGalaxyPath(items[i].chunks[j].md5_compressed)); // Prefer edgecast urls bool bPreferEdgecast = true; unsigned int idx = 0; for (unsigned int k = 0; k < json["urls"].size(); ++k) { std::string endpoint_name = json["urls"][k]["endpoint_name"].asString(); if (bPreferEdgecast) { if (endpoint_name == "edgecast") { idx = k; break; } } } // Build url according to url_format std::string link_base_url = json["urls"][idx]["parameters"]["base_url"].asString(); std::string link_path = json["urls"][idx]["parameters"]["path"].asString(); std::string link_token = json["urls"][idx]["parameters"]["token"].asString(); std::string url = json["urls"][idx]["url_format"].asString(); while(Util::replaceString(url, "{base_url}", link_base_url)); while(Util::replaceString(url, "{path}", link_path)); while(Util::replaceString(url, "{token}", link_token)); curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, WriteChunkMemoryCallback); curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &chunk); curl_easy_setopt(curlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallback); curl_easy_setopt(curlhandle, CURLOPT_XFERINFODATA, this); curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 1L); std::cout << path.string() << " (chunk " << (j + 1) << "/" << items[i].chunks.size() << ")" << std::endl; if (Globals::globalConfig.iWait > 0) usleep(Globals::globalConfig.iWait); // Delay the request by specified time this->TimeAndSize.clear(); this->timer.reset(); CURLcode result = curl_easy_perform(curlhandle); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L); if (result != CURLE_OK) { std::cout << "\033[K" << 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 { // Get timestamp for downloaded file long filetime = -1; result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime); if (result == CURLE_OK && filetime >= 0) timestamp = (std::time_t)filetime; } std::cout << std::endl; std::ofstream ofs(path.string(), std::ofstream::out | std::ofstream::binary | std::ofstream::app); if (ofs) { boost::iostreams::filtering_streambuf output; output.push(boost::iostreams::zlib_decompressor(GlobalConstants::ZLIB_WINDOW_SIZE)); output.push(ofs); boost::iostreams::write(output, chunk.memory, chunk.size); } if (ofs) ofs.close(); free(chunk.memory); } // Set timestamp for downloaded file to same value as file on server if (boost::filesystem::exists(path) && timestamp >= 0) boost::filesystem::last_write_time(path, timestamp); } std::cout << "Checking for orphaned files" << std::endl; std::vector orphans = this->galaxyGetOrphanedFiles(items, install_path); std::cout << "\t" << orphans.size() << " orphaned files" << std::endl; for (unsigned int i = 0; i < orphans.size(); ++i) std::cout << "\t" << orphans[i] << std::endl; } void Downloader::galaxyShowBuilds(const std::string& product_id, int build_index) { std::string sPlatform; unsigned int iPlatform = Globals::globalConfig.dlConf.iGalaxyPlatform; if (iPlatform == GlobalConstants::PLATFORM_LINUX) sPlatform = "linux"; else if (iPlatform == GlobalConstants::PLATFORM_MAC) sPlatform = "osx"; else sPlatform = "windows"; Json::Value json = gogGalaxy->getProductBuilds(product_id, sPlatform); // JSON is empty and platform is Linux. Most likely cause is that Galaxy API doesn't have Linux support if (json.empty() && iPlatform == GlobalConstants::PLATFORM_LINUX) { std::cout << "Galaxy API doesn't have Linux support" << std::endl; return; } if (build_index < 0) { for (unsigned int i = 0; i < json["items"].size(); ++i) { std::cout << i << ": " << "Version " << json["items"][i]["version_name"].asString() << " - " << json["items"][i]["date_published"].asString() << std::endl; } return; } if (json["items"][build_index]["generation"].asInt() != 2) { std::cout << "Only generation 2 builds are supported currently" << std::endl; return; } std::string link = json["items"][build_index]["link"].asString(); std::string buildHash; buildHash.assign(link.begin()+link.find_last_of("/")+1, link.end()); json = gogGalaxy->getManifestV2(buildHash); std::cout << json << std::endl; } std::vector Downloader::galaxyGetOrphanedFiles(const std::vector& items, const std::string& install_path) { std::vector orphans; std::vector item_paths; for (unsigned int i = 0; i < items.size(); ++i) item_paths.push_back(install_path + "/" + items[i].path); std::vector filepath_vector; try { std::size_t pathlen = Globals::globalConfig.dirConf.sDirectory.length(); if (boost::filesystem::exists(install_path)) { if (boost::filesystem::is_directory(install_path)) { // Recursively iterate over files in directory boost::filesystem::recursive_directory_iterator end_iter; boost::filesystem::recursive_directory_iterator dir_iter(install_path); while (dir_iter != end_iter) { if (boost::filesystem::is_regular_file(dir_iter->status())) { std::string filepath = dir_iter->path().string(); if (Globals::globalConfig.ignorelist.isBlacklisted(filepath.substr(pathlen))) { if (Globals::globalConfig.bVerbose) std::cerr << "skipped ignorelisted file " << filepath << std::endl; } else { filepath_vector.push_back(dir_iter->path()); } } dir_iter++; } } } else std::cerr << install_path << " does not exist" << std::endl; } catch (const boost::filesystem::filesystem_error& ex) { std::cout << ex.what() << std::endl; } std::sort(item_paths.begin(), item_paths.end()); std::sort(filepath_vector.begin(), filepath_vector.end()); if (!filepath_vector.empty()) { for (unsigned int i = 0; i < filepath_vector.size(); ++i) { bool bFileIsOrphaned = true; for (std::vector::iterator it = item_paths.begin(); it != item_paths.end(); it++) { boost::filesystem::path item_path = *it; boost::filesystem::path file_path = filepath_vector[i].native(); if (item_path == file_path) { bFileIsOrphaned = false; item_paths.erase(it); break; } } if (bFileIsOrphaned) orphans.push_back(filepath_vector[i].string()); } } return orphans; } lgogdownloader-3.3/src/galaxyapi.cpp000066400000000000000000000417671317707572500176750ustar00rootroot00000000000000/* 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 "galaxyapi.h" #include #include #include #include GalaxyConfig Globals::galaxyConf; size_t galaxyAPI::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; } galaxyAPI::galaxyAPI(CurlConfig& conf) { this->curlConf = conf; 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); curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, curlConf.iTimeout); curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true); curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, curlConf.sCookiePath.c_str()); curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, curlConf.sCookiePath.c_str()); curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, curlConf.bVerifyPeer); curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, curlConf.bVerbose); curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, curlConf.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 (!curlConf.sCACertPath.empty()) curl_easy_setopt(curlhandle, CURLOPT_CAINFO, curlConf.sCACertPath.c_str()); } galaxyAPI::~galaxyAPI() { curl_easy_cleanup(curlhandle); } /* Initialize the API returns 0 if failed returns 1 if successful */ int galaxyAPI::init() { int res = 0; if (!this->isTokenExpired()) { res = 1; } else res = 0; return res; } bool galaxyAPI::refreshLogin() { bool res = false; std::string refresh_url = "https://auth.gog.com/token?client_id=" + Globals::galaxyConf.getClientId() + "&client_secret=" + Globals::galaxyConf.getClientSecret() + "&grant_type=refresh_token" + "&refresh_token=" + Globals::galaxyConf.getRefreshToken(); std::string json = this->getResponse(refresh_url); if (!json.empty()) { Json::Value token_json; Json::Reader *jsonparser = new Json::Reader; if (jsonparser->parse(json, token_json)) { Globals::galaxyConf.setJSON(token_json); res = true; } delete jsonparser; } return res; } bool galaxyAPI::isTokenExpired() { bool res = false; if (Globals::galaxyConf.isExpired()) res = true; return res; } std::string galaxyAPI::getResponse(const std::string& url, const bool& zlib_decompress) { std::ostringstream memory; struct curl_slist *header = NULL; std::string access_token; if (!Globals::galaxyConf.isExpired()) access_token = Globals::galaxyConf.getAccessToken(); if (!access_token.empty()) { std::string bearer = "Authorization: Bearer " + access_token; header = curl_slist_append(header, bearer.c_str()); } curl_easy_setopt(curlhandle, CURLOPT_HTTPHEADER, header); curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, galaxyAPI::writeMemoryCallback); curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); curl_easy_perform(curlhandle); std::string response = memory.str(); memory.str(std::string()); curl_easy_setopt(curlhandle, CURLOPT_HTTPHEADER, NULL); curl_slist_free_all(header); if (zlib_decompress) { std::string response_decompressed; boost::iostreams::filtering_streambuf in; in.push(boost::iostreams::zlib_decompressor(GlobalConstants::ZLIB_WINDOW_SIZE)); in.push(boost::make_iterator_range(response)); boost::iostreams::copy(in, boost::iostreams::back_inserter(response_decompressed)); response = response_decompressed; } return response; } Json::Value galaxyAPI::getProductBuilds(const std::string& product_id, const std::string& platform, const std::string& generation) { Json::Value json; std::string url = "https://content-system.gog.com/products/" + product_id + "/os/" + platform + "/builds?generation=" + generation; std::string response = this->getResponse(url); Json::Reader *jsonparser = new Json::Reader; jsonparser->parse(response, json); delete jsonparser; return json; } Json::Value galaxyAPI::getManifestV1(const std::string& product_id, const std::string& build_id, const std::string& manifest_id, const std::string& platform) { Json::Value json; std::string url = "https://cdn.gog.com/content-system/v1/manifests/" + product_id + "/" + platform + "/" + build_id + "/" + manifest_id + ".json"; std::string response = this->getResponse(url); Json::Reader *jsonparser = new Json::Reader; jsonparser->parse(response, json); delete jsonparser; return json; } Json::Value galaxyAPI::getManifestV2(std::string manifest_hash) { Json::Value json; if (!manifest_hash.empty() && manifest_hash.find("/") == std::string::npos) manifest_hash = this->hashToGalaxyPath(manifest_hash); std::string url = "https://cdn.gog.com/content-system/v2/meta/" + manifest_hash; std::string response = this->getResponse(url, true); Json::Reader *jsonparser = new Json::Reader; jsonparser->parse(response, json); delete jsonparser; return json; } Json::Value galaxyAPI::getSecureLink(const std::string& product_id, const std::string& path) { Json::Value json; std::string url = "https://content-system.gog.com/products/" + product_id + "/secure_link?generation=2&path=" + path + "&_version=2"; std::string response = this->getResponse(url); Json::Reader *jsonparser = new Json::Reader; jsonparser->parse(response, json); delete jsonparser; return json; } std::string galaxyAPI::hashToGalaxyPath(const std::string& hash) { std::string galaxy_path = hash; if (galaxy_path.find("/") == std::string::npos) galaxy_path.assign(hash.begin(), hash.begin()+2).append("/").append(hash.begin()+2, hash.begin()+4).append("/").append(hash); return galaxy_path; } std::vector galaxyAPI::getDepotItemsVector(const std::string& hash) { Json::Value json = this->getManifestV2(hash); std::vector items; for (unsigned int i = 0; i < json["depot"]["items"].size(); ++i) { if (!json["depot"]["items"][i]["chunks"].empty()) { galaxyDepotItem item; item.totalSizeCompressed = 0; item.totalSizeUncompressed = 0; item.path = json["depot"]["items"][i]["path"].asString(); while (Util::replaceString(item.path, "\\", "/")); for (unsigned int j = 0; j < json["depot"]["items"][i]["chunks"].size(); ++j) { galaxyDepotItemChunk chunk; chunk.md5_compressed = json["depot"]["items"][i]["chunks"][j]["compressedMd5"].asString(); chunk.md5_uncompressed = json["depot"]["items"][i]["chunks"][j]["md5"].asString(); chunk.size_compressed = json["depot"]["items"][i]["chunks"][j]["compressedSize"].asLargestUInt(); chunk.size_uncompressed = json["depot"]["items"][i]["chunks"][j]["size"].asLargestUInt(); chunk.offset_compressed = item.totalSizeCompressed; chunk.offset_uncompressed = item.totalSizeUncompressed; item.totalSizeCompressed += chunk.size_compressed; item.totalSizeUncompressed += chunk.size_uncompressed; item.chunks.push_back(chunk); } if (json["depot"]["items"][i].isMember("md5")) item.md5 = json["depot"]["items"][i]["md5"].asString(); else if (json["depot"]["items"][i]["chunks"].size() == 1) item.md5 = json["depot"]["items"][i]["chunks"][0]["md5"].asString(); items.push_back(item); } } return items; } Json::Value galaxyAPI::getProductInfo(const std::string& product_id) { Json::Value json; std::string url = "https://api.gog.com/products/" + product_id + "?expand=downloads,expanded_dlcs,description,screenshots,videos,related_products,changelog&locale=en-US"; std::string response = this->getResponse(url); Json::Reader *jsonparser = new Json::Reader; jsonparser->parse(response, json); delete jsonparser; return json; } gameDetails galaxyAPI::productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf) { gameDetails gamedetails; gamedetails.gamename = json["slug"].asString(); gamedetails.product_id = json["id"].asString(); gamedetails.title = json["title"].asString(); gamedetails.icon = "https:" + json["images"]["icon"].asString(); if (json.isMember("changelog")) gamedetails.changelog = json["changelog"].asString(); if (dlConf.bInstallers) { gamedetails.installers = this->installerJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["installers"], dlConf); } if (dlConf.bExtras) { gamedetails.extras = this->extraJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["bonus_content"]); } if (dlConf.bPatches) { gamedetails.patches = this->patchJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["patches"], dlConf); } if (dlConf.bLanguagePacks) { gamedetails.languagepacks = this->languagepackJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["language_packs"], dlConf); } if (dlConf.bDLC) { if (json.isMember("expanded_dlcs")) { for (unsigned int i = 0; i < json["expanded_dlcs"].size(); ++i) { gameDetails dlc_gamedetails = this->productInfoJsonToGameDetails(json["expanded_dlcs"][i], dlConf); // Add DLC type to all DLC files for (unsigned int j = 0; j < dlc_gamedetails.installers.size(); ++j) dlc_gamedetails.installers[j].type |= GFTYPE_DLC; for (unsigned int j = 0; j < dlc_gamedetails.extras.size(); ++j) dlc_gamedetails.extras[j].type |= GFTYPE_DLC; for (unsigned int j = 0; j < dlc_gamedetails.patches.size(); ++j) dlc_gamedetails.patches[j].type |= GFTYPE_DLC; for (unsigned int j = 0; j < dlc_gamedetails.languagepacks.size(); ++j) dlc_gamedetails.languagepacks[j].type |= GFTYPE_DLC; // Add DLC only if it has any files if (!dlc_gamedetails.installers.empty() || !dlc_gamedetails.extras.empty() || !dlc_gamedetails.patches.empty() || !dlc_gamedetails.languagepacks.empty()) gamedetails.dlcs.push_back(dlc_gamedetails); } } } return gamedetails; } std::vector galaxyAPI::installerJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf) { return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_INSTALLER, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler); } std::vector galaxyAPI::patchJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf) { return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_PATCH, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler); } std::vector galaxyAPI::languagepackJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf) { return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_LANGPACK, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler); } std::vector galaxyAPI::extraJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json) { return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_EXTRA); } std::vector galaxyAPI::fileJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const unsigned int& type, const unsigned int& platform, const unsigned int& lang, const bool& useDuplicateHandler) { std::vector gamefiles; unsigned int iInfoNodes = json.size(); for (unsigned int i = 0; i < iInfoNodes; ++i) { Json::Value infoNode = json[i]; unsigned int iFiles = infoNode["files"].size(); std::string name = infoNode["name"].asString(); unsigned int iPlatform = GlobalConstants::PLATFORM_WINDOWS; unsigned int iLanguage = GlobalConstants::LANGUAGE_EN; if (!(type & GFTYPE_EXTRA)) { iPlatform = Util::getOptionValue(infoNode["os"].asString(), GlobalConstants::PLATFORMS); iLanguage = Util::getOptionValue(infoNode["language"].asString(), GlobalConstants::LANGUAGES); if (!(iPlatform & platform)) continue; if (!(iLanguage & lang)) continue; } for (unsigned int j = 0; j < iFiles; ++j) { Json::Value fileNode = infoNode["files"][j]; std::string downlink = fileNode["downlink"].asString(); std::string downlinkResponse = this->getResponse(downlink); if (downlinkResponse.empty()) continue; Json::Value downlinkJson; Json::Reader *jsonparser = new Json::Reader; jsonparser->parse(downlinkResponse, downlinkJson); delete jsonparser; std::string downlink_url = downlinkJson["downlink"].asString(); std::string downlink_url_unescaped = (std::string)curl_easy_unescape(curlhandle, downlink_url.c_str(), downlink_url.size(), NULL); std::string path; // GOG has changed the url formatting few times between 2 different formats. // Try to get proper file name in both cases. size_t filename_end_pos; if (downlink_url_unescaped.find("?path=") != std::string::npos) filename_end_pos = downlink_url_unescaped.find_first_of("&"); else filename_end_pos = downlink_url_unescaped.find_first_of("?"); if (downlink_url_unescaped.find("/" + gamename + "/") != std::string::npos) { path.assign(downlink_url_unescaped.begin()+downlink_url_unescaped.find("/" + gamename + "/"), downlink_url_unescaped.begin()+filename_end_pos); } else { path.assign(downlink_url_unescaped.begin()+downlink_url_unescaped.find_last_of("/")+1, downlink_url_unescaped.begin()+filename_end_pos); path = "/" + gamename + "/" + path; } // Workaround for filename issue caused by different (currently unknown) url formatting scheme // https://github.com/Sude-/lgogdownloader/issues/126 if (path.find("?") != std::string::npos) { if (path.find_last_of("?") > path.find_last_of("/")) { path.assign(path.begin(), path.begin()+path.find_last_of("?")); } } gameFile gf; gf.gamename = gamename; gf.type = type; gf.id = fileNode["id"].asString(); gf.name = name; gf.path = path; gf.size = Util::getJsonUIntValueAsString(fileNode["size"]); gf.updated = 0; // assume not updated gf.galaxy_downlink_json_url = downlink; if (!(type & GFTYPE_EXTRA)) { gf.platform = iPlatform; gf.language = iLanguage; if (useDuplicateHandler) { bool bDuplicate = false; for (unsigned int k = 0; k < gamefiles.size(); ++k) { if (gamefiles[k].path == gf.path) { gamefiles[k].language |= gf.language; // Add language code to installer bDuplicate = true; break; } } if (bDuplicate) continue; } } gamefiles.push_back(gf); } } return gamefiles; } lgogdownloader-3.3/src/gamedetails.cpp000066400000000000000000000207461317707572500201670ustar00rootroot00000000000000/* 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.dlConf.vPlatformPriority.empty() && config.dlConf.vLanguagePriority.empty()) return; filterListWithPriorities(installers, config); filterListWithPriorities(patches, config); filterListWithPriorities(languagepacks, config); for (unsigned int i = 0; i < dlcs.size(); ++i) { filterListWithPriorities(dlcs[i].installers, config); filterListWithPriorities(dlcs[i].patches, config); filterListWithPriorities(dlcs[i].languagepacks, config); } } void gameDetails::filterListWithPriorities(std::vector& 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::iterator fileDetails = list.begin(); fileDetails != list.end(); fileDetails++) { fileDetails->score = 0; if (!config.dlConf.vPlatformPriority.empty()) { for (size_t i = 0; i != config.dlConf.vPlatformPriority.size(); i++) if (fileDetails->platform & config.dlConf.vPlatformPriority[i]) { fileDetails->score += i; break; } } if (!config.dlConf.vLanguagePriority.empty()) { for (size_t i = 0; i != config.dlConf.vLanguagePriority.size(); i++) if (fileDetails->language & config.dlConf.vLanguagePriority[i]) { fileDetails->score += i; break; } } if ((fileDetails->score < bestscore) or (bestscore < 0)) bestscore = fileDetails->score; } for (std::vector::iterator fileDetails = list.begin(); fileDetails != list.end(); ) { if (fileDetails->score > bestscore) fileDetails = list.erase(fileDetails); else fileDetails++; } } void gameDetails::makeFilepaths(const DirectoryConfig& 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["product_id"] = this->product_id; 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 gameDetails::getGameFileVector() { std::vector 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 vGameFilesDLC = dlcs[i].getGameFileVector(); vGameFiles.insert(std::end(vGameFiles), std::begin(vGameFilesDLC), std::end(vGameFilesDLC)); } } return vGameFiles; } lgogdownloader-3.3/src/gamefile.cpp000066400000000000000000000022531317707572500174520ustar00rootroot00000000000000/* 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; json["galaxy_downlink_json_url"] = this->galaxy_downlink_json_url; return json; } lgogdownloader-3.3/src/progressbar.cpp000066400000000000000000000060701317707572500202330ustar00rootroot00000000000000/* 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 #include 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(); } lgogdownloader-3.3/src/util.cpp000066400000000000000000000455041317707572500166640ustar00rootroot00000000000000/* 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 #include #include #include #include #include /* 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->dlConf.iInstallerLanguage = root["language"].asUInt(); else { Util::parseOptionString(root["language"].asString(), conf->dlConf.vLanguagePriority, conf->dlConf.iInstallerLanguage, GlobalConstants::LANGUAGES); } res++; } if (root.isMember("platform")) { if (root["platform"].isInt()) conf->dlConf.iInstallerPlatform = root["platform"].asUInt(); else { Util::parseOptionString(root["platform"].asString(), conf->dlConf.vPlatformPriority, conf->dlConf.iInstallerPlatform, GlobalConstants::PLATFORMS); } res++; } if (root.isMember("dlc")) { conf->dlConf.bDLC = root["dlc"].asBool(); res++; } if (root.isMember("ignore-dlc-count")) { conf->dlConf.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(w.ws_col); } else width = 10000;//Something sufficiently big return width; } void Util::getDownloaderUrlsFromJSON(const Json::Value &root, std::vector &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 Util::getDLCNamesFromJSON(const Json::Value &root) { std::vector 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 Util::tokenize(const std::string& str, const std::string& separator) { std::vector 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& options, const bool& bAllowStringToIntConversion) { unsigned int value = 0; boost::regex expression("^[+-]?\\d+$", boost::regex::perl); boost::match_results what; if (str == "all") { value = (1 << options.size()) - 1; } else if (boost::regex_search(str, what, expression) && bAllowStringToIntConversion) { 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& 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 &priority, unsigned int &type, const std::vector& options) { type = 0; priority.clear(); std::vector tokens_priority = Util::tokenize(option_string, ","); for (std::vector::iterator it_priority = tokens_priority.begin(); it_priority != tokens_priority.end(); it_priority++) { unsigned int value = 0; std::vector tokens_value = Util::tokenize(*it_priority, "+"); for (std::vector::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(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, "..."); } } std::string Util::getJsonUIntValueAsString(const Json::Value& json_value) { std::string value; try { value = json_value.asString(); } catch (...) { try { uintmax_t value_uint = json_value.asLargestUInt(); value = std::to_string(value_uint); } catch (...) { value = ""; } } return value; } lgogdownloader-3.3/src/website.cpp000066400000000000000000000752771317707572500173630ustar00rootroot00000000000000/* 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 #include Website::Website() { this->retries = 0; curlhandle = curl_easy_init(); curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, Globals::globalConfig.curlConf.sUserAgent.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, Globals::globalConfig.curlConf.iTimeout); curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true); curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, Globals::globalConfig.curlConf.sCookiePath.c_str()); curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, Globals::globalConfig.curlConf.sCookiePath.c_str()); curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, Globals::globalConfig.curlConf.bVerifyPeer); curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, Globals::globalConfig.curlConf.bVerbose); curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, Globals::globalConfig.curlConf.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 (!Globals::globalConfig.curlConf.sCACertPath.empty()) curl_easy_setopt(curlhandle, CURLOPT_CAINFO, Globals::globalConfig.curlConf.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 (Globals::globalConfig.iWait > 0) usleep(Globals::globalConfig.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++ < Globals::globalConfig.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 Website::getGames() { std::vector 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(); if (product.isMember("updates")) { if (product["updates"].isNull()) { /* In some cases the value can be null. * For example when user owns a dlc but not the base game * https://github.com/Sude-/lgogdownloader/issues/101 * Assume that there are no updates in this case */ game.updates = 0; } else if (product["updates"].isInt()) game.updates = product["updates"].asInt(); else { try { game.updates = std::stoi(product["updates"].asString()); } catch (std::invalid_argument& e) { game.updates = 0; // Assume no updates } } } 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 (Globals::globalConfig.bPlatformDetection && !(platform & Globals::globalConfig.dlConf.iInstallerPlatform)) continue; // Filter the game list if (!Globals::globalConfig.sGameRegex.empty()) { // GameRegex filter aliases if (Globals::globalConfig.sGameRegex == "all") Globals::globalConfig.sGameRegex = ".*"; boost::regex expression(Globals::globalConfig.sGameRegex); boost::match_results what; if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex continue; } if (Globals::globalConfig.dlConf.bDLC) { int dlcCount = product["dlcCount"].asInt(); bool bDownloadDLCInfo = (dlcCount != 0); if (!bDownloadDLCInfo && !Globals::globalConfig.sIgnoreDLCCountRegex.empty()) { boost::regex expression(Globals::globalConfig.sIgnoreDLCCountRegex); boost::match_results what; if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex { bDownloadDLCInfo = true; } } if (!bDownloadDLCInfo && !Globals::globalConfig.gamehasdlc.empty()) { if (Globals::globalConfig.gamehasdlc.isBlacklisted(game.name)) bDownloadDLCInfo = true; } // Check game specific config if (!Globals::globalConfig.bUpdateCache) // Disable game specific config files for cache update { gameSpecificConfig conf; conf.dlConf.bIgnoreDLCCount = bDownloadDLCInfo; Util::getGameSpecificConfig(game.name, &conf); bDownloadDLCInfo = conf.dlConf.bIgnoreDLCCount; } if (bDownloadDLCInfo && !Globals::globalConfig.sGameRegex.empty()) { // don't download unnecessary info if user is only interested in a subset of his account boost::regex expression(Globals::globalConfig.sGameRegex); boost::match_results 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 Website::getFreeGames() { Json::Value root; Json::Reader *jsonparser = new Json::Reader; std::vector 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 = "login[username]"; std::string tagname_password = "login[password]"; std::string tagname_login = "login[login]"; std::string tagname_token; std::string auth_url = "https://auth.gog.com/auth?client_id=" + Globals::galaxyConf.getClientId() + "&redirect_uri=" + (std::string)curl_easy_escape(curlhandle, Globals::galaxyConf.getRedirectUri().c_str(), Globals::galaxyConf.getRedirectUri().size()) + "&response_type=code&layout=default&brand=gog"; std::string auth_code; 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 << "Try to login later" << std::endl; return res = 0; } htmlcxx::HTML::ParserDom parser; tree login_dom = parser.parseTree(login_form_html); tree::iterator login_it = login_dom.begin(); tree::iterator login_it_end = login_dom.end(); for (; login_it != login_it_end; ++login_it) { if (login_it->tagName()=="input") { login_it->parseAttributes(); if (login_it->attribute("id").second == "login__token") { token = login_it->attribute("value").second; // login token tagname_token = login_it->attribute("name").second; } } } 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; std::string tagname_two_step_send = "second_step_authentication[send]"; std::string tagname_two_step_auth_letter_1 = "second_step_authentication[token][letter_1]"; std::string tagname_two_step_auth_letter_2 = "second_step_authentication[token][letter_2]"; std::string tagname_two_step_auth_letter_3 = "second_step_authentication[token][letter_3]"; std::string tagname_two_step_auth_letter_4 = "second_step_authentication[token][letter_4]"; std::string tagname_two_step_token; std::string token_two_step; std::string two_step_html = this->getResponse(redirect_url); redirect_url = NULL; tree two_step_dom = parser.parseTree(two_step_html); tree::iterator two_step_it = two_step_dom.begin(); tree::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(); if (two_step_it->attribute("id").second == "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; } } } 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); } if (!std::string(redirect_url).empty()) { long response_code; do { curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); result = curl_easy_perform(curlhandle); memory.str(std::string()); result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); if ((response_code / 100) == 3) curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); std::string redir_url = std::string(redirect_url); boost::regex re(".*code=(.*?)([\?&].*|$)", boost::regex_constants::icase); boost::match_results what; if (boost::regex_search(redir_url, what, re)) { auth_code = what[1]; if (!auth_code.empty()) break; } } while (result == CURLE_OK && (response_code / 100) == 3); } 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 && !auth_code.empty()) { std::string token_url = "https://auth.gog.com/token?client_id=" + Globals::galaxyConf.getClientId() + "&client_secret=" + Globals::galaxyConf.getClientSecret() + "&grant_type=authorization_code&code=" + auth_code + "&redirect_uri=" + (std::string)curl_easy_escape(curlhandle, Globals::galaxyConf.getRedirectUri().c_str(), Globals::galaxyConf.getRedirectUri().size()); std::string json = this->getResponse(token_url); if (!json.empty()) { Json::Value token_json; Json::Reader *jsonparser = new Json::Reader; if (jsonparser->parse(json, token_json)) { Globals::galaxyConf.setJSON(token_json); res = 2; } else { std::cerr << "Failed to parse json" << std::endl << json << std::endl; std::cerr << jsonparser->getFormattedErrorMessages() << std::endl; } delete jsonparser; } } 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/security"); std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it htmlcxx::HTML::ParserDom parser; tree dom = parser.parseTree(html); tree::iterator it = dom.begin(); tree::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::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 Website::getWishlistItems() { Json::Value root; Json::Reader *jsonparser = new Json::Reader; int i = 1; bool bAllPagesParsed = false; std::vector 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 (Globals::globalConfig.bPlatformDetection && !(item.platform & Globals::globalConfig.dlConf.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; }