commit 2d045c4052afb0cafd41046e9fb996ec6cf6f644 Author: Sude Date: Fri Mar 15 22:46:16 2013 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94d6e05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.layout +*~ +*.[oa] +bin/* +obj/* diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5a8e332 --- /dev/null +++ b/COPYING @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8802ecb --- /dev/null +++ b/Makefile @@ -0,0 +1,124 @@ +#------------------------------------------------------------------------------# +# This makefile was generated by 'cbp2make' tool rev.127 # +#------------------------------------------------------------------------------# + + +WORKDIR = `pwd` + +CC = gcc +CXX = g++ +AR = ar +LD = g++ +WINDRES = windres + +INC = -Iinclude +CFLAGS = -std=c++0x -Wall -fexceptions +RESINC = +LIBDIR = +LIB = -lcurl -loauth -ljsoncpp -lhtmlcxx -lboost_system -lboost_filesystem -lboost_regex -lboost_program_options -lboost_date_time -ltinyxml -lrhash +LDFLAGS = + +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 + +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 + +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) $(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 + +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: + +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) $(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 + +clean_release: + rm -f $(OBJ_RELEASE) $(OUT_RELEASE) + rm -rf bin/Release + rm -rf $(OBJDIR_RELEASE) + rm -rf $(OBJDIR_RELEASE)/src + +install: + install $(OUT_RELEASE) /usr/bin + +uninstall: + rm /usr/bin/lgogdownloader + +.PHONY: before_debug after_debug clean_debug before_release after_release clean_release + diff --git a/include/api.h b/include/api.h new file mode 100644 index 0000000..84914ef --- /dev/null +++ b/include/api.h @@ -0,0 +1,112 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#ifndef API_H +#define API_H + +#include +#include +#include +extern "C" { + #include +} +#include + +#define CONSUMER_KEY "1f444d14ea8ec776585524a33f6ecc1c413ed4a5" +#define CONSUMER_SECRET "20d175147f9db9a10fc0584aa128090217b9cf88" +#define OAUTH_VERIFIER_LENGTH 14 +#define OAUTH_TOKEN_LENGTH 11 +#define OAUTH_SECRET_LENGTH 18 + +#define INSTALLER_WINDOWS 1 +#define INSTALLER_MAC 2 + +#define LANGUAGE_EN 1 +#define LANGUAGE_DE 2 +#define LANGUAGE_FR 4 +#define LANGUAGE_PL 8 +#define LANGUAGE_RU 16 + +class gameFile { + public: + gameFile(const bool& t_updated, const std::string& t_id, const std::string& t_name, const std::string& t_path, const std::string& t_size); + bool updated; + std::string id; + std::string name; + std::string path; + std::string size; + virtual ~gameFile(); +}; + +class gameDetails { + public: + std::vector extras; + std::vector installers; + std::string gamename; + std::string title; + std::string icon; +}; + +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: + apiConfig config; + 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& type = INSTALLER_WINDOWS, const unsigned int& lang = LANGUAGE_EN); + 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 getXML(const std::string& game_name, const std::string& id); + void clearError(); + bool getError() { return this->error; }; + std::string getErrorMessage() { return this->error_message; }; + virtual ~API(); + protected: + private: + CURL* curlhandle; + void setError(const std::string& err); + bool error; + std::string error_message; +}; + +#endif // API_H diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..f73a578 --- /dev/null +++ b/include/config.h @@ -0,0 +1,48 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://sam.zoy.org/wtfpl/COPYING for more details. */ + +#ifndef CONFIG_H__ +#define CONFIG_H__ + +#include +#include + +class Config +{ + public: + Config() {}; + virtual ~Config() {}; + bool bVerbose; + bool bNoRemoteXML; + bool bNoCover; + bool bUpdateCheck; + bool bHelp; + bool bDownload; + bool bList; + bool bListDetails; + bool bLogin; + bool bRepair; + bool bNoInstallers; + bool bNoExtras; + bool bNoUnicode; // don't use Unicode in console output + bool bNoColor; // don't use colors + std::string sGameRegex; + std::string sDirectory; + std::string sXMLFile; + std::string sXMLDirectory; + std::string sToken; + std::string sSecret; + std::string sVersionString; + std::string sHome; + std::string sCookiePath; + std::string sConfigFilePath; + unsigned int iInstallerType; + unsigned int iInstallerLanguage; + size_t iChunkSize; + curl_off_t iDownloadRate; +}; + +#endif // CONFIG_H__ diff --git a/include/downloader.h b/include/downloader.h new file mode 100644 index 0000000..ee0bbca --- /dev/null +++ b/include/downloader.h @@ -0,0 +1,85 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#ifndef DOWNLOADER_H +#define DOWNLOADER_H + +#include "config.h" +#include "api.h" +#include "progressbar.h" +#include +#include + +#if __GNUC__ +# if __x86_64__ || __ppc64__ || __LP64__ +# define ENVIRONMENT64 +# else +# define ENVIRONMENT32 +# endif +#endif + +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 Downloader +{ + public: + Downloader(Config &conf); + virtual ~Downloader(); + int init(); + void listGames(); + void updateCheck(); + void repair(); + void download(); + CURL* curlhandle; + Timer timer; + Config config; + ProgressBar* progressbar; + protected: + private: + CURLcode downloadFile(std::string url, std::string filepath); + int repairFile(std::string url, std::string filepath, std::string xml_data = std::string(), std::string xml_dir = std::string()); + int downloadCovers(std::string gamename, std::string directory, std::string cover_xml_data); + int login(); + int getGameDetails(); + void getGameList(); + size_t getResumePosition(); + CURLcode beginDownload(); + std::string getResponse(const std::string& url); + + int HTTP_Login(const std::string& email, const std::string& password); + std::vector getGames(); + std::vector getFreeGames(); + + 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 gameNames; + std::vector games; + std::string coverXML; + + size_t resume_position; +}; + +#endif // DOWNLOADER_H diff --git a/include/progressbar.h b/include/progressbar.h new file mode 100644 index 0000000..379f082 --- /dev/null +++ b/include/progressbar.h @@ -0,0 +1,35 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#ifndef PROGRESSBAR_H +#define PROGRESSBAR_H + +#include +#include + +class ProgressBar +{ + public: + ProgressBar(bool bUnicode, bool bColor); + virtual ~ProgressBar(); + void draw(unsigned int length, double fraction); + protected: + private: + std::vector const m_bar_chars; + std::string const m_left_border; + std::string const m_right_border; + std::string const m_simple_left_border; + std::string const m_simple_right_border; + std::string const m_simple_empty_fill; + std::string const m_simple_bar_char; + std::string const m_bar_color; + std::string const m_border_color; + std::string const COLOR_RESET; + bool m_use_unicode; + bool m_use_color; +}; + +#endif // PROGRESSBAR_H diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..79c0ae7 --- /dev/null +++ b/include/util.h @@ -0,0 +1,26 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#ifndef UTIL_H +#define UTIL_H + +#include +#include +#include +#include +#include +#include +#include + +namespace Util +{ + std::string makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename); + 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()); +} + +#endif // UTIL_H diff --git a/lgogdownloader.cbp b/lgogdownloader.cbp new file mode 100644 index 0000000..94911c8 --- /dev/null +++ b/lgogdownloader.cbp @@ -0,0 +1,67 @@ + + + + + + diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..c124f08 --- /dev/null +++ b/main.cpp @@ -0,0 +1,190 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#include "downloader.h" +#include "config.h" +#include "util.h" + +#include // getpass +#include +#include +#include +#include + +#if __GNUC__ +# if __x86_64__ || __ppc64__ || __LP64__ +# define ENVIRONMENT64 +# else +# define ENVIRONMENT32 +# endif +#endif + +namespace bpo = boost::program_options; + +int main(int argc, char *argv[]) +{ + Config config; + config.sVersionString = "LGOGDownloader 2.2"; + config.sHome = (std::string)getenv("HOME"); + config.sCookiePath = config.sHome + "/.gogdownloader/cookies.txt"; + config.sConfigFilePath = config.sHome + "/.gogdownloader/config.cfg"; + config.sXMLDirectory = config.sHome + "/.gogdownloader/xml"; + + // Create gogdownloader 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; + } + } + + // Create help text for --platform option + std::string platform_text = "Select which installers are downloaded\n" + + std::to_string(INSTALLER_WINDOWS) + " = Windows\n" + + std::to_string(INSTALLER_MAC) + " = Mac\n" + + std::to_string(INSTALLER_WINDOWS | INSTALLER_MAC) + " = Both"; + // Create help text for --language option + std::string language_text = "Select which language installers are downloaded\n" + + std::to_string(LANGUAGE_EN) + " = English\n" + + std::to_string(LANGUAGE_DE) + " = German\n" + + std::to_string(LANGUAGE_FR) + " = French\n" + + std::to_string(LANGUAGE_PL) + " = Polish\n" + + std::to_string(LANGUAGE_RU) + " = Russian\n" + + "Add the values to download multiple languages\n" + + "All = " + std::to_string(LANGUAGE_EN) + "+" + std::to_string(LANGUAGE_DE) + "+" + std::to_string(LANGUAGE_FR) + "+" + std::to_string(LANGUAGE_PL) + "+" + std::to_string(LANGUAGE_RU) + " = " + + std::to_string(LANGUAGE_EN | LANGUAGE_DE | LANGUAGE_FR | LANGUAGE_PL | LANGUAGE_RU) + "\n" + + "French + Polish = " + std::to_string(LANGUAGE_FR) + "+" + std::to_string(LANGUAGE_PL) + " = " + std::to_string(LANGUAGE_FR | LANGUAGE_PL); + + bpo::variables_map vm; + bpo::options_description desc("Options"); + bpo::options_description config_file_options("Configuration"); + try + { + desc.add_options() + ("help,h", "Print help message") + ("login", bpo::value(&config.bLogin)->zero_tokens()->default_value(false), "Login") + ("list", bpo::value(&config.bList)->zero_tokens()->default_value(false), "List games") + ("list-details", bpo::value(&config.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info") + ("download", bpo::value(&config.bDownload)->zero_tokens()->default_value(false), "Download") + ("repair", bpo::value(&config.bRepair)->zero_tokens()->default_value(false), "Repair downloaded files") + ("game", bpo::value(&config.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)\nAliases: \"all\", \"free\"") + ("directory", bpo::value(&config.sDirectory)->default_value(""), "Set download directory") + #ifndef ENVIRONMENT32 + ("limit-rate", bpo::value(&config.iDownloadRate)->default_value(0), "Limit download rate to value in kB\n0 = unlimited") + #endif + ("create-xml", bpo::value(&config.sXMLFile)->default_value(""), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation") + ("xml-directory", bpo::value(&config.sXMLDirectory), "Set directory for GOG XML files") + ("chunk-size", bpo::value(&config.iChunkSize)->default_value(10), "Chunk size (in MB) when creating XML") + ("update-check", bpo::value(&config.bUpdateCheck)->zero_tokens()->default_value(false), "Check for update notifications") + ("platform", bpo::value(&config.iInstallerType)->default_value(INSTALLER_WINDOWS), platform_text.c_str()) + ("language", bpo::value(&config.iInstallerLanguage)->default_value(LANGUAGE_EN), language_text.c_str()) + ("no-installers", bpo::value(&config.bNoInstallers)->zero_tokens()->default_value(false), "Don't download/list/repair installers") + ("no-extras", bpo::value(&config.bNoExtras)->zero_tokens()->default_value(false), "Don't download/list/repair extras") + ("no-cover", bpo::value(&config.bNoCover)->zero_tokens()->default_value(false), "Don't download cover images") + ("no-remote-xml", bpo::value(&config.bNoRemoteXML)->zero_tokens()->default_value(false), "Don't use remote XML for repair") + ("no-unicode", bpo::value(&config.bNoUnicode)->zero_tokens()->default_value(false), "Don't use Unicode in the progress bar") + ("no-color", bpo::value(&config.bNoColor)->zero_tokens()->default_value(false), "Don't use coloring in the progress bar") + ("verbose", bpo::value(&config.bVerbose)->zero_tokens()->default_value(false), "Print lots of information") + ; + + bpo::store(bpo::parse_command_line(argc, argv, desc), vm); + bpo::notify(vm); + + // Read token and secret from config file + config_file_options.add_options() + ("token", bpo::value(&config.sToken)->default_value(""), "oauth token") + ("secret", bpo::value(&config.sSecret)->default_value(""), "oauth secret") + ; + + 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::store(bpo::parse_config_file(ifs, config_file_options), vm); + bpo::notify(vm); + ifs.close(); + } + } + + if (vm.count("help")) + { + std::cout << config.sVersionString << std::endl + << desc << std::endl; + return 0; + } + + 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 + } + 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 < INSTALLER_WINDOWS || config.iInstallerType > (INSTALLER_WINDOWS | INSTALLER_MAC)) + { + std::cout << "Invalid value for --platform" << 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 += "/"; + + Downloader downloader(config); + int result = downloader.init(); + + if (config.bLogin) + return result; + else if (config.bDownload) // Download games + downloader.download(); + else if (config.bRepair) // Repair file + downloader.repair(); + else if (config.bListDetails || config.bList) // Detailed list of games/extras + downloader.listGames(); + else if (config.bUpdateCheck) // Detailed list of games/extras + downloader.updateCheck(); + else + { // Show help message + std::cout << config.sVersionString << std::endl + << desc << std::endl; + } + + return 0; +} diff --git a/src/api.cpp b/src/api.cpp new file mode 100644 index 0000000..b3e1a67 --- /dev/null +++ b/src/api.cpp @@ -0,0 +1,528 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#include "api.h" + +#include +#include +#include +#include + +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; +} + +gameFile::gameFile(const bool& t_updated, const std::string& t_id, const std::string& t_name, const std::string& t_path, const std::string& t_size) +{ + this->updated = t_updated; + this->id = t_id; + this->name = t_name; + this->path = t_path; + this->size = t_size; +} + +gameFile::~gameFile() +{ + +} + +API::API(const std::string& token, const std::string& secret) +{ + curlhandle = curl_easy_init(); + curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, 0); + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); + curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, 5); + curl_easy_setopt(curlhandle, CURLOPT_PROGRESSDATA, this); + curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true); + curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, 0); + + this->error = false; + this->getAPIConfig(); + this->config.oauth_token = token; + this->config.oauth_secret = secret; +} + +int API::init() +{ + int res = 0; + + // Check if we already have token and secret + if (!this->config.oauth_token.empty() && !this->config.oauth_secret.empty()) + { + // Test authorization by getting user details + res = this->getUserDetails(); // res = 1 if successful + } + + return res; +} + + +int API::getAPIConfig() +{ + std::string url = "https://api.gog.com/en/downloader2/status/stable/"; // Stable API + //std::string url = "https://api.gog.com/en/downloader2/status/beta/"; // Beta API + //std::string url = "https://api.gog.com/en/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, CONSUMER_SECRET, 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, CONSUMER_SECRET, 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, CONSUMER_SECRET, 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(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, CONSUMER_SECRET, 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& type, const unsigned int& lang) +{ + std::string url; + gameDetails game; + + 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(); + + // Installer details + std::vector installers; + if (type & INSTALLER_WINDOWS) + { + if (lang & LANGUAGE_EN) + installers.push_back(root["game"]["installer_win_en"]); + if (lang & LANGUAGE_DE) + installers.push_back(root["game"]["installer_win_de"]); + if (lang & LANGUAGE_FR) + installers.push_back(root["game"]["installer_win_fr"]); + if (lang & LANGUAGE_PL) + installers.push_back(root["game"]["installer_win_pl"]); + if (lang & LANGUAGE_RU) + installers.push_back(root["game"]["installer_win_ru"]); + } + if (type & INSTALLER_MAC) + { + if (lang & LANGUAGE_EN) + installers.push_back(root["game"]["installer_mac_en"]); + if (lang & LANGUAGE_DE) + installers.push_back(root["game"]["installer_mac_de"]); + if (lang & LANGUAGE_FR) + installers.push_back(root["game"]["installer_mac_fr"]); + if (lang & LANGUAGE_PL) + installers.push_back(root["game"]["installer_mac_pl"]); + if (lang & LANGUAGE_RU) + installers.push_back(root["game"]["installer_mac_ru"]); + } + for ( unsigned int i = 0; i < installers.size(); ++i ) + { + for ( unsigned int index = 0; index < installers[i].size(); ++index ) + { + Json::Value installer = installers[i][index]; + + game.installers.push_back( + new gameFile( installer["#updated"].isBool() ? installer["id"].asBool() : false, + installer["id"].isInt() ? std::to_string(installer["id"].asInt()) : installer["id"].asString(), + std::string(), // empty string because installer doesn't have "name" + installer["link"].asString(), + installer["size"].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( + new 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() + ) + ); + } + + } + 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; + std::stringstream ss; + ss << this->config.get_installer_link << game_name << "/" << id << "/"; + url = ss.str(); + std::string json = this->getResponseOAuth(url); + + if (!json.empty()) + { + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + if (jsonparser->parse(json, root)) + { + /* + std::string result = root["result"].asString(); + int timestamp = root["timestamp"].asInt(); + int available = root["file"]["available"].asInt(); + std::string link = root["file"]["link"].asString(); + std::string message = root["file"]["message"].asString(); + */ + #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; + std::stringstream ss; + ss << this->config.get_extra_link << game_name << "/" << id << "/"; + url = ss.str(); + std::string json = this->getResponseOAuth(url); + + if (!json.empty()) + { + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + if (jsonparser->parse(json, root)) + { + /* + std::string result = root["result"].asString(); + int timestamp = root["timestamp"].asInt(); + int available = root["file"]["available"].asInt(); + std::string link = root["file"]["link"].asString(); + std::string type = root["file"]["type"].asString(); + std::string name = root["file"]["name"].asString(); + std::string message = root["file"]["message"].asString(); + */ + #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::getXML(const std::string& game_name, const std::string& id) +{ + std::string url, XML; + std::stringstream ss; + ss << this->config.get_installer_link << game_name << "/" << id << "/crc/"; + url = ss.str(); + std::string json = this->getResponseOAuth(url); + + if (!json.empty()) + { + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + if (jsonparser->parse(json, root)) + { + /* + std::string result = root["result"].asString(); + int timestamp = root["timestamp"].asInt(); + int available = root["file"]["available"].asInt(); + std::string link = root["file"]["link"].asString(); + std::string message = root["file"]["message"].asString(); + */ + #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); +} diff --git a/src/downloader.cpp b/src/downloader.cpp new file mode 100644 index 0000000..ae6d666 --- /dev/null +++ b/src/downloader.cpp @@ -0,0 +1,1027 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#include "downloader.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bptime = boost::posix_time; + +Downloader::Downloader(Config &conf) +{ + this->config = conf; +} + +Downloader::~Downloader() +{ + 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; + + curl_global_init(CURL_GLOBAL_ALL); + curlhandle = curl_easy_init(); + curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, 0); + 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, 5); + 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, 0); + + if (config.bVerbose) + curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, 1); + + gogAPI = new API(config.sToken, config.sSecret); + progressbar = new ProgressBar(!config.bNoUnicode, !config.bNoColor); + + if (config.bLogin || !gogAPI->init()) + { + if (this->login()) + return 1; + } + + if (!config.bNoCover && config.bDownload) + coverXML = this->getResponse("https://sites.google.com/site/gogdownloader/GOG_covers_v2.xml"); + + this->getGameList(); + + return 0; +} + +/* Login + returns 1 if login fails + returns 0 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 1; + } + else + { + // Login to website + if (!HTTP_Login(email, password)) + { + std::cout << "HTTP: Login failed" << std::endl; + return 1; + } + // Login to API + if (!gogAPI->login(email, password)) + { + std::cout << "API: Login failed" << std::endl; + return 1; + } + else + { + // Save token and secret to config file + std::ofstream ofs(config.sConfigFilePath.c_str()); + if (ofs) + { + ofs << "token = " << gogAPI->config.oauth_token << std::endl << "secret = " << gogAPI->config.oauth_secret << std::endl; + ofs.close(); + return 0; + } + else + { + std::cout << "Failed to create config: " << config.sConfigFilePath << std::endl; + return 1; + } + } + } +} + +void Downloader::updateCheck() +{ + if (gogAPI->user.notifications_forum) + { + std::cout << gogAPI->user.notifications_forum << " new forum replies" << std::endl; + } + else + { + std::cout << "No new forum replies" << std::endl; + } + if (gogAPI->user.notifications_messages) + { + std::cout << gogAPI->user.notifications_messages << " new private message(s)" << std::endl; + } + else + { + std::cout << "No new private messages" << std::endl; + } + if (gogAPI->user.notifications_games) + { + std::cout << gogAPI->user.notifications_games << " updated game(s)" << std::endl; + } + else + { + std::cout << "No updated games" << std::endl; + } +} + +void Downloader::getGameList() +{ + gameNames = this->getGames(); + + // Filter the game list + if (!config.sGameRegex.empty()) + { + // GameRegex filter aliases + if (config.sGameRegex == "all") + { + config.sGameRegex = ".*"; + } + + if (config.sGameRegex == "free") + { + gameNames = this->getFreeGames(); + } + else + { + std::vector gameNamesFiltered; + boost::regex expression(config.sGameRegex); + boost::match_results what; + for (unsigned int i = 0; i < gameNames.size(); ++i) + { + if (boost::regex_search(gameNames[i], what, expression)) + { + gameNamesFiltered.push_back(gameNames[i]); + } + } + gameNames = gameNamesFiltered; + } + } + + if (config.bListDetails || config.bDownload || config.bRepair) + { + this->getGameDetails(); + } +} + +/* Get detailed info about the games + returns 0 if successful + returns 1 if fails +*/ +int Downloader::getGameDetails() +{ + gameDetails game; + for (unsigned int i = 0; i < gameNames.size(); ++i) + { + std::cout << "Getting game info " << i+1 << " / " << gameNames.size() << "\r" << std::flush; + game = gogAPI->getGameDetails(gameNames[i], config.iInstallerType, config.iInstallerLanguage); + if (!gogAPI->getError()) + games.push_back(game); + else + { + std::cout << gogAPI->getErrorMessage() << std::endl; + return 1; + } + } + std::cout << std::endl; + return 0; +} + +void Downloader::listGames() +{ + if (config.bListDetails) // Detailed list + { + 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.bNoInstallers) + { + std::cout << "installers: " << std::endl; + for (unsigned int j = 0; j < games[i].installers.size(); ++j) + { + std::cout << "\tid: " << games[i].installers[j]->id << std::endl + << "\tpath: " << games[i].installers[j]->path << std::endl + << "\tsize: " << games[i].installers[j]->size << std::endl + << std::endl; + } + } + // List extras + if (!config.bNoExtras) + { + std::cout << "extras: " << std::endl; + for (unsigned int j = 0; j < games[i].extras.size(); ++j) + { + 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; + } + } + } + } + else + { + for (unsigned int i = 0; i < gameNames.size(); ++i) + std::cout << gameNames[i] << std::endl; + } + +} + +void Downloader::repair() +{ + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData); + curl_easy_setopt(curlhandle, CURLOPT_READFUNCTION, Downloader::readData); + curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(curlhandle, CURLOPT_PROGRESSFUNCTION, Downloader::progressCallback); + #ifndef ENVIRONMENT32 + curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate); + #endif + + for (unsigned int i = 0; i < games.size(); ++i) + { + // Installers (use remote or local file) + if (!config.bNoInstallers) + { + for (unsigned int j = 0; j < games[i].installers.size(); ++j) + { + std::string filepath = Util::makeFilepath(config.sDirectory, games[i].installers[j]->path, games[i].gamename); + + // Get XML data + std::string XML = ""; + if (!config.bNoRemoteXML) + { + XML = gogAPI->getXML(games[i].gamename, games[i].installers[j]->id); + if (gogAPI->getError()) + { + std::cout << gogAPI->getErrorMessage() << std::endl; + gogAPI->clearError(); + continue; + } + } + + // Repair + if (!XML.empty() || config.bNoRemoteXML) + { + 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, config.sXMLDirectory); + std::cout << std::endl; + } + } + } + + // Extras (GOG doesn't provide XML data for extras, use local file) + if (!config.bNoExtras) + { + for (unsigned int j = 0; j < games[i].extras.size(); ++j) + { + std::string filepath = Util::makeFilepath(config.sDirectory, games[i].extras[j]->path, games[i].gamename); + + 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(), config.sXMLDirectory); + std::cout << std::endl; + } + } + } +} + +void Downloader::download() +{ + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData); + curl_easy_setopt(curlhandle, CURLOPT_READFUNCTION, Downloader::readData); + curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(curlhandle, CURLOPT_PROGRESSFUNCTION, Downloader::progressCallback); + #ifndef ENVIRONMENT32 + curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate); + #endif + + for (unsigned int i = 0; i < games.size(); ++i) + { + // Download covers + if (!config.bNoCover) + { + // Doesn't work as intended unless we use "games[i].gamename" as base directory for installers and extras + // std::string directory = config.sDirectory + games[i].gamename + "/"; + + // Take path from installer path because for some games the base directory for installer/extra path is not "gamename" + std::string filepath = Util::makeFilepath(config.sDirectory, games[i].installers[0]->path, games[i].gamename); + + // Get base directory from filepath + boost::match_results what; + boost::regex expression("(.*)/.*"); + boost::regex_match(filepath, what, expression); + std::string directory = what[1]; + + this->downloadCovers(games[i].gamename, directory, coverXML); + } + // Download installers + if (!config.bNoInstallers) + { + for (unsigned int j = 0; j < games[i].installers.size(); ++j) + { + // 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; + } + + std::string filepath = Util::makeFilepath(config.sDirectory, games[i].installers[j]->path, games[i].gamename); + + // Download + if (!url.empty()) + { + std::cout << filepath << std::endl; + this->downloadFile(url, filepath); + std::cout << std::endl; + } + } + } + // Download extras + if (!config.bNoExtras) + { + 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 = Util::makeFilepath(config.sDirectory, games[i].extras[j]->path, games[i].gamename); + + // Download + if (!url.empty()) + { + if (!games[i].extras[j]->name.empty()) + std::cout << "Dowloading: " << games[i].extras[j]->name << std::endl; + std::cout << filepath << std::endl; + CURLcode result = downloadFile(url, filepath); + std::cout << std::endl; + if (result==CURLE_OK && config.sXMLFile == "automatic") + { + std::cout << "Starting automatic XML creation" << std::endl; + Util::createXML(filepath, config.iChunkSize, config.sXMLDirectory); + } + } + } + } + } +} + +// Download a file, resume if possible +CURLcode Downloader::downloadFile(std::string url, std::string filepath) +{ + 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(); + + // 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) + { + // 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 doesn't exist, create new file + if ((outfile=fopen(filepath.c_str(), "w"))!=NULL) + { + curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM, offset); // start downloading from the beginning of file + this->resume_position = 0; + } + else + { + std::cout << "Failed to create " << filepath << std::endl; + return res; + } + } + + 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 && !bResume) + { + boost::filesystem::path path = filepath; + if (boost::filesystem::exists(path)) + if (!boost::filesystem::remove(path)) + std::cout << "Failed to delete " << path << std::endl; + } + + return res; +} + +// Repair file +int Downloader::repairFile(std::string url, std::string filepath, std::string xml_data, std::string xml_dir) +{ + int res = 0; + FILE *outfile; + size_t offset=0; + if (xml_dir.empty()) + xml_dir = config.sHome + "/.gogdownloader/xml"; + + size_t from_offset, to_offset; + + struct FileInfo { + std::string filename; + std::string hash; + size_t size; + int chunks; + std::vector chunk_from; + std::vector chunk_to; + std::vector chunk_hash; + } info; + + // Get filename + boost::filesystem::path pathname = filepath; + std::string filename = pathname.filename().string(); + + TiXmlDocument xml; + if (!xml_data.empty()) { + std::cout << "XML: Using remote file" << std::endl; + xml.Parse(xml_data.c_str()); + } + else + { + std::string xml_file = xml_dir + "/" + filename + ".xml"; + std::cout << "XML: Using local file" << std::endl; + xml.LoadFile(xml_file); + } + + TiXmlNode *fileNode = xml.FirstChild("file"); + if (!fileNode) + { + std::cout << "XML: Parsing failed / not valid XML" << std::endl; + return res; + } + else + { + std::cout << "XML: Valid XML" << std::endl; + TiXmlElement *fileElem = fileNode->ToElement(); + info.filename = fileElem->Attribute("name"); + info.hash = fileElem->Attribute("md5"); + std::stringstream(fileElem->Attribute("chunks")) >> info.chunks; + std::stringstream(fileElem->Attribute("total_size")) >> info.size; + + //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; + info.chunk_from.push_back(from_offset); + info.chunk_to.push_back(to_offset); + info.chunk_hash.push_back(chunkElem->GetText()); + chunkNode = fileNode->IterateChildren(chunkNode); + } + } + + std::cout << "XML: Parsing finished" << std::endl << std::endl + << info.filename << std::endl + << "\tMD5:\t" << info.hash << std::endl + << "\tChunks:\t" << info.chunks << std::endl + << "\tSize:\t" << info.size << " bytes" << std::endl << std::endl; + + // Check if file exists + if ((outfile=fopen(filepath.c_str(), "r"))!=NULL) + { + // File exists + if ((outfile = freopen(filepath.c_str(), "r+", outfile))!=NULL ) + { + fseek(outfile, 0, SEEK_END); + offset = ftell(outfile); + } + else + { + std::cout << "Failed to reopen " << filepath << std::endl; + return res; + } + } + else + { + std::cout << "File doesn't exist " << filepath << std::endl; + return res; + } + + if (offset != info.size) + { + std::cout << "Filesizes don't match" << std::endl + << "Incomplete download or different version" << std::endl; + return res; + } + + for (int i=0; ibeginDownload(); //begin chunk download + std::cout << std::endl; + i--; //verify downloaded chunk + } + else + { + std::cout << "OK" << std::endl; + } + free(chunk); + res = 1; + } + + fclose(outfile); + + return res; +} + +// Download cover images +int Downloader::downloadCovers(std::string gamename, std::string directory, std::string cover_xml_data) +{ + int res = 0; + TiXmlDocument xml; + + // Check that directory exists and create subdirectories + boost::filesystem::path path = directory; + if (boost::filesystem::exists(path)) + { + if (!boost::filesystem::is_directory(path)) + { + std::cout << path << " is not directory" << std::endl; + return res; + } + + } + else + { + if (!boost::filesystem::create_directories(path)) + { + std::cout << "Failed to create directory: " << path << std::endl; + return res; + } + } + + xml.Parse(cover_xml_data.c_str()); + TiXmlElement *rootNode = xml.RootElement(); + if (!rootNode) + { + std::cout << "Not valid XML" << std::endl; + return res; + } + else + { + TiXmlNode *gameNode = rootNode->FirstChild(); + while (gameNode) + { + TiXmlElement *gameElem = gameNode->ToElement(); + std::string game_name = gameElem->Attribute("name"); + + if (game_name == gamename) + { + boost::match_results what; + TiXmlNode *coverNode = gameNode->FirstChild(); + while (coverNode) + { + TiXmlElement *coverElem = coverNode->ToElement(); + std::string cover_url = coverElem->GetText(); + // Get file extension for the image + boost::regex e1(".*(\\.\\w+)$", boost::regex::perl | boost::regex::icase); + boost::regex_search(cover_url, what, e1); + std::string file_extension = what[1]; + std::string cover_name = std::string("cover_") + coverElem->Attribute("id") + file_extension; + std::string filepath = directory + "/" + cover_name; + + std::cout << "Downloading cover " << filepath << std::endl; + CURLcode result = this->downloadFile(cover_url, filepath); + std::cout << std::endl; + if (result == CURLE_OK) + res = 1; + else + res = 0; + + if (result == CURLE_HTTP_RETURNED_ERROR) + { + long int response_code = 0; + result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); + std::cout << "HTTP ERROR: "; + if (result == CURLE_OK) + std::cout << response_code << 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; + + 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); + curl_easy_perform(curlhandle); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0); + std::string response = memory.str(); + memory.str(std::string()); + + 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(clientp); + + double rate; // average download speed in B/s + // trying to get rate and setting to NaN if it fails + if (CURLE_OK != curl_easy_getinfo(downloader->curlhandle, CURLINFO_SPEED_DOWNLOAD, &rate)) + rate = std::numeric_limits::quiet_NaN(); + + // (Shmerl): this flag is needed to catch the case before anything was downloaded on resume, + // and there is no way to calculate the fraction, so we set to 0 (otherwise it'd be 1). + // This is to prevent the progress bar from jumping to 100% and then to lower value. + // It's visually better to jump from 0% to higher one. + bool starting = ((0.0 == dlnow) && (0.0 == dltotal)); + + // (Shmerl): DEBUG: strange thing - when resuming a file which is already downloaded, dlnow is correctly 0.0 + // but dltotal is 389.0! This messes things up in the progress bar not showing the very last bar as full. + // enable this debug line to test the problem: + // + // printf("\r\033[0K dlnow: %0.2f, dltotal: %0.2f\r", dlnow, dltotal); fflush(stdout); return 0; + // + // For now making a quirky workaround and setting dltotal to 0.0 in that case. + // It's probably better to find a real fix. + if ((0.0 == dlnow) && (389.0 == dltotal)) dltotal = 0.0; + + // setting full dlwnow and dltotal + double offset = static_cast(downloader->getResumePosition()); + if (offset>0) + { + dlnow += offset; + dltotal += offset; + } + + // Update progress bar every 100ms + if (downloader->timer.getTimeBetweenUpdates()>=100 || dlnow == dltotal) + { + downloader->timer.reset(); + 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); + printf(" %0.2f/%0.2fMB @ %0.2fkB/s ETA: %s\r", dlnow/1024/1024, dltotal/1024/1024, rate/1024, 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 buk; + + // Get "buk" for login form + std::string json = this->getResponse("http://www.gog.com/user/ajax/?a=get"); + + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + bool parsingSuccessful = jsonparser->parse(json, root); + if (!parsingSuccessful) + { + #ifdef DEBUG + std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl << json << std::endl; + #endif + std::cout << jsonparser->getFormatedErrorMessages(); + return res = 0; + } + #ifdef DEBUG + std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl << root << std::endl; + #endif + buk = root["buk"].asString(); + + //Create postdata - escape characters in email/password to support special characters + postdata = "log_email=" + (std::string)curl_easy_escape(curlhandle, email.c_str(),email.size()) + + "&log_password=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size()) + + "&buk=" + (std::string)curl_easy_escape(curlhandle, buk.c_str(), buk.size()); + curl_easy_setopt(curlhandle, CURLOPT_URL, "https://secure.gog.com/login"); + curl_easy_setopt(curlhandle, CURLOPT_POST, 1); + curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); + curl_easy_perform(curlhandle); + memory.str(std::string()); + curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1); + json = this->getResponse("http://www.gog.com/user/ajax/?a=get"); + + parsingSuccessful = jsonparser->parse(json, root); + if (!parsingSuccessful) + { + #ifdef DEBUG + std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl << json << std::endl; + #endif + std::cout << jsonparser->getFormatedErrorMessages(); + return res = 0; + } + #ifdef DEBUG + std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl << root << std::endl; + #endif + + if (root["user"]["email"].asString() == email) + res = 1; // Login successful + else + res = 0; // Login failed + + delete jsonparser; + + return res; +} + +// Get list of games from account page +std::vector Downloader::getGames() +{ + std::vector games; + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; + int i = 1; + std::string html = ""; + std::string page_html = ""; + + do + { + std::string response = this->getResponse("https://secure.gog.com/en/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; + i++; + } while (!page_html.empty()); + + delete jsonparser; + + // Parse HTML to get game names + htmlcxx::HTML::ParserDom parser; + tree dom = parser.parseTree(html); + tree::iterator it = dom.begin(); + tree::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") + { + // Game name is contained in data-gameindex attribute + std::string game = it->attribute("data-gameindex").second; + if (!game.empty()) + games.push_back(game); + } + } + } + + return games; +} + +// Get list of free games +std::vector Downloader::getFreeGames() +{ + std::vector games; + std::string html = this->getResponse("https://secure.gog.com/catalogue/ajax?a=getGames&tab=all_genres&genre=all_genres&price=0&order=alph&publisher=&releaseDate=&availability=&gameMode=&rating=&search=&sort=vote&system=&language=&mixPage=1"); + + // Parse HTML to get game names + htmlcxx::HTML::ParserDom parser; + tree dom = parser.parseTree(html); + tree::iterator it = dom.begin(); + tree::iterator end = dom.end(); + for (; it != end; ++it) + { + if (it->tagName()=="a") + { + it->parseAttributes(); + std::string classname = it->attribute("class").second; + if (classname=="game-title-link") + { + std::string game = it->attribute("href").second; + game.assign(game.begin()+game.find_last_of("/")+1,game.end()); + if (!game.empty()) + games.push_back(game); + } + } + } + + return games; +} diff --git a/src/progressbar.cpp b/src/progressbar.cpp new file mode 100644 index 0000000..f3fea6e --- /dev/null +++ b/src/progressbar.cpp @@ -0,0 +1,85 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#include "progressbar.h" +#include + +ProgressBar::ProgressBar(bool bUnicode, bool bColor) +: + // Based on block characters. + // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Block_elements + // u8"\u2591" - you can try using this ("light shade") instead of space, but it looks worse, + // since partial bar has no shade behind it. + m_bar_chars + { + " ", // 0/8 + u8"\u258F", // 1/8 + u8"\u258E", // 2/8 + u8"\u258D", // 3/8 + u8"\u258C", // 4/8 + u8"\u258B", // 5/8 + u8"\u258A", // 6/8 + u8"\u2589", // 7/8 + u8"\u2588" /* 8/8 */ + }, + m_left_border(u8"\u2595"), // right 1/8th + m_right_border(u8"\u258F"), // left 1/8th + m_simple_left_border("["), + m_simple_right_border("]"), + m_simple_empty_fill(" "), + m_simple_bar_char("="), + // using vt100 escape sequences for colors... See http://ascii-table.com/ansi-escape-sequences.php + m_bar_color("\033[1;34m"), + m_border_color("\033[1;37m"), + COLOR_RESET("\033[0m"), + m_use_unicode(bUnicode), + m_use_color(bColor) +{ } + +ProgressBar::~ProgressBar() +{ + //dtor +} + +void ProgressBar::draw(unsigned int length, double fraction) +{ + // validation + if (!std::isnormal(fraction) || (fraction < 0.0)) fraction = 0.0; + else if (fraction > 1.0) fraction = 1.0; + + double bar_part = fraction * length; + double whole_bar_chars = std::floor(bar_part); + unsigned int whole_bar_chars_i = (unsigned int) whole_bar_chars; + // The bar uses symbols graded with 1/8 + unsigned int partial_bar_char_index = (unsigned int) std::floor((bar_part - whole_bar_chars) * 8.0); + + // left border + if (m_use_color) std::cout << m_border_color; + std::cout << (m_use_unicode ? m_left_border : m_simple_left_border); + + // whole completed bars + if (m_use_color) std::cout << m_bar_color; + unsigned int i = 0; + for (; i < whole_bar_chars_i; i++) + { + std::cout << (m_use_unicode ? m_bar_chars[8] : m_simple_bar_char); + } + + // partial completed bar + if (i < length) std::cout << (m_use_unicode ? m_bar_chars[partial_bar_char_index] : m_simple_empty_fill); + + // whole unfinished bars + if (m_use_color) std::cout << COLOR_RESET; + for (i = whole_bar_chars_i + 1; i < length; i++) + { // first entry in m_bar_chars is assumed to be the empty bar + std::cout << (m_use_unicode ? m_bar_chars[0] : m_simple_empty_fill); + } + + // right border + if (m_use_color) std::cout << m_border_color; + std::cout << (m_use_unicode ? m_right_border : m_simple_right_border); + if (m_use_color) std::cout << COLOR_RESET; +} diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..eb7a642 --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,180 @@ +/* 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://sam.zoy.org/wtfpl/COPYING for more details. */ + +#include "util.h" + +#include +#include + +/* + Create filepath from specified directory and path + Remove the leading slash from path if needed + Use gamename as base directory if specified +*/ +std::string Util::makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename) +{ + std::string filepath; + + if (gamename.empty()) + { + if (path.at(0)=='/') + { + std::string tmp_path = path.substr(1,path.length()); + filepath = directory + tmp_path; + } + else + { + filepath = directory + path; + } + } + else + { + std::string extras = ""; + if (path.find("extras")!=std::string::npos) + { + extras = "/extras"; + } + + std::string filename = path.substr(path.find_last_of("/")+1, path.length()); + filepath = directory + gamename + extras + "/" + 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; + std::string home = (std::string)getenv("HOME"); + if (xml_dir.empty()) + xml_dir = home + "/.gogdownloader/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 + << "MD5: " << std::flush; + std::string file_md5 = Util::getFileHash(filepath.c_str(), RHASH_MD5); + std::cout << file_md5 << std::endl; + + TiXmlDocument xml; + TiXmlElement *fileElem = new TiXmlElement("file"); + fileElem->SetAttribute("name", filename); + fileElem->SetAttribute("md5", file_md5); + fileElem->SetAttribute("chunks", chunks); + fileElem->SetAttribute("total_size", filesize); + + std::cout << "Getting MD5 for chunks" << std::endl; + 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); + free(chunk); + + TiXmlElement *chunkElem = new TiXmlElement("chunk"); + chunkElem->SetAttribute("id", i); + chunkElem->SetAttribute("from", range_begin); + chunkElem->SetAttribute("to", 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); + xml.LinkEndChild(fileElem); + + std::cout << std::endl << "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; +}