Files
op-pedal/src/Tone3000Downloader.cpp
T
2026-05-16 06:26:53 -04:00

1333 lines
39 KiB
C++

/*
* Copyright (c) 2026 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "Tone3000DownloaderImpl.hpp"
#include "Tone3000DownloadProgress.hpp"
#include "Tone3000Download.hpp"
#include "Uri.hpp"
#include <functional>
#include "HtmlHelper.hpp"
#include "Curl.hpp"
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <charconv>
#include <limits>
#include <type_traits>
#include <array>
#include <filesystem>
#include <fstream>
#include "TemporaryFile.hpp"
#include "HtmlHelper.hpp"
using namespace pipedal;
namespace fs = std::filesystem;
// Pulls in the define for T3kConfig.h
#define PIPEDAL_T3K_PUBLISHABLE_KEY "t3k_pub_bHrH8btdwXXTtxz5ryEU8sNLF-2TGRT9"
namespace {
std::string Base64UrlEncode(const unsigned char* data, size_t size)
{
static constexpr char base64Table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string result;
result.reserve(((size + 2) / 3) * 4);
for (size_t i = 0; i + 3 <= size; i += 3)
{
uint32_t value = (static_cast<uint32_t>(data[i]) << 16)
| (static_cast<uint32_t>(data[i + 1]) << 8)
| static_cast<uint32_t>(data[i + 2]);
result.push_back(base64Table[(value >> 18) & 0x3F]);
result.push_back(base64Table[(value >> 12) & 0x3F]);
result.push_back(base64Table[(value >> 6) & 0x3F]);
result.push_back(base64Table[value & 0x3F]);
}
size_t remainder = size % 3;
if (remainder == 1)
{
uint32_t value = static_cast<uint32_t>(data[size - 1]) << 16;
result.push_back(base64Table[(value >> 18) & 0x3F]);
result.push_back(base64Table[(value >> 12) & 0x3F]);
result.push_back('=');
result.push_back('=');
}
else if (remainder == 2)
{
uint32_t value = (static_cast<uint32_t>(data[size - 2]) << 16)
| (static_cast<uint32_t>(data[size - 1]) << 8);
result.push_back(base64Table[(value >> 18) & 0x3F]);
result.push_back(base64Table[(value >> 12) & 0x3F]);
result.push_back(base64Table[(value >> 6) & 0x3F]);
result.push_back('=');
}
for (char &ch : result)
{
if (ch == '+')
{
ch = '-';
}
else if (ch == '/')
{
ch = '_';
}
}
while (!result.empty() && result.back() == '=')
{
result.pop_back();
}
return result;
}
std::string RandomBase64Url(size_t size)
{
std::string bytes(size, '\0');
if (RAND_bytes(reinterpret_cast<unsigned char*>(bytes.data()), static_cast<int>(bytes.size())) != 1)
{
throw std::runtime_error("RAND_bytes failed.");
}
return Base64UrlEncode(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size());
}
}
std::string pipedal::Sha256Base64Url(const std::string &text)
{
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
size_t digestLength = 0;
if (EVP_Q_digest(
nullptr,
"SHA256",
nullptr,
reinterpret_cast<const unsigned char *>(text.data()),
text.size(),
digest.data(),
&digestLength)
!= 1)
{
throw std::runtime_error("EVP_Q_digest(SHA256) failed.");
}
return Base64UrlEncode(digest.data(), digestLength);
}
static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
static const std::filesystem::path TONE3000_THUMBNAIL_PATH{"/var/pipedal/tone3000_thumbnails"};
class Tone3000CancelledError : std::exception
{
public:
Tone3000CancelledError() = default;
virtual ~Tone3000CancelledError() = default;
virtual const char *what() const noexcept override
{
std::exception::what();
return "Cancelled.";
}
};
std::shared_ptr<Tone3000Downloader> Tone3000Downloader::Create()
{
return std::make_shared<Tone3000DownloaderImpl>();
}
Tone3000Downloader::Tone3000Downloader()
{
}
Tone3000Downloader::~Tone3000Downloader()
{
}
Tone3000DownloaderImpl::Tone3000DownloaderImpl()
{
}
Tone3000DownloaderImpl::~Tone3000DownloaderImpl()
{
Close();
this->thread = nullptr;
}
void Tone3000DownloaderImpl::SetListener(Listener *listener)
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
this->listener = listener;
}
Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::NextHandle()
{
handle_t result = this->nextHandle++;
return result;
}
void Tone3000DownloaderImpl::CancelDownload(
handle_t handle)
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
requestQueue.Erase([handle](std::shared_ptr<DownloadRequest> entry)
{ return entry->handle = handle; });
if (activeRequest)
{
if (activeRequest->handle == handle)
{
activeRequest->cancelled = true;
}
}
}
void Tone3000DownloaderImpl::Close()
{
bool notify = false;
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
if (!closed)
{
closed = true;
notify = true;
}
Tone3000DownloadProgress progress;
fgDownloadProgress = progress;
if (listener)
{
listener->OnTone3000Progress(progress);
}
if (this->activeRequest != nullptr)
{
activeRequest->cancelled = true;
}
}
this->requestQueue.CloseAndClear();
}
Tone3000DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus()
{
Tone3000DownloadProgress result;
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
result = fgDownloadProgress;
}
return result;
}
void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress)
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
if (closed)
{
return;
}
if (activeRequest != nullptr && !activeRequest->cancelled)
{
this->fgDownloadProgress = progress;
if (listener)
{
listener->OnTone3000Progress(this->fgDownloadProgress);
}
}
}
static void ValidateTone3000Url(const std::string &url)
{
uri downloadUri{url};
if (!downloadUri.authority().ends_with(".tone3000.com"))
{
throw std::runtime_error("Invalid Tone3000 URL address.");
}
}
namespace
{
template <typename T>
static T ParseIntegerOrThrow(const std::string &text, const char *fieldName)
{
static_assert(std::is_integral_v<T>, "ParseIntegerOrThrow requires an integral type.");
T value{};
const char *begin = text.data();
const char *end = begin + text.size();
auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec == std::errc::invalid_argument || ptr != end)
{
throw std::runtime_error(SS("Tone3000 Auth: invalid " << fieldName << "."));
}
if (ec == std::errc::result_out_of_range)
{
throw std::runtime_error(SS("Tone3000 Auth: " << fieldName << " is out of range."));
}
return value;
}
}
void Tone3000DownloaderImpl::ThreadProc()
{
while (true)
{
Listener *currentListener = nullptr;
Tone3000DownloadProgress progress;
auto request = requestQueue.Take();
if (request == nullptr)
{
return;
}
{
std::unique_lock<std::mutex> lock{this->mutex};
activeRequest = request;
currentListener = listener;
}
// Notify download started
if (currentListener)
{
currentListener->OnStartTone3000Download(request->handle, "");
}
// Create cancellation predicate that checks the atomic flag
auto isCancelled = [request]() -> bool
{
return request->cancelled.load();
};
try
{
this->DownloadTone3000ToneBg(
request);
// std::string outputPath;
// Tone3000DownloadProgress progress;
// progress.total(request->tone3000DownloadRequest.models().size());
// progress.handle(request->handle);
// this->bgUpdateDownloadProgress(progress);
// outputPath = PerformFileDownloads(
// request->tone3000DownloadRequest,
// progress,
// [this](Tone3000DownloadProgress &progress)
// { bgUpdateDownloadProgress(progress); },
// isCancelled);
// // Notify completion
// {
// std::lock_guard lockGuard{mutex};
// if (currentListener)
// {
// currentListener->OnTone3000DownloadComplete(request->handle, outputPath);
// }
// }
}
catch (const std::exception &e)
{
{
std::lock_guard lockGuard{mutex};
// Notify error
if (!request->cancelled.load() && currentListener)
{
currentListener->OnTone3000DownloadError(request->handle, e.what());
}
}
}
{
std::lock_guard<std::mutex> lock{this->mutex};
activeRequest = nullptr;
}
}
}
void Tone3000DownloaderImpl::DownloadTone3000ToneBg(
std::shared_ptr<DownloadRequest> request)
{
handle_t handle = request->handle;
uri requestUri{request->requestUri};
const Tone3000PkceParams &pkceParams = request->pkceParams;
const std::string &downloadPath = request->downloadPath;
Tone3000DownloadProgress progress;
progress.handle(handle);
try
{
if (listener)
{
listener->OnStartTone3000Download(progress.handle(), "Authenticating...");
}
progress.title("Authenticating...");
bgUpdateDownloadProgress(progress);
auto oAuthResult = handleOAuthCallback(requestUri, pkceParams);
if (!oAuthResult.toneId.has_value())
{
throw std::runtime_error("Tone3000 Auth failed. tone ID not received.");
}
std::shared_ptr<Tone3000Download> download = GetTone3000Tone(oAuthResult.toneId.value());
}
catch (const Tone3000CancelledError &e)
{
OnTone3000DownloadCancelled(progress.handle());
return;
}
catch (const std::exception &e)
{
OnTone3000DownloadError(progress.handle(), e.what());
return;
}
OnTone3000DownloadComplete(progress.handle(), "");
}
Tone3000DownloaderImpl::OAuthCallbackResult Tone3000DownloaderImpl::handleOAuthCallback(
const uri &callbackUri,
const Tone3000PkceParams &pkce)
{
OAuthCallbackResult result;
try
{
std::map<std::string, std::string> params;
std::string code = callbackUri.requiredQuery("code");
std::optional<std::string> error = callbackUri.optionalQuery("error");
std::string returnedState = callbackUri.requiredQuery("state");
std::optional<std::string> toneId = callbackUri.optionalQuery("tone_id");
std::optional<std::string> modelId = callbackUri.optionalQuery("model_id");
bool canceled = callbackUri.query("canceled") == "true";
if (error.has_value())
{
throw std::runtime_error(error.value());
}
if (canceled)
{
throw Tone3000CancelledError();
}
std::string codeVerifier = pkce.codeVerifier();
// Access denied — e.g. model is private and user clicked "Back"
if (error.has_value())
{
throw std::runtime_error(error.value());
}
if (code.empty() || codeVerifier.empty())
{
throw std::runtime_error("Tone3000 Auth: missing code.");
}
std::string body = HtmlFormBuilder({{"grant_type", "authorization_code"},
{"code", code},
{"code_verifier", codeVerifier},
{"redirect_uri", pkce.redirectUrl()},
{"client_id", pkce.publishableKey()}})
.build();
auto postResult = Post(
uri("http://www.tone3000.com/api/v1/oauth/token"),
{{"Content-Type: application/x-www-form-urlencoded"}},
body);
if (!postResult.ok)
{
throw std::runtime_error(postResult.error);
}
Tone3000AuthResponse authResponse(postResult.body);
this->accessTokens = std::make_shared<Tone3000AccessTokens>(authResponse);
OAuthCallbackResult result;
if (toneId.has_value())
{
result.toneId = ParseIntegerOrThrow<int64_t>(toneId.value(), "tone_id");
}
if (modelId.has_value())
{
result.modelId = ParseIntegerOrThrow<int64_t>(modelId.value(), "model_id");
}
return result;
}
catch (const std::exception &e)
{
throw;
}
}
std::string Tone3000DownloaderImpl::GetBearerToken() const
{
if (!accessTokens)
{
throw std::runtime_error("Tone3000 auth failure. No access token.");
}
return accessTokens->access_token();
}
std::string Tone3000DownloaderImpl::Tone3000GetText(
const uri &requestedUri
)
{
TemporaryFile tempFile{WEB_TEMP_DIR};
std::vector<std::string> inputHeaders {
SS("Authorization: Bearer " << this->GetBearerToken())
};
int httpCode = CurlGet(requestedUri.str(),tempFile.Path(),nullptr,&inputHeaders);
if (httpCode != 200)
{
throw std::runtime_error(
SS("Http error " << HtmlHelper::httpErrorString(httpCode)));
}
std::string result;
std::ifstream ifs(tempFile.Path(), std::ios::binary);
if (!ifs)
{
throw std::runtime_error("Tone3000: failed to open response file.");
}
result.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
return result;
}
Tone3000DownloaderImpl::PostResult Tone3000DownloaderImpl::Post(
const uri &requestedUri,
std::vector<std::string> headers,
const std::string &body)
{
PostResult result;
try
{
int response = CurlPostStrings(
requestedUri.str(),
body,
result.body,
nullptr,
&headers
);
result.errorCode = response;
if (response != 200)
{
result.ok = false;
result.error = SS("HTPP Error " << response);
}
else
{
result.ok = true;
}
}
catch (const std::exception &e)
{
result.ok = false;
result.error = e.what();
}
return result;
}
Tone3000AuthResponse::Tone3000AuthResponse(const std::string &responseBody)
{
std::stringstream ss(responseBody);
json_reader reader(ss);
reader.read(this);
expires_at_ = clock_t::now() + std::chrono::seconds(expires_in_);
}
Tone3000AccessTokens::Tone3000AccessTokens(const Tone3000AuthResponse &authResponse)
: access_token_(authResponse.access_token()),
expires_in_(authResponse.expires_in()),
refresh_token_(authResponse.refresh_token()),
token_type_(authResponse.token_type())
{
}
Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestTone3000Download(
const uri &uri,
const Tone3000PkceParams &pkceParams,
const std::string &downloadPath,
Tone3000DownloadType downloadType)
{
handle_t handle;
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
handle = NextHandle();
std::shared_ptr<DownloadRequest> request =
std::make_shared<DownloadRequest>(handle, uri.str(), pkceParams, downloadPath, downloadType);
this->requestQueue.Put(request);
request = nullptr;
if (!this->thread)
{
this->thread = std::make_unique<std::jthread>([this]
{ this->ThreadProc(); });
}
}
return handle;
}
static void cancellableSleep(std::chrono::steady_clock::duration duration, std::function<bool()> &isCancelled)
{
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
while (true)
{
if (std::chrono::steady_clock::now() - start >= duration)
{
break;
}
if (isCancelled())
{
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "")
{
std::ostringstream os;
for (char c : str)
{
if ((unsigned char)c < 0x20)
continue;
if (c == '<')
{
os << "&lt;";
continue;
}
auto npos = illegalCharacters.find_first_of(c);
if (npos != std::string::npos)
{
continue;
}
os << c;
}
return os.str();
}
static std::string mdDate(const tone3000_time_point &date_)
{
std::ostringstream os;
// C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z"
std::string date = date_;
// Remove timezone offset if present and replace with Z
size_t plusPos = date.find('+');
if (plusPos != std::string::npos)
{
date = date.substr(0, plusPos) + "Z";
}
// Parse ISO 8601 date string into tm
std::tm tm = {};
std::istringstream ss(date);
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
if (ss.fail())
{
os << date;
return os.str();
}
os << std::put_time(&tm, "%x");
return os.str();
}
static std::string mdEnum(const std::string &value, const std::map<std::string, std::string> &enumValues)
{
auto ff = enumValues.find(value);
if (ff == enumValues.end())
{
return value;
}
return ff->second;
}
static std::string mdEnumList(
const std::vector<std::string> &values,
const std::map<std::string, std::string> &enumValues)
{
if (values.size() == 1)
{
return mdEnum(values[0], enumValues);
}
std::ostringstream os;
os << '[';
bool first = true;
for (const auto &value : values)
{
if (first)
{
first = false;
}
else
{
os << ", ";
}
os << mdEnum(value, enumValues);
}
os << ']';
return os.str();
}
static std::map<std::string, std::string> sizeEnumValues =
{
{"standard", "Standard"},
{"lite", "Lite"},
{"feather", "Feather"},
{"nano", "Nano"},
{"custom", "Custom"}};
static std::string mdSizes(const std::vector<std::string> &sizes)
{
return mdEnumList(sizes, sizeEnumValues);
}
static std::map<std::string, std::string> gearEnumValues =
{
{"amp", "Amp only"},
{"full-rig", "Full rig"},
{"pedal", "Pedal"},
{"outboard", "Outboard"},
{"ir", "I/R"}};
static std::string mdGear(const std::string &gear)
{
return mdEnum(gear, gearEnumValues);
}
namespace
{
enum class LicenseFlags
{
None = 0,
Cc = 1,
By = 2,
CcBy = 3, // = Cc | By
Sa = 4,
Nc = 8,
Nd = 16,
Cc0 = 32,
};
static bool operator&(LicenseFlags v1, LicenseFlags v2)
{
return (((int)v1) & ((int)v2)) != 0;
}
static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2)
{
return (LicenseFlags)(((int)(v1)) | ((int)(v2)));
}
struct LicenseInfo
{
std::string key;
std::string displayName;
std::string url;
LicenseFlags licenseFlags;
};
}
static std::vector<LicenseInfo> licenses{
{.key = "t3k",
.displayName = "T3K",
.url = "licenses/t3k_license.html",
.licenseFlags = LicenseFlags::None},
{.key = "cc-by",
.displayName = "CC BY 4.0",
.url = "https://creativecommons.org/licenses/by/4.0/",
.licenseFlags = LicenseFlags::CcBy},
{.key = "cc-by-sa",
.displayName = "CC BY-SA 4.0",
.url = "https://creativecommons.org/licenses/by-sa/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa},
{.key = "cc-by-nc",
.displayName = "CC BY-NC 4.0",
.url = "https://creativecommons.org/licenses/by-nc/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc},
{.key = "cc-by-nc-sa",
.displayName = "CC BY-NC-SA 4.0",
.url = "https://creativecommons.org/licenses/by-nc-sa/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa},
{.key = "cc-by-nd",
.displayName = "CC BY-ND 4.0",
.url = "https://creativecommons.org/licenses/by-nd/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd},
{.key = "cc-by-nc-nd",
.displayName = "CC BY-NC-ND 4.0",
.url = "https://creativecommons.org/licenses/by-nc-nd/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd},
{
.key = "cco",
.displayName = "CC0",
.url = "https://creativecommons.org/publicdomain/zero/1.0/",
.licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0,
},
};
static std::map<std::string, LicenseInfo> makeLicenseEnum()
{
std::map<std::string, LicenseInfo> result;
for (const auto &license : licenses)
{
result[license.key] = license;
}
return result;
}
static std::map<std::string, LicenseInfo> licenseEnum = makeLicenseEnum();
static std::string mdLicenseIcon(LicenseFlags licenseFlag)
{
std::ostringstream os;
std::string url;
switch (licenseFlag)
{
case LicenseFlags::Cc:
url = "img/cc.svg";
break;
case LicenseFlags::By:
url = "img/by.svg";
break;
case LicenseFlags::Sa:
url = "img/sa.svg";
break;
case LicenseFlags::Nc:
url = "img/nc.svg";
break;
case LicenseFlags::Nd:
url = "img/nd.svg";
break;
case LicenseFlags::Cc0:
url = "img/cc0.svg";
break;
case LicenseFlags::None:
throw std::runtime_error("Invalid argument.");
}
os << "<img id='cc_img' src='" << url << "'/>";
return os.str();
}
static void mdEscapeString(std::ostream &f, const std::string &str)
{
size_t ix = 0;
while (ix < str.size())
{
char c = str[ix++];
if (c == '\n')
{
if (ix < str.size() && str[ix] == '\n')
{
f << "\n\n";
}
else
{
f << " \n"; // a <br> in md.
}
}
else
{
f << c;
}
}
}
static std::string mdHref(const std::string &url)
{
std::ostringstream os;
os << '\'';
for (char c : url)
{
if (c == '\'')
{
continue;
}
os << c;
}
os << '\'';
return os.str();
}
static std::string mdLicense(const std::string &license)
{
auto ff = licenseEnum.find(license);
LicenseInfo licenseInfo;
if (ff != licenseEnum.end())
{
licenseInfo = ff->second;
}
else
{
licenseInfo.displayName = license;
}
std::ostringstream os;
if (licenseInfo.licenseFlags != LicenseFlags::None)
{
for (LicenseFlags licenseFlag : std::vector<LicenseFlags>{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd})
{
if (licenseInfo.licenseFlags & licenseFlag)
{
os << mdLicenseIcon(licenseFlag);
}
}
os << " ";
}
if (licenseInfo.url != "")
{
os << "<a href="
<< mdHref(licenseInfo.url)
<< " target='_blank' rel='noopener noreferrer' >" << mdSanitize(licenseInfo.displayName) << "</a>";
}
else
{
os << mdSanitize(licenseInfo.displayName);
}
return os.str();
}
// static std::string mdTestLicenses()
// {
// std::ostringstream os;
// for (auto license: licenseEnum)
// {
// os << "<br/>" << mdLicense(license.first) << "\n";
// }
// return os.str();
// }
static std::map<std::string, std::string> platformEnumValues =
{
{"nam", "NAM"},
{"ir", "I/R"},
{"aida-x", "Aida X"},
{"aa-snapshot", "aa-snapshot"},
{"proteus", "Proteus"}};
static std::string mdPlatform(const std::string &platform)
{
return mdEnum(platform, platformEnumValues);
}
static std::string mdUser(const std::string &user)
{
return mdSanitize(user);
}
static void mdLink(std::ostream &f, const std::string &label, const std::string &url)
{
f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")";
}
static void writeReadme(const fs::path &path, const std::string &thumbnailUrl, const Tone3000FileDownloadRequest &download)
{
// std::ofstream of{path};
// if (!of.is_open())
// {
// throw std::runtime_error(SS("Unable to create file " << path));
// }
// // clang-format off
// of <<
// "<div id='tone3000_float' >\n";
// if (!thumbnailUrl.empty())
// {
// of << " <img id='tone3000_thumbnail' src='"
// << thumbnailUrl << "' alt='' />\n";
// }
// of << " <div id='tone3000_float_content' >\n"
// << " <h3 id='tone3000_title'>" << mdSanitize(download.title()) << "</h3>\n"
// << " <div>\n"
// << " <p id='tone3000_subtitle'>\n"
// << mdDate(download.updated_at())
// << " "
// << mdUser(download.user())
// << " <br/>\n"
// << " ";
// if (download.sizes().size() != 0) {
// of
// << mdSizes(download.sizes()) << ", "
// ;
// }
// if (download.platform() == "ir")
// {
// of
// << (mdPlatform(download.platform()));
// } else {
// of
// << mdGear(download.gear()) << ", "
// << mdPlatform(download.platform());
// }
// of << "<br/>\n";
// if (download.license() != "") {
// of << " License: " << mdLicense(download.license()) << "\n";
// }
// of
// // << " " << mdTestLicenses()
// << " </p>\n"
// << " </div>\n"
// << " </div>\n"
// << "</div>\n";
// // clang-format on
// of << "\n\n";
// mdEscapeString(of, download.description());
// if (download.links().size() > 0)
// {
// of << std::endl;
// of << "## Links" << std::endl
// << std::endl;
// for (auto &link : download.links())
// {
// of << "- " << "[" << link << "](" << link << ")" << std::endl;
// }
// }
}
static void CleanUpFailedDownload(const std::filesystem::path &folder, const std::vector<CurlDownloadRequest> &requests)
{
std::error_code ec; // do NOT throw errors.
if (fs::exists(folder))
{
for (const auto &request : requests)
{
if (fs::exists(request.outputFile))
{
fs::remove(request.outputFile, ec); // ignoring errors.
}
}
// If there's a readme file, remove that too.
fs::path readmePath = folder / "README.md";
if (fs::exists(readmePath))
{
fs::remove(readmePath, ec); // ignoring errors
}
// if the folder directory is empty, remove the the folder too.
try
{
if (fs::is_empty(folder))
{
fs::remove(folder, ec); // ignoring errors.
}
}
catch (const std::exception &)
{
}
}
}
static std::string GetHeader(const std::string &headerName, const std::vector<std::string> &htmlHeaders)
{
std::string needle = headerName;
std::transform(needle.begin(), needle.end(), needle.begin(), [](unsigned char c)
{ return std::tolower(c); });
for (const auto &header : htmlHeaders)
{
auto pos = header.find(':');
if (pos == std::string::npos)
{
continue;
}
std::string name = header.substr(0, pos);
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c)
{ return std::tolower(c); });
if (name != needle)
{
continue;
}
std::string value = header.substr(pos + 1);
auto first = value.find_first_not_of(" \t\r\n");
if (first == std::string::npos)
{
return "";
}
auto last = value.find_last_not_of(" \t\r\n");
return value.substr(first, last - first + 1);
}
return "";
}
static bool CancellableSleep(std::chrono::steady_clock::duration duration, std::function<bool()> &isCancelled)
{
using clock_t = std::chrono::steady_clock;
auto start = clock_t::now();
while (true)
{
auto now = clock_t::now();
auto elapsed = now - start;
if (elapsed > duration)
{
return true;
}
if (isCancelled())
{
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
std::string Tone3000DownloaderImpl::DownloadTone3000Files(
const Tone3000FileDownloadRequest &request,
Tone3000DownloadProgress &progress)
{
// fs::path bundlePath = fs::path(request.downloadPath()) / StringToSafeFilename(request.title());
// fs::path resultDirectory = bundlePath;
// fs::create_directories(bundlePath);
// std::vector<CurlDownloadRequest> requests;
// auto&models = request.models();
// std::vector<std::string> headers; // yyy: make sure we have a user agent header!
// headers.push_back(
// SS("Authorization: Bearer " << request.authToken())
// );
// try
// {
// size_t downloadIndex = 0;
// while (downloadIndex < models.size())
// {
// if (isCancelled())
// {
// throw std::runtime_error("Cancelled");
// }
// size_t thisTime = models.size() - downloadIndex;
// thisTime = Tone3000Throttler::instance().ReserveDownloadSlots(thisTime);
// if (thisTime != 0)
// {
// std::vector<CurlDownloadRequest> requestsThisTime;
// for (size_t i = 0; i < thisTime; ++i)
// {
// auto &model = models[downloadIndex+i];
// requestsThisTime.push_back(
// CurlDownloadRequest{
// url: model.url(),
// outputFile: bundlePath / StringToSafeFilename(model.name())
// });
// }
// int result = CurlGet(
// requestsThisTime,
// [&](size_t completed, size_t total)
// {
// if (isCancelled())
// {
// return false;
// }
// progress.progress(completed + downloadIndex);
// progress.total(requests.size());
// progressCallback(progress);
// return true;
// },
// &headers);
// if (isCancelled())
// {
// throw std::runtime_error("Cancelled");
// }
// if (result != 200)
// {
// throw std::runtime_error(SS("Server download failed. HTTP Error " << HtmlHelper::httpErrorString(result)));
// }
// downloadIndex += thisTime;
// }
// if (thisTime <= 1)
// {
// CancellableSleep(std::chrono::milliseconds(1000), isCancelled);
// }
// }
// fs::path readmePath = bundlePath / "README.md";
// std::string thumbnailUrl;
// if (!request.imageUrl().empty())
// {
// TemporaryFile imageFile{TONE3000_THUMBNAIL_PATH};
// std::string imageUrl = request.imageUrl();
// std::vector<std::string> imageHeaders;
// try
// {
// size_t reserveCount;
// while (true)
// {
// reserveCount = Tone3000Throttler::instance().ReserveDownloadSlots(1);
// if (reserveCount != 0)
// {
// break;
// }
// std::this_thread::sleep_for(std::chrono::milliseconds(1000));
// if (isCancelled())
// {
// throw std::runtime_error("Cancelled");
// }
// }
// if (CurlGet(imageUrl, imageFile.Path(), &imageHeaders) == 200)
// {
// std::string mediaType = GetHeader("Content-Type", imageHeaders);
// std::string extension;
// if (mediaType == "image/jpeg")
// {
// extension = ".jpeg";
// }
// else if (mediaType == "image/webp")
// {
// extension = ".webp";
// }
// else if (mediaType == "image/png")
// {
// extension = ".png";
// }
// else
// {
// Lv2Log::warning("Tone3000 thumbnail has an unexpected media type of '%s'", mediaType.c_str());
// throw std::runtime_error("Invalid thumbnail media type.");
// }
// if (mediaType == "image/jpeg")
// {
// fs::path thumnbailTempPath = imageFile.Detach();
// fs::path finalPath = TONE3000_THUMBNAIL_PATH / SS(request.id() << extension);
// if (fs::exists(finalPath))
// {
// fs::remove(finalPath);
// }
// fs::create_directories(finalPath.parent_path());
// fs::rename(thumnbailTempPath, finalPath);
// thumbnailUrl = SS("var/tone3000_thumbnail?id=" << request.id());
// }
// }
// }
// catch (const std::exception &)
// {
// // ignore.
// }
// }
// writeReadme(readmePath, thumbnailUrl, request);
// }
// catch (const std::exception &e)
// {
// CleanUpFailedDownload(bundlePath, requests);
// if (isCancelled())
// {
// return "";
// }
// throw;
// }
// return resultDirectory;
throw std::runtime_error("Not implemented");
}
std::shared_ptr<Tone3000Download> Tone3000DownloaderImpl::GetTone3000Tone(
int64_t toneId)
{
std::string jsonResult = Tone3000GetText(SS("https://www.tone3000.com/api/v1/tones/" << toneId));
std::shared_ptr<Tone3000Download> tone = std::make_shared<Tone3000Download>(jsonResult);
return tone;
}
Tone3000PkceParams::Tone3000PkceParams(
const std::string &redirectUrl)
{
this->publishableKey_ = PIPEDAL_T3K_PUBLISHABLE_KEY;
this->redirectUrl_ = redirectUrl;
this->codeVerifier_ = RandomBase64Url(32);
this->codeChallenge_ = Sha256Base64Url(this->codeVerifier_);
this->state_ = RandomBase64Url(16);
}
//////////////////////////////////////////////////////////////////////////////////
JSON_MAP_BEGIN(Tone3000PkceParams)
JSON_MAP_REFERENCE(Tone3000PkceParams, publishableKey)
JSON_MAP_REFERENCE(Tone3000PkceParams, redirectUrl)
JSON_MAP_REFERENCE(Tone3000PkceParams, codeVerifier)
JSON_MAP_REFERENCE(Tone3000PkceParams, codeChallenge)
JSON_MAP_REFERENCE(Tone3000PkceParams, state)
JSON_MAP_END();
JSON_MAP_BEGIN(Tone3000ModelInfo)
JSON_MAP_REFERENCE(Tone3000ModelInfo, url)
JSON_MAP_REFERENCE(Tone3000ModelInfo, name)
JSON_MAP_END();
// JSON_MAP_BEGIN(Tone3000FileDownloadRequest)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, downloadType)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, downloadPath)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, id)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, codeVerifier)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, state)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, codeChallenge)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, authToken)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, title)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, description)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, imageUrl)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, models)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, updated_at)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, user)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, sizes)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, platform)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, gear)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, license)
// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, links)
// JSON_MAP_END();
JSON_MAP_BEGIN(Tone3000AuthResponse)
JSON_MAP_REFERENCE(Tone3000AuthResponse, access_token)
JSON_MAP_REFERENCE(Tone3000AuthResponse, expires_in)
JSON_MAP_REFERENCE(Tone3000AuthResponse, refresh_token)
JSON_MAP_REFERENCE(Tone3000AuthResponse, token_type)
JSON_MAP_REFERENCE(Tone3000AuthResponse, tone_id)
JSON_MAP_REFERENCE(Tone3000AuthResponse, model_id)
JSON_MAP_REFERENCE(Tone3000AuthResponse, canceled)
JSON_MAP_END()