diff --git a/include/config.h b/include/config.h index e9062cf..3fc9a48 100644 --- a/include/config.h +++ b/include/config.h @@ -40,8 +40,11 @@ class Config 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; @@ -64,6 +67,7 @@ class Config unsigned int iInstallerLanguage; int iRetries; int iWait; + int iCacheValid; size_t iChunkSize; curl_off_t iDownloadRate; long int iTimeout; diff --git a/include/downloader.h b/include/downloader.h index 4d4e0ac..8154ce5 100644 --- a/include/downloader.h +++ b/include/downloader.h @@ -25,6 +25,7 @@ #include "api.h" #include "progressbar.h" #include +#include #include #include @@ -65,6 +66,7 @@ class Downloader void download(); void checkOrphans(); void checkStatus(); + void updateCache(); CURL* curlhandle; Timer timer; Config config; @@ -81,7 +83,9 @@ class Downloader std::string getResponse(const std::string& url); std::string getLocalFileHash(const std::string& filepath, const std::string& gamename = std::string()); std::string getRemoteFileHash(const std::string& gamename, const std::string& id); - + int loadGameDetailsCache(); + int saveGameDetailsCache(); + std::vector getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0); int HTTP_Login(const std::string& email, const std::string& password); std::vector getGames(); std::vector getFreeGames(); diff --git a/include/gamedetails.h b/include/gamedetails.h index 26043cc..462268d 100644 --- a/include/gamedetails.h +++ b/include/gamedetails.h @@ -7,6 +7,7 @@ #include #include +#include class gameDetails { @@ -21,6 +22,7 @@ class gameDetails std::string title; std::string icon;; void makeFilepaths(const Config& config); + Json::Value getDetailsAsJson(); virtual ~gameDetails(); protected: private: diff --git a/include/gamefile.h b/include/gamefile.h index 5f6ff90..eb3b27f 100644 --- a/include/gamefile.h +++ b/include/gamefile.h @@ -5,10 +5,12 @@ #include #include +#include class gameFile { public: + gameFile(); gameFile(const int& t_updated, const std::string& t_id, const std::string& t_name, const std::string& t_path, const std::string& t_size, const unsigned int& t_language = GlobalConstants::LANGUAGE_EN, const unsigned int& t_platform = GlobalConstants::PLATFORM_WINDOWS, const int& t_silent = 0); int updated; std::string id; @@ -20,6 +22,7 @@ class gameFile int silent; void setFilepath(const std::string& path); std::string getFilepath(); + Json::Value getAsJson(); virtual ~gameFile(); protected: private: diff --git a/main.cpp b/main.cpp index 36986ac..a3558f9 100644 --- a/main.cpp +++ b/main.cpp @@ -34,6 +34,13 @@ int main(int argc, char *argv[]) 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; @@ -104,6 +111,7 @@ int main(int argc, char *argv[]) ("reset-config", bpo::value(&config.bResetConfig)->zero_tokens()->default_value(false), "Reset config settings to default") ("report", bpo::value(&config.sReportFilePath)->implicit_value("lgogdownloader-report.log"), "Save report of downloaded/repaired files to specified file\nDefault filename: lgogdownloader-report.log") ("no-cover", bpo::value(&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(&config.bUpdateCache)->zero_tokens()->default_value(false), "Update game details cache") ; // Commandline options (config file) options_cli_cfg.add_options() @@ -138,6 +146,8 @@ int main(int argc, char *argv[]) ("subdir-language-packs", bpo::value(&config.sLanguagePackSubdir)->default_value("languagepacks"), ("Set subdirectory for language packs" + subdir_help_text).c_str()) ("subdir-dlc", bpo::value(&config.sDLCSubdir)->default_value("dlc/%dlcname%"), ("Set subdirectory for dlc" + subdir_help_text).c_str()) ("subdir-game", bpo::value(&config.sGameSubdir)->default_value("%gamename%"), ("Set subdirectory for game" + subdir_help_text).c_str()) + ("use-cache", bpo::value(&config.bUseCache)->zero_tokens()->default_value(false), ("Use game details cache")) + ("cache-valid", bpo::value(&config.iCacheValid)->default_value(2880), ("Set how long cached game details are valid (in minutes)\nDefault: 2880 minutes (48 hours)")) ; // Options read from config file options_cfg_only.add_options() @@ -177,11 +187,6 @@ int main(int argc, char *argv[]) config.sConfigFilePath = config.sConfigDirectory + "/config.cfg"; config.sBlacklistFilePath = config.sConfigDirectory + "/blacklist.txt"; - if (xdgcache) - config.sXMLDirectory = (std::string)xdgcache + "/lgogdownloader/xml"; - else - config.sXMLDirectory = home + "/.cache/lgogdownloader/xml"; - // Create lgogdownloader directories boost::filesystem::path path = config.sXMLDirectory; if (!boost::filesystem::exists(path)) @@ -438,6 +443,8 @@ int main(int argc, char *argv[]) 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 diff --git a/src/downloader.cpp b/src/downloader.cpp index bad9fbc..c518211 100644 --- a/src/downloader.cpp +++ b/src/downloader.cpp @@ -205,6 +205,31 @@ void Downloader::getGameList() */ 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) @@ -216,8 +241,11 @@ int Downloader::getGameDetails() conf.bDLC = config.bDLC; conf.iInstallerLanguage = config.iInstallerLanguage; conf.iInstallerType = config.iInstallerType; - 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; + 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()) @@ -2617,3 +2645,204 @@ std::string Downloader::getRemoteFileHash(const std::string& gamename, const std } 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 Downloader::getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level) +{ + std::vector details; + + // If root node is not array and we use root.size() it will return the number of nodes --> limit to 1 "array" node to make sure it is handled properly + for (unsigned int i = 0; i < (root.isArray() ? root.size() : 1); ++i) + { + Json::Value gameDetailsNode = (root.isArray() ? root[i] : root); // This json node can be array or non-array so take that into account + gameDetails game; + game.gamename = gameDetailsNode["gamename"].asString(); + + // DLCs are handled as part of the game so make sure that filtering is done with base game name + if (recursion_level == 0) // recursion level is 0 when handling base game + { + boost::regex expression(config.sGameRegex); + boost::match_results what; + if (!boost::regex_search(game.gamename, what, expression)) // Check if name matches the specified regex + continue; + } + game.title = gameDetailsNode["title"].asString(); + game.icon = gameDetailsNode["icon"].asString(); + + // Make a vector of valid node names to make things easier + std::vector nodes; + nodes.push_back("extras"); + nodes.push_back("installers"); + nodes.push_back("patches"); + nodes.push_back("languagepacks"); + nodes.push_back("dlcs"); + + gameSpecificConfig conf; + conf.bDLC = config.bDLC; + conf.iInstallerLanguage = config.iInstallerLanguage; + conf.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; +} diff --git a/src/gamedetails.cpp b/src/gamedetails.cpp index e4eb1cb..9829475 100644 --- a/src/gamedetails.cpp +++ b/src/gamedetails.cpp @@ -69,3 +69,31 @@ void gameDetails::makeFilepaths(const Config& config) } } } + +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; +} diff --git a/src/gamefile.cpp b/src/gamefile.cpp index c807033..86ef3c0 100644 --- a/src/gamefile.cpp +++ b/src/gamefile.cpp @@ -12,6 +12,11 @@ gameFile::gameFile(const int& t_updated, const std::string& t_id, const std::str this->silent = t_silent; } +gameFile::gameFile() +{ + //ctor +} + gameFile::~gameFile() { //dtor @@ -26,3 +31,19 @@ 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; +}