From 8e9c0949299538fb2e5c4cf29f56b1f4fedaaddf Mon Sep 17 00:00:00 2001 From: Sude Date: Sat, 21 Jul 2018 23:59:08 +0300 Subject: [PATCH] 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 --- include/config.h | 3 +- include/downloader.h | 3 +- include/galaxyapi.h | 2 + include/website.h | 2 - main.cpp | 13 +++- src/downloader.cpp | 181 ++++++++++++++++++------------------------- src/galaxyapi.cpp | 69 +++++++---------- src/website.cpp | 44 +---------- src/ziputil.cpp | 2 +- 9 files changed, 122 insertions(+), 197 deletions(-) diff --git a/include/config.h b/include/config.h index b055129..1159cfa 100644 --- a/include/config.h +++ b/include/config.h @@ -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 diff --git a/include/downloader.h b/include/downloader.h index f872a3a..f4c426b 100644 --- a/include/downloader.h +++ b/include/downloader.h @@ -91,7 +91,8 @@ class Downloader int init(); int login(); int listGames(); - void updateCheck(); + void checkNotifications(); + void clearUpdateNotifications(); void repair(); void download(); void checkOrphans(); diff --git a/include/galaxyapi.h b/include/galaxyapi.h index 960f6a7..21fafa5 100644 --- a/include/galaxyapi.h +++ b/include/galaxyapi.h @@ -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 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; diff --git a/include/website.h b/include/website.h index a25fdbc..4f92cda 100644 --- a/include/website.h +++ b/include/website.h @@ -22,7 +22,6 @@ class Website std::string getResponse(const std::string& url); Json::Value getGameDetailsJSON(const std::string& gameid); std::vector getGames(); - std::vector getFreeGames(); std::vector 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; diff --git a/main.cpp b/main.cpp index 834fcc4..bc4fd7d 100644 --- a/main.cpp +++ b/main.cpp @@ -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(&Globals::globalConfig.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info") ("download", bpo::value(&Globals::globalConfig.bDownload)->zero_tokens()->default_value(false), "Download") ("repair", bpo::value(&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(&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(&Globals::globalConfig.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)") ("create-xml", bpo::value(&Globals::globalConfig.sXMLFile)->implicit_value("automatic"), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation") - ("update-check", bpo::value(&Globals::globalConfig.bUpdateCheck)->zero_tokens()->default_value(false), "Check for update notifications") + ("notifications", bpo::value(&Globals::globalConfig.bNotifications)->zero_tokens()->default_value(false), "Check notifications") + ("updated", bpo::value(&Globals::globalConfig.bUpdated)->zero_tokens()->default_value(false), "List/download only games with update flag set") + ("clear-update-flags", bpo::value(&bClearUpdateNotifications)->zero_tokens()->default_value(false), "Clear update notification flags") ("check-orphans", bpo::value(&Globals::globalConfig.sOrphanRegex)->implicit_value(""), check_orphans_text.c_str()) ("status", bpo::value(&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(&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::iterator it = vFileIdStrings.begin(); it != vFileIdStrings.end(); it++) diff --git a/src/downloader.cpp b/src/downloader.cpp index 9b2c2f4..993825b 100644 --- a/src/downloader.cpp +++ b/src/downloader.cpp @@ -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& 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; } diff --git a/src/galaxyapi.cpp b/src/galaxyapi.cpp index aff1194..4759b4f 100644 --- a/src/galaxyapi.cpp +++ b/src/galaxyapi.cpp @@ -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 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 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 galaxyAPI::fileJsonNodeToGameFileVector(const std::string& return gamefiles; } + +Json::Value galaxyAPI::getUserData() +{ + std::string url = "https://embed.gog.com/userData.json"; + + return this->getResponseJson(url); +} diff --git a/src/website.cpp b/src/website.cpp index 133ab7e..8af2e4f 100644 --- a/src/website.cpp +++ b/src/website.cpp @@ -124,10 +124,11 @@ std::vector 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 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 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 what; if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex @@ -272,41 +269,6 @@ std::vector Website::getGames() return games; } -// Get list of free games -std::vector Website::getFreeGames() -{ - Json::Value root; - std::vector 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) { diff --git a/src/ziputil.cpp b/src/ziputil.cpp index 81ea3bf..0e7f5f5 100644 --- a/src/ziputil.cpp +++ b/src/ziputil.cpp @@ -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);