--- /dev/null
+*.layout
+*~
+*.[oa]
+bin/*
+obj/*
+*.1
+*.gz
--- /dev/null
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
+
--- /dev/null
+#------------------------------------------------------------------------------#
+# 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
+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:
+
+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)
+
+$(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
+ help2man -N -i $(MAN_DIR)/lgogdownloader.supplemental.groff -o $(MAN_DIR)/$(MAN_PAGE) $(OUT_RELEASE)
+ gzip -f -9 $(MAN_DIR)/$(MAN_PAGE)
+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)
+
+$(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/
+ install -m 755 $(OUT_RELEASE) $(DESTDIR)/$(PREFIX)/bin/lgogdownloader
+ 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
+
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef API_H
+#define API_H
+
+#include "globalconstants.h"
+#include "gamedetails.h"
+
+#include <iostream>
+#include <vector>
+#include <curl/curl.h>
+extern "C" {
+ #include <oauth.h>
+}
+#include <cstring>
+#include <sys/time.h>
+
+class userDetails {
+ public:
+ std::string avatar_small;
+ std::string avatar_big;
+ std::string username;
+ std::string email;
+ unsigned long long id;
+ int notifications_forum;
+ int notifications_games;
+ int notifications_messages;
+};
+
+class apiConfig {
+ public:
+ std::string oauth_authorize_temp_token;
+ std::string oauth_get_temp_token;
+ std::string oauth_get_token;
+ std::string get_user_games;
+ std::string get_user_details;
+ std::string get_installer_link;
+ std::string get_game_details;
+ std::string get_extra_link;
+ std::string set_app_status;
+ std::string oauth_token;
+ std::string oauth_secret;
+};
+
+size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
+
+class API
+{
+ public:
+ userDetails user;
+
+ API(const std::string& token,const std::string& secret);
+ int init();
+ int login(const std::string& email, const std::string& password);
+ int getAPIConfig();
+ std::string getResponse(const std::string& url);
+ std::string getResponseOAuth(const std::string& url);
+ int getUserDetails();
+ int getGames();
+ gameDetails getGameDetails(const std::string& game_name, const unsigned int& platform = (GlobalConstants::PLATFORM_WINDOWS | GlobalConstants::PLATFORM_LINUX), const unsigned int& lang = GlobalConstants::LANGUAGE_EN, const bool& useDuplicateHandler = false);
+ std::string getInstallerLink(const std::string& game_name, const std::string& id);
+ std::string getExtraLink(const std::string& game_name, const std::string& id);
+ std::string getPatchLink(const std::string& game_name, const std::string& id);
+ std::string getLanguagePackLink(const std::string& game_name, const std::string& id);
+ std::string getXML(const std::string& game_name, const std::string& id);
+ void clearError();
+ bool getError() { return this->error; };
+ std::string getErrorMessage() { return this->error_message; };
+ std::string getToken() { return this->config.oauth_token; };
+ std::string getSecret() { return this->config.oauth_secret; };
+ template <typename T> CURLcode curlSetOpt(CURLoption option, T value) { return curl_easy_setopt(this->curlhandle, option, value); };
+ virtual ~API();
+ protected:
+ private:
+ apiConfig config;
+ CURL* curlhandle;
+ void setError(const std::string& err);
+ bool error;
+ std::string error_message;
+
+ // API constants
+ const std::string CONSUMER_KEY = "1f444d14ea8ec776585524a33f6ecc1c413ed4a5";
+ const std::string CONSUMER_SECRET = "20d175147f9db9a10fc0584aa128090217b9cf88";
+ const int OAUTH_VERIFIER_LENGTH = 14;
+ const int OAUTH_TOKEN_LENGTH = 11;
+ const int OAUTH_SECRET_LENGTH = 18;
+};
+
+#endif // API_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef BLACKLIST_H__
+#define BLACKLIST_H__
+
+#include <boost/regex.hpp>
+#include <string>
+#include <vector>
+
+class Config;
+class gameFile;
+
+class BlacklistItem {
+ public:
+ unsigned int linenr; // where the blacklist item is defined in blacklist.txt
+ unsigned int flags;
+ std::string source; // source representation of the item
+ boost::regex regex;
+};
+
+class Blacklist
+{
+ public:
+ Blacklist() {};
+
+ void initialize(const std::vector<std::string>& lines);
+ bool isBlacklisted(const std::string& path);
+ bool isBlacklisted(const std::string& path, const std::string& gamename, std::string subdirectory = "");
+
+ private:
+ std::vector<BlacklistItem> blacklist_;
+};
+
+#endif // BLACKLIST_H_
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef CONFIG_H__
+#define CONFIG_H__
+
+#include <iostream>
+#include <curl/curl.h>
+
+#include "blacklist.h"
+
+class Config
+{
+ public:
+ Config() {};
+ virtual ~Config() {};
+ bool bVerbose;
+ bool bRemoteXML;
+ bool bCover;
+ bool bUpdateCheck;
+ bool bDownload;
+ bool bList;
+ bool bListDetails;
+ bool bLogin;
+ 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;
+ 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 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;
+ unsigned int iInstallerType;
+ unsigned int iInstallerLanguage;
+ int iRetries;
+ int iWait;
+ int iCacheValid;
+ size_t iChunkSize;
+ curl_off_t iDownloadRate;
+ long int iTimeout;
+ Blacklist blacklist;
+};
+
+#endif // CONFIG_H__
--- /dev/null
+/* 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 <curl/curl.h>
+#include <jsoncpp/json/json.h>
+#include <ctime>
+#include <fstream>
+
+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<std::string> 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();
+ CURL* curlhandle;
+ Timer timer;
+ Config config;
+ ProgressBar* progressbar;
+ 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();
+ size_t getResumePosition();
+ CURLcode beginDownload();
+ std::string getResponse(const std::string& url);
+ std::string getLocalFileHash(const std::string& filepath, const std::string& gamename = std::string());
+ std::string getRemoteFileHash(const std::string& gamename, const std::string& id);
+ int loadGameDetailsCache();
+ int saveGameDetailsCache();
+ std::vector<gameDetails> getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0);
+ int HTTP_Login(const std::string& email, const std::string& password);
+ std::vector<gameItem> getGames();
+ std::vector<gameItem> getFreeGames();
+ std::vector<gameFile> getExtras(const std::string& gamename, const std::string& gameid);
+
+ 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<gameItem> gameItems;
+ std::vector<gameDetails> games;
+ std::string coverXML;
+
+ size_t resume_position;
+ int retries;
+ std::ofstream report_ofs;
+};
+
+#endif // DOWNLOADER_H
--- /dev/null
+#ifndef GAMEDETAILS_H
+#define GAMEDETAILS_H
+
+#include "globalconstants.h"
+#include "gamefile.h"
+#include "config.h"
+
+#include <iostream>
+#include <vector>
+#include <jsoncpp/json/json.h>
+
+class gameDetails
+{
+ public:
+ gameDetails();
+ std::vector<gameFile> extras;
+ std::vector<gameFile> installers;
+ std::vector<gameFile> patches;
+ std::vector<gameFile> languagepacks;
+ std::vector<gameDetails> dlcs;
+ std::string gamename;
+ std::string title;
+ std::string icon;;
+ void makeFilepaths(const Config& config);
+ Json::Value getDetailsAsJson();
+ virtual ~gameDetails();
+ protected:
+ private:
+};
+
+#endif // GAMEDETAILS_H
--- /dev/null
+#ifndef GAMEFILE_H
+#define GAMEFILE_H
+
+#include "globalconstants.h"
+
+#include <iostream>
+#include <vector>
+#include <jsoncpp/json/json.h>
+
+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 silent;
+ void setFilepath(const std::string& path);
+ std::string getFilepath();
+ Json::Value getAsJson();
+ virtual ~gameFile();
+ protected:
+ private:
+ std::string filepath;
+};
+
+#endif // GAMEFILE_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GLOBALCONSTANTS_H_INCLUDED
+#define GLOBALCONSTANTS_H_INCLUDED
+
+#include <iostream>
+#include <vector>
+
+namespace GlobalConstants
+{
+ // Language constants
+ const unsigned int LANGUAGE_EN = 1;
+ const unsigned int LANGUAGE_DE = 2;
+ const unsigned int LANGUAGE_FR = 4;
+ const unsigned int LANGUAGE_PL = 8;
+ const unsigned int LANGUAGE_RU = 16;
+ const unsigned int LANGUAGE_CN = 32;
+ const unsigned int LANGUAGE_CZ = 64;
+ const unsigned int LANGUAGE_ES = 128;
+ const unsigned int LANGUAGE_HU = 256;
+ const unsigned int LANGUAGE_IT = 512;
+ const unsigned int LANGUAGE_JP = 1024;
+ const unsigned int LANGUAGE_TR = 2048;
+ const unsigned int LANGUAGE_PT = 4096;
+ const unsigned int LANGUAGE_KO = 8192;
+ const unsigned int LANGUAGE_NL = 16384;
+ const unsigned int LANGUAGE_SV = 32768;
+ const unsigned int LANGUAGE_NO = 65536;
+ const unsigned int LANGUAGE_DA = 131072;
+ const unsigned int LANGUAGE_FI = 262144;
+
+ struct languageStruct {const unsigned int languageId; const std::string languageCode; const std::string languageString;};
+ const std::vector<languageStruct> LANGUAGES =
+ {
+ { LANGUAGE_EN, "en", "English" },
+ { LANGUAGE_DE, "de", "German" },
+ { LANGUAGE_FR, "fr", "French" },
+ { LANGUAGE_PL, "pl", "Polish" },
+ { LANGUAGE_RU, "ru", "Russian" },
+ { LANGUAGE_CN, "cn", "Chinese" },
+ { LANGUAGE_CZ, "cz", "Czech" },
+ { LANGUAGE_ES, "es", "Spanish" },
+ { LANGUAGE_HU, "hu", "Hungarian" },
+ { LANGUAGE_IT, "it", "Italian" },
+ { LANGUAGE_JP, "jp", "Japanese" },
+ { LANGUAGE_TR, "tr", "Turkish" },
+ { LANGUAGE_PT, "pt", "Portuguese"},
+ { LANGUAGE_KO, "ko", "Korean" },
+ { LANGUAGE_NL, "nl", "Dutch" },
+ { LANGUAGE_SV, "sv", "Swedish" },
+ { LANGUAGE_NO, "no", "Norwegian" },
+ { LANGUAGE_DA, "da", "Danish" },
+ { LANGUAGE_FI, "fi", "Finnish" }
+ };
+
+ // Platform constants
+ const unsigned int PLATFORM_WINDOWS = 1;
+ const unsigned int PLATFORM_MAC = 2;
+ const unsigned int PLATFORM_LINUX = 4;
+
+ struct platformStruct {const unsigned int platformId; const std::string platformCode; const std::string platformString;};
+ const std::vector<platformStruct> PLATFORMS =
+ {
+ { PLATFORM_WINDOWS, "win", "Windows" },
+ { PLATFORM_MAC, "mac", "Mac" },
+ { PLATFORM_LINUX, "linux", "Linux" }
+ };
+};
+
+#endif // GLOBALCONSTANTS_H_INCLUDED
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef PROGRESSBAR_H
+#define PROGRESSBAR_H
+
+#include <iostream>
+#include <vector>
+
+class ProgressBar
+{
+ public:
+ ProgressBar(bool bUnicode, bool bColor);
+ virtual ~ProgressBar();
+ void draw(unsigned int length, double fraction);
+ protected:
+ private:
+ std::vector<std::string> const m_bar_chars;
+ std::string const m_left_border;
+ std::string const m_right_border;
+ std::string const m_simple_left_border;
+ std::string const m_simple_right_border;
+ std::string const m_simple_empty_fill;
+ std::string const m_simple_bar_char;
+ std::string const m_bar_color;
+ std::string const m_border_color;
+ std::string const COLOR_RESET;
+ bool m_use_unicode;
+ bool m_use_color;
+};
+
+#endif // PROGRESSBAR_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef UTIL_H
+#define UTIL_H
+
+#include "globalconstants.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <cerrno>
+#include <iostream>
+#include <sstream>
+#include <rhash.h>
+
+struct gameSpecificConfig
+{
+ unsigned int iInstallerType;
+ unsigned int iInstallerLanguage;
+ bool bDLC;
+};
+
+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, size_t chunk_size, unsigned hash_id);
+ int createXML(std::string filepath, size_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 = "");
+}
+
+#endif // UTIL_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<CodeBlocks_project_file>
+ <FileVersion major="1" minor="6" />
+ <Project>
+ <Option title="lgogdownloader" />
+ <Option pch_mode="2" />
+ <Option compiler="gcc" />
+ <Build>
+ <Target title="Debug">
+ <Option output="bin/Debug/lgogdownloader" prefix_auto="1" extension_auto="1" />
+ <Option object_output="obj/Debug/" />
+ <Option type="1" />
+ <Option compiler="gcc" />
+ <Compiler>
+ <Add option="-g" />
+ <Add option="-DDEBUG" />
+ <Add directory="include" />
+ </Compiler>
+ </Target>
+ <Target title="Release">
+ <Option output="bin/Release/lgogdownloader" prefix_auto="1" extension_auto="1" />
+ <Option object_output="obj/Release/" />
+ <Option type="1" />
+ <Option compiler="gcc" />
+ <Compiler>
+ <Add option="-O2" />
+ <Add directory="include" />
+ </Compiler>
+ <Linker>
+ <Add option="-s" />
+ </Linker>
+ </Target>
+ </Build>
+ <Compiler>
+ <Add option="-std=c++0x" />
+ <Add option="-Wall" />
+ <Add option="-fexceptions" />
+ <Add directory="include" />
+ </Compiler>
+ <Linker>
+ <Add library="curl" />
+ <Add library="oauth" />
+ <Add library="jsoncpp" />
+ <Add library="htmlcxx" />
+ <Add library="boost_system" />
+ <Add library="boost_filesystem" />
+ <Add library="boost_regex" />
+ <Add library="boost_program_options" />
+ <Add library="boost_date_time" />
+ <Add library="tinyxml" />
+ <Add library="rhash" />
+ </Linker>
+ <Unit filename="include/api.h" />
+ <Unit filename="include/blacklist.h" />
+ <Unit filename="include/config.h" />
+ <Unit filename="include/downloader.h" />
+ <Unit filename="include/gamedetails.h" />
+ <Unit filename="include/gamefile.h" />
+ <Unit filename="include/globalconstants.h" />
+ <Unit filename="include/progressbar.h" />
+ <Unit filename="include/util.h" />
+ <Unit filename="main.cpp" />
+ <Unit filename="src/api.cpp" />
+ <Unit filename="src/blacklist.cpp" />
+ <Unit filename="src/downloader.cpp" />
+ <Unit filename="src/gamedetails.cpp" />
+ <Unit filename="src/gamefile.cpp" />
+ <Unit filename="src/progressbar.cpp" />
+ <Unit filename="src/util.cpp" />
+ <Extensions>
+ <code_completion />
+ <debugger />
+ <envvars />
+ </Extensions>
+ </Project>
+</CodeBlocks_project_file>
--- /dev/null
+/* 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 <fstream>
+#include <boost/filesystem.hpp>
+#include <boost/program_options.hpp>
+
+#define VERSION_NUMBER "2.19"
+
+#ifndef VERSION_STRING
+# define VERSION_STRING "LGOGDownloader " VERSION_NUMBER
+#endif
+
+namespace bpo = boost::program_options;
+
+template<typename T> void set_vm_value(std::map<std::string, bpo::variable_value>& vm, const std::string& option, const T& value)
+{
+ vm[option].value() = boost::any(value);
+}
+
+int main(int argc, char *argv[])
+{
+ Config config;
+ config.sVersionString = VERSION_STRING;
+ char *xdgconfig = getenv("XDG_CONFIG_HOME");
+ char *xdgcache = getenv("XDG_CACHE_HOME");
+ std::string home = (std::string)getenv("HOME");
+
+ if (xdgcache)
+ config.sCacheDirectory = (std::string)xdgcache + "/lgogdownloader";
+ else
+ config.sCacheDirectory = home + "/.cache/lgogdownloader";
+
+ config.sXMLDirectory = config.sCacheDirectory + "/xml";
+
+ // Create help text for --platform option
+ std::string platform_text = "Select which installers are downloaded\n";
+ unsigned int platform_sum = 0;
+ for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+ {
+ platform_text += std::to_string(GlobalConstants::PLATFORMS[i].platformId) + " = " + GlobalConstants::PLATFORMS[i].platformString + "\n";
+ platform_sum += GlobalConstants::LANGUAGES[i].languageId;
+ }
+ platform_text += std::to_string(platform_sum) + " = All";
+
+ // Create help text for --language option
+ std::string language_text = "Select which language installers are downloaded\n";
+ unsigned int language_sum = 0;
+ for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+ {
+ language_text += std::to_string(GlobalConstants::LANGUAGES[i].languageId) + " = " + GlobalConstants::LANGUAGES[i].languageString + "\n";
+ language_sum += GlobalConstants::LANGUAGES[i].languageId;
+ }
+ language_text += "Add the values to download multiple languages\nAll = " + std::to_string(language_sum) + "\n"
+ + "French + Polish = " + std::to_string(GlobalConstants::LANGUAGE_FR) + "+" + std::to_string(GlobalConstants::LANGUAGE_PL) + " = " + std::to_string(GlobalConstants::LANGUAGE_FR | GlobalConstants::LANGUAGE_PL);
+
+ // Create help text for --check-orphans
+ std::string orphans_regex_default = ".*\\.(zip|exe|bin|dmg|old|deb|tar\\.gz|pkg)$"; // 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%";
+
+ std::vector<std::string> unrecognized_options_cfg;
+ 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 bNoInstallers = false;
+ bool bNoExtras = false;
+ bool bNoPatches = false;
+ bool bNoLanguagePacks = false;
+ bool bNoDLC = false;
+ bool bNoRemoteXML = false;
+ bool bNoSubDirectories = false;
+ bool bNoDeb = false;
+ bool bNoTarGz = false;
+ bool bNoCover = false;
+ config.bReport = false;
+ // Commandline options (no config file)
+ options_cli_no_cfg.add_options()
+ ("help,h", "Print help message")
+ ("version", "Print version information")
+ ("login", bpo::value<bool>(&config.bLogin)->zero_tokens()->default_value(false), "Login")
+ ("list", bpo::value<bool>(&config.bList)->zero_tokens()->default_value(false), "List games")
+ ("list-details", bpo::value<bool>(&config.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info")
+ ("download", bpo::value<bool>(&config.bDownload)->zero_tokens()->default_value(false), "Download")
+ ("repair", bpo::value<bool>(&config.bRepair)->zero_tokens()->default_value(false), "Repair downloaded files\nUse --repair --download to redownload files when filesizes don't match (possibly different version). Redownload will rename the old file (appends .old to filename)")
+ ("game", bpo::value<std::string>(&config.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)\nAliases: \"all\", \"free\"")
+ ("create-xml", bpo::value<std::string>(&config.sXMLFile)->default_value(""), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation")
+ ("update-check", bpo::value<bool>(&config.bUpdateCheck)->zero_tokens()->default_value(false), "Check for update notifications")
+ ("check-orphans", bpo::value<std::string>(&config.sOrphanRegex)->implicit_value(""), check_orphans_text.c_str())
+ ("status", bpo::value<bool>(&config.bCheckStatus)->zero_tokens()->default_value(false), "Show status of files\n\nOutput format:\nstatuscode gamename filename filesize filehash\n\nStatus codes:\nOK - File is OK\nND - File is not downloaded\nMD5 - MD5 mismatch, different version")
+ ("save-config", bpo::value<bool>(&config.bSaveConfig)->zero_tokens()->default_value(false), "Create config file with current settings")
+ ("reset-config", bpo::value<bool>(&config.bResetConfig)->zero_tokens()->default_value(false), "Reset config settings to default")
+ ("report", bpo::value<std::string>(&config.sReportFilePath)->implicit_value("lgogdownloader-report.log"), "Save report of downloaded/repaired files to specified file\nDefault filename: lgogdownloader-report.log")
+ ("no-cover", bpo::value<bool>(&bNoCover)->zero_tokens()->default_value(false), "Don't download cover images. Overrides --cover option.\nUseful for making exceptions when \"cover\" is set to true in config file.")
+ ("update-cache", bpo::value<bool>(&config.bUpdateCache)->zero_tokens()->default_value(false), "Update game details cache")
+ ;
+ // Commandline options (config file)
+ options_cli_cfg.add_options()
+ ("directory", bpo::value<std::string>(&config.sDirectory)->default_value("."), "Set download directory")
+ ("limit-rate", bpo::value<curl_off_t>(&config.iDownloadRate)->default_value(0), "Limit download rate to value in kB\n0 = unlimited")
+ ("xml-directory", bpo::value<std::string>(&config.sXMLDirectory), "Set directory for GOG XML files")
+ ("chunk-size", bpo::value<size_t>(&config.iChunkSize)->default_value(10), "Chunk size (in MB) when creating XML")
+ ("platform", bpo::value<unsigned int>(&config.iInstallerType)->default_value(GlobalConstants::PLATFORM_WINDOWS|GlobalConstants::PLATFORM_LINUX), platform_text.c_str())
+ ("language", bpo::value<unsigned int>(&config.iInstallerLanguage)->default_value(GlobalConstants::LANGUAGE_EN), language_text.c_str())
+ ("no-installers", bpo::value<bool>(&bNoInstallers)->zero_tokens()->default_value(false), "Don't download/list/repair installers")
+ ("no-extras", bpo::value<bool>(&bNoExtras)->zero_tokens()->default_value(false), "Don't download/list/repair extras")
+ ("no-patches", bpo::value<bool>(&bNoPatches)->zero_tokens()->default_value(false), "Don't download/list/repair patches")
+ ("no-language-packs", bpo::value<bool>(&bNoLanguagePacks)->zero_tokens()->default_value(false), "Don't download/list/repair language packs")
+ ("no-dlc", bpo::value<bool>(&bNoDLC)->zero_tokens()->default_value(false), "Don't download/list/repair DLCs")
+ ("no-deb", bpo::value<bool>(&bNoDeb)->zero_tokens()->default_value(false), "Don't download/list/repair deb packages")
+ ("no-targz", bpo::value<bool>(&bNoTarGz)->zero_tokens()->default_value(false), "Don't download/list/repair tarballs")
+ ("cover", bpo::value<bool>(&config.bCover)->zero_tokens()->default_value(false), "Download cover images")
+ ("no-remote-xml", bpo::value<bool>(&bNoRemoteXML)->zero_tokens()->default_value(false), "Don't use remote XML for repair")
+ ("no-unicode", bpo::value<bool>(&bNoUnicode)->zero_tokens()->default_value(false), "Don't use Unicode in the progress bar")
+ ("no-color", bpo::value<bool>(&bNoColor)->zero_tokens()->default_value(false), "Don't use coloring in the progress bar")
+ ("no-duplicate-handling", bpo::value<bool>(&bNoDuplicateHandler)->zero_tokens()->default_value(false), "Don't use duplicate handler for installers\nDuplicate installers from different languages are handled separately")
+ ("no-subdirectories", bpo::value<bool>(&bNoSubDirectories)->zero_tokens()->default_value(false), "Don't create subdirectories for extras, patches and language packs")
+ ("verbose", bpo::value<bool>(&config.bVerbose)->zero_tokens()->default_value(false), "Print lots of information")
+ ("insecure", bpo::value<bool>(&bInsecure)->zero_tokens()->default_value(false), "Don't verify authenticity of SSL certificates")
+ ("timeout", bpo::value<long int>(&config.iTimeout)->default_value(10), "Set timeout for connection\nMaximum time in seconds that connection phase is allowed to take")
+ ("retries", bpo::value<int>(&config.iRetries)->default_value(3), "Set maximum number of retries on failed download")
+ ("wait", bpo::value<int>(&config.iWait)->default_value(0), "Time to wait between requests (milliseconds)")
+ ("cover-list", bpo::value<std::string>(&config.sCoverList)->default_value("https://sites.google.com/site/gogdownloader/covers.xml"), "Set URL for cover list")
+ ("subdir-installers", bpo::value<std::string>(&config.sInstallersSubdir)->default_value(""), ("Set subdirectory for extras" + subdir_help_text).c_str())
+ ("subdir-extras", bpo::value<std::string>(&config.sExtrasSubdir)->default_value("extras"), ("Set subdirectory for extras" + subdir_help_text).c_str())
+ ("subdir-patches", bpo::value<std::string>(&config.sPatchesSubdir)->default_value("patches"), ("Set subdirectory for patches" + subdir_help_text).c_str())
+ ("subdir-language-packs", bpo::value<std::string>(&config.sLanguagePackSubdir)->default_value("languagepacks"), ("Set subdirectory for language packs" + subdir_help_text).c_str())
+ ("subdir-dlc", bpo::value<std::string>(&config.sDLCSubdir)->default_value("dlc/%dlcname%"), ("Set subdirectory for dlc" + subdir_help_text).c_str())
+ ("subdir-game", bpo::value<std::string>(&config.sGameSubdir)->default_value("%gamename%"), ("Set subdirectory for game" + subdir_help_text).c_str())
+ ("use-cache", bpo::value<bool>(&config.bUseCache)->zero_tokens()->default_value(false), ("Use game details cache"))
+ ("cache-valid", bpo::value<int>(&config.iCacheValid)->default_value(2880), ("Set how long cached game details are valid (in minutes)\nDefault: 2880 minutes (48 hours)"))
+ ;
+ // Options read from config file
+ options_cfg_only.add_options()
+ ("token", bpo::value<std::string>(&config.sToken)->default_value(""), "oauth token")
+ ("secret", bpo::value<std::string>(&config.sSecret)->default_value(""), "oauth secret")
+ ;
+
+ options_cli_all.add(options_cli_no_cfg).add(options_cli_cfg);
+ options_cfg_all.add(options_cfg_only).add(options_cli_cfg);
+
+ bpo::store(bpo::parse_command_line(argc, argv, options_cli_all), vm);
+ 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;
+ }
+
+ if (xdgconfig)
+ {
+ config.sConfigDirectory = (std::string)xdgconfig + "/lgogdownloader";
+ }
+ else
+ {
+ config.sConfigDirectory = home + "/.config/lgogdownloader";
+ }
+
+ config.sCookiePath = config.sConfigDirectory + "/cookies.txt";
+ config.sConfigFilePath = config.sConfigDirectory + "/config.cfg";
+ config.sBlacklistFilePath = config.sConfigDirectory + "/blacklist.txt";
+
+ // 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<std::string> lines;
+ while (!ifs.eof())
+ {
+ std::getline(ifs, line);
+ lines.push_back(std::move(line));
+ }
+ if (bNoDeb)
+ lines.push_back("Rp .*\\.deb$");
+ if (bNoTarGz)
+ lines.push_back("Rp .*\\.tar\\.gz$");
+ config.blacklist.initialize(lines);
+ }
+ }
+ else if (bNoDeb || bNoTarGz)
+ {
+ std::vector<std::string> lines;
+ if (bNoDeb)
+ lines.push_back("Rp .*\\.deb$");
+ if (bNoTarGz)
+ lines.push_back("Rp .*\\.tar\\.gz$");
+ 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.bInstallers = !bNoInstallers;
+ config.bExtras = !bNoExtras;
+ config.bPatches = !bNoPatches;
+ config.bLanguagePacks = !bNoLanguagePacks;
+ config.bDLC = !bNoDLC;
+ config.bRemoteXML = !bNoRemoteXML;
+ config.bSubDirectories = !bNoSubDirectories;
+
+ // Override cover option
+ if (bNoCover)
+ config.bCover = false;
+ }
+ 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.iInstallerType < GlobalConstants::PLATFORMS[0].platformId || config.iInstallerType > platform_sum)
+ {
+ std::cout << "Invalid value for --platform" << std::endl;
+ return 1;
+ }
+
+ if (config.iInstallerLanguage < GlobalConstants::LANGUAGES[0].languageId || config.iInstallerLanguage > language_sum)
+ {
+ 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.bLogin || initResult == 1)
+ {
+ iLoginResult = downloader.login();
+ if (iLoginResult == 0)
+ return 1;
+ }
+
+ 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<std::string>();
+ else if ( type == typeid(int) )
+ option_value_string = std::to_string(option_value.as<int>());
+ else if ( type == typeid(size_t) )
+ option_value_string = std::to_string(option_value.as<size_t>());
+ else if ( type == typeid(unsigned int) )
+ option_value_string = std::to_string(option_value.as<unsigned int>());
+ else if ( type == typeid(long int) )
+ option_value_string = std::to_string(option_value.as<long int>());
+ else if ( type == typeid(bool) )
+ {
+ if (option_value.as<bool>() == true)
+ option_value_string = "true";
+ else
+ option_value_string = "false";
+ }
+ }
+ }
+ }
+ catch (...)
+ {
+ continue;
+ }
+
+ if (!option_value_string.empty())
+ {
+ ofs << option << " = " << option_value_string << std::endl;
+ }
+ }
+ ofs.close();
+ 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();
+ return 0;
+ }
+ else
+ {
+ std::cout << "Failed to create config: " << config.sConfigFilePath << std::endl;
+ return 1;
+ }
+ }
+ else if (config.bUpdateCache)
+ downloader.updateCache();
+ else if (config.bUpdateCheck) // Update check has priority over download and list
+ downloader.updateCheck();
+ 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;
+}
--- /dev/null
+[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/gamename.conf\fP
+JSON formatted file. Sets game specific settings for \fBgamename\fP.
+.br
+Allowed settings are \fBlanguage\fP, \fBplatform\fP and \fBdlc\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" : <int>,
+ "platform" : <int>,
+ "dlc" : <bool>
+.br
+}
+
+[availability]
+The latest version of this distribution is available from \fIhttps://github.com/Sude-/lgogdownloader\fP
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "api.h"
+#include "gamefile.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <sstream>
+#include <jsoncpp/json/json.h>
+
+#if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) >= 40900
+# define _regex_namespace_ std
+# include <regex>
+#else
+# define _regex_namespace_ boost
+# include <boost/regex.hpp>
+#endif
+
+size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) {
+ std::ostringstream *stream = (std::ostringstream*)userp;
+ size_t count = 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->getFormatedErrorMessages());
+ 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->getFormatedErrorMessages());
+ 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));
+ else
+ this->setError("HTTP ERROR: failed to get error code: " + static_cast<std::string>(curl_easy_strerror(result)));
+ }
+
+ 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;
+ unsigned int type = platform;
+ struct gameFileInfo
+ {
+ Json::Value jsonNode;
+ unsigned int platform;
+ unsigned int language;
+ };
+
+ url = this->config.get_game_details + game_name + "/" + "installer_win_en"; // can't get game details without file id, any file id seems to return all details which is good for us
+ std::string json = this->getResponseOAuth(url);
+
+ if (!json.empty())
+ {
+ Json::Value root;
+ Json::Reader *jsonparser = new Json::Reader;
+ if (jsonparser->parse(json, root))
+ {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (API::getGameDetails)" << std::endl << root << std::endl;
+ #endif
+ game.gamename = game_name;
+ game.title = root["game"]["title"].asString();
+ game.icon = root["game"]["icon"].asString();
+ std::vector<std::string> membernames = root["game"].getMemberNames();
+
+ // Installer details
+ // Create a list of installers from JSON
+ std::vector<gameFileInfo> installers;
+ for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+ { // Check against the specified platforms
+ if (type & GlobalConstants::PLATFORMS[i].platformId)
+ {
+ std::string installer = "installer_" + GlobalConstants::PLATFORMS[i].platformCode + "_";
+ for (unsigned int j = 0; j < GlobalConstants::LANGUAGES.size(); ++j)
+ { // Check against the specified languages
+ if (lang & GlobalConstants::LANGUAGES[j].languageId)
+ { // Make sure that the installer exists in the JSON
+ if (root["game"].isMember(installer+GlobalConstants::LANGUAGES[j].languageCode))
+ {
+ gameFileInfo installerInfo;
+ installerInfo.jsonNode = root["game"][installer+GlobalConstants::LANGUAGES[j].languageCode];
+ installerInfo.platform = GlobalConstants::PLATFORMS[i].platformId;
+ installerInfo.language = GlobalConstants::LANGUAGES[j].languageId;
+ 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].languageId)
+ {
+ // Try to find a patch
+ _regex_namespace_::regex re(GlobalConstants::LANGUAGES[i].languageCode + "\\d+patch\\d+", _regex_namespace_::regex_constants::icase); // regex for patch node names
+ std::vector<gameFileInfo> patches;
+ for (unsigned int j = 0; j < membernames.size(); ++j)
+ {
+ if (_regex_namespace_::regex_match(membernames[j], re))
+ { // Regex matches, we have a patch node
+ gameFileInfo patchInfo;
+ patchInfo.jsonNode = root["game"][membernames[j]];
+ patchInfo.language = GlobalConstants::LANGUAGES[i].languageId;
+ 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 (type & 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].languageId; // Add language code to patch
+ bDuplicate = true;
+ break;
+ }
+ }
+ if (bDuplicate)
+ continue;
+ }
+
+ // Treat tarball archives as installers
+ if (patch["link"].asString().find("/linux/") != std::string::npos && patch["name"].asString() == "Tarball archive")
+ {
+ game.installers.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].languageId,
+ patches[j].platform
+ )
+ );
+ }
+ else
+ {
+ 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].languageId,
+ 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].languageId; // Add language code to patch
+ bDuplicate = true;
+ break;
+ }
+ }
+ if (bDuplicate)
+ continue;
+ }
+
+ // Treat tarball archives as installers
+ if (patchnode["link"].asString().find("/linux/") != std::string::npos && patchnode["name"].asString() == "Tarball archive")
+ {
+ game.installers.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].languageId,
+ patches[j].platform
+ )
+ );
+ }
+ else
+ {
+ 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].languageId,
+ 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].languageId)
+ {
+ // Try to find a language pack
+ _regex_namespace_::regex re(GlobalConstants::LANGUAGES[i].languageCode + "\\d+langpack\\d+", _regex_namespace_::regex_constants::icase); // regex for language pack node names
+ std::vector<std::string> langpacknames;
+ for (unsigned int j = 0; j < membernames.size(); ++j)
+ {
+ if (_regex_namespace_::regex_match(membernames[j], re))
+ langpacknames.push_back(membernames[j]);
+ }
+
+ if (!langpacknames.empty()) // found at least one language pack
+ {
+ for (unsigned int i = 0; i < langpacknames.size(); ++i)
+ {
+ Json::Value langpack = root["game"][langpacknames[i]];
+ 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].languageId
+ )
+ );
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (API::getGameDetails)" << std::endl << json << std::endl;
+ #endif
+ this->setError(jsonparser->getFormatedErrorMessages());
+ }
+ 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->getFormatedErrorMessages());
+ }
+ 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->getFormatedErrorMessages());
+ }
+ 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->getFormatedErrorMessages());
+ }
+ 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);
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "blacklist.h"
+#include "config.h"
+#include "api.h"
+#include "util.h"
+
+#include <iostream>
+#include <utility>
+
+enum {
+ BLFLAG_RX = 1 << 0,
+ BLFLAG_PERL = 1 << 1
+};
+
+void Blacklist::initialize(const std::vector<std::string>& lines) {
+ int linenr = 1;
+ for (auto it = lines.begin(); it != lines.end(); ++it, ++linenr) {
+ BlacklistItem item;
+ const std::string& s = *it;
+
+ if (s.length() == 0 || s[0] == '#')
+ continue;
+
+ std::size_t i;
+ for (i = 0; i < s.length() && s[i] != '\x20'; ++i) {
+ switch (s[i]) {
+ case 'R':
+ item.flags |= BLFLAG_RX;
+ break;
+ case 'p':
+ item.flags |= BLFLAG_PERL;
+ break;
+ default:
+ std::cout << "unknown flag '" << s[i] << "' in blacklist line " << linenr << std::endl;
+ break;
+ }
+ }
+ ++i;
+ if (i == s.length()) {
+ std::cout << "empty expression in blacklist line " << linenr << std::endl;
+ continue;
+ }
+ if (item.flags & BLFLAG_RX) {
+ boost::regex::flag_type rx_flags = boost::regex::normal;
+
+ // we only support perl-like syntax for now, which is boost default (normal). Add further flag processing
+ // here if that changes.
+
+ rx_flags |= boost::regex::nosubs;
+
+ item.linenr = linenr;
+ item.source.assign(s.substr(i).c_str());
+ item.regex.assign(item.source, rx_flags);
+ blacklist_.push_back(std::move(item));
+ } else {
+ std::cout << "unknown expression type in blacklist line " << linenr << std::endl;
+ }
+ }
+}
+
+bool Blacklist::isBlacklisted(const std::string& path) {
+ for (auto it = blacklist_.begin(); it != blacklist_.end(); ++it) {
+ const BlacklistItem& item = *it;
+ if (item.flags & BLFLAG_RX && boost::regex_search(path, item.regex))
+ return true;
+ }
+ return false;
+}
+
+bool Blacklist::isBlacklisted(const std::string& path, const std::string& gamename, std::string subdirectory)
+{
+ std::string filepath = Util::makeRelativeFilepath(path, gamename, subdirectory);
+ return isBlacklisted(filepath);
+}
+
--- /dev/null
+/* 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 <cstdio>
+#include <cstdlib>
+#include <ctime>
+#include <iostream>
+#include <sstream>
+#include <unistd.h>
+#include <fstream>
+#include <iomanip>
+#include <boost/filesystem.hpp>
+#include <boost/regex.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <tinyxml.h>
+#include <jsoncpp/json/json.h>
+#include <htmlcxx/html/ParserDom.h>
+#include <htmlcxx/html/Uri.h>
+#include <boost/algorithm/string/case_conv.hpp>
+
+namespace bptime = boost::posix_time;
+
+Downloader::Downloader(Config &conf)
+{
+ this->config = conf;
+}
+
+Downloader::~Downloader()
+{
+ if (config.bReport)
+ if (this->report_ofs)
+ this->report_ofs.close();
+ delete progressbar;
+ delete gogAPI;
+ curl_easy_cleanup(curlhandle);
+ curl_global_cleanup();
+}
+
+
+/* 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);
+
+ // 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)
+ 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)) )
+ 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;
+ 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 (!HTTP_Login(email, password))
+ {
+ std::cout << "HTTP: Login failed" << std::endl;
+ return 0;
+ }
+ else
+ {
+ std::cout << "HTTP: Login successful" << std::endl;
+ }
+ // Login to API
+ 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;
+ }
+ }
+}
+
+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()
+{
+ gameItems = this->getGames();
+
+ // Filter the game list
+ if (!config.sGameRegex.empty())
+ {
+ // GameRegex filter aliases
+ if (config.sGameRegex == "all")
+ config.sGameRegex = ".*";
+
+ if (config.sGameRegex == "free")
+ {
+ gameItems = this->getFreeGames();
+ }
+ else
+ { // Filter the names
+ std::vector<gameItem> gameItemsFiltered;
+ boost::regex expression(config.sGameRegex);
+ boost::match_results<std::string::const_iterator> what;
+ for (unsigned int i = 0; i < gameItems.size(); ++i)
+ {
+ if (boost::regex_search(gameItems[i].name, what, expression)) // Check if name matches the specified regex
+ gameItemsFiltered.push_back(gameItems[i]);
+ }
+ gameItems = gameItemsFiltered;
+ }
+ }
+}
+
+/* Get detailed info about the games
+ returns 0 if successful
+ returns 1 if fails
+*/
+int Downloader::getGameDetails()
+{
+ if (config.bUseCache)
+ {
+ 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.iInstallerLanguage = config.iInstallerLanguage;
+ conf.iInstallerType = config.iInstallerType;
+ 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.iInstallerType << ", DLC: " << (conf.bDLC ? "true" : "false") << std::endl;
+ }
+
+ game = gogAPI->getGameDetails(gameItems[i].name, conf.iInstallerType, conf.iInstallerLanguage, config.bDuplicateHandler);
+ if (!gogAPI->getError())
+ {
+ if (game.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras
+ {
+ game.extras = this->getExtras(gameItems[i].name, gameItems[i].id);
+ }
+ 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.iInstallerType, conf.iInstallerLanguage, config.bDuplicateHandler);
+ if (dlc.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras
+ {
+ dlc.extras = this->getExtras(gameItems[i].dlcnames[j], gameItems[i].id);
+ }
+ 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;
+ return 1;
+ }
+ }
+ 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;
+ // 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;
+ for (unsigned int k = 0; k < GlobalConstants::LANGUAGES.size(); k++) // Check which languages the installer supports
+ {
+ if (games[i].installers[j].language & GlobalConstants::LANGUAGES[k].languageId)
+ languages += (languages.empty() ? "" : ", ")+GlobalConstants::LANGUAGES[k].languageString;
+ }
+
+ 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;
+ for (unsigned int k = 0; k < GlobalConstants::LANGUAGES.size(); k++) // Check which languages the patch supports
+ {
+ if (games[i].patches[j].language & GlobalConstants::LANGUAGES[k].languageId)
+ languages += (languages.empty() ? "" : ", ")+GlobalConstants::LANGUAGES[k].languageString;
+ }
+
+ 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)
+ {
+ 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)
+ {
+ // 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<std::string::const_iterator> 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 (!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 (!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 (!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.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 (!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 (!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;
+ size_t offset=0;
+
+ // Get directory from filepath
+ boost::filesystem::path pathname = filepath;
+ 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);
+ offset = ftell(outfile);
+ curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM, 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)
+ {
+ 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<std::string>(curl_easy_strerror(res));
+ if (bResume && res == CURLE_RANGE_ERROR) // CURLE_RANGE_ERROR on resume attempts is not an error that user needs to know about
+ status = "No error";
+ std::string report_line = "Downloaded [" + status + "] " + filepath;
+ this->report_ofs << report_line << std::endl;
+ }
+
+ // Retry partially downloaded file
+ if (res == CURLE_PARTIAL_FILE && (this->retries < config.iRetries) )
+ {
+ this->retries++;
+ 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;
+ size_t offset=0, from_offset, to_offset, filesize;
+ std::string filehash;
+ int chunks;
+ std::vector<size_t> chunk_from, chunk_to;
+ std::vector<std::string> chunk_hash;
+ bool bParsingFailed = false;
+
+ // Get filename
+ boost::filesystem::path pathname = filepath;
+ std::string filename = pathname.filename().string();
+ std::string xml_directory;
+ if (!gamename.empty())
+ xml_directory = config.sXMLDirectory + "/" + gamename;
+ else
+ xml_directory = config.sXMLDirectory;
+ std::string xml_file = xml_directory + "/" + filename + ".xml";
+ bool bFileExists = boost::filesystem::exists(pathname);
+ bool bLocalXMLExists = boost::filesystem::exists(xml_file);
+
+ 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);
+ offset = ftell(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;
+ }
+
+ 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
+ {
+ CURLcode result = this->downloadFile(url, filepath, xml_data, gamename);
+ std::cout << std::endl;
+ if (result == CURLE_OK)
+ res = 1;
+ else
+ res = 0;
+ }
+ }
+ return res;
+ }
+
+ // Check all chunks
+ int iChunksRepaired = 0;
+ for (int i=0; i<chunks; i++)
+ {
+ size_t chunk_begin = chunk_from.at(i);
+ size_t chunk_end = chunk_to.at(i);
+ size_t size=0, chunk_size = chunk_end - chunk_begin + 1;
+ std::string range = std::to_string(chunk_begin) + "-" + std::to_string(chunk_end); // Download range string for curl
+
+ std::cout << "\033[0K\rChunk " << i << " (" << chunk_size << " bytes): ";
+ fseek(outfile, chunk_begin, SEEK_SET);
+ unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *));
+ if (chunk == NULL)
+ {
+ std::cout << "Memory error" << std::endl;
+ fclose(outfile);
+ return res;
+ }
+ size = fread(chunk, 1, chunk_size, outfile);
+ if (size != chunk_size)
+ {
+ std::cout << "Read error" << std::endl;
+ free(chunk);
+ fclose(outfile);
+ return res;
+ }
+ std::string hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5);
+ if (hash != chunk_hash.at(i))
+ {
+ std::cout << "Failed - downloading chunk" << std::endl;
+ fseek(outfile, chunk_begin, SEEK_SET);
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, outfile);
+ curl_easy_setopt(curlhandle, CURLOPT_RANGE, range.c_str()); //download range
+ this->beginDownload(); //begin chunk download
+ std::cout << std::endl;
+ if (config.bReport)
+ iChunksRepaired++;
+ i--; //verify downloaded chunk
+ }
+ else
+ {
+ std::cout << "OK\r" << std::flush;
+ }
+ free(chunk);
+ res = 1;
+ }
+ std::cout << std::endl;
+ fclose(outfile);
+
+ if (config.bReport)
+ {
+ std::string report_line = "Repaired [" + std::to_string(iChunksRepaired) + "/" + std::to_string(chunks) + "] " + filepath;
+ this->report_ofs << report_line << std::endl;
+ }
+
+ return res;
+}
+
+// Download cover images
+int Downloader::downloadCovers(const std::string& gamename, const std::string& directory, const std::string& cover_xml_data)
+{
+ int res = 0;
+ 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<std::string::const_iterator> 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 << std::endl;
+ else
+ std::cout << "failed to get error code: " << curl_easy_strerror(result) << 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->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 << std::endl;
+ else
+ std::cout << "failed to get error code: " << curl_easy_strerror(result) << 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)
+ unsigned int bar_length = 26;
+ Downloader* downloader = static_cast<Downloader*>(clientp);
+
+ double rate; // average download speed in B/s
+ // trying to get rate and setting to NaN if it fails
+ if (CURLE_OK != curl_easy_getinfo(downloader->curlhandle, CURLINFO_SPEED_DOWNLOAD, &rate))
+ rate = std::numeric_limits<double>::quiet_NaN();
+
+ // (Shmerl): this flag is needed to catch the case before anything was downloaded on resume,
+ // and there is no way to calculate the fraction, so we set to 0 (otherwise it'd be 1).
+ // This is to prevent the progress bar from jumping to 100% and then to lower value.
+ // It's visually better to jump from 0% to higher one.
+ bool starting = ((0.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<double>(downloader->getResumePosition());
+ if (offset>0)
+ {
+ dlnow += offset;
+ dltotal += offset;
+ }
+
+ // Update progress bar every 100ms
+ if (downloader->timer.getTimeBetweenUpdates()>=100 || dlnow == dltotal)
+ {
+ downloader->timer.reset();
+ 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);
+ downloader->progressbar->draw(bar_length, fraction);
+
+ // 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";
+ }
+ printf(" %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());
+ fflush(stdout);
+ }
+
+ 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);
+}
+
+size_t Downloader::getResumePosition()
+{
+ return this->resume_position;
+}
+
+// Login to GOG website
+int Downloader::HTTP_Login(const std::string& email, const std::string& password)
+{
+ int res = 0;
+ std::string postdata;
+ std::ostringstream memory;
+ std::string token;
+ std::string tagname_username;
+ std::string tagname_password;
+ std::string tagname_login;
+ std::string tagname_token;
+
+ // Get login token
+ std::string html = this->getResponse("https://www.gog.com/");
+ htmlcxx::HTML::ParserDom parser;
+ tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
+ tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+ // Find auth_url
+ bool bFoundAuthUrl = false;
+ for (; it != end; ++it)
+ {
+ if (it->tagName()=="script")
+ {
+ std::string auth_url;
+ for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
+ {
+ tree<htmlcxx::HTML::Node>::iterator script_it = dom.child(it, i);
+ if (!script_it->isTag() && !script_it->isComment())
+ {
+ if (script_it->text().find("GalaxyAccounts") != std::string::npos)
+ {
+ boost::match_results<std::string::const_iterator> what;
+ boost::regex expression(".*'(https://auth.gog.com/.*?)'.*");
+ boost::regex_match(script_it->text(), what, expression);
+ auth_url = what[1];
+ break;
+ }
+ }
+ }
+
+ if (!auth_url.empty())
+ { // Found auth_url, get the necessary info for login
+ bFoundAuthUrl = true;
+ std::string login_form_html = this->getResponse(auth_url);
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl;
+ std::cerr << login_form_html << std::endl;
+ #endif
+ tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html);
+ tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator login_it_end = login_dom.end();
+ for (; login_it != login_it_end; ++login_it)
+ {
+ if (login_it->tagName()=="input")
+ {
+ login_it->parseAttributes();
+ std::string id_login = login_it->attribute("id").second;
+ if (id_login == "login_username")
+ {
+ tagname_username = login_it->attribute("name").second;
+ }
+ else if (id_login == "login_password")
+ {
+ tagname_password = login_it->attribute("name").second;
+ }
+ else if (id_login == "login__token")
+ {
+ token = login_it->attribute("value").second; // login token
+ tagname_token = login_it->attribute("name").second;
+ }
+ }
+ else if (login_it->tagName()=="button")
+ {
+ login_it->parseAttributes();
+ std::string id_login = login_it->attribute("id").second;
+ if (id_login == "login_login")
+ {
+ tagname_login = login_it->attribute("name").second;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (!bFoundAuthUrl)
+ {
+ std::cout << "Failed to find url for login form" << std::endl;
+ }
+
+ if (token.empty())
+ {
+ std::cout << "Failed to get login token" << std::endl;
+ return res = 0;
+ }
+
+ //Create postdata - escape characters in email/password to support special characters
+ postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size())
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size())
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "="
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size());
+ curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check");
+ curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
+ curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
+
+ // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first.
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
+ CURLcode result = curl_easy_perform(curlhandle);
+ memory.str(std::string());
+
+ if (result != CURLE_OK)
+ {
+ // Expected to hit maximum amount of redirects so don't print error on it
+ if (result != CURLE_TOO_MANY_REDIRECTS)
+ std::cout << curl_easy_strerror(result) << std::endl;
+ }
+
+ // Get redirect url
+ char *redirect_url;
+ curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
+
+ 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");
+
+ std::string account_email, username;
+ dom = parser.parseTree(html);
+ it = dom.begin();
+ end = dom.end();
+ bool bEmailFound = false;
+ bool bUsernameFound = false;
+ for (; it != end; ++it)
+ {
+ if (it->tagName()=="input")
+ {
+ it->parseAttributes();
+ if (it->attribute("id").second == "accountEditEmail")
+ {
+ account_email = it->attribute("value").second;
+ bEmailFound = true;
+ }
+ }
+ else if (it->tagName()=="span")
+ {
+ it->parseAttributes();
+ if (it->attribute("class").second == "nickname")
+ {
+ for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
+ {
+ tree<htmlcxx::HTML::Node>::iterator nick_it = dom.child(it, i);
+ if (!nick_it->isTag() && !nick_it->isComment())
+ {
+ username = nick_it->text();
+ bUsernameFound = true;
+ }
+ }
+ }
+ }
+
+ if (bUsernameFound && bEmailFound)
+ break;
+ }
+
+ // Convert to lowercase for comparison
+ 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
+ boost::algorithm::to_lower(account_email);
+ boost::algorithm::to_lower(username);
+
+ if (email_lowercase == account_email || email_lowercase == username)
+ {
+ res = 1; // Login successful
+ }
+ else
+ {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl;
+ if (!bEmailFound || !bUsernameFound)
+ {
+ if (!bEmailFound)
+ std::cerr << "Could not find \"accountEditEmail\" input field on account settings page." << std::endl;
+ if (!bUsernameFound)
+ std::cerr << "Could not find username on account settings page." << std::endl;
+ }
+ else
+ {
+ if (email_lowercase != account_email)
+ std::cerr << "Email (" << email_lowercase << ") doesn't match account email (" << account_email << ")" << std::endl;
+ if (email_lowercase != username)
+ std::cerr << "Username (" << email_lowercase << ") doesn't match account username (" << username << ")" << std::endl;
+ }
+ #endif
+ res = 0; // Login failed
+ }
+
+ return res;
+}
+
+// Get list of games from account page
+std::vector<gameItem> Downloader::getGames()
+{
+ std::vector<gameItem> games;
+ Json::Value root;
+ Json::Reader *jsonparser = new Json::Reader;
+ int i = 1;
+ std::string html = "";
+ std::string page_html = "";
+
+ do
+ {
+ std::string response = this->getResponse("https://www.gog.com/account/ajax?a=gamesShelfMore&s=title&q=&t=0&p=" + 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->getFormatedErrorMessages();
+ delete jsonparser;
+ exit(1);
+ }
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << root << std::endl;
+ #endif
+ page_html = root["html"].asString();
+ html += page_html;
+ if (page_html.empty() && i == 1)
+ {
+ std::cout << "No games were found on your account. Try --login to refresh your authorization." << std::endl;
+ }
+ i++;
+ } while (!page_html.empty());
+
+ delete jsonparser;
+
+ // Parse HTML to get game names
+ htmlcxx::HTML::ParserDom parser;
+ tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
+ tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+ for (; it != end; ++it)
+ {
+ if (it->tagName()=="div")
+ {
+ it->parseAttributes();
+ std::string classname = it->attribute("class").second;
+ if (classname=="shelf_game")
+ {
+ gameItem game;
+ // Game name is contained in data-gameindex attribute
+ game.name = it->attribute("data-gameindex").second;
+ game.id = it->attribute("data-gameid").second;
+
+ // Get platform info
+ std::string tags = it->attribute("data-title").second;
+ unsigned int platform = GlobalConstants::PLATFORM_WINDOWS; // The tags don't specify Windows support so assume that there's always a Windows version
+
+ if (tags.find("linux") != std::string::npos)
+ platform |= GlobalConstants::PLATFORM_LINUX;
+ if (tags.find("osx mac") != std::string::npos)
+ platform |= GlobalConstants::PLATFORM_MAC;
+
+ // Skip if platform doesn't match
+ if (!(platform & config.iInstallerType))
+ continue;
+
+ if (!game.name.empty() && !game.id.empty())
+ {
+ // Check for DLC
+ if (config.bDLC)
+ {
+ tree<htmlcxx::HTML::Node>::iterator dlc_it = it;
+ tree<htmlcxx::HTML::Node>::iterator dlc_end = it.end();
+ for (; dlc_it != dlc_end; ++dlc_it)
+ {
+ if (dlc_it->tagName()=="div")
+ {
+ dlc_it->parseAttributes();
+ std::string classname_dlc = dlc_it->attribute("class").second;
+ if (classname_dlc == "shelf-game-dlc-counter")
+ {
+ std::string content;
+ for (unsigned int i = 0; i < dom.number_of_children(dlc_it); ++i)
+ {
+ tree<htmlcxx::HTML::Node>::iterator it = dom.child(dlc_it, i);
+ if (!it->isTag() && !it->isComment())
+ content += it->text();
+ }
+ // Get game names if game has DLC
+ if (content.find("DLC")!=std::string::npos)
+ {
+ Json::Value root;
+ Json::Reader *jsonparser = new Json::Reader;
+
+ std::string gameDataUrl = "https://www.gog.com/account/ajax?a=gamesListDetails&g=" + game.id;
+ std::string json = this->getResponse(gameDataUrl);
+ // Parse JSON
+ if (!jsonparser->parse(json, root))
+ {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << json << std::endl;
+ #endif
+ std::cout << jsonparser->getFormatedErrorMessages();
+ delete jsonparser;
+ exit(1);
+ }
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << root << std::endl;
+ #endif
+ std::string html = root["details"]["html"].asString();
+ delete jsonparser;
+
+ // Parse HTML to get game names for DLC
+ htmlcxx::HTML::ParserDom parser;
+ tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
+ tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+ for (; it != end; ++it)
+ {
+ if (it->tagName()=="div")
+ {
+ it->parseAttributes();
+ std::string gamename = it->attribute("data-gameindex").second;
+ if (!gamename.empty() && gamename!=game.name)
+ {
+ bool bDuplicate = false;
+ for (unsigned int i = 0; i < game.dlcnames.size(); ++i)
+ {
+ if (gamename == game.dlcnames[i])
+ {
+ bDuplicate = true;
+ break;
+ }
+ }
+ if (!bDuplicate)
+ game.dlcnames.push_back(gamename);
+ }
+ }
+ }
+
+ // Try getting game names for DLCs from extra links. Catches game names for DLCs that don't have installers.
+ it = dom.begin();
+ end = dom.end();
+ for (; it != end; ++it)
+ {
+ if (it->tagName()=="a")
+ {
+ it->parseAttributes();
+ std::string href = it->attribute("href").second;
+ std::string search_string = "/downlink/file/"; // Extra links: https://www.gog.com/downlink/file/gamename/id_number
+ if (href.find(search_string)!=std::string::npos)
+ {
+ std::string gamename;
+ gamename.assign(href.begin()+href.find(search_string)+search_string.length(), href.begin()+href.find_last_of("/"));
+ if (!gamename.empty() && gamename!=game.name)
+ {
+ bool bDuplicate = false;
+ for (unsigned int i = 0; i < game.dlcnames.size(); ++i)
+ {
+ if (gamename == game.dlcnames[i])
+ {
+ bDuplicate = true;
+ break;
+ }
+ }
+ if (!bDuplicate)
+ game.dlcnames.push_back(gamename);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ games.push_back(game);
+ }
+ }
+ }
+ }
+
+ return games;
+}
+
+// Get list of free games
+std::vector<gameItem> Downloader::getFreeGames()
+{
+ Json::Value root;
+ Json::Reader *jsonparser = new Json::Reader;
+ std::vector<gameItem> games;
+ std::string json = this->getResponse("https://www.gog.com/games/ajax/filtered?mediaType=game&page=1&price=free&sort=title");
+
+ // Parse JSON
+ if (!jsonparser->parse(json, root))
+ {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::getFreeGames)" << std::endl << json << std::endl;
+ #endif
+ std::cout << jsonparser->getFormatedErrorMessages();
+ 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;
+}
+
+std::vector<gameFile> Downloader::getExtras(const std::string& gamename, const std::string& gameid)
+{
+ Json::Value root;
+ Json::Reader *jsonparser = new Json::Reader;
+ std::vector<gameFile> extras;
+
+ std::string gameDataUrl = "https://www.gog.com/account/ajax?a=gamesListDetails&g=" + gameid;
+ std::string json = this->getResponse(gameDataUrl);
+ // Parse JSON
+ if (!jsonparser->parse(json, root))
+ {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::getExtras)" << std::endl << json << std::endl;
+ #endif
+ std::cout << jsonparser->getFormatedErrorMessages();
+ delete jsonparser;
+ exit(1);
+ }
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Downloader::getExtras)" << std::endl << root << std::endl;
+ #endif
+ std::string html = root["details"]["html"].asString();
+ delete jsonparser;
+
+ htmlcxx::HTML::ParserDom parser;
+ tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
+ tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+ for (; it != end; ++it)
+ {
+ if (it->tagName()=="a")
+ {
+ it->parseAttributes();
+ std::string href = it->attribute("href").second;
+ // Extra links https://www.gog.com/downlink/file/gamename/id_number
+ if (href.find("/downlink/file/" + gamename + "/")!=std::string::npos)
+ {
+ std::string id, name, path;
+ id.assign(href.begin()+href.find_last_of("/")+1, href.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 name from path
+ name.assign(path.begin()+path.find_last_of("/")+1,path.end());
+
+ extras.push_back(
+ gameFile ( false,
+ id,
+ name,
+ path,
+ std::string()
+ )
+ );
+ }
+ }
+ }
+
+ return extras;
+}
+
+void Downloader::checkOrphans()
+{
+ // Always check everything when checking for orphaned files
+ config.bInstallers = true;
+ config.bExtras = true;
+ config.bPatches = true;
+ config.bLanguagePacks = true;
+
+ if (this->games.empty())
+ this->getGameDetails();
+
+ std::vector<std::string> orphans;
+ for (unsigned int i = 0; i < games.size(); ++i)
+ {
+ std::cout << "Checking for orphaned files " << i+1 << " / " << games.size() << "\r" << std::flush;
+ std::vector<boost::filesystem::path> filepath_vector;
+
+ try
+ {
+ std::vector<boost::filesystem::path> paths;
+ std::vector<unsigned int> platformIds;
+ platformIds.push_back(0);
+ for (unsigned int j = 0; j < GlobalConstants::PLATFORMS.size(); ++j)
+ {
+ platformIds.push_back(GlobalConstants::PLATFORMS[j].platformId);
+ }
+ 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<std::string::const_iterator> 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
+ size_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))
+ {
+ filesize = boost::filesystem::file_size(filepath);
+
+ if (remoteHash != localHash)
+ bHashOK = false;
+
+ 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);
+ size_t filesize;
+
+ if (boost::filesystem::exists(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);
+ size_t filesize;
+
+ if (boost::filesystem::exists(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);
+ size_t filesize;
+
+ if (boost::filesystem::exists(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
+ size_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))
+ {
+ filesize = boost::filesystem::file_size(filepath);
+
+ if (remoteHash != localHash)
+ bHashOK = false;
+
+ 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.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);
+ size_t filesize;
+
+ if (boost::filesystem::exists(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.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);
+ size_t filesize;
+
+ if (boost::filesystem::exists(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;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ 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))
+ {
+ 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 (!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->getFormatedErrorMessages() << 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;
+ std::string cachepath = config.sCacheDirectory + "/gamedetails.json";
+
+ Json::Value json;
+
+ json["date"] = bptime::to_iso_string(bptime::second_clock::local_time());
+
+ for (unsigned int i = 0; i < this->games.size(); ++i)
+ json["games"].append(this->games[i].getDetailsAsJson());
+
+ std::ofstream ofs(cachepath);
+ if (!ofs)
+ {
+ res = 1;
+ }
+ else
+ {
+ Json::StyledStreamWriter jsonwriter;
+ jsonwriter.write(ofs, json);
+ ofs.close();
+ }
+ return res;
+}
+
+std::vector<gameDetails> Downloader::getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level)
+{
+ std::vector<gameDetails> details;
+
+ // If root node is not array and we use root.size() it will return the number of nodes --> limit to 1 "array" node to make sure it is handled properly
+ for (unsigned int i = 0; i < (root.isArray() ? root.size() : 1); ++i)
+ {
+ Json::Value gameDetailsNode = (root.isArray() ? root[i] : root); // This json node can be array or non-array so take that into account
+ gameDetails game;
+ game.gamename = gameDetailsNode["gamename"].asString();
+
+ // DLCs are handled as part of the game so make sure that filtering is done with base game name
+ if (recursion_level == 0) // recursion level is 0 when handling base game
+ {
+ boost::regex expression(config.sGameRegex);
+ boost::match_results<std::string::const_iterator> what;
+ if (!boost::regex_search(game.gamename, what, expression)) // Check if name matches the specified regex
+ continue;
+ }
+ game.title = gameDetailsNode["title"].asString();
+ game.icon = gameDetailsNode["icon"].asString();
+
+ // Make a vector of valid node names to make things easier
+ std::vector<std::string> nodes;
+ nodes.push_back("extras");
+ nodes.push_back("installers");
+ nodes.push_back("patches");
+ nodes.push_back("languagepacks");
+ nodes.push_back("dlcs");
+
+ gameSpecificConfig conf;
+ conf.bDLC = config.bDLC;
+ conf.iInstallerLanguage = config.iInstallerLanguage;
+ conf.iInstallerType = config.iInstallerType;
+ if (Util::getGameSpecificConfig(game.gamename, &conf) > 0)
+ std::cout << game.gamename << " - Language: " << conf.iInstallerLanguage << ", Platform: " << conf.iInstallerType << ", 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.iInstallerType))
+ 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)
+ game.dlcs = this->getGameDetailsFromJsonNode(fileDetailsNode, recursion_level + 1);
+ }
+ }
+ }
+ if (!game.extras.empty() || !game.installers.empty() || !game.patches.empty() || !game.languagepacks.empty() || !game.dlcs.empty())
+ details.push_back(game);
+ }
+ return details;
+}
+
+void Downloader::updateCache()
+{
+ // Make sure that all details get cached
+ unsigned int all_platforms = GlobalConstants::PLATFORM_WINDOWS;
+ unsigned int all_languages = GlobalConstants::LANGUAGE_EN;
+ for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+ all_platforms |= GlobalConstants::PLATFORMS[i].platformId;
+ for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+ all_languages |= GlobalConstants::LANGUAGES[i].languageId;
+
+ config.bExtras = true;
+ config.bInstallers = true;
+ config.bPatches = true;
+ config.bLanguagePacks = true;
+ config.bDLC = true;
+ config.sGameRegex = ".*";
+ config.iInstallerLanguage = all_languages;
+ config.iInstallerType = all_platforms;
+
+ this->getGameList();
+ this->getGameDetails();
+ if (this->saveGameDetailsCache())
+ std::cout << "Failed to save cache" << std::endl;
+
+ return;
+}
--- /dev/null
+#include "gamedetails.h"
+#include "util.h"
+
+gameDetails::gameDetails()
+{
+ //ctor
+}
+
+gameDetails::~gameDetails()
+{
+ //dtor
+}
+
+void gameDetails::makeFilepaths(const Config& config)
+{
+ std::string filepath;
+ std::string directory = config.sDirectory + "/" + config.sGameSubdir + "/";
+ std::string subdir;
+
+ 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)
+ {
+ 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;
+
+ 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;
+}
--- /dev/null
+#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;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "progressbar.h"
+#include <cmath>
+
+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;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "util.h"
+
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <tinyxml.h>
+#include <jsoncpp/json/json.h>
+#include <fstream>
+
+/*
+ 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, size_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, size_t chunk_size, std::string xml_dir)
+{
+ int res = 0;
+ FILE *infile;
+ FILE *xmlfile;
+ size_t filesize, size;
+ int chunks, i;
+
+ if (xml_dir.empty())
+ {
+ char *xdgcache = getenv("XDG_CACHE_HOME");
+ if (xdgcache)
+ xml_dir = (std::string)xdgcache + "/lgogdownloader/xml";
+ else
+ {
+ std::string home = (std::string)getenv("HOME");
+ xml_dir = home + "/.cache/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++) {
+ size_t range_begin = i*chunk_size;
+ fseek(infile, range_begin, SEEK_SET);
+ if ((i == chunks-1) && (remaining != 0))
+ chunk_size = remaining;
+ size_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())
+ {
+ char *xdghome = getenv("XDG_CONFIG_HOME");
+ if (xdghome)
+ directory = (std::string)xdghome + "/lgogdownloader";
+ else
+ {
+ std::string home = (std::string)getenv("HOME");
+ directory = home + "/.config/lgogdownloader";
+ }
+ }
+
+ 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->iInstallerType = root["platform"].asUInt();
+ res++;
+ }
+ if (root.isMember("dlc"))
+ {
+ conf->bDLC = root["dlc"].asBool();
+ res++;
+ }
+ }
+ else
+ {
+ std::cout << "Failed to parse game specific config" << std::endl;
+ std::cout << jsonparser->getFormatedErrorMessages() << 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].platformId) == GlobalConstants::PLATFORMS[i].platformId)
+ {
+ platform = boost::algorithm::to_lower_copy(GlobalConstants::PLATFORMS[i].platformString);
+ 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
+}
--- /dev/null
+#!/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"