mirror of
https://github.com/Sude-/lgogdownloader.git
synced 2025-03-08 04:15:13 +01:00
Download cloud save files
This commit is contained in:
parent
c4f604aec0
commit
c97a828aa7
@ -103,6 +103,16 @@ class GalaxyConfig
|
||||
return this->token_json;
|
||||
}
|
||||
|
||||
std::string getUserId() {
|
||||
std::unique_lock<std::mutex> lock(m);
|
||||
|
||||
if(this->token_json.isMember("user_id")) {
|
||||
return this->token_json["user_id"].asString();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void setJSON(Json::Value json)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m);
|
||||
@ -135,13 +145,20 @@ class GalaxyConfig
|
||||
std::string getClientId()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m);
|
||||
return this->client_id;
|
||||
if(token_json.isMember("client_id")) {
|
||||
return token_json["client_id"].asString();
|
||||
}
|
||||
|
||||
return default_client_id;
|
||||
}
|
||||
|
||||
std::string getClientSecret()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m);
|
||||
return this->client_secret;
|
||||
if(token_json.isMember("client_secret")) {
|
||||
return token_json["client_secret"].asString();
|
||||
}
|
||||
|
||||
return default_client_secret;
|
||||
}
|
||||
|
||||
std::string getRedirectUri()
|
||||
@ -155,8 +172,6 @@ class GalaxyConfig
|
||||
GalaxyConfig(const GalaxyConfig& other)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(other.m);
|
||||
client_id = other.client_id;
|
||||
client_secret = other.client_secret;
|
||||
redirect_uri = other.redirect_uri;
|
||||
filepath = other.filepath;
|
||||
token_json = other.token_json;
|
||||
@ -170,8 +185,6 @@ class GalaxyConfig
|
||||
std::unique_lock<std::mutex> lock1(m, std::defer_lock);
|
||||
std::unique_lock<std::mutex> lock2(other.m, std::defer_lock);
|
||||
std::lock(lock1, lock2);
|
||||
client_id = other.client_id;
|
||||
client_secret = other.client_secret;
|
||||
redirect_uri = other.redirect_uri;
|
||||
filepath = other.filepath;
|
||||
token_json = other.token_json;
|
||||
@ -179,8 +192,9 @@ class GalaxyConfig
|
||||
}
|
||||
protected:
|
||||
private:
|
||||
std::string client_id = "46899977096215655";
|
||||
std::string client_secret = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9";
|
||||
const std::string default_client_id = "46899977096215655";
|
||||
const std::string default_client_secret = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9";
|
||||
|
||||
std::string redirect_uri = "https://embed.gog.com/on_login_success?origin=client";
|
||||
std::string filepath;
|
||||
Json::Value token_json;
|
||||
|
@ -32,9 +32,11 @@
|
||||
#include <curl/curl.h>
|
||||
#include <json/json.h>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <fstream>
|
||||
#include <deque>
|
||||
|
||||
class cloudSaveFile;
|
||||
class Timer
|
||||
{
|
||||
public:
|
||||
@ -99,6 +101,7 @@ class Downloader
|
||||
void clearUpdateNotifications();
|
||||
void repair();
|
||||
void download();
|
||||
void downloadCloudSaves(const std::string& product_id, int build_index = -1);
|
||||
void checkOrphans();
|
||||
void checkStatus();
|
||||
void updateCache();
|
||||
@ -118,6 +121,8 @@ class Downloader
|
||||
void galaxyShowCloudSavesById(const std::string& product_id, int build_index = -1);
|
||||
protected:
|
||||
private:
|
||||
int cloudSaveListByIdForEach(const std::string& product_id, int build_index, const std::function<void(cloudSaveFile &)> &f);
|
||||
|
||||
CURLcode downloadFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string());
|
||||
int repairFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string());
|
||||
int getGameDetails();
|
||||
@ -135,6 +140,7 @@ class Downloader
|
||||
static std::string getChangelogFromJSON(const Json::Value& json);
|
||||
void saveChangelog(const std::string& changelog, const std::string& filepath);
|
||||
static void processDownloadQueue(Config conf, const unsigned int& tid);
|
||||
static void processCloudSaveDownloadQueue(Config conf, const unsigned int& tid);
|
||||
static int progressCallbackForThread(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
|
||||
template <typename T> void printProgress(const ThreadSafeQueue<T>& download_queue);
|
||||
static void getGameDetailsThread(Config config, const unsigned int& tid);
|
||||
|
@ -46,8 +46,11 @@ class galaxyAPI
|
||||
galaxyAPI(CurlConfig& conf);
|
||||
virtual ~galaxyAPI();
|
||||
int init();
|
||||
|
||||
bool isTokenExpired();
|
||||
bool refreshLogin();
|
||||
bool refreshLogin(const std::string &clientId, const std::string &clientSecret, const std::string &refreshToken, bool newSession);
|
||||
|
||||
Json::Value getProductBuilds(const std::string& product_id, const std::string& platform = "windows", const std::string& generation = "2");
|
||||
Json::Value getManifestV1(const std::string& product_id, const std::string& build_id, const std::string& manifest_id = "repository", const std::string& platform = "windows");
|
||||
Json::Value getManifestV1(const std::string& manifest_url);
|
||||
@ -55,8 +58,8 @@ class galaxyAPI
|
||||
Json::Value getCloudPathAsJson(const std::string &clientId);
|
||||
Json::Value getSecureLink(const std::string& product_id, const std::string& path);
|
||||
Json::Value getDependencyLink(const std::string& path);
|
||||
std::string getResponse(const std::string& url);
|
||||
Json::Value getResponseJson(const std::string& url);
|
||||
std::string getResponse(const std::string& url, const char *encoding = nullptr);
|
||||
Json::Value getResponseJson(const std::string& url, const char *encoding = nullptr);
|
||||
std::string hashToGalaxyPath(const std::string& hash);
|
||||
std::vector<galaxyDepotItem> getDepotItemsVector(const std::string& hash, const bool& is_dependency = false);
|
||||
Json::Value getProductInfo(const std::string& product_id);
|
||||
|
14
main.cpp
14
main.cpp
@ -164,6 +164,7 @@ int main(int argc, char *argv[])
|
||||
std::string galaxy_product_id_install;
|
||||
std::string galaxy_product_id_show_builds;
|
||||
std::string galaxy_product_id_show_cloud_paths;
|
||||
std::string galaxy_product_cloud_saves;
|
||||
std::string tags;
|
||||
|
||||
std::vector<std::string> vFileIdStrings;
|
||||
@ -285,7 +286,8 @@ int main(int argc, char *argv[])
|
||||
options_cli_experimental.add_options()
|
||||
("galaxy-install", bpo::value<std::string>(&galaxy_product_id_install)->default_value(""), "Install game using product id [product_id/build_index] or gamename regex [gamename/build_id]\nBuild index is used to select a build and defaults to 0 if not specified.\n\nExample: 12345/2 selects build 2 for product 12345")
|
||||
("galaxy-show-builds", bpo::value<std::string>(&galaxy_product_id_show_builds)->default_value(""), "Show game builds using product id [product_id/build_index] or gamename regex [gamename/build_id]\nBuild index is used to select a build and defaults to 0 if not specified.\n\nExample: 12345/2 selects build 2 for product 12345")
|
||||
("galaxy-show-cloud-save-paths", bpo::value<std::string>(&galaxy_product_id_show_cloud_paths)->default_value(""), "Show game cloud-save paths using product id [product_id/build_index] or gamename regex [gamename/build_id]\nBuild index is used to select a build and defaults to 0 if not specified.\n\nExample: 12345/2 selects build 2 for product 12345")
|
||||
("galaxy-download-cloud-saves", bpo::value<std::string>(&galaxy_product_cloud_saves)->default_value(""), "Download cloud saves using product-id [product_id/build_index] or gamename regex [gamename/build_id]\nBuild index is used to select a build and defaults to 0 if not specified.\n\nExample: 12345/2 selects build 2 for product 12345")
|
||||
("galaxy-show-cloud-saves", bpo::value<std::string>(&galaxy_product_id_show_cloud_paths)->default_value(""), "Show game cloud-saves using product id [product_id/build_index] or gamename regex [gamename/build_id]\nBuild index is used to select a build and defaults to 0 if not specified.\n\nExample: 12345/2 selects build 2 for product 12345")
|
||||
("galaxy-platform", bpo::value<std::string>(&sGalaxyPlatform)->default_value("w"), galaxy_platform_text.c_str())
|
||||
("galaxy-language", bpo::value<std::string>(&sGalaxyLanguage)->default_value("en"), galaxy_language_text.c_str())
|
||||
("galaxy-arch", bpo::value<std::string>(&sGalaxyArch)->default_value("x64"), galaxy_arch_text.c_str())
|
||||
@ -807,6 +809,16 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
downloader.galaxyInstallGame(product_id, build_index, Globals::globalConfig.dlConf.iGalaxyArch);
|
||||
}
|
||||
else if (!galaxy_product_cloud_saves.empty()) {
|
||||
int build_index = -1;
|
||||
std::vector<std::string> tokens = Util::tokenize(galaxy_product_cloud_saves, "/");
|
||||
std::string product_id = tokens[0];
|
||||
if (tokens.size() == 2)
|
||||
{
|
||||
build_index = std::stoi(tokens[1]);
|
||||
}
|
||||
downloader.downloadCloudSaves(product_id, build_index);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Globals::globalConfig.bLogin)
|
||||
|
@ -31,6 +31,7 @@
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include <boost/iostreams/filtering_streambuf.hpp>
|
||||
#include <boost/iostreams/copy.hpp>
|
||||
@ -39,8 +40,16 @@
|
||||
|
||||
namespace bptime = boost::posix_time;
|
||||
|
||||
struct cloudSaveFile {
|
||||
boost::posix_time::ptime lastModified;
|
||||
unsigned long long fileSize;
|
||||
std::string path;
|
||||
std::string location;
|
||||
};
|
||||
|
||||
std::vector<DownloadInfo> vDownloadInfo;
|
||||
ThreadSafeQueue<gameFile> dlQueue;
|
||||
ThreadSafeQueue<cloudSaveFile> dlCloudSaveQueue;
|
||||
ThreadSafeQueue<Message> msgQueue;
|
||||
ThreadSafeQueue<gameFile> createXMLQueue;
|
||||
ThreadSafeQueue<gameItem> gameItemQueue;
|
||||
@ -2521,6 +2530,252 @@ void Downloader::showWishlist()
|
||||
return;
|
||||
}
|
||||
|
||||
void Downloader::processCloudSaveDownloadQueue(Config conf, const unsigned int& tid) {
|
||||
std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
|
||||
|
||||
std::unique_ptr<galaxyAPI> galaxy { new galaxyAPI(Globals::globalConfig.curlConf) };
|
||||
if (!galaxy->init())
|
||||
{
|
||||
if (!galaxy->refreshLogin())
|
||||
{
|
||||
msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
|
||||
vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CURL* dlhandle = curl_easy_init();
|
||||
|
||||
Util::CurlHandleSetDefaultOptions(dlhandle, conf.curlConf);
|
||||
|
||||
curl_slist *header = nullptr;
|
||||
|
||||
std::string access_token;
|
||||
if (!Globals::galaxyConf.isExpired())
|
||||
access_token = Globals::galaxyConf.getAccessToken();
|
||||
if (!access_token.empty())
|
||||
{
|
||||
std::string bearer = "Authorization: Bearer " + access_token;
|
||||
header = curl_slist_append(header, bearer.c_str());
|
||||
}
|
||||
|
||||
curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0);
|
||||
curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
|
||||
curl_easy_setopt(dlhandle, CURLOPT_READFUNCTION, Downloader::readData);
|
||||
curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 1L);
|
||||
|
||||
xferInfo xferinfo;
|
||||
xferinfo.tid = tid;
|
||||
xferinfo.curlhandle = dlhandle;
|
||||
|
||||
curl_easy_setopt(dlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallbackForThread);
|
||||
curl_easy_setopt(dlhandle, CURLOPT_XFERINFODATA, &xferinfo);
|
||||
|
||||
cloudSaveFile csf;
|
||||
while(dlCloudSaveQueue.try_pop(csf)) {
|
||||
CURLcode result = CURLE_RECV_ERROR; // assume network error
|
||||
int iRetryCount = 0;
|
||||
off_t iResumePosition = 0;
|
||||
|
||||
bool bResume = false;
|
||||
|
||||
iTotalRemainingBytes.fetch_sub(csf.fileSize);
|
||||
|
||||
// Get directory from filepath
|
||||
boost::filesystem::path filepath = csf.location;
|
||||
filepath = boost::filesystem::absolute(filepath, boost::filesystem::current_path());
|
||||
boost::filesystem::path directory = filepath.parent_path();
|
||||
|
||||
vDownloadInfo[tid].setFilename(filepath.filename().string());
|
||||
|
||||
if(boost::filesystem::exists(filepath)) {
|
||||
auto size = boost::filesystem::file_size(filepath);
|
||||
|
||||
// last_write_time minus a single second, since time_t is only accurate to the second unlike boost::posix_time::ptime
|
||||
auto time = boost::filesystem::last_write_time(filepath) - 1;
|
||||
|
||||
if(csf.fileSize < size) {
|
||||
bResume = true;
|
||||
}
|
||||
else if(boost::posix_time::from_time_t(time) <= csf.lastModified) {
|
||||
msgQueue.push(Message("Already up to date -- skipping: " + filepath.filename().string(), MSGTYPE_INFO, msg_prefix));
|
||||
continue; // This file is already completed
|
||||
}
|
||||
}
|
||||
|
||||
msgQueue.push(Message("Begin download: " + filepath.string(), MSGTYPE_INFO, msg_prefix));
|
||||
|
||||
// Check that directory exists and create subdirectories
|
||||
std::unique_lock<std::mutex> ul { mtx_create_directories }; // Use mutex to avoid possible race conditions
|
||||
if (boost::filesystem::exists(directory))
|
||||
{
|
||||
if (!boost::filesystem::is_directory(directory)) {
|
||||
msgQueue.push(Message(directory.string() + " is not directory, skipping file (" + filepath.filename().string() + ")", MSGTYPE_WARNING, msg_prefix));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (!boost::filesystem::create_directories(directory))
|
||||
{
|
||||
msgQueue.push(Message("Failed to create directory (" + directory.string() + "), skipping file (" + filepath.filename().string() + ")", MSGTYPE_ERROR, msg_prefix));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto url = "https://cloudstorage.gog.com/v1/" + Globals::galaxyConf.getUserId() + '/' + Globals::galaxyConf.getClientId() + '/' + csf.path;
|
||||
msgQueue.push(Message(url));
|
||||
curl_easy_setopt(dlhandle, CURLOPT_HTTPHEADER, header);
|
||||
curl_easy_setopt(dlhandle, CURLOPT_URL, url.c_str());
|
||||
long int response_code = 0;
|
||||
bool bShouldRetry = false;
|
||||
std::string retry_reason;
|
||||
do
|
||||
{
|
||||
if (conf.iWait > 0)
|
||||
usleep(conf.iWait); // Wait before continuing
|
||||
|
||||
response_code = 0; // Make sure that response code is reset
|
||||
|
||||
if (iRetryCount != 0)
|
||||
{
|
||||
std::string retry_msg = "Retry " + std::to_string(iRetryCount) + "/" + std::to_string(conf.iRetries) + ": " + filepath.filename().string();
|
||||
if (!retry_reason.empty())
|
||||
retry_msg += " (" + retry_reason + ")";
|
||||
msgQueue.push(Message(retry_msg, MSGTYPE_INFO, msg_prefix));
|
||||
}
|
||||
retry_reason = ""; // reset retry reason
|
||||
|
||||
FILE* outfile;
|
||||
// File exists, resume
|
||||
if (bResume)
|
||||
{
|
||||
iResumePosition = boost::filesystem::file_size(filepath);
|
||||
if ((outfile=fopen(filepath.string().c_str(), "r+"))!=NULL)
|
||||
{
|
||||
fseek(outfile, 0, SEEK_END);
|
||||
curl_easy_setopt(dlhandle, CURLOPT_RESUME_FROM_LARGE, iResumePosition);
|
||||
curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
msgQueue.push(Message("Failed to open " + filepath.string(), MSGTYPE_ERROR, msg_prefix));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else // File doesn't exist, create new file
|
||||
{
|
||||
if ((outfile=fopen(filepath.string().c_str(), "w"))!=NULL)
|
||||
{
|
||||
curl_easy_setopt(dlhandle, CURLOPT_RESUME_FROM_LARGE, 0); // start downloading from the beginning of file
|
||||
curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
msgQueue.push(Message("Failed to create " + filepath.string(), MSGTYPE_ERROR, msg_prefix));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
xferinfo.offset = iResumePosition;
|
||||
xferinfo.timer.reset();
|
||||
xferinfo.TimeAndSize.clear();
|
||||
result = curl_easy_perform(dlhandle);
|
||||
fclose(outfile);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
// Retry on these errors
|
||||
case CURLE_PARTIAL_FILE:
|
||||
case CURLE_OPERATION_TIMEDOUT:
|
||||
case CURLE_RECV_ERROR:
|
||||
case CURLE_SSL_CONNECT_ERROR:
|
||||
bShouldRetry = true;
|
||||
break;
|
||||
// Retry on CURLE_HTTP_RETURNED_ERROR if response code is not "416 Range Not Satisfiable"
|
||||
case CURLE_HTTP_RETURNED_ERROR:
|
||||
curl_easy_getinfo(dlhandle, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
if (response_code == 416)
|
||||
bShouldRetry = false;
|
||||
else
|
||||
bShouldRetry = true;
|
||||
break;
|
||||
default:
|
||||
bShouldRetry = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (bShouldRetry)
|
||||
{
|
||||
iRetryCount++;
|
||||
retry_reason = std::string(curl_easy_strerror(result));
|
||||
if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath)) {
|
||||
bResume = true;
|
||||
}
|
||||
}
|
||||
|
||||
} while (bShouldRetry && (iRetryCount <= conf.iRetries));
|
||||
|
||||
if (result == CURLE_OK || result == CURLE_RANGE_ERROR || (result == CURLE_HTTP_RETURNED_ERROR && response_code == 416))
|
||||
{
|
||||
// Set timestamp for downloaded file to same value as file on server
|
||||
long filetime = -1;
|
||||
CURLcode res = curl_easy_getinfo(dlhandle, CURLINFO_FILETIME, &filetime);
|
||||
if (res == CURLE_OK && filetime >= 0)
|
||||
{
|
||||
std::time_t timestamp = (std::time_t)filetime;
|
||||
try
|
||||
{
|
||||
boost::filesystem::last_write_time(filepath, timestamp);
|
||||
}
|
||||
catch(const boost::filesystem::filesystem_error& e)
|
||||
{
|
||||
msgQueue.push(Message(e.what(), MSGTYPE_WARNING, msg_prefix));
|
||||
}
|
||||
}
|
||||
|
||||
// Average download speed
|
||||
std::ostringstream dlrate_avg;
|
||||
std::string rate_unit;
|
||||
progressInfo progress_info = vDownloadInfo[tid].getProgressInfo();
|
||||
if (progress_info.rate_avg > 1048576) // 1 MB
|
||||
{
|
||||
progress_info.rate_avg /= 1048576;
|
||||
rate_unit = "MB/s";
|
||||
}
|
||||
else
|
||||
{
|
||||
progress_info.rate_avg /= 1024;
|
||||
rate_unit = "kB/s";
|
||||
}
|
||||
dlrate_avg << std::setprecision(2) << std::fixed << progress_info.rate_avg << rate_unit;
|
||||
|
||||
msgQueue.push(Message("Download complete: " + filepath.filename().string() + " (@ " + dlrate_avg.str() + ")", MSGTYPE_SUCCESS, msg_prefix));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string msg = "Download complete (" + static_cast<std::string>(curl_easy_strerror(result));
|
||||
if (response_code > 0)
|
||||
msg += " (" + std::to_string(response_code) + ")";
|
||||
msg += "): " + filepath.filename().string();
|
||||
msgQueue.push(Message(msg, MSGTYPE_WARNING, msg_prefix));
|
||||
|
||||
// Delete the file if download failed and was not a resume attempt or the result is zero length file
|
||||
if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
|
||||
{
|
||||
if ((result != CURLE_PARTIAL_FILE && !bResume && result != CURLE_OPERATION_TIMEDOUT) || boost::filesystem::file_size(filepath) == 0)
|
||||
{
|
||||
if (!boost::filesystem::remove(filepath))
|
||||
msgQueue.push(Message("Failed to delete " + filepath.filename().string(), MSGTYPE_ERROR, msg_prefix));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curl_slist_free_all(header);
|
||||
curl_easy_cleanup(dlhandle);
|
||||
|
||||
vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
|
||||
msgQueue.push(Message("Finished all tasks", MSGTYPE_INFO, msg_prefix));
|
||||
}
|
||||
|
||||
void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
|
||||
{
|
||||
std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
|
||||
@ -4130,6 +4385,21 @@ std::string parseLocation(const std::string &location, const std::map<std::strin
|
||||
return parsedLocation;
|
||||
}
|
||||
|
||||
std::pair<std::string::const_iterator, std::string::const_iterator> getline(std::string::const_iterator begin, std::string::const_iterator end) {
|
||||
while(begin != end) {
|
||||
if(*begin == '\r') {
|
||||
return { begin, begin + 2 };
|
||||
}
|
||||
if(*begin == '\n') {
|
||||
return { begin, begin + 1 };
|
||||
}
|
||||
|
||||
++begin;
|
||||
}
|
||||
|
||||
return { end, end };
|
||||
}
|
||||
|
||||
void Downloader::galaxyShowCloudSaves(const std::string& product_id, int build_index)
|
||||
{
|
||||
std::string id;
|
||||
@ -4140,14 +4410,13 @@ void Downloader::galaxyShowCloudSaves(const std::string& product_id, int build_i
|
||||
}
|
||||
}
|
||||
|
||||
void Downloader::galaxyShowCloudSavesById(const std::string& product_id, int build_index)
|
||||
{
|
||||
int Downloader::cloudSaveListByIdForEach(const std::string& product_id, int build_index, const std::function<void(cloudSaveFile &)> &f) {
|
||||
std::string sPlatform;
|
||||
unsigned int iPlatform = Globals::globalConfig.dlConf.iGalaxyPlatform;
|
||||
if (iPlatform == GlobalConstants::PLATFORM_LINUX) {
|
||||
// Linux is not yet supported for cloud saves
|
||||
std::cout << "Cloud saves for Linux builds not yet supported" << std::endl;
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
else if (iPlatform == GlobalConstants::PLATFORM_MAC)
|
||||
sPlatform = "osx";
|
||||
@ -4156,64 +4425,26 @@ void Downloader::galaxyShowCloudSavesById(const std::string& product_id, int bui
|
||||
|
||||
Json::Value json = gogGalaxy->getProductBuilds(product_id, sPlatform);
|
||||
|
||||
// JSON is empty and platform is Linux. Most likely cause is that Galaxy API doesn't have Linux support
|
||||
if (json.empty() && iPlatform == GlobalConstants::PLATFORM_LINUX)
|
||||
{
|
||||
std::cout << "Galaxy API doesn't have Linux support" << std::endl;
|
||||
|
||||
std::cout << "Checking for installers that can be used as repository" << std::endl;
|
||||
DownloadConfig dlConf = Globals::globalConfig.dlConf;
|
||||
dlConf.bInstallers = true;
|
||||
dlConf.bExtras = false;
|
||||
dlConf.bLanguagePacks = false;
|
||||
dlConf.bPatches = false;
|
||||
dlConf.bDLC = true;
|
||||
dlConf.iInstallerPlatform = dlConf.iGalaxyPlatform;
|
||||
dlConf.iInstallerLanguage = dlConf.iGalaxyLanguage;
|
||||
|
||||
Json::Value product_info = gogGalaxy->getProductInfo(product_id);
|
||||
gameDetails game = gogGalaxy->productInfoJsonToGameDetails(product_info, dlConf);
|
||||
|
||||
std::vector<gameFile> vInstallers;
|
||||
if (!game.installers.empty())
|
||||
{
|
||||
vInstallers.push_back(game.installers[0]);
|
||||
for (unsigned int i = 0; i < game.dlcs.size(); ++i)
|
||||
{
|
||||
if (!game.dlcs[i].installers.empty())
|
||||
vInstallers.push_back(game.dlcs[i].installers[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (vInstallers.empty())
|
||||
{
|
||||
std::cout << "No installers found" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Using these installers" << std::endl;
|
||||
for (unsigned int i = 0; i < vInstallers.size(); ++i)
|
||||
std::cout << "\t" << vInstallers[i].gamename << "/" << vInstallers[i].id << std::endl;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
build_index = std::max(0, build_index);
|
||||
|
||||
std::string link = json["items"][build_index]["link"].asString();
|
||||
|
||||
Json::Value manifest;
|
||||
if (json["items"][build_index]["generation"].asInt() == 2)
|
||||
{
|
||||
std::string buildHash;
|
||||
buildHash.assign(link.begin()+link.find_last_of("/")+1, link.end());
|
||||
manifest = gogGalaxy->getManifestV2(buildHash);
|
||||
}
|
||||
else
|
||||
if (json["items"][build_index]["generation"].asInt() != 2)
|
||||
{
|
||||
std::cout << "Only generation 2 builds are supported currently" << std::endl;
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
std::string buildHash;
|
||||
buildHash.assign(link.begin()+link.find_last_of("/")+1, link.end());
|
||||
manifest = gogGalaxy->getManifestV2(buildHash);
|
||||
|
||||
std::string clientId = manifest["clientId"].asString();
|
||||
std::string secret = manifest["clientSecret"].asString();
|
||||
|
||||
if(!gogGalaxy->refreshLogin(clientId, secret, Globals::galaxyConf.getRefreshToken(), false)) {
|
||||
std::cout << "Couldn't refresh login" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string install_directory;
|
||||
@ -4221,13 +4452,6 @@ void Downloader::galaxyShowCloudSavesById(const std::string& product_id, int bui
|
||||
{
|
||||
install_directory = this->getGalaxyInstallDirectory(gogGalaxy, json);
|
||||
}
|
||||
std::string install_path = Globals::globalConfig.dirConf.sDirectory + install_directory;
|
||||
std::string document_path = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/Documents/";
|
||||
std::string appdata_roaming = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/AppData/Roaming/";
|
||||
std::string appdata_local_path = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/AppData/Local/";
|
||||
std::string appdata_local_low_path = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/AppData/LocalLow/";
|
||||
std::string saved_games = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/Save Games/";
|
||||
|
||||
|
||||
std::string platform;
|
||||
switch(iPlatform) {
|
||||
@ -4238,13 +4462,16 @@ void Downloader::galaxyShowCloudSavesById(const std::string& product_id, int bui
|
||||
platform = "Windows";
|
||||
}
|
||||
|
||||
json = gogGalaxy->getCloudPathAsJson(manifest["clientId"].asString());
|
||||
json = json["content"][platform]["cloudStorage"]["locations"];
|
||||
std::string install_path = Globals::globalConfig.dirConf.sDirectory + install_directory;
|
||||
std::string document_path = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/Documents/";
|
||||
std::string appdata_roaming = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/AppData/Roaming/";
|
||||
std::string appdata_local_path = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/AppData/Local/";
|
||||
std::string appdata_local_low_path = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/AppData/LocalLow/";
|
||||
std::string saved_games = Globals::globalConfig.dirConf.sWinePrefix + "drive_c/users/" + username() + "/Save Games/";
|
||||
|
||||
struct cloud_save_t {
|
||||
std::string name;
|
||||
std::string location;
|
||||
};
|
||||
std::string url = "https://cloudstorage.gog.com/v1/" + Globals::galaxyConf.getUserId() + "/" + clientId;
|
||||
|
||||
auto cloud_saves_json = gogGalaxy->getCloudPathAsJson(manifest["clientId"].asString())["content"][platform]["cloudStorage"]["locations"];
|
||||
|
||||
std::map<std::string, std::string> vars {
|
||||
{ "INSTALL", std::move(install_path) },
|
||||
@ -4255,22 +4482,110 @@ void Downloader::galaxyShowCloudSavesById(const std::string& product_id, int bui
|
||||
{ "SAVED_GAMES", std::move(saved_games) },
|
||||
};
|
||||
|
||||
std::vector<cloud_save_t> cloud_saves;
|
||||
for(auto &cloud_save : json) {
|
||||
std::map<std::string, std::string> name_to_location;
|
||||
for(auto &cloud_save : cloud_saves_json) {
|
||||
std::string location = parseLocation(cloud_save["location"].asString(), vars);
|
||||
std::string name = cloud_save["name"].asString();
|
||||
cloud_saves.emplace_back(cloud_save_t { std::move(name), std::move(location) });
|
||||
|
||||
name_to_location.insert({cloud_save["name"].asString(), std::move(location)});
|
||||
}
|
||||
|
||||
if(cloud_saves.empty()) {
|
||||
if(name_to_location.empty()) {
|
||||
std::cout << "No cloud save locations found" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
auto fileList = gogGalaxy->getResponseJson(url, "application/json");
|
||||
|
||||
for(auto &fileJson : fileList) {
|
||||
auto path = fileJson["name"].asString();
|
||||
auto pos = path.find_first_of('/');
|
||||
|
||||
auto location = name_to_location[path.substr(0, pos)] + path.substr(pos);
|
||||
// std::cout << location << std::endl;
|
||||
// std::cout << path <<std::endl;
|
||||
|
||||
auto filesize = fileJson["bytes"].asUInt64();
|
||||
|
||||
auto last_modified = boost::posix_time::from_iso_extended_string(fileJson["last_modified"].asString());
|
||||
|
||||
cloudSaveFile csf {
|
||||
last_modified,
|
||||
filesize,
|
||||
std::move(path),
|
||||
std::move(location)
|
||||
};
|
||||
|
||||
f(csf);
|
||||
|
||||
// iTotalRemainingBytes.fetch_add(filesize);
|
||||
}
|
||||
|
||||
for(auto &cloud_save : cloud_saves) {
|
||||
std::cout << cloud_save.name << "::" << cloud_save.location << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Downloader::downloadCloudSaves(const std::string& product_id, int build_index)
|
||||
{
|
||||
auto res = this->cloudSaveListByIdForEach(product_id, build_index, [](cloudSaveFile &csf) {
|
||||
iTotalRemainingBytes.fetch_add(csf.fileSize);
|
||||
|
||||
dlCloudSaveQueue.push(std::move(csf));
|
||||
});
|
||||
|
||||
if(res || dlCloudSaveQueue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
// Limit thread count to number of items in download queue
|
||||
unsigned int iThreads = std::min(Globals::globalConfig.iThreads, static_cast<unsigned int>(dlCloudSaveQueue.size()));
|
||||
|
||||
// Create download threads
|
||||
std::vector<std::thread> vThreads;
|
||||
for (unsigned int i = 0; i < iThreads; ++i)
|
||||
{
|
||||
DownloadInfo dlInfo;
|
||||
dlInfo.setStatus(DLSTATUS_NOTSTARTED);
|
||||
vDownloadInfo.push_back(dlInfo);
|
||||
vThreads.push_back(std::thread(Downloader::processCloudSaveDownloadQueue, Globals::globalConfig, i));
|
||||
}
|
||||
|
||||
this->printProgress(dlQueue);
|
||||
|
||||
// Join threads
|
||||
for (unsigned int i = 0; i < vThreads.size(); ++i) {
|
||||
vThreads[i].join();
|
||||
}
|
||||
|
||||
vThreads.clear();
|
||||
vDownloadInfo.clear();
|
||||
}
|
||||
|
||||
void Downloader::galaxyShowCloudSavesById(const std::string& product_id, int build_index)
|
||||
{
|
||||
this->cloudSaveListByIdForEach(product_id, build_index, [](cloudSaveFile &csf) {
|
||||
boost::filesystem::path filepath = csf.location;
|
||||
filepath = boost::filesystem::absolute(filepath, boost::filesystem::current_path());
|
||||
|
||||
if(boost::filesystem::exists(filepath)) {
|
||||
auto size = boost::filesystem::file_size(filepath);
|
||||
|
||||
// last_write_time minus a single second, since time_t is only accurate to the second unlike boost::posix_time::ptime
|
||||
auto time = boost::filesystem::last_write_time(filepath) - 1;
|
||||
|
||||
if(csf.fileSize < size) {
|
||||
std::cout << filepath << " :: not yet completed download" << std::endl;
|
||||
}
|
||||
else if(boost::posix_time::from_time_t(time) <= csf.lastModified) {
|
||||
std::cout << filepath << " :: Already up to date" << std::endl;
|
||||
}
|
||||
else {
|
||||
std::cout << filepath << " :: Out of date" << std::endl;
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::cout << filepath << " :: Isn't downloaded yet" << std::endl;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> Downloader::galaxyGetOrphanedFiles(const std::vector<galaxyDepotItem>& items, const std::string& install_path)
|
||||
|
@ -53,23 +53,33 @@ int galaxyAPI::init()
|
||||
return res;
|
||||
}
|
||||
|
||||
bool galaxyAPI::refreshLogin()
|
||||
bool galaxyAPI::refreshLogin(const std::string &clientId, const std::string &clientSecret, const std::string &refreshToken, bool newSession)
|
||||
{
|
||||
std::string refresh_url = "https://auth.gog.com/token?client_id=" + Globals::galaxyConf.getClientId()
|
||||
+ "&client_secret=" + Globals::galaxyConf.getClientSecret()
|
||||
std::string refresh_url = "https://auth.gog.com/token?client_id=" + clientId
|
||||
+ "&client_secret=" + clientSecret
|
||||
+ "&grant_type=refresh_token"
|
||||
+ "&refresh_token=" + Globals::galaxyConf.getRefreshToken();
|
||||
+ "&refresh_token=" + refreshToken
|
||||
+ (newSession ? "" : "&without_new_session=1");
|
||||
|
||||
// std::cout << refresh_url << std::endl;
|
||||
Json::Value token_json = this->getResponseJson(refresh_url);
|
||||
|
||||
if (token_json.empty())
|
||||
return false;
|
||||
|
||||
token_json["client_id"] = clientId;
|
||||
token_json["client_secret"] = clientSecret;
|
||||
|
||||
Globals::galaxyConf.setJSON(token_json);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool galaxyAPI::refreshLogin()
|
||||
{
|
||||
return refreshLogin(Globals::galaxyConf.getClientId(), Globals::galaxyConf.getClientSecret(), Globals::galaxyConf.getRefreshToken(), true);
|
||||
}
|
||||
|
||||
bool galaxyAPI::isTokenExpired()
|
||||
{
|
||||
bool res = false;
|
||||
@ -80,7 +90,7 @@ bool galaxyAPI::isTokenExpired()
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string galaxyAPI::getResponse(const std::string& url)
|
||||
std::string galaxyAPI::getResponse(const std::string& url, const char *encoding)
|
||||
{
|
||||
struct curl_slist *header = NULL;
|
||||
|
||||
@ -92,13 +102,26 @@ std::string galaxyAPI::getResponse(const std::string& url)
|
||||
std::string bearer = "Authorization: Bearer " + access_token;
|
||||
header = curl_slist_append(header, bearer.c_str());
|
||||
}
|
||||
|
||||
if(encoding) {
|
||||
auto accept = "Accept: " + std::string(encoding);
|
||||
header = curl_slist_append(header, accept.c_str());
|
||||
}
|
||||
|
||||
curl_easy_setopt(curlhandle, CURLOPT_HTTPHEADER, header);
|
||||
curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curlhandle, CURLOPT_ACCEPT_ENCODING, "");
|
||||
|
||||
int max_retries = std::min(3, Globals::globalConfig.iRetries);
|
||||
std::string response;
|
||||
Util::CurlHandleGetResponse(curlhandle, response, max_retries);
|
||||
auto res = Util::CurlHandleGetResponse(curlhandle, response, max_retries);
|
||||
|
||||
if(res) {
|
||||
long int response_code = 0;
|
||||
curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
std::cout << "Response code for " << url << " is [" << response_code << ']' << std::endl;
|
||||
}
|
||||
|
||||
curl_easy_setopt(curlhandle, CURLOPT_ACCEPT_ENCODING, NULL);
|
||||
curl_easy_setopt(curlhandle, CURLOPT_HTTPHEADER, NULL);
|
||||
@ -107,9 +130,9 @@ std::string galaxyAPI::getResponse(const std::string& url)
|
||||
return response;
|
||||
}
|
||||
|
||||
Json::Value galaxyAPI::getResponseJson(const std::string& url)
|
||||
Json::Value galaxyAPI::getResponseJson(const std::string& url, const char *encoding)
|
||||
{
|
||||
std::istringstream response(this->getResponse(url));
|
||||
std::istringstream response(this->getResponse(url, encoding));
|
||||
Json::Value json;
|
||||
|
||||
if (!response.str().empty())
|
||||
@ -149,8 +172,13 @@ Json::Value galaxyAPI::getResponseJson(const std::string& url)
|
||||
catch(const Json::Exception& exc)
|
||||
{
|
||||
// Failed to parse json
|
||||
|
||||
std::cout << "Failed to parse json: " << exc.what();
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::cout << "Failed to parse json: " << exc.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -839,8 +839,9 @@ CURLcode Util::CurlHandleGetResponse(CURL* curlhandle, std::string& response, in
|
||||
// Retry on CURLE_HTTP_RETURNED_ERROR if response code is not "404 Not Found"
|
||||
case CURLE_HTTP_RETURNED_ERROR:
|
||||
curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
if (response_code == 404)
|
||||
if (response_code == 404 || response_code == 403) {
|
||||
bShouldRetry = false;
|
||||
}
|
||||
else
|
||||
bShouldRetry = true;
|
||||
break;
|
||||
|
Loading…
x
Reference in New Issue
Block a user