pax_global_header00006660000000000000000000000064126074445420014522gustar00rootroot0000000000000052 comment=97d01b2de6afd891804c6eb381de4468ca8ddd25 lgogdownloader-2.26/000077500000000000000000000000001260744454200144625ustar00rootroot00000000000000lgogdownloader-2.26/.gitignore000066400000000000000000000000501260744454200164450ustar00rootroot00000000000000*.layout *~ *.[oa] bin/* obj/* *.1 *.gz lgogdownloader-2.26/COPYING000066400000000000000000000007441260744454200155220ustar00rootroot00000000000000 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-2.26/Makefile000066400000000000000000000145401260744454200161260ustar00rootroot00000000000000#------------------------------------------------------------------------------# # This makefile was generated by 'cbp2make' tool rev.127 # #------------------------------------------------------------------------------# WORKDIR = `pwd` DESTDIR = PREFIX = /usr MANPREFIX = $(PREFIX)/share CC = gcc CXX = g++ AR = ar LD = g++ WINDRES = windres INC = -Iinclude -I/usr/include/rhash -I/usr/include/jsoncpp CFLAGS = -std=c++11 -Wall -fexceptions -D_FILE_OFFSET_BITS=64 RESINC = LIBDIR = LIB = -lcurl -loauth -ljsoncpp -lhtmlcxx -lboost_system -lboost_filesystem -lboost_regex -lboost_program_options -lboost_date_time -ltinyxml -lrhash LDFLAGS = VERSION = -DVERSION_STRING="\"$(shell sh version.sh)\"" HELP2MAN = $(shell which help2man 2> /dev/null) MAN_DIR = man MAN_PAGE = lgogdownloader.1 INC_DEBUG = $(INC) CFLAGS_DEBUG = $(CFLAGS) -g -DDEBUG RESINC_DEBUG = $(RESINC) RCFLAGS_DEBUG = $(RCFLAGS) LIBDIR_DEBUG = $(LIBDIR) LIB_DEBUG = $(LIB) LDFLAGS_DEBUG = $(LDFLAGS) OBJDIR_DEBUG = obj/Debug DEP_DEBUG = OUT_DEBUG = bin/Debug/lgogdownloader INC_RELEASE = $(INC) CFLAGS_RELEASE = $(CFLAGS) -O2 RESINC_RELEASE = $(RESINC) RCFLAGS_RELEASE = $(RCFLAGS) LIBDIR_RELEASE = $(LIBDIR) LIB_RELEASE = $(LIB) LDFLAGS_RELEASE = $(LDFLAGS) -s OBJDIR_RELEASE = obj/Release DEP_RELEASE = OUT_RELEASE = bin/Release/lgogdownloader OBJ_DEBUG = $(OBJDIR_DEBUG)/main.o $(OBJDIR_DEBUG)/src/api.o $(OBJDIR_DEBUG)/src/downloader.o $(OBJDIR_DEBUG)/src/progressbar.o $(OBJDIR_DEBUG)/src/util.o $(OBJDIR_DEBUG)/src/blacklist.o $(OBJDIR_DEBUG)/src/gamedetails.o $(OBJDIR_DEBUG)/src/gamefile.o OBJ_RELEASE = $(OBJDIR_RELEASE)/main.o $(OBJDIR_RELEASE)/src/api.o $(OBJDIR_RELEASE)/src/downloader.o $(OBJDIR_RELEASE)/src/progressbar.o $(OBJDIR_RELEASE)/src/util.o $(OBJDIR_RELEASE)/src/blacklist.o $(OBJDIR_RELEASE)/src/gamedetails.o $(OBJDIR_RELEASE)/src/gamefile.o all: debug release clean: clean_debug clean_release before_debug: test -d bin/Debug || mkdir -p bin/Debug test -d $(OBJDIR_DEBUG) || mkdir -p $(OBJDIR_DEBUG) test -d $(OBJDIR_DEBUG)/src || mkdir -p $(OBJDIR_DEBUG)/src after_debug: out_debug ifdef HELP2MAN if ! test -f $(MAN_DIR)/$(MAN_PAGE).gz; then \ help2man -N -i $(MAN_DIR)/lgogdownloader.supplemental.groff -o $(MAN_DIR)/$(MAN_PAGE) $(OUT_DEBUG); \ gzip -f -9 $(MAN_DIR)/$(MAN_PAGE); \ fi endif debug: before_debug out_debug after_debug out_debug: $(OBJ_DEBUG) $(DEP_DEBUG) $(LD) $(LDFLAGS_DEBUG) $(LIBDIR_DEBUG) $(OBJ_DEBUG) $(LIB_DEBUG) -o $(OUT_DEBUG) $(OBJ_DEBUG): | before_debug $(OBJDIR_DEBUG)/main.o: main.cpp $(CXX) $(CFLAGS_DEBUG) $(VERSION) $(INC_DEBUG) -c main.cpp -o $(OBJDIR_DEBUG)/main.o $(OBJDIR_DEBUG)/src/api.o: src/api.cpp $(CXX) $(CFLAGS_DEBUG) $(INC_DEBUG) -c src/api.cpp -o $(OBJDIR_DEBUG)/src/api.o $(OBJDIR_DEBUG)/src/downloader.o: src/downloader.cpp $(CXX) $(CFLAGS_DEBUG) $(INC_DEBUG) -c src/downloader.cpp -o $(OBJDIR_DEBUG)/src/downloader.o $(OBJDIR_DEBUG)/src/progressbar.o: src/progressbar.cpp $(CXX) $(CFLAGS_DEBUG) $(INC_DEBUG) -c src/progressbar.cpp -o $(OBJDIR_DEBUG)/src/progressbar.o $(OBJDIR_DEBUG)/src/util.o: src/util.cpp $(CXX) $(CFLAGS_DEBUG) $(INC_DEBUG) -c src/util.cpp -o $(OBJDIR_DEBUG)/src/util.o $(OBJDIR_DEBUG)/src/blacklist.o: src/blacklist.cpp $(CXX) $(CFLAGS_DEBUG) $(INC_DEBUG) -c src/blacklist.cpp -o $(OBJDIR_DEBUG)/src/blacklist.o $(OBJDIR_DEBUG)/src/gamefile.o: src/gamefile.cpp $(CXX) $(CFLAGS_DEBUG) $(INC_DEBUG) -c src/gamefile.cpp -o $(OBJDIR_DEBUG)/src/gamefile.o $(OBJDIR_DEBUG)/src/gamedetails.o: src/gamedetails.cpp $(CXX) $(CFLAGS_DEBUG) $(INC_DEBUG) -c src/gamedetails.cpp -o $(OBJDIR_DEBUG)/src/gamedetails.o clean_debug: rm -f $(OBJ_DEBUG) $(OUT_DEBUG) rm -rf bin/Debug rm -rf $(OBJDIR_DEBUG) rm -rf $(OBJDIR_DEBUG)/src before_release: test -d bin/Release || mkdir -p bin/Release test -d $(OBJDIR_RELEASE) || mkdir -p $(OBJDIR_RELEASE) test -d $(OBJDIR_RELEASE)/src || mkdir -p $(OBJDIR_RELEASE)/src after_release: out_release ifdef HELP2MAN if ! test -f $(MAN_DIR)/$(MAN_PAGE).gz; then \ help2man -N -i $(MAN_DIR)/lgogdownloader.supplemental.groff -o $(MAN_DIR)/$(MAN_PAGE) $(OUT_RELEASE); \ gzip -f -9 $(MAN_DIR)/$(MAN_PAGE); \ fi endif release: before_release out_release after_release out_release: $(OBJ_RELEASE) $(DEP_RELEASE) $(LD) $(LDFLAGS_RELEASE) $(LIBDIR_RELEASE) $(OBJ_RELEASE) $(LIB_RELEASE) -o $(OUT_RELEASE) $(OBJ_RELEASE): | before_release $(OBJDIR_RELEASE)/main.o: main.cpp $(CXX) $(CFLAGS_RELEASE) $(VERSION) $(INC_RELEASE) -c main.cpp -o $(OBJDIR_RELEASE)/main.o $(OBJDIR_RELEASE)/src/api.o: src/api.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c src/api.cpp -o $(OBJDIR_RELEASE)/src/api.o $(OBJDIR_RELEASE)/src/downloader.o: src/downloader.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c src/downloader.cpp -o $(OBJDIR_RELEASE)/src/downloader.o $(OBJDIR_RELEASE)/src/progressbar.o: src/progressbar.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c src/progressbar.cpp -o $(OBJDIR_RELEASE)/src/progressbar.o $(OBJDIR_RELEASE)/src/util.o: src/util.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c src/util.cpp -o $(OBJDIR_RELEASE)/src/util.o $(OBJDIR_RELEASE)/src/blacklist.o: src/blacklist.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c src/blacklist.cpp -o $(OBJDIR_RELEASE)/src/blacklist.o $(OBJDIR_RELEASE)/src/gamefile.o: src/gamefile.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c src/gamefile.cpp -o $(OBJDIR_RELEASE)/src/gamefile.o $(OBJDIR_RELEASE)/src/gamedetails.o: src/gamedetails.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c src/gamedetails.cpp -o $(OBJDIR_RELEASE)/src/gamedetails.o clean_release: rm -f $(OBJ_RELEASE) $(OUT_RELEASE) rm -rf bin/Release rm -rf $(OBJDIR_RELEASE) rm -rf $(OBJDIR_RELEASE)/src rm -f $(MAN_DIR)/$(MAN_PAGE) $(MAN_DIR)/$(MAN_PAGE).gz install: release install -d $(DESTDIR)/$(PREFIX)/bin/ if test -f $(OUT_DEBUG); then \ install -m 755 $(OUT_DEBUG) $(DESTDIR)/$(PREFIX)/bin/lgogdownloader; \ else \ install -m 755 $(OUT_RELEASE) $(DESTDIR)/$(PREFIX)/bin/lgogdownloader; \ fi if test -f $(MAN_DIR)/$(MAN_PAGE).gz; then \ install -d $(DESTDIR)/$(MANPREFIX)/man/man1/; \ install -m 644 $(MAN_DIR)/$(MAN_PAGE).gz $(DESTDIR)/$(MANPREFIX)/man/man1/$(MAN_PAGE).gz; \ fi uninstall: rm $(DESTDIR)/$(PREFIX)/bin/lgogdownloader if test -f $(DESTDIR)/$(MANPREFIX)/man/man1/$(MAN_PAGE).gz; then \ rm $(DESTDIR)/$(MANPREFIX)/man/man1/$(MAN_PAGE).gz; \ fi .PHONY: before_debug after_debug clean_debug before_release after_release clean_release lgogdownloader-2.26/README.md000066400000000000000000000021721260744454200157430ustar00rootroot00000000000000# GOG Downloader This repository contains the code of unofficial [GOG](http://www.gog.com/) downloader. ## Dependencies * libcurl * liboauth * librhash * jsoncpp * htmlcxx * tinyxml * boost (regex, date-time, system, filesystem, program-options) * help2man ### Debian/Ubuntu # apt install build-essential libcurl4-openssl-dev libboost-regex-dev \ libjsoncpp-dev liboauth-dev librhash-dev libtinyxml-dev libhtmlcxx-dev\ libboost-system-dev libboost-filesystem-dev libboost-program-options-dev\ libboost-date-time-dev help2man ## Build and install $ make release # 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-2.26/include/000077500000000000000000000000001260744454200161055ustar00rootroot00000000000000lgogdownloader-2.26/include/api.h000066400000000000000000000066151260744454200170370ustar00rootroot00000000000000/* 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 #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(); 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-2.26/include/blacklist.h000066400000000000000000000020621260744454200202260ustar00rootroot00000000000000/* 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 = ""); private: std::vector blacklist_; }; #endif // BLACKLIST_H_ lgogdownloader-2.26/include/config.h000066400000000000000000000051471260744454200175320ustar00rootroot00000000000000/* 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 "blacklist.h" class Config { public: Config() {}; virtual ~Config() {}; bool bVerbose; bool bRemoteXML; bool bCover; bool bUpdateCheck; bool bDownload; bool bList; bool bListDetails; bool bLoginHTTP; bool bLoginAPI; bool bRepair; bool bInstallers; bool bExtras; bool bPatches; bool bLanguagePacks; bool bDLC; bool bUnicode; // use Unicode in console output bool bColor; // use colors bool bVerifyPeer; bool bCheckStatus; bool bDuplicateHandler; bool bSaveConfig; bool bResetConfig; bool bReport; bool bSubDirectories; bool bUseCache; bool bUpdateCache; bool bSaveSerials; bool bPlatformDetection; bool bShowWishlist; std::string sGameRegex; std::string sDirectory; std::string sCacheDirectory; std::string sXMLFile; std::string sXMLDirectory; std::string sToken; std::string sSecret; std::string sVersionString; std::string sVersionNumber; std::string sConfigDirectory; std::string sCookiePath; std::string sConfigFilePath; std::string sBlacklistFilePath; std::string sOrphanRegex; std::string sCoverList; std::string sReportFilePath; std::string sInstallersSubdir; std::string sExtrasSubdir; std::string sPatchesSubdir; std::string sLanguagePackSubdir; std::string sDLCSubdir; std::string sGameSubdir; std::string sFileIdString; std::string sOutputFilename; std::string sLanguagePriority; std::string sPlatformPriority; std::string sIgnoreDLCCountRegex; std::vector vLanguagePriority; std::vector vPlatformPriority; unsigned int iInstallerPlatform; unsigned int iInstallerLanguage; unsigned int iInclude; int iRetries; int iWait; int iCacheValid; size_t iChunkSize; curl_off_t iDownloadRate; long int iTimeout; Blacklist blacklist; }; #endif // CONFIG_H__ lgogdownloader-2.26/include/downloader.h000066400000000000000000000105011260744454200204110ustar00rootroot00000000000000/* 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 #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; }; class gameItem { public: std::string name; std::string id; std::vector dlcnames; }; class Downloader { public: Downloader(Config &conf); virtual ~Downloader(); int init(); int login(); void listGames(); void updateCheck(); void repair(); void download(); void checkOrphans(); void checkStatus(); void updateCache(); void downloadFileWithId(const std::string& fileid_string, const std::string& output_filepath); void showWishlist(); CURL* curlhandle; Timer timer; Config config; ProgressBar* progressbar; std::deque< std::pair > TimeAndSize; protected: private: CURLcode downloadFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string()); int repairFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string()); int downloadCovers(const std::string& gamename, const std::string& directory, const std::string& cover_xml_data); int getGameDetails(); void getGameList(); uintmax_t getResumePosition(); CURLcode beginDownload(); std::string getResponse(const std::string& url); std::string getLocalFileHash(const std::string& filepath, const std::string& gamename = std::string()); std::string getRemoteFileHash(const std::string& gamename, const std::string& id); int loadGameDetailsCache(); int saveGameDetailsCache(); std::vector getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0); int HTTP_Login(const std::string& email, const std::string& password); std::vector getGames(); std::vector getFreeGames(); std::vector getExtrasFromJSON(const Json::Value& json, const std::string& gamename); Json::Value getGameDetailsJSON(const std::string& gameid); std::string getSerialsFromJSON(const Json::Value& json); void saveSerials(const std::string& serials, const std::string& filepath); static int progressCallback(void *clientp, double dltotal, double dlnow, double ultotal, double 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); API *gogAPI; std::vector gameItems; std::vector games; std::string coverXML; off_t resume_position; int retries; std::ofstream report_ofs; }; #endif // DOWNLOADER_H lgogdownloader-2.26/include/gamedetails.h000066400000000000000000000016711260744454200205420ustar00rootroot00000000000000#ifndef GAMEDETAILS_H #define GAMEDETAILS_H #include "globalconstants.h" #include "gamefile.h" #include "config.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 title; std::string icon; std::string serials; void filterWithPriorities(const Config& config); void makeFilepaths(const Config& config); std::string getSerialsFilepath(); Json::Value getDetailsAsJson(); virtual ~gameDetails(); protected: void filterListWithPriorities(std::vector& list, const Config& config); private: std::string serialsFilepath; }; #endif // GAMEDETAILS_H lgogdownloader-2.26/include/gamefile.h000066400000000000000000000016511260744454200200320ustar00rootroot00000000000000#ifndef GAMEFILE_H #define GAMEFILE_H #include "globalconstants.h" #include #include #include class gameFile { public: gameFile(); gameFile(const int& t_updated, const std::string& t_id, const std::string& t_name, const std::string& t_path, const std::string& t_size, const unsigned int& t_language = GlobalConstants::LANGUAGE_EN, const unsigned int& t_platform = GlobalConstants::PLATFORM_WINDOWS, const int& t_silent = 0); int updated; std::string id; std::string name; std::string path; std::string size; unsigned int platform; unsigned int language; 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-2.26/include/globalconstants.h000066400000000000000000000071311260744454200214550ustar00rootroot00000000000000/* 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 { 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 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" } }; // 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" } }; }; #endif // GLOBALCONSTANTS_H_INCLUDED lgogdownloader-2.26/include/progressbar.h000066400000000000000000000021451260744454200206110ustar00rootroot00000000000000/* 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); 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-2.26/include/util.h000066400000000000000000000047101260744454200172350ustar00rootroot00000000000000/* 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 #include #include #include #include #include #include #include #include #include struct gameSpecificConfig { unsigned int iInstallerPlatform; unsigned int iInstallerLanguage; bool bDLC; bool bIgnoreDLCCount; }; 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); std::string getOptionNameString(const unsigned int& value, const std::vector& options); } #endif // UTIL_H lgogdownloader-2.26/lgogdownloader.cbp000066400000000000000000000043401260744454200201600ustar00rootroot00000000000000 lgogdownloader-2.26/main.cpp000066400000000000000000000724441260744454200161250ustar00rootroot00000000000000/* 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 #include #include #define VERSION_NUMBER "2.26" #ifndef VERSION_STRING # define VERSION_STRING "LGOGDownloader " VERSION_NUMBER #endif namespace bpo = boost::program_options; template void set_vm_value(std::map& vm, const std::string& option, const T& value) { vm[option].value() = boost::any(value); } // Parse the options string void parseOptionString(const std::string &option_string, std::vector &priority, unsigned int &type, const std::vector& options) { type = 0; 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; } } 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" } }; Config config; config.sVersionString = VERSION_STRING; config.sVersionNumber = VERSION_NUMBER; config.sCacheDirectory = Util::getCacheHome() + "/lgogdownloader"; config.sXMLDirectory = config.sCacheDirectory + "/xml"; config.sConfigDirectory = Util::getConfigHome() + "/lgogdownloader"; config.sCookiePath = config.sConfigDirectory + "/cookies.txt"; config.sConfigFilePath = config.sConfigDirectory + "/config.cfg"; config.sBlacklistFilePath = config.sConfigDirectory + "/blacklist.txt"; std::string priority_help_text = "Set priority by separating values with \",\"\nCombine values by separating with \"+\""; // Create help text for --platform option std::string platform_text = "Select which installers are downloaded\n"; unsigned int platform_all = Util::getOptionValue("all", GlobalConstants::PLATFORMS); for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i) { platform_text += GlobalConstants::PLATFORMS[i].str + " = " + GlobalConstants::PLATFORMS[i].regexp + "|" + std::to_string(GlobalConstants::PLATFORMS[i].id) + "\n"; } platform_text += "All = all|" + std::to_string(platform_all); platform_text += "\n\n" + priority_help_text; platform_text += "\nExample: Linux if available otherwise Windows and Mac: l,w+m"; // Create help text for --language option std::string language_text = "Select which language installers are downloaded\n"; unsigned int language_all = Util::getOptionValue("all", GlobalConstants::LANGUAGES); for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i) { language_text += GlobalConstants::LANGUAGES[i].str + " = " + GlobalConstants::LANGUAGES[i].regexp + "|" + std::to_string(GlobalConstants::LANGUAGES[i].id) + "\n"; } language_text += "Add the values to download multiple languages\nAll = all|" + std::to_string(language_all) + "\n" + "French + Polish = \"fr+pl\"|" + std::to_string(GlobalConstants::LANGUAGE_FR | GlobalConstants::LANGUAGE_PL) + " (" + std::to_string(GlobalConstants::LANGUAGE_FR) + "+" + std::to_string(GlobalConstants::LANGUAGE_PL) + "=" + std::to_string(GlobalConstants::LANGUAGE_FR | GlobalConstants::LANGUAGE_PL) + ")"; language_text += "\n\n" + priority_help_text; language_text += "\nExample: German if available otherwise English and French: de,en+fr"; // Create help text for --check-orphans std::string orphans_regex_default = ".*\\.(zip|exe|bin|dmg|old|deb|tar\\.gz|pkg|sh)$"; // Limit to files with these extensions (".old" is for renamed older version files) std::string check_orphans_text = "Check for orphaned files (files found on local filesystem that are not found on GOG servers). Sets regular expression filter (Perl syntax) for files to check. If no argument is given then the regex defaults to '" + orphans_regex_default + "'"; // Help text for subdir options std::string subdir_help_text = "\nTemplates:\n- %platform%\n- %gamename%\n- %dlcname%"; // Help text for include and exclude options std::string include_options_text; for (unsigned int i = 0; i < INCLUDE_OPTIONS.size(); ++i) { include_options_text += INCLUDE_OPTIONS[i].str + " = " + INCLUDE_OPTIONS[i].regexp + "|" + std::to_string(INCLUDE_OPTIONS[i].id) + "\n"; } include_options_text += "Separate with \",\" to use multiple values"; std::vector 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_cfg; bpo::options_description options_cfg_only; bpo::options_description options_cfg_all("Configuration"); try { bool bInsecure = false; bool bNoColor = false; bool bNoUnicode = false; bool bNoDuplicateHandler = false; bool bNoRemoteXML = false; bool bNoSubDirectories = false; bool bNoPlatformDetection = false; bool bLogin = false; std::string sInstallerPlatform; std::string sInstallerLanguage; std::string sIncludeOptions; std::string sExcludeOptions; config.bReport = false; // Commandline options (no config file) options_cli_no_cfg.add_options() ("help,h", "Print help message") ("version", "Print version information") ("login", bpo::value(&bLogin)->zero_tokens()->default_value(false), "Login") ("list", bpo::value(&config.bList)->zero_tokens()->default_value(false), "List games") ("list-details", bpo::value(&config.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info") ("download", bpo::value(&config.bDownload)->zero_tokens()->default_value(false), "Download") ("repair", bpo::value(&config.bRepair)->zero_tokens()->default_value(false), "Repair downloaded files\nUse --repair --download to redownload files when filesizes don't match (possibly different version). Redownload will rename the old file (appends .old to filename)") ("game", bpo::value(&config.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)\nAliases: \"all\", \"free\"\nAlias \"free\" doesn't work with cached details") ("create-xml", bpo::value(&config.sXMLFile)->implicit_value("automatic"), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation") ("update-check", bpo::value(&config.bUpdateCheck)->zero_tokens()->default_value(false), "Check for update notifications") ("check-orphans", bpo::value(&config.sOrphanRegex)->implicit_value(""), check_orphans_text.c_str()) ("status", bpo::value(&config.bCheckStatus)->zero_tokens()->default_value(false), "Show status of files\n\nOutput format:\nstatuscode gamename filename filesize filehash\n\nStatus codes:\nOK - File is OK\nND - File is not downloaded\nMD5 - MD5 mismatch, different version\nFS - File size mismatch, incomplete download") ("save-config", bpo::value(&config.bSaveConfig)->zero_tokens()->default_value(false), "Create config file with current settings") ("reset-config", bpo::value(&config.bResetConfig)->zero_tokens()->default_value(false), "Reset config settings to default") ("report", bpo::value(&config.sReportFilePath)->implicit_value("lgogdownloader-report.log"), "Save report of downloaded/repaired files to specified file\nDefault filename: lgogdownloader-report.log") ("update-cache", bpo::value(&config.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(&config.sFileIdString)->default_value(""), "Download files using fileid\n\nFormat:\n\"gamename/fileid\"\nor: \"gogdownloader://gamename/fileid\"\n\nMultiple files:\n\"gamename1/fileid1,gamename2/fileid2\"\nor: \"gogdownloader://gamename1/fileid1,gamename2/fileid2\"\n\nThis option ignores all subdir options. The files are downloaded to directory specified with --directory option.") ("output-file,o", bpo::value(&config.sOutputFilename)->default_value(""), "Set filename of file downloaded with --download-file.") ("wishlist", bpo::value(&config.bShowWishlist)->zero_tokens()->default_value(false), "Show wishlist") ("login-api", bpo::value(&config.bLoginAPI)->zero_tokens()->default_value(false), "Login (API only)") ("login-website", bpo::value(&config.bLoginHTTP)->zero_tokens()->default_value(false), "Login (website only)") ; // Commandline options (config file) options_cli_cfg.add_options() ("directory", bpo::value(&config.sDirectory)->default_value("."), "Set download directory") ("limit-rate", bpo::value(&config.iDownloadRate)->default_value(0), "Limit download rate to value in kB\n0 = unlimited") ("xml-directory", bpo::value(&config.sXMLDirectory), "Set directory for GOG XML files") ("chunk-size", bpo::value(&config.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") ("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(&config.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(&config.iTimeout)->default_value(10), "Set timeout for connection\nMaximum time in seconds that connection phase is allowed to take") ("retries", bpo::value(&config.iRetries)->default_value(3), "Set maximum number of retries on failed download") ("wait", bpo::value(&config.iWait)->default_value(0), "Time to wait between requests (milliseconds)") ("cover-list", bpo::value(&config.sCoverList)->default_value("https://sites.google.com/site/gogdownloader/covers.xml"), "Set URL for cover list") ("subdir-installers", bpo::value(&config.sInstallersSubdir)->default_value(""), ("Set subdirectory for extras" + subdir_help_text).c_str()) ("subdir-extras", bpo::value(&config.sExtrasSubdir)->default_value("extras"), ("Set subdirectory for extras" + subdir_help_text).c_str()) ("subdir-patches", bpo::value(&config.sPatchesSubdir)->default_value("patches"), ("Set subdirectory for patches" + subdir_help_text).c_str()) ("subdir-language-packs", bpo::value(&config.sLanguagePackSubdir)->default_value("languagepacks"), ("Set subdirectory for language packs" + subdir_help_text).c_str()) ("subdir-dlc", bpo::value(&config.sDLCSubdir)->default_value("dlc/%dlcname%"), ("Set subdirectory for dlc" + subdir_help_text).c_str()) ("subdir-game", bpo::value(&config.sGameSubdir)->default_value("%gamename%"), ("Set subdirectory for game" + subdir_help_text).c_str()) ("use-cache", bpo::value(&config.bUseCache)->zero_tokens()->default_value(false), ("Use game details cache")) ("cache-valid", bpo::value(&config.iCacheValid)->default_value(2880), ("Set how long cached game details are valid (in minutes)\nDefault: 2880 minutes (48 hours)")) ("save-serials", bpo::value(&config.bSaveSerials)->zero_tokens()->default_value(false), "Save serial numbers when downloading") ("ignore-dlc-count", bpo::value(&config.sIgnoreDLCCountRegex)->implicit_value(".*"), "Set regular expression filter for games to ignore DLC count information\nIgnoring DLC count information helps in situations where the account page doesn't provide accurate information about DLCs") ("include", bpo::value(&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()) ; // Options read from config file options_cfg_only.add_options() ("token", bpo::value(&config.sToken)->default_value(""), "oauth token") ("secret", bpo::value(&config.sSecret)->default_value(""), "oauth secret") ; options_cli_all.add(options_cli_no_cfg).add(options_cli_cfg); options_cfg_all.add(options_cfg_only).add(options_cli_cfg); bpo::parsed_options parsed = bpo::parse_command_line(argc, argv, options_cli_all); bpo::store(parsed, vm); unrecognized_options_cli = bpo::collect_unrecognized(parsed.options, bpo::include_positional); bpo::notify(vm); if (vm.count("help")) { std::cout << config.sVersionString << std::endl << options_cli_all << std::endl; return 0; } if (vm.count("version")) { std::cout << VERSION_STRING << std::endl; return 0; } // Create lgogdownloader directories boost::filesystem::path path = config.sXMLDirectory; if (!boost::filesystem::exists(path)) { if (!boost::filesystem::create_directories(path)) { std::cout << "Failed to create directory: " << path << std::endl; return 1; } } path = config.sConfigDirectory; if (!boost::filesystem::exists(path)) { if (!boost::filesystem::create_directories(path)) { std::cout << "Failed to create directory: " << path << std::endl; return 1; } } path = config.sCacheDirectory; if (!boost::filesystem::exists(path)) { if (!boost::filesystem::create_directories(path)) { std::cout << "Failed to create directory: " << path << std::endl; return 1; } } if (boost::filesystem::exists(config.sConfigFilePath)) { std::ifstream ifs(config.sConfigFilePath.c_str()); if (!ifs) { std::cout << "Could not open config file: " << config.sConfigFilePath << std::endl; return 1; } else { bpo::parsed_options parsed = bpo::parse_config_file(ifs, options_cfg_all, true); bpo::store(parsed, vm); bpo::notify(vm); ifs.close(); unrecognized_options_cfg = bpo::collect_unrecognized(parsed.options, bpo::include_positional); } } if (boost::filesystem::exists(config.sBlacklistFilePath)) { std::ifstream ifs(config.sBlacklistFilePath.c_str()); if (!ifs) { std::cout << "Could not open blacklist file: " << config.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)); } config.blacklist.initialize(lines); } } if (vm.count("chunk-size")) config.iChunkSize <<= 20; // Convert chunk size from bytes to megabytes if (vm.count("limit-rate")) config.iDownloadRate <<= 10; // Convert download rate from bytes to kilobytes if (vm.count("check-orphans")) if (config.sOrphanRegex.empty()) config.sOrphanRegex = orphans_regex_default; if (vm.count("report")) config.bReport = true; if (config.iWait > 0) config.iWait *= 1000; config.bVerifyPeer = !bInsecure; config.bColor = !bNoColor; config.bUnicode = !bNoUnicode; config.bDuplicateHandler = !bNoDuplicateHandler; config.bRemoteXML = !bNoRemoteXML; config.bSubDirectories = !bNoSubDirectories; config.bPlatformDetection = !bNoPlatformDetection; for (auto i = unrecognized_options_cli.begin(); i != unrecognized_options_cli.end(); ++i) if (i->compare(0, GlobalConstants::PROTOCOL_PREFIX.length(), GlobalConstants::PROTOCOL_PREFIX) == 0) config.sFileIdString = *i; if (!config.sFileIdString.empty()) { if (config.sFileIdString.compare(0, GlobalConstants::PROTOCOL_PREFIX.length(), GlobalConstants::PROTOCOL_PREFIX) == 0) { config.sFileIdString.replace(0, GlobalConstants::PROTOCOL_PREFIX.length(), ""); } vFileIdStrings = Util::tokenize(config.sFileIdString, ","); } if (!config.sOutputFilename.empty() && vFileIdStrings.size() > 1) { std::cerr << "Cannot specify an output file name when downloading multiple files." << std::endl; return 1; } if (bLogin) { config.bLoginAPI = true; config.bLoginHTTP = true; } parseOptionString(sInstallerLanguage, config.vLanguagePriority, config.iInstallerLanguage, GlobalConstants::LANGUAGES); parseOptionString(sInstallerPlatform, config.vPlatformPriority, config.iInstallerPlatform, GlobalConstants::PLATFORMS); 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); } config.iInclude = include_value & ~exclude_value; // Assign values // TODO: Use config.iInclude in Downloader class directly and get rid of this value assignment config.bCover = (config.iInclude & OPTION_COVERS); config.bInstallers = (config.iInclude & OPTION_INSTALLERS); config.bExtras = (config.iInclude & OPTION_EXTRAS); config.bPatches = (config.iInclude & OPTION_PATCHES); config.bLanguagePacks = (config.iInclude & OPTION_LANGPACKS); config.bDLC = (config.iInclude & OPTION_DLCS); } catch (std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 1; } catch (...) { std::cerr << "Exception of unknown type!" << std::endl; return 1; } if (config.iInstallerPlatform < GlobalConstants::PLATFORMS[0].id || config.iInstallerPlatform > platform_all) { std::cout << "Invalid value for --platform" << std::endl; return 1; } if (config.iInstallerLanguage < GlobalConstants::LANGUAGES[0].id || config.iInstallerLanguage > language_all) { std::cout << "Invalid value for --language" << std::endl; return 1; } if (!config.sXMLDirectory.empty()) { // Make sure that xml directory doesn't have trailing slash if (config.sXMLDirectory.at(config.sXMLDirectory.length()-1)=='/') config.sXMLDirectory.assign(config.sXMLDirectory.begin(),config.sXMLDirectory.end()-1); } // Create GOG XML for a file if (!config.sXMLFile.empty() && (config.sXMLFile != "automatic")) { Util::createXML(config.sXMLFile, config.iChunkSize, config.sXMLDirectory); return 0; } // Make sure that directory has trailing slash if (!config.sDirectory.empty()) { if (config.sDirectory.at(config.sDirectory.length()-1)!='/') config.sDirectory += "/"; } else { config.sDirectory = "./"; // Directory wasn't specified, use current directory } if (!unrecognized_options_cfg.empty() && (!config.bSaveConfig || !config.bResetConfig)) { std::cerr << "Unrecognized options in " << config.sConfigFilePath << std::endl; for (unsigned int i = 0; i < unrecognized_options_cfg.size(); i+=2) { std::cerr << unrecognized_options_cfg[i] << " = " << unrecognized_options_cfg[i+1] << std::endl; } std::cerr << std::endl; } Downloader downloader(config); int initResult = downloader.init(); int iLoginResult = 0; if (config.bLoginAPI || config.bLoginHTTP || initResult == 1) { if (!config.bLoginAPI && !config.bLoginHTTP) downloader.config.bLoginAPI = true; iLoginResult = downloader.login(); if (iLoginResult == 0) return 1; } // Make sure that config file and cookie file are only readable/writable by owner Util::setFilePermissions(config.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write); Util::setFilePermissions(config.sCookiePath, boost::filesystem::owner_read | boost::filesystem::owner_write); if (config.bSaveConfig || iLoginResult == 1) { if (iLoginResult == 1) { set_vm_value(vm, "token", downloader.config.sToken); set_vm_value(vm, "secret", downloader.config.sSecret); bpo::notify(vm); } std::ofstream ofs(config.sConfigFilePath.c_str()); if (ofs) { std::cout << "Saving config: " << config.sConfigFilePath << std::endl; for (bpo::variables_map::iterator it = vm.begin(); it != vm.end(); ++it) { std::string option = it->first; std::string option_value_string; const bpo::variable_value& option_value = it->second; try { if (options_cfg_all.find(option, false).long_name() == option) { if (!option_value.empty()) { const std::type_info& type = option_value.value().type() ; if ( type == typeid(std::string) ) option_value_string = option_value.as(); 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(); Util::setFilePermissions(config.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write); if (config.bSaveConfig) return 0; } else { std::cout << "Failed to create config: " << config.sConfigFilePath << std::endl; return 1; } } else if (config.bResetConfig) { std::ofstream ofs(config.sConfigFilePath.c_str()); if (ofs) { if (!config.sToken.empty() && !config.sSecret.empty()) { ofs << "token = " << config.sToken << std::endl; ofs << "secret = " << config.sSecret << std::endl; } ofs.close(); Util::setFilePermissions(config.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write); return 0; } else { std::cout << "Failed to create config: " << config.sConfigFilePath << std::endl; return 1; } } if (config.bShowWishlist) downloader.showWishlist(); else if (config.bUpdateCache) downloader.updateCache(); else if (config.bUpdateCheck) // Update check has priority over download and list downloader.updateCheck(); else if (!vFileIdStrings.empty()) { for (std::vector::iterator it = vFileIdStrings.begin(); it != vFileIdStrings.end(); it++) { downloader.downloadFileWithId(*it, config.sOutputFilename); } } else if (config.bRepair) // Repair file downloader.repair(); else if (config.bDownload) // Download games downloader.download(); else if (config.bListDetails || config.bList) // Detailed list of games/extras downloader.listGames(); else if (!config.sOrphanRegex.empty()) // Check for orphaned files if regex for orphans is set downloader.checkOrphans(); else if (config.bCheckStatus) downloader.checkStatus(); else { // Show help message std::cout << config.sVersionString << std::endl << options_cli_all << std::endl; } // Orphan check was called at the same time as download. Perform it after download has finished if (!config.sOrphanRegex.empty() && config.bDownload) downloader.checkOrphans(); return 0; } lgogdownloader-2.26/man/000077500000000000000000000000001260744454200152355ustar00rootroot00000000000000lgogdownloader-2.26/man/lgogdownloader.supplemental.groff000066400000000000000000000074051260744454200240070ustar00rootroot00000000000000[synopsis] .B lgogdownloader [\fIOPTION\fP]... [description] An open-source GOG.com downloader for Linux users which uses the same API as the official GOGDownloader. .PP LGOGDownloader can download purchased games, query GOG.com to see if game files have changed, as well as downloading extras such as artwork and manuals. It is capable of downloading language-specific installers for games where they exist. .PP These games are currently offered only for the Microsoft Windows\[rg] and Apple OS X\[rg] operating systems. To play these games under GNU/Linux will require a compatibility layer such as Wine. Usage of such a program is outside the scope of this document. /--update-check/ .nf /--no-installers/ .fi /Status codes:/ .nf [blacklist] .fi Allows user to specify individual files that should not be downloaded or mentioned as orphans. .sp 1 Each line in the file specifies one blacklist expression, except for empty lines and lines starting with #. First few characters specify blacklist item type and flags. So far, only regular expression (perl variant) are supported, so each line must start with "Rp" characters. After a space comes the expression itself. Expressions are matched against file path relative to what was specified as \fI--directory\fP. \fIExample black list\fP .br # used to store manually downloaded mods/patches/maps/, don't mention it as orphans .br Rp ^[^/]*/manual/.* .br # included with every *divinity game, once is enough .br Rp beyond_divinity/extras/bd_ladymageknight\.zip .br Rp divinity_2_developers_cut/extras/divinity_2_ladymageknight\.zip .sp # extra 6GB is A LOT of space if you don't actually plan to mod your game .br Rp the_witcher_2/extras/the_witcher_2_redkit\.zip .br Rp the_witcher_2/extras/extras_pack_3_hu_pl_ru_tr_zh_\.zip .br Rp the_witcher_2/extras/extras_pack_2_fr_it_jp_\.zip [files] .fi .TP \fI$XDG_CONFIG_HOME/lgogdownloader/\fP Storage for configuration files and cookies .br If \fB$XDG_CONFIG_HOME\fP is not set, it will use \fI$HOME/.config/lgogdownloader/\fP. .TP \fI$XDG_CACHE_HOME/lgogdownloader/xml/\fP Storage for XML files .br If \fB$XDG_CACHE_HOME\fP is not set, it will use \fI$HOME/.cache/lgogdownloader/xml/\fP. .TP \fI$XDG_CONFIG_HOME/lgogdownloader/blacklist.txt\fP Allows user to specify individual files that should not be downloaded or mentioned as orphans. .br It doesn't have to exist, but if it does exist, it must be readable to lgogdownloader. .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 and \fBignore-dlc-count\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" : .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-2.26/src/000077500000000000000000000000001260744454200152515ustar00rootroot00000000000000lgogdownloader-2.26/src/api.cpp000066400000000000000000000712331260744454200165340ustar00rootroot00000000000000/* 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 #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); this->error = false; this->config.oauth_token = token; this->config.oauth_secret = secret; } int API::init() { int res = 0; this->getAPIConfig(); // Check if we already have token and secret if (!this->config.oauth_token.empty() && !this->config.oauth_secret.empty()) { // Test authorization by getting user details res = this->getUserDetails(); // res = 1 if successful } return res; } int API::getAPIConfig() { std::string url = "https://api.gog.com/downloader2/status/stable/"; // Stable API //std::string url = "https://api.gog.com/downloader2/status/beta/"; // Beta API //std::string url = "https://api.gog.com/downloader2/status/e77989ed21758e78331b20e477fc5582/"; // Development API? Not sure because the downloader version number it reports is lower than beta. int res = 0; std::string json = this->getResponse(url); if (!json.empty()) { Json::Value root; Json::Reader *jsonparser = new Json::Reader; if (jsonparser->parse(json, root)) { #ifdef DEBUG std::cerr << "DEBUG INFO (API::getAPIConfig)" << std::endl << root << std::endl; #endif this->config.oauth_authorize_temp_token = root["config"]["oauth_authorize_temp_token"].asString() + "/"; this->config.oauth_get_temp_token = root["config"]["oauth_get_temp_token"].asString() + "/"; this->config.oauth_get_token = root["config"]["oauth_get_token"].asString() + "/"; this->config.get_user_games = root["config"]["get_user_games"].asString() + "/"; this->config.get_user_details = root["config"]["get_user_details"].asString() + "/"; this->config.get_installer_link = root["config"]["get_installer_link"].asString() + "/"; this->config.get_game_details = root["config"]["get_game_details"].asString() + "/"; this->config.get_extra_link = root["config"]["get_extra_link"].asString() + "/"; this->config.set_app_status = root["config"]["set_app_status"].asString() + "/"; res = 1; } else { #ifdef DEBUG std::cerr << "DEBUG INFO (API::getAPIConfig)" << std::endl << json << std::endl; #endif this->setError(jsonparser->getFormattedErrorMessages()); res = 0; } delete jsonparser; } else { this->setError("Found nothing in " + url); res = 0; } return res; } int API::login(const std::string& email, const std::string& password) { int res = 0; std::string url; std::string token, secret; // Get temporary request token url = oauth_sign_url2(this->config.oauth_get_temp_token.c_str(), NULL, OA_HMAC, NULL, CONSUMER_KEY.c_str(), CONSUMER_SECRET.c_str(), NULL /* token */, NULL /* secret */); std::string request_token_resp = this->getResponse(url); char **rv = NULL; int rc = oauth_split_url_parameters(request_token_resp.c_str(), &rv); qsort(rv, rc, sizeof(char *), oauth_cmpstringp); if (rc == 3 && !strncmp(rv[1], "oauth_token=", OAUTH_TOKEN_LENGTH) && !strncmp(rv[2], "oauth_token_secret=", OAUTH_SECRET_LENGTH)) { token = rv[1]+OAUTH_TOKEN_LENGTH+1; secret = rv[2]+OAUTH_SECRET_LENGTH+1; rv = NULL; } else { return res; } // Authorize temporary token and get verifier url = this->config.oauth_authorize_temp_token + "?username=" + oauth_url_escape(email.c_str()) + "&password=" + oauth_url_escape(password.c_str()); url = oauth_sign_url2(url.c_str(), NULL, OA_HMAC, NULL, CONSUMER_KEY.c_str(), CONSUMER_SECRET.c_str(), token.c_str(), secret.c_str()); std::string authorize_resp = this->getResponse(url); std::string verifier; rc = oauth_split_url_parameters(authorize_resp.c_str(), &rv); qsort(rv, rc, sizeof(char *), oauth_cmpstringp); if (rc == 2 && !strncmp(rv[1], "oauth_verifier=", OAUTH_VERIFIER_LENGTH)) { verifier = rv[1]+OAUTH_VERIFIER_LENGTH+1; rv = NULL; } else { return res; } // Get final token and secret url = this->config.oauth_get_token + "?oauth_verifier=" + verifier; url = oauth_sign_url2(url.c_str(), NULL, OA_HMAC, NULL, CONSUMER_KEY.c_str(), CONSUMER_SECRET.c_str(), token.c_str(), secret.c_str()); std::string token_resp = this->getResponse(url); rc = oauth_split_url_parameters(token_resp.c_str(), &rv); qsort(rv, rc, sizeof(char *), oauth_cmpstringp); if (rc == 2 && !strncmp(rv[0], "oauth_token=", OAUTH_TOKEN_LENGTH) && !strncmp(rv[1], "oauth_token_secret=", OAUTH_SECRET_LENGTH)) { this->config.oauth_token = rv[0]+OAUTH_TOKEN_LENGTH+1; this->config.oauth_secret = rv[1]+OAUTH_SECRET_LENGTH+1; free(rv); res = 1; } return res; } int API::getUserDetails() { int res = 0; std::string url; url = this->config.get_user_details; std::string json = this->getResponseOAuth(url); if (!json.empty()) { Json::Value root; Json::Reader *jsonparser = new Json::Reader; if (jsonparser->parse(json, root)) { #ifdef DEBUG std::cerr << "DEBUG INFO (API::getUserDetails)" << std::endl << root << std::endl; #endif this->user.id = std::stoull(root["user"]["id"].asString()); this->user.username = root["user"]["xywka"].asString(); this->user.email = root["user"]["email"].asString(); this->user.avatar_big = root["user"]["avatar"]["big"].asString(); this->user.avatar_small = root["user"]["avatar"]["small"].asString(); this->user.notifications_forum = root["user"]["notifications"]["forum"].isInt() ? root["user"]["notifications"]["forum"].asInt() : std::stoi(root["user"]["notifications"]["forum"].asString()); this->user.notifications_games = root["user"]["notifications"]["games"].isInt() ? root["user"]["notifications"]["games"].asInt() : std::stoi(root["user"]["notifications"]["games"].asString()); this->user.notifications_messages = root["user"]["notifications"]["messages"].isInt() ? root["user"]["notifications"]["messages"].asInt() : std::stoi(root["user"]["notifications"]["messages"].asString()); res = 1; } else { #ifdef DEBUG std::cerr << "DEBUG INFO (API::getUserDetails)" << std::endl << json << std::endl; #endif this->setError(jsonparser->getFormattedErrorMessages()); res = 0; } delete jsonparser; } else { this->setError("Found nothing in " + url); res = 0; } return res; } int API::getGames() { // Not implemented on the server side currently //std::string json = this->getResponseOAuth(this->config.get_user_games); return 0; } std::string API::getResponse(const std::string& url) { #ifdef DEBUG std::cerr << "DEBUG INFO (API::getResponse)" << std::endl << "URL: " << url << std::endl; #endif std::ostringstream memory; curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback); curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); CURLcode result = curl_easy_perform(curlhandle); std::string response = memory.str(); memory.str(std::string()); if (result == CURLE_HTTP_RETURNED_ERROR) { long int response_code = 0; result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); if (result == CURLE_OK) this->setError("HTTP ERROR: " + std::to_string(response_code) + " (" + url + ")"); else this->setError("HTTP ERROR: failed to get error code: " + static_cast(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; // Check for duplicate installers in different languages and add languageId of duplicate installer to the original installer // https://secure.gog.com/forum/general/introducing_the_beta_release_of_the_new_gogcom_downloader/post1483 if (useDuplicateHandler) { bool bDuplicate = false; for (unsigned int j = 0; j < game.installers.size(); ++j) { if (game.installers[j].path == installer["link"].asString()) { game.installers[j].language |= language; // Add language code to installer bDuplicate = true; break; } } if (bDuplicate) continue; } game.installers.push_back( gameFile( installer["notificated"].isInt() ? installer["notificated"].asInt() : std::stoi(installer["notificated"].asString()), installer["id"].isInt() ? std::to_string(installer["id"].asInt()) : installer["id"].asString(), installer["name"].asString(), installer["link"].asString(), installer["size"].asString(), language, installers[i].platform, installer["silent"].isInt() ? installer["silent"].asInt() : std::stoi(installer["silent"].asString()) ) ); } } // Extra details const Json::Value extras = root["game"]["extras"]; for ( unsigned int index = 0; index < extras.size(); ++index ) { Json::Value extra = extras[index]; game.extras.push_back( gameFile( false, /* extras don't have "updated" flag */ extra["id"].isInt() ? std::to_string(extra["id"].asInt()) : extra["id"].asString(), extra["name"].asString(), extra["link"].asString(), extra["size_mb"].asString() ) ); } // 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]; // Check for duplicate patches in different languages and add languageId of duplicate patch to the original patch if (useDuplicateHandler) { bool bDuplicate = false; for (unsigned int j = 0; j < game.patches.size(); ++j) { if (game.patches[j].path == patch["link"].asString()) { game.patches[j].language |= GlobalConstants::LANGUAGES[i].id; // Add language code to patch bDuplicate = true; break; } } if (bDuplicate) continue; } game.patches.push_back( gameFile( patch["notificated"].isInt() ? patch["notificated"].asInt() : std::stoi(patch["notificated"].asString()), patch["id"].isInt() ? std::to_string(patch["id"].asInt()) : patch["id"].asString(), patch["name"].asString(), patch["link"].asString(), patch["size"].asString(), GlobalConstants::LANGUAGES[i].id, patches[j].platform ) ); } } else // Patch is a single file { // Check for duplicate patches in different languages and add languageId of duplicate patch to the original patch if (useDuplicateHandler) { bool bDuplicate = false; for (unsigned int k = 0; k < game.patches.size(); ++k) { if (game.patches[k].path == patchnode["link"].asString()) { game.patches[k].language |= GlobalConstants::LANGUAGES[i].id; // Add language code to patch bDuplicate = true; break; } } if (bDuplicate) continue; } game.patches.push_back( gameFile( patchnode["notificated"].isInt() ? patchnode["notificated"].asInt() : std::stoi(patchnode["notificated"].asString()), patchnode["id"].isInt() ? std::to_string(patchnode["id"].asInt()) : patchnode["id"].asString(), patchnode["name"].asString(), patchnode["link"].asString(), patchnode["size"].asString(), GlobalConstants::LANGUAGES[i].id, patches[j].platform ) ); } } } } } // 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]]; game.languagepacks.push_back( gameFile( false, /* language packs don't have "updated" flag */ langpack["id"].isInt() ? std::to_string(langpack["id"].asInt()) : langpack["id"].asString(), langpack["name"].asString(), langpack["link"].asString(), langpack["size"].asString(), GlobalConstants::LANGUAGES[i].id ) ); } } } } } 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-2.26/src/blacklist.cpp000066400000000000000000000050611260744454200177270ustar00rootroot00000000000000/* 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-2.26/src/downloader.cpp000066400000000000000000004153421260744454200201240ustar00rootroot00000000000000/* This program is free software. It comes without any warranty, to * the extent permitted by applicable law. You can redistribute it * and/or modify it under the terms of the Do What The Fuck You Want * To Public License, Version 2, as published by Sam Hocevar. See * http://www.wtfpl.net/ for more details. */ #include "downloader.h" #include "util.h" #include "globalconstants.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace bptime = boost::posix_time; Downloader::Downloader(Config &conf) { this->config = conf; if (config.bLoginHTTP && boost::filesystem::exists(config.sCookiePath)) if (!boost::filesystem::remove(config.sCookiePath)) std::cout << "Failed to delete " << config.sCookiePath << std::endl; } Downloader::~Downloader() { if (config.bReport) if (this->report_ofs) this->report_ofs.close(); delete progressbar; delete gogAPI; curl_easy_cleanup(curlhandle); curl_global_cleanup(); // Make sure that cookie file is only readable/writable by owner Util::setFilePermissions(config.sCookiePath, boost::filesystem::owner_read | boost::filesystem::owner_write); } /* Initialize the downloader returns 0 if successful returns 1 if failed */ int Downloader::init() { this->resume_position = 0; this->retries = 0; // Initialize curl and set curl options curl_global_init(CURL_GLOBAL_ALL); curlhandle = curl_easy_init(); curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, config.sVersionString.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, config.iTimeout); curl_easy_setopt(curlhandle, CURLOPT_PROGRESSDATA, this); curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true); curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, config.sCookiePath.c_str()); curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, config.sCookiePath.c_str()); curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer); curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, config.bVerbose); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData); curl_easy_setopt(curlhandle, CURLOPT_READFUNCTION, Downloader::readData); curl_easy_setopt(curlhandle, CURLOPT_PROGRESSFUNCTION, Downloader::progressCallback); curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate); // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30); curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200); // Create new API handle and set curl options for the API gogAPI = new API(config.sToken, config.sSecret); gogAPI->curlSetOpt(CURLOPT_VERBOSE, config.bVerbose); gogAPI->curlSetOpt(CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer); gogAPI->curlSetOpt(CURLOPT_CONNECTTIMEOUT, config.iTimeout); progressbar = new ProgressBar(config.bUnicode, config.bColor); bool bInitOK = gogAPI->init(); // Initialize the API if (!bInitOK || config.bLoginHTTP || config.bLoginAPI) return 1; if (config.bCover && config.bDownload && !config.bUpdateCheck) coverXML = this->getResponse(config.sCoverList); // updateCheck() calls getGameList() if needed // getGameList() is not needed when using cache unless we want to list games from account if ( !config.bUpdateCheck && (!config.bUseCache || (config.bUseCache && config.bList)) && config.sFileIdString.empty() && !config.bShowWishlist ) this->getGameList(); if (config.bReport && (config.bDownload || config.bRepair)) { this->report_ofs.open(config.sReportFilePath); if (!this->report_ofs) { std::cout << "Failed to create " << config.sReportFilePath << std::endl; return 1; } } return 0; } /* Login returns 0 if login fails returns 1 if successful */ int Downloader::login() { char *pwd; std::string email; if (!isatty(STDIN_FILENO)) { std::cout << "Unable to read email and password" << std::endl; return 0; } std::cout << "Email: "; std::getline(std::cin,email); pwd = getpass("Password: "); std::string password = (std::string)pwd; if (email.empty() || password.empty()) { std::cout << "Email and/or password empty" << std::endl; return 0; } else { // Login to website if (config.bLoginHTTP) { if (!HTTP_Login(email, password)) { std::cout << "HTTP: Login failed" << std::endl; return 0; } else { std::cout << "HTTP: Login successful" << std::endl; if (!config.bLoginAPI) return 1; } } // Login to API if (config.bLoginAPI) { if (!gogAPI->login(email, password)) { std::cout << "API: Login failed" << std::endl; return 0; } else { std::cout << "API: Login successful" << std::endl; config.sToken = gogAPI->getToken(); config.sSecret = gogAPI->getSecret(); return 1; } } } return 0; } void Downloader::updateCheck() { std::cout << "New forum replies: " << gogAPI->user.notifications_forum << std::endl; std::cout << "New private messages: " << gogAPI->user.notifications_messages << std::endl; std::cout << "Updated games: " << gogAPI->user.notifications_games << std::endl; if (gogAPI->user.notifications_games) { config.sGameRegex = ".*"; // Always check all games if (config.bList || config.bListDetails || config.bDownload) { if (config.bList) config.bListDetails = true; // Always list details this->getGameList(); if (config.bDownload) this->download(); else this->listGames(); } } } void Downloader::getGameList() { if (config.sGameRegex == "free") { gameItems = this->getFreeGames(); } else { gameItems = this->getGames(); } } /* Get detailed info about the games returns 0 if successful returns 1 if fails */ int Downloader::getGameDetails() { if (config.bUseCache && !config.bUpdateCache) { // GameRegex filter alias for all games if (config.sGameRegex == "all") config.sGameRegex = ".*"; else if (config.sGameRegex == "free") std::cout << "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) this->games[i].makeFilepaths(config); return 0; } else { if (result == 1) { std::cout << "Cache doesn't exist." << std::endl; std::cout << "Create cache with --update-cache" << std::endl; } else if (result == 3) { std::cout << "Cache is too old." << std::endl; std::cout << "Update cache with --update-cache or use bigger --cache-valid" << std::endl; } return 1; } } gameDetails game; int updated = 0; for (unsigned int i = 0; i < gameItems.size(); ++i) { std::cout << "Getting game info " << i+1 << " / " << gameItems.size() << "\r" << std::flush; bool bHasDLC = !gameItems[i].dlcnames.empty(); gameSpecificConfig conf; conf.bDLC = config.bDLC; conf.bIgnoreDLCCount = false; conf.iInstallerLanguage = config.iInstallerLanguage; conf.iInstallerPlatform = config.iInstallerPlatform; if (!config.bUpdateCache) // Disable game specific config files for cache update { if (Util::getGameSpecificConfig(gameItems[i].name, &conf) > 0) std::cout << std::endl << gameItems[i].name << " - Language: " << conf.iInstallerLanguage << ", Platform: " << conf.iInstallerPlatform << ", DLC: " << (conf.bDLC ? "true" : "false") << ", Ignore DLC count: " << (conf.bIgnoreDLCCount ? "true" : "false") << std::endl; } game = gogAPI->getGameDetails(gameItems[i].name, conf.iInstallerPlatform, conf.iInstallerLanguage, config.bDuplicateHandler); if (!gogAPI->getError()) { game.filterWithPriorities(config); Json::Value gameDetailsJSON; if (game.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras { gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); game.extras = this->getExtrasFromJSON(gameDetailsJSON, gameItems[i].name); } if (config.bSaveSerials) { if (gameDetailsJSON.empty()) gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); game.serials = this->getSerialsFromJSON(gameDetailsJSON); } // Ignore DLC count and try to get DLCs from JSON if (game.dlcs.empty() && !bHasDLC && conf.bDLC && conf.bIgnoreDLCCount) { if (gameDetailsJSON.empty()) gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); gameItems[i].dlcnames = Util::getDLCNamesFromJSON(gameDetailsJSON["dlcs"]); bHasDLC = !gameItems[i].dlcnames.empty(); } if (game.dlcs.empty() && bHasDLC && conf.bDLC) { for (unsigned int j = 0; j < gameItems[i].dlcnames.size(); ++j) { gameDetails dlc; dlc = gogAPI->getGameDetails(gameItems[i].dlcnames[j], conf.iInstallerPlatform, conf.iInstallerLanguage, config.bDuplicateHandler); dlc.filterWithPriorities(config); if (dlc.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras { if (gameDetailsJSON.empty()) gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); // Make sure we get extras for the right DLC for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k) { std::vector urls; if (gameDetailsJSON["dlcs"][k].isMember("extras")) Util::getDownloaderUrlsFromJSON(gameDetailsJSON["dlcs"][k]["extras"], urls); if (!urls.empty()) { if (urls[0].find("/" + gameItems[i].dlcnames[j] + "/") != std::string::npos) { dlc.extras = this->getExtrasFromJSON(gameDetailsJSON["dlcs"][k], gameItems[i].dlcnames[j]); } } } } if (config.bSaveSerials) { if (gameDetailsJSON.empty()) gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id); // Make sure we save serial for the right DLC for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k) { std::vector urls; if (gameDetailsJSON["dlcs"][k].isMember("cdKey") && gameDetailsJSON["dlcs"][k].isMember("downloads")) { // Assuming that only DLC with installers can have serial Util::getDownloaderUrlsFromJSON(gameDetailsJSON["dlcs"][k]["downloads"], urls); } if (!urls.empty()) { if (urls[0].find("/" + gameItems[i].dlcnames[j] + "/") != std::string::npos) { dlc.serials = this->getSerialsFromJSON(gameDetailsJSON["dlcs"][k]); } } } } game.dlcs.push_back(dlc); } } game.makeFilepaths(config); if (!config.bUpdateCheck) games.push_back(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) { games.push_back(game); updated++; break; // add the game only once } } if (updated >= gogAPI->user.notifications_games) { // Gone through all updated games. No need to go through the rest. std::cout << std::endl << "Got info for all updated games. Moving on..." << std::endl; break; } } } else { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } } std::cout << std::endl; return 0; } void Downloader::listGames() { if (config.bListDetails) // Detailed list { if (this->games.empty()) this->getGameDetails(); for (unsigned int i = 0; i < games.size(); ++i) { std::cout << "gamename: " << games[i].gamename << std::endl << "title: " << games[i].title << std::endl << "icon: " << "http://static.gog.com" << games[i].icon << std::endl; if (!games[i].serials.empty()) std::cout << "serials:" << std::endl << games[i].serials << std::endl; // List installers if (config.bInstallers) { std::cout << "installers: " << std::endl; for (unsigned int j = 0; j < games[i].installers.size(); ++j) { if (!config.bUpdateCheck || games[i].installers[j].updated) // Always list updated files { std::string filepath = games[i].installers[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::string languages = Util::getOptionNameString(games[i].installers[j].language, GlobalConstants::LANGUAGES); std::cout << "\tid: " << games[i].installers[j].id << std::endl << "\tname: " << games[i].installers[j].name << std::endl << "\tpath: " << games[i].installers[j].path << std::endl << "\tsize: " << games[i].installers[j].size << std::endl << "\tupdated: " << (games[i].installers[j].updated ? "True" : "False") << std::endl << "\tlanguage: " << languages << std::endl << std::endl; } } } // List extras if (config.bExtras && !config.bUpdateCheck && !games[i].extras.empty()) { std::cout << "extras: " << std::endl; for (unsigned int j = 0; j < games[i].extras.size(); ++j) { std::string filepath = games[i].extras[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tid: " << games[i].extras[j].id << std::endl << "\tname: " << games[i].extras[j].name << std::endl << "\tpath: " << games[i].extras[j].path << std::endl << "\tsize: " << games[i].extras[j].size << std::endl << std::endl; } } // List patches if (config.bPatches && !config.bUpdateCheck && !games[i].patches.empty()) { std::cout << "patches: " << std::endl; for (unsigned int j = 0; j < games[i].patches.size(); ++j) { std::string filepath = games[i].patches[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::string languages = Util::getOptionNameString(games[i].patches[j].language, GlobalConstants::LANGUAGES); std::cout << "\tid: " << games[i].patches[j].id << std::endl << "\tname: " << games[i].patches[j].name << std::endl << "\tpath: " << games[i].patches[j].path << std::endl << "\tsize: " << games[i].patches[j].size << std::endl << "\tupdated: " << (games[i].patches[j].updated ? "True" : "False") << std::endl << "\tlanguage: " << languages << std::endl << std::endl; } } // List language packs if (config.bLanguagePacks && !config.bUpdateCheck && !games[i].languagepacks.empty()) { std::cout << "language packs: " << std::endl; for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j) { std::string filepath = games[i].languagepacks[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tid: " << games[i].languagepacks[j].id << std::endl << "\tname: " << games[i].languagepacks[j].name << std::endl << "\tpath: " << games[i].languagepacks[j].path << std::endl << "\tsize: " << games[i].languagepacks[j].size << std::endl << std::endl; } } if (config.bDLC && !games[i].dlcs.empty()) { std::cout << "DLCs: " << std::endl; for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) { if (!games[i].dlcs[j].serials.empty()) { std::cout << "\tDLC gamename: " << games[i].dlcs[j].gamename << std::endl << "\tserials:" << games[i].dlcs[j].serials << std::endl; } for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) { std::string filepath = games[i].dlcs[j].installers[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl << "\tid: " << games[i].dlcs[j].installers[k].id << std::endl << "\tname: " << games[i].dlcs[j].installers[k].name << std::endl << "\tpath: " << games[i].dlcs[j].installers[k].path << std::endl << "\tsize: " << games[i].dlcs[j].installers[k].size << std::endl << "\tupdated: " << (games[i].dlcs[j].installers[k].updated ? "True" : "False") << std::endl << std::endl; } for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k) { std::string filepath = games[i].dlcs[j].patches[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl << "\tid: " << games[i].dlcs[j].patches[k].id << std::endl << "\tname: " << games[i].dlcs[j].patches[k].name << std::endl << "\tpath: " << games[i].dlcs[j].patches[k].path << std::endl << "\tsize: " << games[i].dlcs[j].patches[k].size << std::endl << std::endl; } for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k) { std::string filepath = games[i].dlcs[j].extras[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl << "\tid: " << games[i].dlcs[j].extras[k].id << std::endl << "\tname: " << games[i].dlcs[j].extras[k].name << std::endl << "\tpath: " << games[i].dlcs[j].extras[k].path << std::endl << "\tsize: " << games[i].dlcs[j].extras[k].size << std::endl << std::endl; } } } } } else { // List game names for (unsigned int i = 0; i < gameItems.size(); ++i) { std::cout << gameItems[i].name << std::endl; for (unsigned int j = 0; j < gameItems[i].dlcnames.size(); ++j) std::cout << "+> " << gameItems[i].dlcnames[j] << std::endl; } } } void Downloader::repair() { if (this->games.empty()) this->getGameDetails(); for (unsigned int i = 0; i < games.size(); ++i) { // Installers (use remote or local file) if (config.bInstallers) { for (unsigned int j = 0; j < games[i].installers.size(); ++j) { std::string filepath = games[i].installers[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get XML data std::string XML = ""; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].gamename, games[i].installers[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } } // Repair bool bUseLocalXML = !config.bRemoteXML; if (!XML.empty() || bUseLocalXML) { std::string url = gogAPI->getInstallerLink(games[i].gamename, games[i].installers[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, XML, games[i].gamename); std::cout << std::endl; } } } // Extras (GOG doesn't provide XML data for extras, use local file) if (config.bExtras) { for (unsigned int j = 0; j < games[i].extras.size(); ++j) { std::string filepath = games[i].extras[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::string url = gogAPI->getExtraLink(games[i].gamename, games[i].extras[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, std::string(), games[i].gamename); std::cout << std::endl; } } // Patches (use remote or local file) if (config.bPatches) { for (unsigned int j = 0; j < games[i].patches.size(); ++j) { std::string filepath = games[i].patches[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get XML data std::string XML = ""; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].gamename, games[i].patches[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } std::string url = gogAPI->getPatchLink(games[i].gamename, games[i].patches[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, XML, games[i].gamename); std::cout << std::endl; } } // Language packs (GOG doesn't provide XML data for language packs, use local file) if (config.bLanguagePacks) { for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j) { std::string filepath = games[i].languagepacks[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::string url = gogAPI->getLanguagePackLink(games[i].gamename, games[i].languagepacks[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, std::string(), games[i].gamename); std::cout << std::endl; } } if (config.bDLC && !games[i].dlcs.empty()) { for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) { if (config.bInstallers) { for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) { std::string filepath = games[i].dlcs[j].installers[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get XML data std::string XML = ""; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].dlcs[j].gamename, games[i].dlcs[j].installers[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } } // Repair bool bUseLocalXML = !config.bRemoteXML; if (!XML.empty() || bUseLocalXML) { std::string url = gogAPI->getInstallerLink(games[i].dlcs[j].gamename, games[i].dlcs[j].installers[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, XML, games[i].dlcs[j].gamename); std::cout << std::endl; } } } if (config.bPatches) { for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k) { std::string filepath = games[i].dlcs[j].patches[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get XML data std::string XML = ""; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].dlcs[j].gamename, games[i].dlcs[j].patches[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } std::string url = gogAPI->getPatchLink(games[i].dlcs[j].gamename, games[i].dlcs[j].patches[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, XML, games[i].dlcs[j].gamename); std::cout << std::endl; } } if (config.bExtras) { for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k) { std::string filepath = games[i].dlcs[j].extras[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } std::string url = gogAPI->getExtraLink(games[i].dlcs[j].gamename, games[i].dlcs[j].extras[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::cout << "Repairing file " << filepath << std::endl; this->repairFile(url, filepath, std::string(), games[i].dlcs[j].gamename); std::cout << std::endl; } } } } } } void Downloader::download() { if (this->games.empty()) this->getGameDetails(); for (unsigned int i = 0; i < games.size(); ++i) { if (config.bSaveSerials && !games[i].serials.empty()) { std::string filepath = games[i].getSerialsFilepath(); this->saveSerials(games[i].serials, filepath); } // Download covers if (config.bCover && !config.bUpdateCheck) { if (!games[i].installers.empty()) { // Take path from installer path because for some games the base directory for installer/extra path is not "gamename" std::string filepath = games[i].installers[0].getFilepath(); // Get base directory from filepath boost::match_results what; boost::regex expression("(.*)/.*"); boost::regex_match(filepath, what, expression); std::string directory = what[1]; this->downloadCovers(games[i].gamename, directory, coverXML); } } // Download installers if (config.bInstallers) { for (unsigned int j = 0; j < games[i].installers.size(); ++j) { // Not updated, skip to next installer if (config.bUpdateCheck && !games[i].installers[j].updated) continue; std::string filepath = games[i].installers[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get link std::string url = gogAPI->getInstallerLink(games[i].gamename, games[i].installers[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } // Download if (!url.empty()) { std::string XML; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].gamename, games[i].installers[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } if (!games[i].installers[j].name.empty()) std::cout << "Downloading: " << games[i].installers[j].name << std::endl; std::cout << filepath << std::endl; this->downloadFile(url, filepath, XML, games[i].gamename); std::cout << std::endl; } } } // Download extras if (config.bExtras && !config.bUpdateCheck) { // Save some time and don't process extras when running update check. Extras don't have updated flag, all of them would be skipped anyway. for (unsigned int j = 0; j < games[i].extras.size(); ++j) { // Get link std::string url = gogAPI->getExtraLink(games[i].gamename, games[i].extras[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::string filepath = games[i].extras[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Download if (!url.empty()) { if (!games[i].extras[j].name.empty()) std::cout << "Downloading: " << games[i].extras[j].name << std::endl; std::cout << filepath << std::endl; CURLcode result = this->downloadFile(url, filepath); std::cout << std::endl; if (result==CURLE_OK && config.sXMLFile == "automatic") { std::cout << "Starting automatic XML creation" << std::endl; std::string xml_dir = config.sXMLDirectory + "/" + games[i].gamename; Util::createXML(filepath, config.iChunkSize, xml_dir); std::cout << std::endl; } } } } // Download patches if (config.bPatches) { for (unsigned int j = 0; j < games[i].patches.size(); ++j) { // Not updated, skip to next patch if (config.bUpdateCheck && !games[i].patches[j].updated) continue; // Get link std::string url = gogAPI->getPatchLink(games[i].gamename, games[i].patches[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::string filepath = games[i].patches[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Download if (!url.empty()) { std::string XML; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].gamename, games[i].patches[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } if (!games[i].patches[j].name.empty()) std::cout << "Downloading: " << games[i].patches[j].name << std::endl; std::cout << filepath << std::endl; this->downloadFile(url, filepath, XML, games[i].gamename); std::cout << std::endl; } } } // Download language packs if (config.bLanguagePacks) { for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j) { // Get link std::string url = gogAPI->getLanguagePackLink(games[i].gamename, games[i].languagepacks[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } std::string filepath = games[i].languagepacks[j].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Download if (!url.empty()) { std::string XML; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].gamename, games[i].languagepacks[j].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } if (!games[i].languagepacks[j].name.empty()) std::cout << "Downloading: " << games[i].gamename << " " << games[i].languagepacks[j].name << std::endl; std::cout << filepath << std::endl; this->downloadFile(url, filepath, XML, games[i].gamename); std::cout << std::endl; } } } if (config.bDLC && !games[i].dlcs.empty()) { for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) { if (config.bSaveSerials && !games[i].dlcs[j].serials.empty()) { std::string filepath = games[i].dlcs[j].getSerialsFilepath(); this->saveSerials(games[i].dlcs[j].serials, filepath); } if (config.bInstallers) { for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) { std::string filepath = games[i].dlcs[j].installers[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get link std::string url = gogAPI->getInstallerLink(games[i].dlcs[j].gamename, games[i].dlcs[j].installers[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } // Download if (!url.empty()) { std::string XML; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].dlcs[j].gamename, games[i].dlcs[j].installers[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } if (!games[i].dlcs[j].installers[k].name.empty()) std::cout << "Downloading: " << games[i].dlcs[j].installers[k].name << std::endl; std::cout << filepath << std::endl; this->downloadFile(url, filepath, XML, games[i].dlcs[j].gamename); std::cout << std::endl; } } } if (config.bPatches) { for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k) { std::string filepath = games[i].dlcs[j].patches[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get link std::string url = gogAPI->getPatchLink(games[i].dlcs[j].gamename, games[i].dlcs[j].patches[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } // Download if (!url.empty()) { std::string XML; if (config.bRemoteXML) { XML = gogAPI->getXML(games[i].dlcs[j].gamename, games[i].dlcs[j].patches[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } if (!games[i].dlcs[j].patches[k].name.empty()) std::cout << "Downloading: " << games[i].dlcs[j].patches[k].name << std::endl; std::cout << filepath << std::endl; this->downloadFile(url, filepath, XML, games[i].dlcs[j].gamename); std::cout << std::endl; } } } if (config.bExtras) { for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k) { std::string filepath = games[i].dlcs[j].extras[k].getFilepath(); if (config.blacklist.isBlacklisted(filepath)) { if (config.bVerbose) std::cout << "skipped blacklisted file " << filepath << std::endl; continue; } // Get link std::string url = gogAPI->getExtraLink(games[i].dlcs[j].gamename, games[i].dlcs[j].extras[k].id); if (gogAPI->getError()) { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); continue; } // Download if (!url.empty()) { if (!games[i].dlcs[j].extras[k].name.empty()) std::cout << "Dowloading: " << games[i].dlcs[j].extras[k].name << std::endl; CURLcode result = this->downloadFile(url, filepath); std::cout << std::endl; if (result==CURLE_OK && config.sXMLFile == "automatic") { std::cout << "Starting automatic XML creation" << std::endl; std::string xml_dir = config.sXMLDirectory + "/" + games[i].dlcs[j].gamename; Util::createXML(filepath, config.iChunkSize, xml_dir); std::cout << std::endl; } } } } } } } } // Download a file, resume if possible CURLcode Downloader::downloadFile(const std::string& url, const std::string& filepath, const std::string& xml_data, const std::string& gamename) { CURLcode res = CURLE_RECV_ERROR; // assume network error bool bResume = false; FILE *outfile; off_t offset=0; // Get directory from filepath boost::filesystem::path pathname = filepath; pathname = boost::filesystem::absolute(pathname, boost::filesystem::current_path()); std::string directory = pathname.parent_path().string(); std::string filenameXML = pathname.filename().string() + ".xml"; std::string xml_directory; if (!gamename.empty()) xml_directory = config.sXMLDirectory + "/" + gamename; else xml_directory = config.sXMLDirectory; // Using local XML data for version check before resuming boost::filesystem::path local_xml_file; local_xml_file = xml_directory + "/" + filenameXML; bool bSameVersion = true; // assume same version bool bLocalXMLExists = boost::filesystem::exists(local_xml_file); // This is additional check to see if remote xml should be saved to speed up future version checks if (!xml_data.empty()) { std::string localHash = this->getLocalFileHash(filepath, gamename); // Do version check if local hash exists if (!localHash.empty()) { TiXmlDocument remote_xml; remote_xml.Parse(xml_data.c_str()); TiXmlNode *fileNodeRemote = remote_xml.FirstChild("file"); if (fileNodeRemote) { TiXmlElement *fileElemRemote = fileNodeRemote->ToElement(); 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::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; } } // 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::cout << "Failed to reopen " << filepath << std::endl; return res; } } else { // File exists but is not the same version fclose(outfile); std::cout << "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::cout << "Failed to rename " << filepath << " to " << new_name.string() << std::endl; std::cout << "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::cout << "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::cout << "Failed to create " << filepath << std::endl; return res; } } // Save remote XML if (!xml_data.empty()) { if ((bLocalXMLExists && (!bSameVersion || config.bRepair)) || !bLocalXMLExists) { // Check that directory exists and create subdirectories boost::filesystem::path path = xml_directory; if (boost::filesystem::exists(path)) { if (!boost::filesystem::is_directory(path)) { std::cout << path << " is not directory" << std::endl; } } else { if (!boost::filesystem::create_directories(path)) { std::cout << "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::cout << "Can't create " << local_xml_file.string() << std::endl; } } } curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, outfile); res = this->beginDownload(); fclose(outfile); // Download failed and was not a resume attempt so delete the file if (res != CURLE_OK && res != CURLE_PARTIAL_FILE && !bResume && res != CURLE_OPERATION_TIMEDOUT) { boost::filesystem::path path = filepath; if (boost::filesystem::exists(path)) if (!boost::filesystem::remove(path)) std::cout << "Failed to delete " << path << std::endl; } if (config.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 < config.iRetries) ) { this->retries++; std::cerr << std::endl << "Retry " << this->retries << "/" << config.iRetries; if (res == CURLE_PARTIAL_FILE) std::cerr << " (partial download)"; else if (res == CURLE_OPERATION_TIMEDOUT) std::cerr << " (timeout)"; std::cerr << std::endl; res = this->downloadFile(url, filepath, xml_data, gamename); } else { this->retries = 0; // Reset retries counter } return res; } // Repair file int Downloader::repairFile(const std::string& url, const std::string& filepath, const std::string& xml_data, const std::string& gamename) { int res = 0; FILE *outfile; off_t offset=0, from_offset, to_offset, filesize; std::string filehash; int chunks; std::vector 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 = config.sXMLDirectory + "/" + gamename; else xml_directory = config.sXMLDirectory; std::string xml_file = xml_directory + "/" + filename + ".xml"; bool bFileExists = boost::filesystem::exists(pathname); bool bLocalXMLExists = boost::filesystem::exists(xml_file); TiXmlDocument 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); } // Check if file node exists in XML data TiXmlNode *fileNode = xml.FirstChild("file"); if (!fileNode) { // File node doesn't exist std::cout << "XML: Parsing failed / not valid XML" << std::endl; if (config.bDownload) bParsingFailed = true; else return res; } else { // File node exists --> valid XML std::cout << "XML: Valid XML" << std::endl; TiXmlElement *fileElem = fileNode->ToElement(); 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 TiXmlNode *chunkNode = fileNode->FirstChild(); while (chunkNode) { TiXmlElement *chunkElem = chunkNode->ToElement(); 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()); chunkNode = fileNode->IterateChildren(chunkNode); } std::cout << "XML: Parsing finished" << std::endl << std::endl << filename << std::endl << "\tMD5:\t" << filehash << std::endl << "\tChunks:\t" << chunks << std::endl << "\tSize:\t" << filesize << " bytes" << std::endl << std::endl; } // No local XML file and parsing failed. if (bParsingFailed && !bLocalXMLExists) { if (this->config.bDownload) { std::cout << "Downloading: " << filepath << std::endl; CURLcode result = this->downloadFile(url, filepath, xml_data, gamename); std::cout << std::endl; if ( (!bFileExists && result == CURLE_OK) || /* File doesn't exist so only accept if everything was OK */ (bFileExists && (result == CURLE_OK || result == CURLE_RANGE_ERROR )) /* File exists so accept also CURLE_RANGE_ERROR because curl will return CURLE_RANGE_ERROR */ ) /* if the file is already fully downloaded and we want to resume it */ { bLocalXMLExists = boost::filesystem::exists(xml_file); // Check to see if downloadFile saved XML data if (config.sXMLFile == "automatic" && !bLocalXMLExists) { std::cout << "Starting automatic XML creation" << std::endl; Util::createXML(filepath, config.iChunkSize, xml_directory); } res = 1; } } else { std::cout << "Can't repair file." << std::endl; } return res; } // Check if file exists if (bFileExists) { // File exists if ((outfile = fopen(filepath.c_str(), "r+"))!=NULL ) { fseek(outfile, 0, SEEK_END); // use ftello to support large files on 32 bit platforms offset = ftello(outfile); } else { std::cout << "Failed to open " << filepath << std::endl; return res; } } else { std::cout << "File doesn't exist " << filepath << std::endl; if (this->config.bDownload) { std::cout << "Downloading: " << filepath << std::endl; CURLcode result = this->downloadFile(url, filepath, xml_data, gamename); std::cout << std::endl; if (result == CURLE_OK) { if (config.sXMLFile == "automatic" && bParsingFailed) { std::cout << "Starting automatic XML creation" << std::endl; Util::createXML(filepath, config.iChunkSize, xml_directory); } res = 1; } } return res; } // check if file sizes match if (offset != filesize) { std::cout << "Filesizes don't match" << std::endl << "Incomplete download or different version" << std::endl; fclose(outfile); if (this->config.bDownload) { std::cout << "Redownloading file" << std::endl; std::string date_old = "." + bptime::to_iso_string(bptime::second_clock::local_time()) + ".old"; boost::filesystem::path new_name = filepath + date_old; // Rename old file by appending date and ".old" to filename std::cout << "Renaming old file to " << new_name.string() << std::endl; boost::system::error_code ec; boost::filesystem::rename(pathname, new_name, ec); // Rename the file if (ec) { std::cout << "Failed to rename " << filepath << " to " << new_name.string() << std::endl; std::cout << "Skipping file" << std::endl; res = 0; } else { if (bLocalXMLExists) { std::cout << "Deleting old XML data" << std::endl; boost::filesystem::remove(xml_file, ec); // Delete old XML data if (ec) { std::cout << "Failed to delete " << xml_file << std::endl; } } CURLcode result = this->downloadFile(url, filepath, xml_data, gamename); std::cout << std::endl; if (result == CURLE_OK) { bLocalXMLExists = boost::filesystem::exists(xml_file); // Check to see if downloadFile saved XML data if (!bLocalXMLExists) { std::cout << "Starting automatic XML creation" << std::endl; Util::createXML(filepath, config.iChunkSize, xml_directory); } res = 1; } else { res = 0; } } } return res; } // Check all chunks int iChunksRepaired = 0; for (int i=0; ibeginDownload(); //begin chunk download std::cout << std::endl; if (config.bReport) iChunksRepaired++; i--; //verify downloaded chunk } else { std::cout << "OK\r" << std::flush; } free(chunk); res = 1; } std::cout << std::endl; fclose(outfile); if (config.bReport) { std::string report_line = "Repaired [" + std::to_string(iChunksRepaired) + "/" + std::to_string(chunks) + "] " + filepath; this->report_ofs << report_line << std::endl; } return res; } // Download cover images int Downloader::downloadCovers(const std::string& gamename, const std::string& directory, const std::string& cover_xml_data) { int res = 0; TiXmlDocument 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()); TiXmlElement *rootNode = xml.RootElement(); if (!rootNode) { std::cout << "Not valid XML" << std::endl; return res; } else { TiXmlNode *gameNode = rootNode->FirstChild(); while (gameNode) { TiXmlElement *gameElem = gameNode->ToElement(); std::string game_name = gameElem->Attribute("name"); if (game_name == gamename) { boost::match_results what; TiXmlNode *coverNode = gameNode->FirstChild(); while (coverNode) { TiXmlElement *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 = gameNode->IterateChildren(coverNode); } break; // Found cover for game, no need to go through rest of the game nodes } gameNode = rootNode->IterateChildren(gameNode); } } return res; } CURLcode Downloader::beginDownload() { this->TimeAndSize.clear(); this->timer.reset(); CURLcode result = curl_easy_perform(curlhandle); this->resume_position = 0; return result; } std::string Downloader::getResponse(const std::string& url) { std::ostringstream memory; std::string response; curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback); curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); CURLcode result; do { if (config.iWait > 0) usleep(config.iWait); // Delay the request by specified time result = curl_easy_perform(curlhandle); response = memory.str(); memory.str(std::string()); } while ((result != CURLE_OK) && response.empty() && (this->retries++ < config.iRetries)); this->retries = 0; // reset retries counter curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); if (result != CURLE_OK) { std::cout << curl_easy_strerror(result) << std::endl; if (result == CURLE_HTTP_RETURNED_ERROR) { long int response_code = 0; result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); std::cout << "HTTP ERROR: "; if (result == CURLE_OK) std::cout << response_code << " (" << url << ")" << std::endl; else std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl; } } return response; } int Downloader::progressCallback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow) { // on entry: dltotal - how much remains to download till the end of the file (bytes) // dlnow - how much was downloaded from the start of the program (bytes) 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.0 == dlnow) && (0.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.0 == dlnow) && (389.0 == dltotal)) dltotal = 0.0; // setting full dlwnow and dltotal double 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 : dlnow / dltotal; // assuming that config is provided. printf("\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"; } char status_text[200]; // We're probably never going to go as high as 200 characters but it's better to use too big number here than too small sprintf(status_text, " %0.2f/%0.2fMB @ %0.2f%s ETA: %s\r", dlnow/1024/1024, dltotal/1024/1024, rate, rate_unit.c_str(), eta_ss.str().c_str()); int status_text_length = strlen(status_text) + 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; } // Login to GOG website int Downloader::HTTP_Login(const std::string& email, const std::string& password) { int res = 0; std::string postdata; std::ostringstream memory; std::string token; std::string tagname_username; std::string tagname_password; std::string tagname_login; std::string tagname_token; // Get login token std::string html = this->getResponse("https://www.gog.com/"); htmlcxx::HTML::ParserDom parser; tree dom = parser.parseTree(html); tree::iterator it = dom.begin(); tree::iterator end = dom.end(); // Find auth_url bool bFoundAuthUrl = false; for (; it != end; ++it) { if (it->tagName()=="script") { std::string auth_url; for (unsigned int i = 0; i < dom.number_of_children(it); ++i) { tree::iterator script_it = dom.child(it, i); if (!script_it->isTag() && !script_it->isComment()) { if (script_it->text().find("GalaxyAccounts") != std::string::npos) { boost::match_results what; boost::regex expression(".*'(https://auth.gog.com/.*?)'.*"); boost::regex_match(script_it->text(), what, expression); auth_url = what[1]; break; } } } if (!auth_url.empty()) { // Found auth_url, get the necessary info for login bFoundAuthUrl = true; std::string login_form_html = this->getResponse(auth_url); #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl; std::cerr << login_form_html << std::endl; #endif if (login_form_html.find("google.com/recaptcha") != std::string::npos) { std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl << "Login with browser and export cookies to \"" << config.sCookiePath << "\"" << std::endl; return res = 0; } tree 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(); std::string id_login = login_it->attribute("id").second; if (id_login == "login_username") { tagname_username = login_it->attribute("name").second; } else if (id_login == "login_password") { tagname_password = login_it->attribute("name").second; } else if (id_login == "login__token") { token = login_it->attribute("value").second; // login token tagname_token = login_it->attribute("name").second; } } else if (login_it->tagName()=="button") { login_it->parseAttributes(); std::string id_login = login_it->attribute("id").second; if (id_login == "login_login") { tagname_login = login_it->attribute("name").second; } } } break; } } } if (!bFoundAuthUrl) { std::cout << "Failed to find url for login form" << std::endl; } if (token.empty()) { std::cout << "Failed to get login token" << std::endl; return res = 0; } //Create postdata - escape characters in email/password to support special characters postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size()) + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size()) + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "=" + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size()); curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check"); curl_easy_setopt(curlhandle, CURLOPT_POST, 1); curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback); curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first. curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); CURLcode result = curl_easy_perform(curlhandle); memory.str(std::string()); if (result != CURLE_OK) { // Expected to hit maximum amount of redirects so don't print error on it if (result != CURLE_TOO_MANY_REDIRECTS) std::cout << curl_easy_strerror(result) << std::endl; } // Get redirect url char *redirect_url; curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1); curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1); curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); result = curl_easy_perform(curlhandle); if (result != CURLE_OK) { std::cout << curl_easy_strerror(result) << std::endl; } html = this->getResponse("https://www.gog.com/account/settings/personal"); std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it dom = parser.parseTree(html); it = dom.begin(); end = dom.end(); for (; it != end; ++it) { if (it->tagName()=="strong") { it->parseAttributes(); if (it->attribute("class").second == "settings-item__value settings-item__section") { for (unsigned int i = 0; i < dom.number_of_children(it); ++i) { tree::iterator tag_it = dom.child(it, i); if (!tag_it->isTag() && !tag_it->isComment()) { std::string tag_text = boost::algorithm::to_lower_copy(tag_it->text()); if (tag_text == email_lowercase) { res = 1; // Login successful break; } } } } } if (res == 1) // Login was successful so no need to go through the remaining tags break; } // Simple login check if complex check failed. Check login by trying to get account page. If response code isn't 200 then login failed. if (res == 0) { std::string url = "https://www.gog.com/account"; curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str()); curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback); curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); curl_easy_perform(curlhandle); memory.str(std::string()); long int response_code = 0; curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); if (response_code == 200) res = 1; // Login successful } return res; } // Get list of games from account page std::vector Downloader::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 (Downloader::getGames)" << std::endl << response << std::endl; #endif std::cout << jsonparser->getFormattedErrorMessages(); delete jsonparser; if (!response.empty()) { if(response[0] != '{') { // Response was not JSON. Assume that cookies have expired. std::cerr << "Response was not JSON. Cookies have most likely expired. Try --login first." << std::endl; } } exit(1); } #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << root << std::endl; #endif if (root["page"].asInt() == root["totalPages"].asInt()) bAllPagesParsed = true; if (root["products"].isArray()) { for (unsigned int i = 0; i < root["products"].size(); ++i) { Json::Value product = root["products"][i]; gameItem game; game.name = product["slug"].asString(); game.id = product["id"].isInt() ? std::to_string(product["id"].asInt()) : product["id"].asString(); unsigned int platform = 0; if (product["worksOn"]["Windows"].asBool()) platform |= GlobalConstants::PLATFORM_WINDOWS; if (product["worksOn"]["Mac"].asBool()) platform |= GlobalConstants::PLATFORM_MAC; if (product["worksOn"]["Linux"].asBool()) platform |= GlobalConstants::PLATFORM_LINUX; // Skip if platform doesn't match if (config.bPlatformDetection && !(platform & config.iInstallerPlatform)) continue; // Filter the game list if (!config.sGameRegex.empty()) { // GameRegex filter aliases if (config.sGameRegex == "all") config.sGameRegex = ".*"; boost::regex expression(config.sGameRegex); boost::match_results what; if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex continue; } if (config.bDLC) { int dlcCount = product["dlcCount"].asInt(); bool bDownloadDLCInfo = (dlcCount != 0); if (!bDownloadDLCInfo && !config.sIgnoreDLCCountRegex.empty()) { boost::regex expression(config.sIgnoreDLCCountRegex); boost::match_results what; if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex { bDownloadDLCInfo = true; } } // Check game specific config if (!config.bUpdateCache) // Disable game specific config files for cache update { gameSpecificConfig conf; conf.bIgnoreDLCCount = false; // Assume false Util::getGameSpecificConfig(game.name, &conf); if (conf.bIgnoreDLCCount) bDownloadDLCInfo = true; } if (bDownloadDLCInfo && !config.sGameRegex.empty()) { // don't download unnecessary info if user is only interested in a subset of his account boost::regex expression(config.sGameRegex); boost::match_results what; if (!boost::regex_search(game.name, what, expression)) { bDownloadDLCInfo = false; } } if (bDownloadDLCInfo) { std::string gameinfo = this->getResponse("https://www.gog.com/account/gameDetails/" + game.id + ".json"); Json::Value info; if (!jsonparser->parse(gameinfo, info)) { #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << gameinfo << std::endl; #endif std::cout << jsonparser->getFormattedErrorMessages(); delete jsonparser; exit(1); } else { #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << info << std::endl; #endif game.dlcnames = Util::getDLCNamesFromJSON(info["dlcs"]); } } } games.push_back(game); } } i++; } while (!bAllPagesParsed); delete jsonparser; return games; } // Get list of free games std::vector Downloader::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 (Downloader::getFreeGames)" << std::endl << json << std::endl; #endif std::cout << jsonparser->getFormattedErrorMessages(); delete jsonparser; exit(1); } #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::getFreeGames)" << std::endl << root << std::endl; #endif Json::Value products = root["products"]; for (unsigned int i = 0; i < products.size(); ++i) { gameItem game; game.name = products[i]["slug"].asString(); game.id = products[i]["id"].isInt() ? std::to_string(products[i]["id"].asInt()) : products[i]["id"].asString(); games.push_back(game); } delete jsonparser; return games; } Json::Value Downloader::getGameDetailsJSON(const std::string& gameid) { std::string gameDataUrl = "https://www.gog.com/account/gameDetails/" + gameid + ".json"; std::string json = this->getResponse(gameDataUrl); // Parse JSON Json::Value root; Json::Reader *jsonparser = new Json::Reader; if (!jsonparser->parse(json, root)) { #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::getGameDetailsJSON)" << std::endl << json << std::endl; #endif std::cout << jsonparser->getFormattedErrorMessages(); delete jsonparser; exit(1); } #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::getGameDetailsJSON)" << std::endl << root << std::endl; #endif delete jsonparser; return root; } std::vector Downloader::getExtrasFromJSON(const Json::Value& json, const std::string& gamename) { std::vector extras; std::vector downloaderUrls; Util::getDownloaderUrlsFromJSON(json["extras"], downloaderUrls); 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 = gogAPI->getExtraLink(gamename, id); 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; } extras.push_back( gameFile ( false, id, name, path, std::string() ) ); } 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) { serials << cdkey << 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(); } void Downloader::checkOrphans() { // Always check everything when checking for orphaned files config.bInstallers = true; config.bExtras = true; config.bPatches = true; config.bLanguagePacks = true; if (this->games.empty()) this->getGameDetails(); std::vector orphans; for (unsigned int i = 0; i < games.size(); ++i) { std::cout << "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.sDirectory + "/" + config.sGameSubdir + "/"; Util::filepathReplaceReservedStrings(directory, games[i].gamename, platformIds[j]); boost::filesystem::path path (directory); if (boost::filesystem::exists(path)) { bool bDuplicate = false; for (unsigned int k = 0; k < paths.size(); ++k) { if (path == paths[k]) { bDuplicate = true; break; } } if (!bDuplicate) paths.push_back(path); } } for (unsigned int j = 0; j < paths.size(); ++j) { std::size_t pathlen = config.sDirectory.length(); if (boost::filesystem::exists(paths[j])) { if (boost::filesystem::is_directory(paths[j])) { // Recursively iterate over files in directory boost::filesystem::recursive_directory_iterator end_iter; boost::filesystem::recursive_directory_iterator dir_iter(paths[j]); while (dir_iter != end_iter) { if (boost::filesystem::is_regular_file(dir_iter->status())) { std::string filepath = dir_iter->path().string(); if (config.blacklist.isBlacklisted(filepath.substr(pathlen))) { if (config.bVerbose) std::cout << "skipped blacklisted 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::cout << 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 = false; // Assume that the file is orphaned // Check installers for (unsigned int k = 0; k < games[i].installers.size(); ++k) { if (games[i].installers[k].path.find(filepath_vector[j].filename().string()) != std::string::npos) { bFoundFile = true; break; } } if (!bFoundFile) { // Check extras for (unsigned int k = 0; k < games[i].extras.size(); ++k) { if (games[i].extras[k].path.find(filepath_vector[j].filename().string()) != std::string::npos) { bFoundFile = true; break; } } } if (!bFoundFile) { // Check patches for (unsigned int k = 0; k < games[i].patches.size(); ++k) { if (games[i].patches[k].path.find(filepath_vector[j].filename().string()) != std::string::npos) { bFoundFile = true; break; } } } if (!bFoundFile) { // Check language packs for (unsigned int k = 0; k < games[i].languagepacks.size(); ++k) { if (games[i].languagepacks[k].path.find(filepath_vector[j].filename().string()) != std::string::npos) { bFoundFile = true; break; } } } if (!bFoundFile) { // Check dlcs for (unsigned int k = 0; k < games[i].dlcs.size(); ++k) { for (unsigned int index = 0; index < games[i].dlcs[k].installers.size(); ++index) { if (games[i].dlcs[k].installers[index].path.find(filepath_vector[j].filename().string()) != std::string::npos) { bFoundFile = true; break; } } if (bFoundFile) break; for (unsigned int index = 0; index < games[i].dlcs[k].patches.size(); ++index) { if (games[i].dlcs[k].patches[index].path.find(filepath_vector[j].filename().string()) != std::string::npos) { bFoundFile = true; break; } } for (unsigned int index = 0; index < games[i].dlcs[k].extras.size(); ++index) { if (games[i].dlcs[k].extras[index].path.find(filepath_vector[j].filename().string()) != std::string::npos) { bFoundFile = true; break; } } 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(); for (unsigned int i = 0; i < games.size(); ++i) { if (config.bInstallers) { for (unsigned int j = 0; j < games[i].installers.size(); ++j) { boost::filesystem::path filepath = games[i].installers[j].getFilepath(); std::string remoteHash; std::string localHash; bool bHashOK = true; // assume hash OK uintmax_t filesize; localHash = this->getLocalFileHash(filepath.string(), games[i].gamename); remoteHash = this->getRemoteFileHash(games[i].gamename, games[i].installers[j].id); if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) { filesize = boost::filesystem::file_size(filepath); 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 (!games[i].gamename.empty()) local_xml_file = config.sXMLDirectory + "/" + games[i].gamename + "/" + path.filename().string() + ".xml"; else local_xml_file = config.sXMLDirectory + "/" + path.filename().string() + ".xml"; if (boost::filesystem::exists(local_xml_file)) { TiXmlDocument local_xml; local_xml.LoadFile(local_xml_file.string()); TiXmlNode *fileNodeLocal = local_xml.FirstChild("file"); if (fileNodeLocal) { TiXmlElement *fileElemLocal = fileNodeLocal->ToElement(); 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 " << games[i].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; continue; } } std::cout << (bHashOK ? "OK " : "MD5 ") << games[i].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; } else { std::cout << "ND " << games[i].gamename << " " << filepath.filename().string() << std::endl; } } } if (config.bExtras) { for (unsigned int j = 0; j < games[i].extras.size(); ++j) { boost::filesystem::path filepath = games[i].extras[j].getFilepath(); std::string localHash = this->getLocalFileHash(filepath.string(), games[i].gamename); uintmax_t filesize; if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) { filesize = boost::filesystem::file_size(filepath); std::cout << "OK " << games[i].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; } else { std::cout << "ND " << games[i].gamename << " " << filepath.filename().string() << std::endl; } } } if (config.bPatches) { for (unsigned int j = 0; j < games[i].patches.size(); ++j) { boost::filesystem::path filepath = games[i].patches[j].getFilepath(); std::string localHash = this->getLocalFileHash(filepath.string(), games[i].gamename); uintmax_t filesize; if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) { filesize = boost::filesystem::file_size(filepath); std::cout << "OK " << games[i].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; } else { std::cout << "ND " << games[i].gamename << " " << filepath.filename().string() << std::endl; } } } if (config.bLanguagePacks) { for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j) { boost::filesystem::path filepath = games[i].languagepacks[j].getFilepath(); std::string localHash = this->getLocalFileHash(filepath.string(), games[i].gamename); uintmax_t filesize; if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) { filesize = boost::filesystem::file_size(filepath); std::cout << "OK " << games[i].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; } else { std::cout << "ND " << games[i].gamename << " " << filepath.filename().string() << std::endl; } } } if (config.bDLC) { for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) { if (config.bInstallers) { for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) { boost::filesystem::path filepath = games[i].dlcs[j].installers[k].getFilepath(); std::string remoteHash; std::string localHash; bool bHashOK = true; // assume hash OK uintmax_t filesize; localHash = this->getLocalFileHash(filepath.string(), games[i].dlcs[j].gamename); remoteHash = this->getRemoteFileHash(games[i].dlcs[j].gamename, games[i].dlcs[j].installers[k].id); if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) { filesize = boost::filesystem::file_size(filepath); 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 (!games[i].dlcs[j].gamename.empty()) local_xml_file = config.sXMLDirectory + "/" + games[i].dlcs[j].gamename + "/" + path.filename().string() + ".xml"; else local_xml_file = config.sXMLDirectory + "/" + path.filename().string() + ".xml"; if (boost::filesystem::exists(local_xml_file)) { TiXmlDocument local_xml; local_xml.LoadFile(local_xml_file.string()); TiXmlNode *fileNodeLocal = local_xml.FirstChild("file"); if (fileNodeLocal) { TiXmlElement *fileElemLocal = fileNodeLocal->ToElement(); 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 " << games[i].dlcs[j].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; continue; } } std::cout << (bHashOK ? "OK " : "MD5 ") << games[i].dlcs[j].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; } else { std::cout << "ND " << games[i].dlcs[j].gamename << " " << filepath.filename().string() << std::endl; } } } if (config.bPatches) { for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k) { boost::filesystem::path filepath = games[i].dlcs[j].patches[k].getFilepath(); std::string localHash = this->getLocalFileHash(filepath.string(), games[i].dlcs[j].gamename); uintmax_t filesize; if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) { filesize = boost::filesystem::file_size(filepath); std::cout << "OK " << games[i].dlcs[j].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; } else { std::cout << "ND " << games[i].dlcs[j].gamename << " " << filepath.filename().string() << std::endl; } } } if (config.bExtras) { for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k) { boost::filesystem::path filepath = games[i].dlcs[j].extras[k].getFilepath(); std::string localHash = this->getLocalFileHash(filepath.string(), games[i].dlcs[j].gamename); uintmax_t filesize; if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) { filesize = boost::filesystem::file_size(filepath); std::cout << "OK " << games[i].dlcs[j].gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl; } else { std::cout << "ND " << games[i].dlcs[j].gamename << " " << filepath.filename().string() << std::endl; } } } } } } return; } std::string Downloader::getLocalFileHash(const std::string& filepath, const std::string& gamename) { std::string localHash; boost::filesystem::path path = filepath; boost::filesystem::path local_xml_file; if (!gamename.empty()) local_xml_file = config.sXMLDirectory + "/" + gamename + "/" + path.filename().string() + ".xml"; else local_xml_file = config.sXMLDirectory + "/" + path.filename().string() + ".xml"; if (boost::filesystem::exists(local_xml_file)) { TiXmlDocument local_xml; local_xml.LoadFile(local_xml_file.string()); TiXmlNode *fileNodeLocal = local_xml.FirstChild("file"); if (fileNodeLocal) { TiXmlElement *fileElemLocal = fileNodeLocal->ToElement(); localHash = fileElemLocal->Attribute("md5"); } } else { if (boost::filesystem::exists(path) && boost::filesystem::is_regular_file(path)) { localHash = Util::getFileHash(path.string(), RHASH_MD5); } } 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()) { TiXmlDocument remote_xml; remote_xml.Parse(xml_data.c_str()); TiXmlNode *fileNodeRemote = remote_xml.FirstChild("file"); if (fileNodeRemote) { TiXmlElement *fileElemRemote = fileNodeRemote->ToElement(); 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 */ int Downloader::loadGameDetailsCache() { int res = 0; std::string cachepath = config.sCacheDirectory + "/gamedetails.json"; // Make sure file exists boost::filesystem::path path = cachepath; if (!boost::filesystem::exists(path)) { return res = 1; } bptime::ptime now = bptime::second_clock::local_time(); bptime::ptime cachedate; std::ifstream json(cachepath, std::ifstream::binary); Json::Value root; Json::Reader *jsonparser = new Json::Reader; if (jsonparser->parse(json, root)) { if (root.isMember("date")) { cachedate = bptime::from_iso_string(root["date"].asString()); if ((now - cachedate) > bptime::minutes(config.iCacheValid)) { // cache is too old delete jsonparser; json.close(); return res = 3; } } if (root.isMember("games")) { this->games = getGameDetailsFromJsonNode(root["games"]); res = 0; } else { res = 4; } } else { res = 2; std::cout << "Failed to parse cache" << std::endl; std::cout << jsonparser->getFormattedErrorMessages() << std::endl; } delete jsonparser; if (json) json.close(); return res; } /* Save game details to cache file returns 0 if successful returns 1 if fails */ int Downloader::saveGameDetailsCache() { int res = 0; // Don't try to save cache if we don't have any game details if (this->games.empty()) { return 1; } std::string cachepath = config.sCacheDirectory + "/gamedetails.json"; Json::Value json; json["version-string"] = config.sVersionString; json["version-number"] = config.sVersionNumber; json["date"] = bptime::to_iso_string(bptime::second_clock::local_time()); for (unsigned int i = 0; i < this->games.size(); ++i) json["games"].append(this->games[i].getDetailsAsJson()); std::ofstream ofs(cachepath); if (!ofs) { res = 1; } else { Json::StyledStreamWriter jsonwriter; jsonwriter.write(ofs, json); ofs.close(); } return res; } std::vector 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(config.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(); // 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.bDLC = config.bDLC; conf.iInstallerLanguage = config.iInstallerLanguage; conf.iInstallerPlatform = config.iInstallerPlatform; if (Util::getGameSpecificConfig(game.gamename, &conf) > 0) std::cout << game.gamename << " - Language: " << conf.iInstallerLanguage << ", Platform: " << conf.iInstallerPlatform << ", DLC: " << (conf.bDLC ? "true" : "false") << std::endl; for (unsigned int j = 0; j < nodes.size(); ++j) { std::string nodeName = nodes[j]; if (gameDetailsNode.isMember(nodeName)) { Json::Value fileDetailsNodeVector = gameDetailsNode[nodeName]; for (unsigned int index = 0; index < fileDetailsNodeVector.size(); ++index) { Json::Value fileDetailsNode = fileDetailsNodeVector[index]; gameFile fileDetails; if (nodeName != "dlcs") { fileDetails.updated = fileDetailsNode["updated"].asInt(); fileDetails.id = fileDetailsNode["id"].asString(); fileDetails.name = fileDetailsNode["name"].asString(); fileDetails.path = fileDetailsNode["path"].asString(); fileDetails.size = fileDetailsNode["size"].asString(); fileDetails.platform = fileDetailsNode["platform"].asUInt(); fileDetails.language = fileDetailsNode["language"].asUInt(); fileDetails.silent = fileDetailsNode["silent"].asInt(); if (nodeName != "extras" && !(fileDetails.platform & conf.iInstallerPlatform)) continue; if (nodeName != "extras" && !(fileDetails.language & conf.iInstallerLanguage)) continue; } if (nodeName == "extras" && config.bExtras) game.extras.push_back(fileDetails); else if (nodeName == "installers" && config.bInstallers) game.installers.push_back(fileDetails); else if (nodeName == "patches" && config.bPatches) game.patches.push_back(fileDetails); else if (nodeName == "languagepacks" && config.bLanguagePacks) game.languagepacks.push_back(fileDetails); else if (nodeName == "dlcs" && conf.bDLC) { std::vector 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(config); details.push_back(game); } } return details; } void Downloader::updateCache() { // Make sure that all details get cached config.bExtras = true; config.bInstallers = true; config.bPatches = true; config.bLanguagePacks = true; config.bDLC = true; config.sGameRegex = ".*"; config.iInstallerLanguage = Util::getOptionValue("all", GlobalConstants::LANGUAGES); config.iInstallerPlatform = Util::getOptionValue("all", GlobalConstants::PLATFORMS); config.vLanguagePriority.clear(); config.vPlatformPriority.clear(); 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; } void Downloader::downloadFileWithId(const std::string& fileid_string, const std::string& output_filepath) { 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 url = gogAPI->getExtraLink(gamename, fileid); if (!gogAPI->getError()) { std::string filename, filepath; filename.assign(url.begin()+url.find_last_of("/")+1, url.begin()+url.find_first_of("?")); if (output_filepath.empty()) filepath = Util::makeFilepath(config.sDirectory, filename, gamename); else filepath = output_filepath; std::cout << "Downloading: " << filepath << std::endl; this->downloadFile(url, filepath, std::string(), gamename); std::cout << std::endl; } else { std::cout << gogAPI->getErrorMessage() << std::endl; gogAPI->clearError(); } } return; } void Downloader::showWishlist() { Json::Value root; Json::Reader *jsonparser = new Json::Reader; int i = 1; bool bAllPagesParsed = false; do { std::string response = this->getResponse("https://www.gog.com/account/wishlist/search?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=0&sortBy=title&system=&page=" + std::to_string(i)); // Parse JSON if (!jsonparser->parse(response, root)) { #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::showWishlist)" << std::endl << response << std::endl; #endif std::cout << jsonparser->getFormattedErrorMessages(); delete jsonparser; exit(1); } #ifdef DEBUG std::cerr << "DEBUG INFO (Downloader::showWishlist)" << std::endl << root << std::endl; #endif if (root["page"].asInt() >= root["totalPages"].asInt()) bAllPagesParsed = true; if (root["products"].isArray()) { for (unsigned int i = 0; i < root["products"].size(); ++i) { Json::Value product = root["products"][i]; unsigned int platform = 0; std::string platforms_text; bool bIsMovie = product["isMovie"].asBool(); if (!bIsMovie) { if (product["worksOn"]["Windows"].asBool()) platform |= GlobalConstants::PLATFORM_WINDOWS; if (product["worksOn"]["Mac"].asBool()) platform |= GlobalConstants::PLATFORM_MAC; if (product["worksOn"]["Linux"].asBool()) platform |= GlobalConstants::PLATFORM_LINUX; // Skip if platform doesn't match if (config.bPlatformDetection && !(platform & config.iInstallerPlatform)) continue; platforms_text = Util::getOptionNameString(platform, GlobalConstants::PLATFORMS); } std::vector tags; if (product["isComingSoon"].asBool()) tags.push_back("Coming soon"); if (product["isDiscounted"].asBool()) tags.push_back("Discount"); if (bIsMovie) tags.push_back("Movie"); std::string tags_text; for (unsigned int j = 0; j < tags.size(); ++j) { tags_text += (tags_text.empty() ? "" : ", ")+tags[j]; } if (!tags_text.empty()) tags_text = "[" + tags_text + "]"; time_t release_date_time; std::string release_date; bool bShowReleaseDate = false; if (product.isMember("releaseDate") && product["isComingSoon"].asBool()) { if (!product["releaseDate"].empty()) { if (product["releaseDate"].isInt()) { release_date_time = product["releaseDate"].asInt(); bShowReleaseDate = true; } else { std::string release_date_time_string = product["releaseDate"].asString(); if (!release_date_time_string.empty()) { try { release_date_time = std::stoi(release_date_time_string); bShowReleaseDate = true; } catch (std::invalid_argument& e) { bShowReleaseDate = false; } } } } if (bShowReleaseDate) release_date = bptime::to_simple_string(bptime::from_time_t(release_date_time)); } std::string price_text; std::string currency = product["price"]["symbol"].asString(); std::string price = product["price"]["finalAmount"].isDouble() ? std::to_string(product["price"]["finalAmount"].asDouble()) + currency : product["price"]["finalAmount"].asString() + currency; std::string discount_percent = product["price"]["discountPercentage"].isInt() ? std::to_string(product["price"]["discountPercentage"].asInt()) + "%" : product["price"]["discountPercentage"].asString() + "%"; std::string discount = product["price"]["discountDifference"].isDouble() ? std::to_string(product["price"]["discountDifference"].asDouble()) + currency : product["price"]["discountDifference"].asString() + currency; std::string store_credit = product["price"]["bonusStoreCreditAmount"].isDouble() ? std::to_string(product["price"]["bonusStoreCreditAmount"].asDouble()) + currency : product["price"]["bonusStoreCreditAmount"].asString() + currency; price_text = price; if (product["isDiscounted"].asBool()) price_text += " (-" + discount_percent + " | -" + discount + ")"; std::string url = product["url"].asString(); if (url.find("/game/") == 0) url = "https://www.gog.com" + url; else if (url.find("/movie/") == 0) url = "https://www.gog.com" + url; std::cout << product["title"].asString(); if (!tags_text.empty()) std::cout << " " << tags_text; std::cout << std::endl; std::cout << "\t" << url << std::endl; if (!bIsMovie) std::cout << "\tPlatforms: " << platforms_text << std::endl; if (bShowReleaseDate) std::cout << "\tRelease date: " << release_date << std::endl; std::cout << "\tPrice: " << price_text << std::endl; if (product["price"]["isBonusStoreCreditIncluded"].asBool()) std::cout << "\tStore credit: " << store_credit << std::endl; std::cout << std::endl; } } i++; } while (!bAllPagesParsed); delete jsonparser; return; } lgogdownloader-2.26/src/gamedetails.cpp000066400000000000000000000143371260744454200202440ustar00rootroot00000000000000#include "gamedetails.h" #include "util.h" gameDetails::gameDetails() { //ctor } gameDetails::~gameDetails() { //dtor } void gameDetails::filterWithPriorities(const Config& config) { if (config.vPlatformPriority.empty() && config.vLanguagePriority.empty()) return; filterListWithPriorities(installers, config); filterListWithPriorities(patches, config); filterListWithPriorities(languagepacks, config); } void gameDetails::filterListWithPriorities(std::vector& list, const Config& 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.vPlatformPriority.empty()) { for (size_t i = 0; i != config.vPlatformPriority.size(); i++) if (fileDetails->platform & config.vPlatformPriority[i]) { fileDetails->score += i; break; } } if (!config.vLanguagePriority.empty()) { for (size_t i = 0; i != config.vLanguagePriority.size(); i++) if (fileDetails->language & config.vLanguagePriority[i]) { fileDetails->score += i; break; } } if ((fileDetails->score < bestscore) or (bestscore < 0)) bestscore = fileDetails->score; } for (std::vector::iterator fileDetails = list.begin(); fileDetails != list.end(); ) { if (fileDetails->score > bestscore) fileDetails = list.erase(fileDetails); else fileDetails++; } } void gameDetails::makeFilepaths(const Config& 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); 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); 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); } } } Json::Value gameDetails::getDetailsAsJson() { Json::Value json; json["gamename"] = this->gamename; json["title"] = this->title; json["icon"] = this->icon; json["serials"] = this->serials; 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; } lgogdownloader-2.26/src/gamefile.cpp000066400000000000000000000020421260744454200175240ustar00rootroot00000000000000#include "gamefile.h" gameFile::gameFile(const int& t_updated, const std::string& t_id, const std::string& t_name, const std::string& t_path, const std::string& t_size, const unsigned int& t_language, const unsigned int& t_platform, const int& t_silent) { this->updated = t_updated; this->id = t_id; this->name = t_name; this->path = t_path; this->size = t_size; this->platform = t_platform; this->language = t_language; this->silent = t_silent; } gameFile::gameFile() { //ctor } 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; return json; } lgogdownloader-2.26/src/progressbar.cpp000066400000000000000000000056621260744454200203170ustar00rootroot00000000000000/* 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 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) { // 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) std::cout << m_border_color; std::cout << (m_use_unicode ? m_left_border : m_simple_left_border); // whole completed bars if (m_use_color) std::cout << m_bar_color; unsigned int i = 0; for (; i < whole_bar_chars_i; i++) { std::cout << (m_use_unicode ? m_bar_chars[8] : m_simple_bar_char); } // partial completed bar if (i < length) std::cout << (m_use_unicode ? m_bar_chars[partial_bar_char_index] : m_simple_empty_fill); // whole unfinished bars if (m_use_color) std::cout << COLOR_RESET; for (i = whole_bar_chars_i + 1; i < length; i++) { // first entry in m_bar_chars is assumed to be the empty bar std::cout << (m_use_unicode ? m_bar_chars[0] : m_simple_empty_fill); } // right border if (m_use_color) std::cout << m_border_color; std::cout << (m_use_unicode ? m_right_border : m_simple_right_border); if (m_use_color) std::cout << COLOR_RESET; } lgogdownloader-2.26/src/util.cpp000066400000000000000000000341031260744454200167330ustar00rootroot00000000000000/* 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::cout << "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::cout << "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::cout << "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::cout << 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; TiXmlDocument xml; TiXmlElement *fileElem = new TiXmlElement("file"); fileElem->SetAttribute("name", filename); fileElem->SetAttribute("chunks", chunks); fileElem->SetAttribute("total_size", std::to_string(filesize)); 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::cout << "Memory error" << std::endl; return res; } size = fread(chunk, 1, chunk_size, infile); if (size != chunk_size) { std::cout << "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); TiXmlElement *chunkElem = new TiXmlElement("chunk"); chunkElem->SetAttribute("id", i); chunkElem->SetAttribute("from", std::to_string(range_begin)); chunkElem->SetAttribute("to", std::to_string(range_end)); chunkElem->SetAttribute("method", "md5"); TiXmlText *text = new TiXmlText(hash); 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::string file_md5 = rhash_result; std::cout << std::endl << "MD5: " << file_md5 << std::endl; fileElem->SetAttribute("md5", file_md5); xml.LinkEndChild(fileElem); std::cout << "Writing XML: " << filenameXML << std::endl; if ((xmlfile=fopen(filenameXML.c_str(), "w"))!=NULL) { xml.Print(xmlfile); fclose(xmlfile); res = 1; } else { std::cout << "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")) { conf->iInstallerLanguage = root["language"].asUInt(); res++; } if (root.isMember("platform")) { conf->iInstallerPlatform = root["platform"].asUInt(); res++; } if (root.isMember("dlc")) { conf->bDLC = root["dlc"].asBool(); res++; } if (root.isMember("ignore-dlc-count")) { conf->bIgnoreDLCCount = root["ignore-dlc-count"].asBool(); res++; } } else { std::cout << "Failed to parse game specific config" << std::endl; std::cout << 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::cout << "Failed to set file permissions for " << path.string() << std::endl; } } } } } int Util::getTerminalWidth() { struct winsize w; ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); return static_cast(w.ws_col); } void Util::getDownloaderUrlsFromJSON(const Json::Value &root, std::vector &urls) { if(root.size() > 0) { for(Json::ValueIterator 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) { 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)) { 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; } lgogdownloader-2.26/version.sh000066400000000000000000000004451260744454200165060ustar00rootroot00000000000000#!/bin/bash version="LGOGDownloader `grep VERSION_NUMBER < main.cpp | head -n 1 | sed -e 's/.*\([0-9]\+\.[0-9]\+\).*/\1/'`" if [ -e .git/HEAD ]; then if git status | grep -q 'modified:'; then version="${version}M" fi version="$version git `git rev-parse --short HEAD`" fi echo "$version"