Replace --update-check option and remove --game aliases

Replace --update-check option with --updated and --notifications options
--updated restricts downloader to operate only on games that have update flag set in account page
--notifications shows the number of new forum replies, updates games, unread chat messages and pending friend requests
--clear-update-flags clears update notification flags for all games

Remove aliases for --game option
"free" could no longer be used as originally intended and "all" was unnecessary because leaving regex empty has the same effect
This commit is contained in:
Sude 2018-07-21 23:59:08 +03:00
parent 9dc5d4124c
commit 8e9c094929
9 changed files with 122 additions and 197 deletions

View File

@ -213,11 +213,12 @@ class Config
bool bDownload;
bool bRepair;
bool bUpdateCheck;
bool bUpdated;
bool bList;
bool bListDetails;
bool bCheckStatus;
bool bShowWishlist;
bool bNotifications;
bool bVerbose;
bool bUnicode; // use Unicode in console output

View File

@ -91,7 +91,8 @@ class Downloader
int init();
int login();
int listGames();
void updateCheck();
void checkNotifications();
void clearUpdateNotifications();
void repair();
void download();
void checkOrphans();

View File

@ -53,10 +53,12 @@ class galaxyAPI
Json::Value getManifestV2(std::string manifest_hash);
Json::Value getSecureLink(const std::string& product_id, const std::string& path);
std::string getResponse(const std::string& url, const bool& zlib_decompress = false);
Json::Value getResponseJson(const std::string& url, const bool& zlib_decompress = false);
std::string hashToGalaxyPath(const std::string& hash);
std::vector<galaxyDepotItem> getDepotItemsVector(const std::string& hash);
Json::Value getProductInfo(const std::string& product_id);
gameDetails productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf);
Json::Value getUserData();
protected:
private:
CurlConfig curlConf;

View File

@ -22,7 +22,6 @@ class Website
std::string getResponse(const std::string& url);
Json::Value getGameDetailsJSON(const std::string& gameid);
std::vector<gameItem> getGames();
std::vector<gameItem> getFreeGames();
std::vector<wishlistItem> getWishlistItems();
bool IsLoggedIn();
virtual ~Website();
@ -30,7 +29,6 @@ class Website
private:
static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
CURL* curlhandle;
Config config;
bool IsloggedInSimple();
bool IsLoggedInComplex(const std::string& email);
int retries;

View File

@ -133,6 +133,7 @@ int main(int argc, char *argv[])
bpo::options_description options_cli_cfg;
bpo::options_description options_cfg_only;
bpo::options_description options_cfg_all("Configuration");
bool bClearUpdateNotifications = false;
try
{
bool bInsecure = false;
@ -160,9 +161,11 @@ int main(int argc, char *argv[])
("list-details", bpo::value<bool>(&Globals::globalConfig.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info")
("download", bpo::value<bool>(&Globals::globalConfig.bDownload)->zero_tokens()->default_value(false), "Download")
("repair", bpo::value<bool>(&Globals::globalConfig.bRepair)->zero_tokens()->default_value(false), "Repair downloaded files\nUse --repair --download to redownload files when filesizes don't match (possibly different version). Redownload will rename the old file (appends .old to filename)")
("game", bpo::value<std::string>(&Globals::globalConfig.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)\nAliases: \"all\", \"free\"\nAlias \"free\" doesn't work with cached details")
("game", bpo::value<std::string>(&Globals::globalConfig.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)")
("create-xml", bpo::value<std::string>(&Globals::globalConfig.sXMLFile)->implicit_value("automatic"), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation")
("update-check", bpo::value<bool>(&Globals::globalConfig.bUpdateCheck)->zero_tokens()->default_value(false), "Check for update notifications")
("notifications", bpo::value<bool>(&Globals::globalConfig.bNotifications)->zero_tokens()->default_value(false), "Check notifications")
("updated", bpo::value<bool>(&Globals::globalConfig.bUpdated)->zero_tokens()->default_value(false), "List/download only games with update flag set")
("clear-update-flags", bpo::value<bool>(&bClearUpdateNotifications)->zero_tokens()->default_value(false), "Clear update notification flags")
("check-orphans", bpo::value<std::string>(&Globals::globalConfig.sOrphanRegex)->implicit_value(""), check_orphans_text.c_str())
("status", bpo::value<bool>(&Globals::globalConfig.bCheckStatus)->zero_tokens()->default_value(false), "Show status of files\n\nOutput format:\nstatuscode gamename filename filesize filehash\n\nStatus codes:\nOK - File is OK\nND - File is not downloaded\nMD5 - MD5 mismatch, different version\nFS - File size mismatch, incomplete download")
("save-config", bpo::value<bool>(&Globals::globalConfig.bSaveConfig)->zero_tokens()->default_value(false), "Create config file with current settings")
@ -721,8 +724,10 @@ int main(int argc, char *argv[])
downloader.showWishlist();
else if (Globals::globalConfig.bUpdateCache)
downloader.updateCache();
else if (Globals::globalConfig.bUpdateCheck) // Update check has priority over download and list
downloader.updateCheck();
else if (Globals::globalConfig.bNotifications)
downloader.checkNotifications();
else if (bClearUpdateNotifications)
downloader.clearUpdateNotifications();
else if (!vFileIdStrings.empty())
{
for (std::vector<std::string>::iterator it = vFileIdStrings.begin(); it != vFileIdStrings.end(); it++)

View File

@ -354,38 +354,62 @@ int Downloader::login()
return 0;
}
void Downloader::updateCheck()
void Downloader::checkNotifications()
{
std::cout << "New forum replies: " << gogAPI->user.notifications_forum << std::endl;
std::cout << "New private messages: " << gogAPI->user.notifications_messages << std::endl;
std::cout << "Updated games: " << gogAPI->user.notifications_games << std::endl;
Json::Value userData = gogGalaxy->getUserData();
if (gogAPI->user.notifications_games)
if (userData.empty())
{
Globals::globalConfig.sGameRegex = ".*"; // Always check all games
if (Globals::globalConfig.bList || Globals::globalConfig.bListDetails || Globals::globalConfig.bDownload)
{
if (Globals::globalConfig.bList)
Globals::globalConfig.bListDetails = true; // Always list details
this->getGameList();
if (Globals::globalConfig.bDownload)
this->download();
else
this->listGames();
}
std::cout << "Empty JSON response" << std::endl;
return;
}
if (!userData.isMember("updates"))
{
std::cout << "Invalid JSON response" << std::endl;
return;
}
std::cout << "New forum replies: " << userData["updates"]["messages"].asInt() << std::endl;
std::cout << "Updated games: " << userData["updates"]["products"].asInt() << std::endl;
std::cout << "Unread chat messages: " << userData["updates"]["unreadChatMessages"].asInt() << std::endl;
std::cout << "Pending friend requests: " << userData["updates"]["pendingFriendRequests"].asInt() << std::endl;
}
void Downloader::clearUpdateNotifications()
{
Json::Value userData = gogGalaxy->getUserData();
if (userData.empty())
{
return;
}
if (!userData.isMember("updates"))
{
return;
}
if (userData["updates"]["products"].asInt() < 1)
{
std::cout << "No updates" << std::endl;
return;
}
Globals::globalConfig.bUpdated = true;
this->getGameList();
for (unsigned int i = 0; i < gameItems.size(); ++i)
{
// Getting game details should remove the update flag
std::cerr << "\033[KClearing update flags " << i+1 << " / " << gameItems.size() << "\r" << std::flush;
Json::Value details = gogWebsite->getGameDetailsJSON(gameItems[i].id);
}
std::cerr << std::endl;
}
void Downloader::getGameList()
{
if (Globals::globalConfig.sGameRegex == "free")
{
gameItems = gogWebsite->getFreeGames();
}
else
{
gameItems = gogWebsite->getGames();
}
gameItems = gogWebsite->getGames();
}
/* Get detailed info about the games
@ -399,12 +423,6 @@ int Downloader::getGameDetails()
if (Globals::globalConfig.bUseCache && !Globals::globalConfig.bUpdateCache)
{
// GameRegex filter alias for all games
if (Globals::globalConfig.sGameRegex == "all")
Globals::globalConfig.sGameRegex = ".*";
else if (Globals::globalConfig.sGameRegex == "free")
std::cerr << "Warning: regex alias \"free\" doesn't work with cached details" << std::endl;
int result = this->loadGameDetailsCache();
if (result == 0)
{
@ -531,35 +549,32 @@ int Downloader::listGames()
std::cout << "serials:" << std::endl << games[i].serials << std::endl;
// List installers
if (Globals::globalConfig.dlConf.bInstallers)
if (Globals::globalConfig.dlConf.bInstallers && !games[i].installers.empty())
{
std::cout << "installers: " << std::endl;
for (unsigned int j = 0; j < games[i].installers.size(); ++j)
{
if (!Globals::globalConfig.bUpdateCheck || games[i].installers[j].updated) // Always list updated files
std::string filepath = games[i].installers[j].getFilepath();
if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
{
std::string filepath = games[i].installers[j].getFilepath();
if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
{
if (Globals::globalConfig.bVerbose)
std::cerr << "skipped blacklisted file " << filepath << std::endl;
continue;
}
std::string languages = Util::getOptionNameString(games[i].installers[j].language, GlobalConstants::LANGUAGES);
std::cout << "\tid: " << games[i].installers[j].id << std::endl
<< "\tname: " << games[i].installers[j].name << std::endl
<< "\tpath: " << games[i].installers[j].path << std::endl
<< "\tsize: " << games[i].installers[j].size << std::endl
<< "\tupdated: " << (games[i].installers[j].updated ? "True" : "False") << std::endl
<< "\tlanguage: " << languages << std::endl
<< std::endl;
if (Globals::globalConfig.bVerbose)
std::cerr << "skipped blacklisted file " << filepath << std::endl;
continue;
}
std::string languages = Util::getOptionNameString(games[i].installers[j].language, GlobalConstants::LANGUAGES);
std::cout << "\tid: " << games[i].installers[j].id << std::endl
<< "\tname: " << games[i].installers[j].name << std::endl
<< "\tpath: " << games[i].installers[j].path << std::endl
<< "\tsize: " << games[i].installers[j].size << std::endl
<< "\tupdated: " << (games[i].installers[j].updated ? "True" : "False") << std::endl
<< "\tlanguage: " << languages << std::endl
<< std::endl;
}
}
// List extras
if (Globals::globalConfig.dlConf.bExtras && !Globals::globalConfig.bUpdateCheck && !games[i].extras.empty())
if (Globals::globalConfig.dlConf.bExtras && !games[i].extras.empty())
{
std::cout << "extras: " << std::endl;
for (unsigned int j = 0; j < games[i].extras.size(); ++j)
@ -580,7 +595,7 @@ int Downloader::listGames()
}
}
// List patches
if (Globals::globalConfig.dlConf.bPatches && !Globals::globalConfig.bUpdateCheck && !games[i].patches.empty())
if (Globals::globalConfig.dlConf.bPatches && !games[i].patches.empty())
{
std::cout << "patches: " << std::endl;
for (unsigned int j = 0; j < games[i].patches.size(); ++j)
@ -605,7 +620,7 @@ int Downloader::listGames()
}
}
// List language packs
if (Globals::globalConfig.dlConf.bLanguagePacks && !Globals::globalConfig.bUpdateCheck && !games[i].languagepacks.empty())
if (Globals::globalConfig.dlConf.bLanguagePacks && !games[i].languagepacks.empty())
{
std::cout << "language packs: " << std::endl;
for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j)
@ -782,19 +797,11 @@ void Downloader::repair()
}
}
Json::Value downlinkJson;
std::string response = gogGalaxy->getResponse(vGameFiles[i].galaxy_downlink_json_url);
Json::Value downlinkJson = gogGalaxy->getResponseJson(vGameFiles[i].galaxy_downlink_json_url);
if (response.empty())
if (downlinkJson.empty())
{
std::cerr << "Found nothing in " << vGameFiles[i].galaxy_downlink_json_url << ", skipping file" << std::endl;
continue;
}
try {
std::istringstream iss(response);
iss >> downlinkJson;
} catch (const Json::Exception& exc) {
std::cerr << "Could not parse JSON response, skipping file" << std::endl;
std::cerr << "Empty JSON response, skipping file" << std::endl;
continue;
}
@ -2653,20 +2660,11 @@ void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
}
// Get downlink JSON from Galaxy API
Json::Value downlinkJson;
std::string response = galaxy->getResponse(gf.galaxy_downlink_json_url);
Json::Value downlinkJson = galaxy->getResponseJson(gf.galaxy_downlink_json_url);
if (response.empty())
if (downlinkJson.empty())
{
msgQueue.push(Message("Found nothing in " + gf.galaxy_downlink_json_url + ", skipping file", MSGTYPE_WARNING, msg_prefix));
continue;
}
try {
std::istringstream iss(response);
iss >> downlinkJson;
} catch (const Json::Exception& exc) {
msgQueue.push(Message("Could not parse JSON response, skipping file", MSGTYPE_WARNING, msg_prefix));
msgQueue.push(Message("Empty JSON response, skipping file", MSGTYPE_WARNING, msg_prefix));
continue;
}
@ -3226,20 +3224,7 @@ void Downloader::getGameDetailsThread(Config config, const unsigned int& tid)
}
game.makeFilepaths(conf.dirConf);
if (!config.bUpdateCheck)
gameDetailsQueue.push(game);
else
{ // Update check, only add games that have updated files
for (unsigned int j = 0; j < game.installers.size(); ++j)
{
if (game.installers[j].updated)
{
gameDetailsQueue.push(game);
break; // add the game only once
}
}
}
gameDetailsQueue.push(game);
}
vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
@ -4458,23 +4443,11 @@ void Downloader::processGalaxyDownloadQueue_MojoSetupHack(Config conf, const uns
int Downloader::mojoSetupGetFileVector(const gameFile& gf, std::vector<zipFileEntry>& vFiles)
{
Json::Value downlinkJson;
std::string response = gogGalaxy->getResponse(gf.galaxy_downlink_json_url);
Json::Value downlinkJson = gogGalaxy->getResponseJson(gf.galaxy_downlink_json_url);
if (response.empty())
if (downlinkJson.empty())
{
std::cerr << "Found nothing in " << gf.galaxy_downlink_json_url << std::endl;
return 1;
}
try
{
std::istringstream iss(response);
iss >> downlinkJson;
}
catch (const Json::Exception& exc)
{
std::cerr << "Could not parse JSON response" << std::endl;
std::cerr << "Empty JSON response" << std::endl;
return 1;
}

View File

@ -77,18 +77,12 @@ bool galaxyAPI::refreshLogin()
+ "&grant_type=refresh_token"
+ "&refresh_token=" + Globals::galaxyConf.getRefreshToken();
std::string json = this->getResponse(refresh_url);
if (json.empty())
Json::Value token_json = this->getResponseJson(refresh_url);
if (token_json.empty())
return false;
Json::Value token_json;
std::istringstream json_stream(json);
try {
json_stream >> token_json;
Globals::galaxyConf.setJSON(token_json);
} catch (const Json::Exception& exc) {
return false;
}
Globals::galaxyConf.setJSON(token_json);
return true;
}
@ -143,10 +137,9 @@ std::string galaxyAPI::getResponse(const std::string& url, const bool& zlib_deco
return response;
}
Json::Value galaxyAPI::getProductBuilds(const std::string& product_id, const std::string& platform, const std::string& generation)
Json::Value galaxyAPI::getResponseJson(const std::string& url, const bool& zlib_decompress)
{
std::string url = "https://content-system.gog.com/products/" + product_id + "/os/" + platform + "/builds?generation=" + generation;
std::istringstream response(this->getResponse(url));
std::istringstream response(this->getResponse(url, zlib_decompress));
Json::Value json;
if (!response.str().empty())
@ -164,6 +157,13 @@ Json::Value galaxyAPI::getProductBuilds(const std::string& product_id, const std
return json;
}
Json::Value galaxyAPI::getProductBuilds(const std::string& product_id, const std::string& platform, const std::string& generation)
{
std::string url = "https://content-system.gog.com/products/" + product_id + "/os/" + platform + "/builds?generation=" + generation;
return this->getResponseJson(url);
}
Json::Value galaxyAPI::getManifestV1(const std::string& product_id, const std::string& build_id, const std::string& manifest_id, const std::string& platform)
{
std::string url = "https://cdn.gog.com/content-system/v1/manifests/" + product_id + "/" + platform + "/" + build_id + "/" + manifest_id + ".json";
@ -173,12 +173,7 @@ Json::Value galaxyAPI::getManifestV1(const std::string& product_id, const std::s
Json::Value galaxyAPI::getManifestV1(const std::string& manifest_url)
{
std::istringstream response(this->getResponse(manifest_url));
Json::Value json;
response >> json;
return json;
return this->getResponseJson(manifest_url);
}
Json::Value galaxyAPI::getManifestV2(std::string manifest_hash)
@ -187,23 +182,15 @@ Json::Value galaxyAPI::getManifestV2(std::string manifest_hash)
manifest_hash = this->hashToGalaxyPath(manifest_hash);
std::string url = "https://cdn.gog.com/content-system/v2/meta/" + manifest_hash;
std::istringstream response(this->getResponse(url, true));
Json::Value json;
response >> json;
return json;
return this->getResponseJson(url, true);
}
Json::Value galaxyAPI::getSecureLink(const std::string& product_id, const std::string& path)
{
std::string url = "https://content-system.gog.com/products/" + product_id + "/secure_link?generation=2&path=" + path + "&_version=2";
std::istringstream response(this->getResponse(url));
Json::Value json;
response >>json;
return json;
return this->getResponseJson(url);
}
std::string galaxyAPI::hashToGalaxyPath(const std::string& hash)
@ -262,12 +249,8 @@ std::vector<galaxyDepotItem> galaxyAPI::getDepotItemsVector(const std::string& h
Json::Value galaxyAPI::getProductInfo(const std::string& product_id)
{
std::string url = "https://api.gog.com/products/" + product_id + "?expand=downloads,expanded_dlcs,description,screenshots,videos,related_products,changelog&locale=en-US";
std::istringstream response(this->getResponse(url));
Json::Value json;
response >> json;
return json;
return this->getResponseJson(url);
}
gameDetails galaxyAPI::productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf)
@ -379,17 +362,10 @@ std::vector<gameFile> galaxyAPI::fileJsonNodeToGameFileVector(const std::string&
Json::Value fileNode = infoNode["files"][j];
std::string downlink = fileNode["downlink"].asString();
std::string downlinkResponse = this->getResponse(downlink);
if (downlinkResponse.empty())
Json::Value downlinkJson = this->getResponseJson(downlink);
if (downlinkJson.empty())
continue;
Json::Value downlinkJson;
Json::CharReaderBuilder builder;
std::istringstream downlink_stream(downlinkResponse);
std::string errs;
Json::parseFromStream(builder, downlink_stream, &downlinkJson, &errs);
std::string downlink_url = downlinkJson["downlink"].asString();
std::string downlink_url_unescaped = (std::string)curl_easy_unescape(curlhandle, downlink_url.c_str(), downlink_url.size(), NULL);
std::string path;
@ -460,3 +436,10 @@ std::vector<gameFile> galaxyAPI::fileJsonNodeToGameFileVector(const std::string&
return gamefiles;
}
Json::Value galaxyAPI::getUserData()
{
std::string url = "https://embed.gog.com/userData.json";
return this->getResponseJson(url);
}

View File

@ -124,10 +124,11 @@ std::vector<gameItem> Website::getGames()
Json::Value root;
int i = 1;
bool bAllPagesParsed = false;
int iUpdated = Globals::globalConfig.bUpdated ? 1 : 0;
do
{
std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=1&sortBy=title&system=&page=" + std::to_string(i));
std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hasHiddenProducts=false&hiddenFlag=0&isUpdated=" + std::to_string(iUpdated) + "&mediaType=1&sortBy=title&system=&page=" + std::to_string(i));
std::istringstream json_stream(response);
try {
@ -151,7 +152,7 @@ std::vector<gameItem> Website::getGames()
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << root << std::endl;
#endif
if (root["page"].asInt() == root["totalPages"].asInt())
if (root["page"].asInt() == root["totalPages"].asInt() || root["totalPages"].asInt() == 0)
bAllPagesParsed = true;
if (root["products"].isArray())
{
@ -203,10 +204,6 @@ std::vector<gameItem> Website::getGames()
// Filter the game list
if (!Globals::globalConfig.sGameRegex.empty())
{
// GameRegex filter aliases
if (Globals::globalConfig.sGameRegex == "all")
Globals::globalConfig.sGameRegex = ".*";
boost::regex expression(Globals::globalConfig.sGameRegex);
boost::match_results<std::string::const_iterator> what;
if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
@ -272,41 +269,6 @@ std::vector<gameItem> Website::getGames()
return games;
}
// Get list of free games
std::vector<gameItem> Website::getFreeGames()
{
Json::Value root;
std::vector<gameItem> games;
std::string json = this->getResponse("https://www.gog.com/games/ajax/filtered?mediaType=game&page=1&price=free&sort=title");
std::istringstream json_stream(json);
try {
// Parse JSON
json_stream >> root;
} catch (const Json::Exception& exc) {
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << json << std::endl;
#endif
std::cout << exc.what();
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << root << std::endl;
#endif
Json::Value products = root["products"];
for (unsigned int i = 0; i < products.size(); ++i)
{
gameItem game;
game.name = products[i]["slug"].asString();
game.id = products[i]["id"].isInt() ? std::to_string(products[i]["id"].asInt()) : products[i]["id"].asString();
games.push_back(game);
}
return games;
}
// Login to GOG website
int Website::Login(const std::string& email, const std::string& password)
{

View File

@ -64,7 +64,7 @@ struct tm ZipUtil::date_time_to_tm(uint64_t date, uint64_t time)
uint64_t dos_time_base_year = 1980;
struct tm timeinfo;
timeinfo.tm_year = (uint16_t)(((date & 0xFE00) >> 9) - local_time_base_year + dos_time_base_year);
timeinfo.tm_year = (uint16_t)(((date & 0xFE00) >> 9) - local_time_base_year + dos_time_base_year);
timeinfo.tm_mon = (uint16_t)(((date & 0x1E0) >> 5) - 1);
timeinfo.tm_mday = (uint16_t)(date & 0x1F);
timeinfo.tm_hour = (uint16_t)((time & 0xF800) >> 11);