diff --git a/include/config.h b/include/config.h index 01d2939..f50e433 100644 --- a/include/config.h +++ b/include/config.h @@ -42,6 +42,7 @@ class Config bool bSubDirectories; bool bUseCache; bool bUpdateCache; + bool bSaveSerials; std::string sGameRegex; std::string sDirectory; std::string sCacheDirectory; diff --git a/include/downloader.h b/include/downloader.h index 21174a1..49cab87 100644 --- a/include/downloader.h +++ b/include/downloader.h @@ -89,7 +89,10 @@ class Downloader int HTTP_Login(const std::string& email, const std::string& password); std::vector getGames(); std::vector getFreeGames(); - std::vector getExtras(const std::string& gamename, const std::string& gameid); + std::vector getExtrasFromHTML(const std::string& html, const std::string& gamename, const std::string& gameid); + std::string getGameDetailsHTML(const std::string& gamename, const std::string& gameid); + std::string getSerialsFromHTML(const std::string& html); + void saveSerials(const std::string& serials, const std::string& filepath); static int progressCallback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow); static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp); diff --git a/include/gamedetails.h b/include/gamedetails.h index 76252fb..4ef052a 100644 --- a/include/gamedetails.h +++ b/include/gamedetails.h @@ -20,14 +20,17 @@ class gameDetails std::vector dlcs; std::string gamename; std::string title; - std::string icon;; + std::string icon; + std::string serials; void filterWithPriorities(const Config& config); void makeFilepaths(const Config& config); + std::string getSerialsFilepath(); Json::Value getDetailsAsJson(); virtual ~gameDetails(); protected: void filterListWithPriorities(std::vector& list, const Config& config); private: + std::string serialsFilepath; }; #endif // GAMEDETAILS_H diff --git a/main.cpp b/main.cpp index bdaf253..5a2d202 100644 --- a/main.cpp +++ b/main.cpp @@ -186,7 +186,7 @@ int main(int argc, char *argv[]) ("cache-valid", bpo::value(&config.iCacheValid)->default_value(2880), ("Set how long cached game details are valid (in minutes)\nDefault: 2880 minutes (48 hours)")) ("language-priority", bpo::value(&config.sLanguagePriority)->default_value(""), ("Set priority of systems" + priority_help_text + ", like \"4,1\" for French first, then English if no French version").c_str()) ("platform-priority", bpo::value(&config.sPlatformPriority)->default_value(""), ("Set priority of platforms" + priority_help_text + ", like \"4,1\" for Linux first, then Windows if no Linux version").c_str()) - + ("save-serials", bpo::value(&config.bSaveSerials)->zero_tokens()->default_value(false), "Save serial numbers when downloading") ; // Options read from config file options_cfg_only.add_options() diff --git a/src/downloader.cpp b/src/downloader.cpp index d7ea6cc..dae67af 100644 --- a/src/downloader.cpp +++ b/src/downloader.cpp @@ -261,21 +261,37 @@ int Downloader::getGameDetails() if (!gogAPI->getError()) { game.filterWithPriorities(config); + std::string gameDetailsHTML; 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); + gameDetailsHTML = this->getGameDetailsHTML(gameItems[i].name, gameItems[i].id); + game.extras = this->getExtrasFromHTML(gameDetailsHTML, gameItems[i].name, gameItems[i].id); + } + if (config.bSaveSerials) + { + if (gameDetailsHTML.empty()) + gameDetailsHTML = this->getGameDetailsHTML(gameItems[i].name, gameItems[i].id); + game.serials = this->getSerialsFromHTML(gameDetailsHTML); } if (game.dlcs.empty() && bHasDLC && conf.bDLC) { for (unsigned int j = 0; j < gameItems[i].dlcnames.size(); ++j) { gameDetails dlc; + std::string gameDetailsHTML_dlc; dlc = gogAPI->getGameDetails(gameItems[i].dlcnames[j], conf.iInstallerType, conf.iInstallerLanguage, config.bDuplicateHandler); dlc.filterWithPriorities(config); if (dlc.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras { - dlc.extras = this->getExtras(gameItems[i].dlcnames[j], gameItems[i].id); + gameDetailsHTML_dlc = this->getGameDetailsHTML(gameItems[i].dlcnames[j], gameItems[i].id); + dlc.extras = this->getExtrasFromHTML(gameDetailsHTML_dlc, gameItems[i].dlcnames[j], gameItems[i].id); + } + if (config.bSaveSerials) + { + if (gameDetailsHTML_dlc.empty()) + gameDetailsHTML_dlc = this->getGameDetailsHTML(gameItems[i].dlcnames[j], gameItems[i].id); + dlc.serials = this->getSerialsFromHTML(gameDetailsHTML_dlc); } game.dlcs.push_back(dlc); } @@ -326,6 +342,9 @@ void Downloader::listGames() std::cout << "gamename: " << games[i].gamename << std::endl << "title: " << games[i].title << std::endl << "icon: " << "http://static.gog.com" << games[i].icon << std::endl; + if (!games[i].serials.empty()) + std::cout << "serials:" << std::endl << games[i].serials << std::endl; + // List installers if (config.bInstallers) { @@ -436,6 +455,12 @@ void Downloader::listGames() std::cout << "DLCs: " << std::endl; for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) { + if (!games[i].dlcs[j].serials.empty()) + { + std::cout << "\tDLC gamename: " << games[i].dlcs[j].gamename << std::endl + << "\tserials:" << games[i].dlcs[j].serials << std::endl; + } + for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) { std::string filepath = games[i].dlcs[j].installers[k].getFilepath(); @@ -758,6 +783,12 @@ void Downloader::download() for (unsigned int i = 0; i < games.size(); ++i) { + if (config.bSaveSerials && !games[i].serials.empty()) + { + std::string filepath = games[i].getSerialsFilepath(); + this->saveSerials(games[i].serials, filepath); + } + // Download covers if (config.bCover && !config.bUpdateCheck) { @@ -935,6 +966,12 @@ void Downloader::download() { for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) { + if (config.bSaveSerials && !games[i].dlcs[j].serials.empty()) + { + std::string filepath = games[i].dlcs[j].getSerialsFilepath(); + this->saveSerials(games[i].dlcs[j].serials, filepath); + } + if (config.bInstallers) { for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) @@ -2180,30 +2217,36 @@ std::vector Downloader::getFreeGames() return games; } -std::vector Downloader::getExtras(const std::string& gamename, const std::string& gameid) +std::string Downloader::getGameDetailsHTML(const std::string& gamename, const std::string& gameid) { - Json::Value root; - Json::Reader *jsonparser = new Json::Reader; - std::vector extras; - std::string gameDataUrl = "https://www.gog.com/account/ajax?a=gamesListDetails&g=" + gameid; std::string json = this->getResponse(gameDataUrl); + // Parse JSON + Json::Value root; + Json::Reader *jsonparser = new Json::Reader; if (!jsonparser->parse(json, root)) { #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getExtras)" << std::endl << json << std::endl; + std::cerr << "DEBUG INFO (Downloader::getGameDetailsHTML)" << 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; + std::cerr << "DEBUG INFO (Downloader::getGameDetailsHTML)" << std::endl << root << std::endl; #endif std::string html = root["details"]["html"].asString(); delete jsonparser; + return html; +} + +std::vector Downloader::getExtrasFromHTML(const std::string& html, const std::string& gamename, const std::string& gameid) +{ + std::vector extras; + htmlcxx::HTML::ParserDom parser; tree dom = parser.parseTree(html); tree::iterator it = dom.begin(); @@ -2240,7 +2283,7 @@ std::vector Downloader::getExtras(const std::string& gamename, const s if (name.empty()) { #ifdef DEBUG - std::cerr << "DEBUG INFO (Downloader::getExtras)" << std::endl; + std::cerr << "DEBUG INFO (getExtrasFromHTML)" << std::endl; std::cerr << "Skipped file without a name (game: " << gamename << ", gameid: " << gameid << ", fileid: " << id << ")" << std::endl; #endif continue; @@ -2261,6 +2304,60 @@ std::vector Downloader::getExtras(const std::string& gamename, const s return extras; } + +std::string Downloader::getSerialsFromHTML(const std::string& html) +{ + std::ostringstream serials; + + 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 == "list_serial_h") + { + for (unsigned int i = 0; i < dom.number_of_children(it); ++i) + { + tree::iterator serials_it = dom.child(it, i); + if (!serials_it->isComment()) + { + std::string tag_text; + if (!serials_it->isTag()) + { + if (!serials_it->text().empty()) + tag_text = serials_it->text(); + } + else if (serials_it->tagName() == "span") + { + for (unsigned int j = 0; j < dom.number_of_children(serials_it); ++j) + { + tree::iterator serials_span_it = dom.child(serials_it, j); + if (!serials_span_it->isTag() && !serials_span_it->isComment()) + tag_text = serials_span_it->text(); + } + } + + if (!tag_text.empty()) + { + boost::regex expression("^\\h+|\\h+$"); + std::string text = boost::regex_replace(tag_text, expression, ""); + if (!text.empty()) + serials << text << std::endl; + } + } + } + } + } + } + + return serials.str(); +} + void Downloader::checkOrphans() { // Always check everything when checking for orphaned files @@ -2798,6 +2895,7 @@ std::vector Downloader::getGameDetailsFromJsonNode(Json::Value root } game.title = gameDetailsNode["title"].asString(); game.icon = gameDetailsNode["icon"].asString(); + game.serials = gameDetailsNode["serials"].asString(); // Make a vector of valid node names to make things easier std::vector nodes; @@ -2892,3 +2990,49 @@ void Downloader::updateCache() return; } + +// Save serials to file +void Downloader::saveSerials(const std::string& serials, const std::string& filepath) +{ + bool bFileExists = boost::filesystem::exists(filepath); + + if (bFileExists) + return; + + // Get directory from filepath + boost::filesystem::path pathname = filepath; + std::string directory = pathname.parent_path().string(); + + // Check that directory exists and create subdirectories + boost::filesystem::path path = directory; + if (boost::filesystem::exists(path)) + { + if (!boost::filesystem::is_directory(path)) + { + std::cout << path << " is not directory" << std::endl; + return; + } + } + else + { + if (!boost::filesystem::create_directories(path)) + { + std::cout << "Failed to create directory: " << path << std::endl; + return; + } + } + + std::ofstream ofs(filepath); + if (ofs) + { + std::cout << "Saving serials: " << filepath << std::endl; + ofs << serials; + ofs.close(); + } + else + { + std::cout << "Failed to create file: " << filepath << std::endl; + } + + return; +} diff --git a/src/gamedetails.cpp b/src/gamedetails.cpp index ae02b18..5e57777 100644 --- a/src/gamedetails.cpp +++ b/src/gamedetails.cpp @@ -71,6 +71,7 @@ void gameDetails::makeFilepaths(const Config& config) std::string filepath; std::string directory = config.sDirectory + "/" + config.sGameSubdir + "/"; std::string subdir; + this->serialsFilepath = Util::makeFilepath(directory, "serials.txt", this->gamename, subdir, 0); for (unsigned int i = 0; i < this->installers.size(); ++i) { @@ -102,6 +103,8 @@ void gameDetails::makeFilepaths(const Config& config) for (unsigned int i = 0; i < this->dlcs.size(); ++i) { + subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sInstallersSubdir : ""; + this->dlcs[i].serialsFilepath = Util::makeFilepath(directory, "serials.txt", this->gamename, subdir, 0); for (unsigned int j = 0; j < this->dlcs[i].installers.size(); ++j) { subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sInstallersSubdir : ""; @@ -132,6 +135,7 @@ Json::Value gameDetails::getDetailsAsJson() json["gamename"] = this->gamename; json["title"] = this->title; json["icon"] = this->icon; + json["serials"] = this->serials; for (unsigned int i = 0; i < this->extras.size(); ++i) json["extras"].append(this->extras[i].getAsJson()); @@ -152,3 +156,8 @@ Json::Value gameDetails::getDetailsAsJson() return json; } + +std::string gameDetails::getSerialsFilepath() +{ + return this->serialsFilepath; +}