Achievements: Tidy up badge downloader

This commit is contained in:
Stenzek
2025-12-06 13:55:11 +10:00
parent 5b91141f09
commit 972dcd7920
3 changed files with 96 additions and 131 deletions

View File

@@ -22,12 +22,12 @@
#include "common/assert.h"
#include "common/binary_reader_writer.h"
#include "common/error.h"
#include "common/progress_callback.h"
#include "common/file_system.h"
#include "common/heap_array.h"
#include "common/log.h"
#include "common/md5_digest.h"
#include "common/path.h"
#include "common/progress_callback.h"
#include "common/ryml_helpers.h"
#include "common/scoped_guard.h"
#include "common/sha256_digest.h"
@@ -153,6 +153,7 @@ static void ClientMessageCallback(const char* message, const rc_client_t* client
static uint32_t ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client);
static void ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data,
rc_client_t* client);
static rc_api_server_response_t MakeRCAPIServerResponse(s32 status_code, const std::vector<u8>& data);
static void ClientEventHandler(const rc_client_event_t* event, rc_client_t* client);
static void HandleResetEvent(const rc_client_event_t* event);
@@ -797,14 +798,7 @@ void Achievements::ClientServerCall(const rc_api_request_t* request, rc_client_s
if (status_code != HTTPDownloader::HTTP_STATUS_OK)
ERROR_LOG("Server call failed: {}", error.GetDescription());
rc_api_server_response_t rr;
rr.http_status_code = (status_code <= 0) ? (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED ?
RC_API_SERVER_RESPONSE_CLIENT_ERROR :
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) :
status_code;
rr.body_length = data.size();
rr.body = data.empty() ? nullptr : reinterpret_cast<const char*>(data.data());
const rc_api_server_response_t rr = MakeRCAPIServerResponse(status_code, data);
callback(&rr, callback_data);
};
@@ -823,6 +817,18 @@ void Achievements::ClientServerCall(const rc_api_request_t* request, rc_client_s
}
}
rc_api_server_response_t Achievements::MakeRCAPIServerResponse(s32 status_code, const std::vector<u8>& data)
{
return rc_api_server_response_t{
.body = data.empty() ? nullptr : reinterpret_cast<const char*>(data.data()),
.body_length = data.size(),
.http_status_code = (status_code <= 0) ? (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED ?
RC_API_SERVER_RESPONSE_CLIENT_ERROR :
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) :
status_code,
};
}
void Achievements::IdleUpdate()
{
if (!IsActive())
@@ -2063,8 +2069,6 @@ std::string Achievements::GetGameBadgePath(std::string_view badge_name)
bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
{
progress->SetStatusText(TRANSLATE_SV("Achievements", "Collecting games..."));
// Collect all unique game IDs that don't have icons yet
std::vector<u32> game_ids;
{
@@ -2086,26 +2090,22 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
if (game_ids.empty())
{
progress->SetStatusText(TRANSLATE_SV("Achievements", "No games need icon downloads."));
return true;
Error::SetStringView(error, TRANSLATE_SV("Achievements", "No games need badge downloads."));
return false;
}
INFO_LOG("Downloading icons for {} games from RetroAchievements", game_ids.size());
progress->FormatStatusText(TRANSLATE_FS("Achievements", "Fetching icon info for {} games..."), game_ids.size());
// Create HTTP downloader
std::unique_ptr<HTTPDownloader> http = HTTPDownloader::Create(Host::GetHTTPUserAgent());
std::unique_ptr<HTTPDownloader> http = HTTPDownloader::Create(Host::GetHTTPUserAgent(), error);
if (!http)
{
Error::SetStringView(error, "Failed to create HTTP downloader.");
return false;
}
http->SetTimeout(30.0f);
// Fetch game titles (includes badge names) from RetroAchievements
rc_api_fetch_game_titles_request_t titles_request;
titles_request.game_ids = game_ids.data();
titles_request.num_game_ids = static_cast<u32>(game_ids.size());
const rc_api_fetch_game_titles_request_t titles_request = {
.game_ids = game_ids.data(),
.num_game_ids = static_cast<u32>(game_ids.size()),
};
rc_api_request_t request;
if (rc_api_init_fetch_game_titles_request(&request, &titles_request) != RC_OK)
@@ -2114,85 +2114,45 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
return false;
}
std::vector<u8> response_data;
bool request_success = false;
HTTPDownloader::Request::Callback callback = [&response_data, &request_success](
s32 status_code, const Error&, const std::string&,
HTTPDownloader::Request::Data data) {
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
{
response_data = std::move(data);
request_success = true;
}
};
if (request.post_data)
http->CreatePostRequest(request.url, request.post_data, std::move(callback));
else
http->CreateRequest(request.url, std::move(callback));
std::optional<rc_api_fetch_game_titles_response_t> titles_response;
http->CreatePostRequest(
request.url, request.post_data,
[&titles_response, error](s32 status_code, const Error&, const std::string&, HTTPDownloader::Request::Data data) {
const rc_api_server_response_t rr = MakeRCAPIServerResponse(status_code, data);
const int parse_result = rc_api_process_fetch_game_titles_server_response(&titles_response.emplace(), &rr);
if (parse_result != RC_OK)
{
Error::SetStringFmt(error, "rc_api_process_fetch_game_titles_server_response() failed: {}",
rc_error_str(parse_result));
titles_response.reset();
}
});
rc_api_destroy_request(&request);
http->WaitForAllRequests();
if (!request_success || response_data.empty())
if (!titles_response.has_value())
return false;
const ScopedGuard response_guard(
[&titles_response]() { rc_api_destroy_fetch_game_titles_response(&titles_response.value()); });
if (titles_response->num_entries == 0)
{
Error::SetStringView(error, "Failed to fetch game info from RetroAchievements.");
Error::SetStringView(error, TRANSLATE_SV("Achievements", "No badge names returned."));
return false;
}
// Parse response
rc_api_fetch_game_titles_response_t titles_response;
rc_api_server_response_t server_response;
server_response.body = reinterpret_cast<const char*>(response_data.data());
server_response.body_length = response_data.size();
server_response.http_status_code = 200;
const int parse_result = rc_api_process_fetch_game_titles_server_response(&titles_response, &server_response);
if (parse_result != RC_OK)
// Create all download requests in parallel
u32 badges_to_download = 0;
for (u32 i = 0; i < titles_response->num_entries; i++)
{
const std::string_view response_preview(server_response.body,
std::min<size_t>(server_response.body_length, 500));
ERROR_LOG("Failed to parse game titles response ({}): {}", parse_result, response_preview);
if (titles_response.response.error_message)
Error::SetStringFmt(error, "RetroAchievements error: {}", titles_response.response.error_message);
else
Error::SetStringFmt(error, "Failed to parse API response (code {})", parse_result);
rc_api_destroy_fetch_game_titles_response(&titles_response);
return false;
}
ScopedGuard response_guard([&titles_response]() { rc_api_destroy_fetch_game_titles_response(&titles_response); });
if (titles_response.num_entries == 0)
{
progress->SetStatusText(TRANSLATE_SV("Achievements", "No icon information found."));
return true;
}
// Collect icons to download
struct PendingDownload
{
u32 game_id;
std::string image_name;
std::string local_path;
std::string url;
std::vector<u8> data;
bool success = false;
};
std::vector<PendingDownload> downloads;
downloads.reserve(titles_response.num_entries);
for (u32 i = 0; i < titles_response.num_entries; i++)
{
const rc_api_game_title_entry_t& entry = titles_response.entries[i];
const rc_api_game_title_entry_t& entry = titles_response->entries[i];
if (!entry.image_name || entry.image_name[0] == '\0')
continue;
std::string local_path = GetLocalImagePath(entry.image_name, RC_IMAGE_TYPE_GAME);
if (FileSystem::FileExists(local_path.c_str()))
std::string path = GetLocalImagePath(entry.image_name, RC_IMAGE_TYPE_GAME);
if (FileSystem::FileExists(path.c_str()))
{
// Already have this icon, just update the cache
GameList::UpdateAchievementBadgeName(entry.id, entry.image_name);
@@ -2203,50 +2163,39 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
if (url.empty())
continue;
downloads.push_back({entry.id, entry.image_name, std::move(local_path), std::move(url), {}, false});
}
if (downloads.empty())
{
progress->SetStatusText(TRANSLATE_SV("Achievements", "All icons already downloaded."));
return true;
}
// Create all download requests in parallel
progress->SetProgressRange(static_cast<u32>(downloads.size()));
progress->FormatStatusText(TRANSLATE_FS("Achievements", "Downloading {} game icons..."), downloads.size());
std::atomic<u32> completed_count{0};
for (PendingDownload& dl : downloads)
{
http->CreateRequest(dl.url, [&dl, &completed_count, progress](s32 status_code, const Error&, const std::string&,
HTTPDownloader::Request::Data data) {
badges_to_download++;
http->CreateRequest(std::move(url), [path = std::move(path), progress](s32 status_code, const Error& http_error,
const std::string&,
HTTPDownloader::Request::Data data) {
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
{
dl.data = std::move(data);
dl.success = true;
INFO_LOG("Writing badge to {}...", Path::GetFileName(path));
Error write_error;
if (!FileSystem::FileExists(path.c_str()) && !FileSystem::WriteBinaryFile(path.c_str(), data, &write_error))
{
ERROR_LOG("Failed to write badge to {}: {}", Path::GetFileName(path), write_error.GetDescription());
FileSystem::DeleteFile(path.c_str());
}
}
progress->SetProgressValue(completed_count.fetch_add(1, std::memory_order_relaxed) + 1);
else
{
ERROR_LOG("Failed to download badge: HTTP {}: {}", status_code, http_error.GetDescription());
}
progress->IncrementProgressValue();
});
}
http->WaitForAllRequests();
// Process completed downloads
u32 downloaded = 0;
for (const PendingDownload& dl : downloads)
if (badges_to_download == 0)
{
if (dl.success && !dl.data.empty())
{
if (FileSystem::WriteBinaryFile(dl.local_path.c_str(), dl.data))
{
GameList::UpdateAchievementBadgeName(dl.game_id, dl.image_name);
downloaded++;
}
}
Error::SetStringView(error, TRANSLATE_SV("Achievements", "All badges have already been downloaded."));
return false;
}
INFO_LOG("Downloaded {} game icons", downloaded);
progress->SetProgressRange(badges_to_download);
progress->FormatStatusText(TRANSLATE_FS("Achievements", "Downloading {} game badges..."), badges_to_download);
http->WaitForAllRequests();
return true;
}

View File

@@ -2165,15 +2165,21 @@ void GameListWidget::setPreferAchievementGameIcons(bool enabled)
void GameListWidget::downloadAllGameIcons()
{
QtAsyncTaskWithProgressDialog::create(
this, tr("Loading Game Icons").toStdString(), tr("Downloading game icons...").toStdString(), true, 0, 0, 0.0f,
[](ProgressCallback* progress) -> std::function<void()> {
Error error;
if (!Achievements::DownloadGameIcons(progress, &error))
WARNING_LOG("Failed to download game icons: {}", error.GetDescription());
QtAsyncTaskWithProgressDialog::create(this, TRANSLATE_STR("GameListWidget", "Download Game Badges"),
TRANSLATE_STR("GameListWidget", "Downloading game badges..."), true, 0, 0, 0.0f,
[](ProgressCallback* progress) -> std::function<void()> {
Error error;
const bool result = Achievements::DownloadGameIcons(progress, &error);
return [error = std::move(error), result]() {
if (!result)
{
g_main_window->reportError(
tr("Error"), QString::fromStdString(error.GetDescription()));
}
return []() { g_main_window->refreshGameListModel(); };
});
g_main_window->refreshGameListModel();
};
});
}
void GameListWidget::setShowCoverTitles(bool enabled)

View File

@@ -6,6 +6,7 @@
#include "qtutils.h"
#include "common/assert.h"
#include "common/log.h"
#include <QtWidgets/QDialogButtonBox>
#include <QtWidgets/QLabel>
@@ -16,6 +17,8 @@
#include "moc_qtprogresscallback.cpp"
LOG_CHANNEL(Host);
QtProgressCallback::QtProgressCallback(QObject* parent /* = nullptr */) : QObject(parent)
{
}
@@ -36,6 +39,8 @@ void QtProgressCallback::SetStatusText(const std::string_view text)
{
ProgressCallback::SetStatusText(text);
emit statusTextUpdated(QtUtils::StringViewToQString(text));
if (!text.empty())
INFO_LOG(text);
}
void QtProgressCallback::SetProgressRange(u32 range)
@@ -97,6 +102,8 @@ void QtAsyncTaskWithProgress::SetStatusText(const std::string_view text)
{
ProgressCallback::SetStatusText(text);
emit statusTextUpdated(QtUtils::StringViewToQString(text));
if (!text.empty())
INFO_LOG(text);
}
void QtAsyncTaskWithProgress::SetProgressRange(u32 range)
@@ -345,6 +352,9 @@ void QtAsyncTaskWithProgressDialog::SetStatusText(const std::string_view text)
{
CheckForDelayedShow();
}
if (!text.empty())
INFO_LOG(text);
}
void QtAsyncTaskWithProgressDialog::SetProgressRange(u32 range)