Complete except for throttling bug.

This commit is contained in:
Robin E.R. Davies
2026-02-10 19:59:05 -05:00
parent aec88fa9f1
commit 22b63a32db
42 changed files with 2597 additions and 564 deletions
+1 -1
View File
@@ -157,7 +157,7 @@ namespace pipedal
{
public:
virtual void OnNotifyLv2StateChanged(uint64_t instanceId) = 0;
virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) = 0;
virtual bool OnNotifyMaybeLv2StateChanged(uint64_t instanceId) = 0;
virtual void OnNotifyVusSubscription(const std::vector<VuUpdate> &updates) = 0;
virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0;
virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 0;
+2
View File
@@ -204,6 +204,8 @@ else()
endif()
set (PIPEDAL_SOURCES
Curl.cpp Curl.hpp
Tone3000DownloadType.cpp Tone3000DownloadType.hpp
Tone3000DownloadProgress.cpp Tone3000DownloadProgress.hpp
Tone3000Download.cpp Tone3000Download.hpp
Tone3000Downloader.cpp Tone3000Downloader.hpp
+706
View File
@@ -0,0 +1,706 @@
/*
* 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 "Curl.hpp"
#include "ss.hpp"
#include "TemporaryFile.hpp"
#include <filesystem>
#include "SysExec.hpp"
#include <stdexcept>
#include "util.hpp"
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <algorithm>
#include "Finally.hpp"
#include "Lv2Log.hpp"
static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
static bool enableLogging = true;
namespace pipedal
{
static void logHeaders(const std::vector<std::string>&headers)
{
if (enableLogging)
{
std::ofstream os {"/tmp/PipedalCurl.log"};
for (const auto&header: headers)
{
os << header << "\n";
}
}
}
static std::string getCurlExitCodeString(int exitCode)
{
// Translate documented curl exit codes to strings.
// https://curl.se/libcurl/c/libcurl-errors.html
switch (exitCode)
{
case 0: return "OK"; // CURLE_OK - All fine
case 1: return "Unsupported protocol.";
case 2: return "Failed to initialize.";
case 3: return "URL malformed.";
case 4: return "Feature not built-in.";
case 5: return "Couldn't resolve proxy.";
case 6: return "Couldn't resolve host.";
case 7: return "Failed to connect to host.";
case 8: return "Weird server reply.";
case 9: return "Access denied to remote resource.";
case 10: return "FTP accept failed.";
case 11: return "FTP weird PASS reply.";
case 12: return "FTP accept timeout.";
case 13: return "FTP weird PASV reply.";
case 14: return "FTP weird 227 format.";
case 15: return "FTP can't get host.";
case 16: return "HTTP/2 error.";
case 17: return "FTP couldn't set type.";
case 18: return "Partial file transfer.";
case 19: return "FTP couldn't retrieve file.";
case 21: return "FTP quote error.";
case 22: return "HTTP page not retrieved.";
case 23: return "Write error.";
case 25: return "Upload failed.";
case 26: return "Read error.";
case 27: return "Out of memory.";
case 28: return "Operation timeout.";
case 30: return "FTP PORT failed.";
case 31: return "FTP couldn't use REST.";
case 33: return "Range error.";
case 35: return "SSL connect error.";
case 36: return "Bad download resume.";
case 37: return "FILE couldn't read file.";
case 38: return "LDAP cannot bind.";
case 39: return "LDAP search failed.";
case 42: return "Aborted by callback.";
case 43: return "Bad function argument.";
case 45: return "Interface failed.";
case 47: return "Too many redirects.";
case 48: return "Unknown option.";
case 49: return "Setopt option syntax error.";
case 52: return "Got nothing from server.";
case 53: return "SSL engine not found.";
case 54: return "SSL engine set failed.";
case 55: return "Failed sending network data.";
case 56: return "Failure receiving network data.";
case 58: return "Problem with local client certificate.";
case 59: return "Couldn't use specified SSL cipher.";
case 60: return "Peer SSL certificate or SSH fingerprint verification failed.";
case 61: return "Unrecognized transfer encoding.";
case 63: return "Maximum file size exceeded.";
case 64: return "Requested FTP SSL level failed.";
case 65: return "Send failed to rewind.";
case 66: return "SSL engine initialization failed.";
case 67: return "Login denied.";
case 68: return "TFTP file not found.";
case 69: return "TFTP permission problem.";
case 70: return "Remote disk full.";
case 71: return "TFTP illegal operation.";
case 72: return "TFTP unknown transfer ID.";
case 73: return "Remote file already exists.";
case 74: return "TFTP no such user.";
case 77: return "Problem reading SSL CA cert.";
case 78: return "Remote file not found.";
case 79: return "SSH error.";
case 80: return "SSL shutdown failed.";
case 82: return "Failed to load CRL file.";
case 83: return "SSL issuer check failed.";
case 84: return "FTP PRET command failed.";
case 85: return "RTSP CSeq mismatch.";
case 86: return "RTSP session ID mismatch.";
case 87: return "Unable to parse FTP file list.";
case 88: return "Chunk callback error.";
case 89: return "No connection available.";
case 90: return "SSL pinned public key mismatch.";
case 91: return "SSL invalid certificate status.";
case 92: return "HTTP/2 stream error.";
case 93: return "Recursive API call.";
case 94: return "Authentication error.";
case 95: return "HTTP/3 error.";
case 96: return "QUIC connection error.";
case 97: return "Proxy handshake error.";
case 98: return "SSL client certificate required.";
case 99: return "Unrecoverable poll error.";
case 100: return "Value or data field too large.";
case 101: return "ECH failed.";
default:
return SS("Failed with exit code " << exitCode << ".");
}
}
static void throwCurlExitCode(int exitCode)
{
if (exitCode == 0) return;
std::string message = getCurlExitCodeString(exitCode);
throw std::runtime_error(SS("Server download failed. " << message));
}
static int checkCurlHttpResponse(const std::vector<std::string> &headers)
{
if (headers.size() == 0)
{
throw std::runtime_error("Download failed. Invalid curl response: no headers.");
}
const std::string&httpResponse = headers[0];
// verify that the string starts with HTTP/, and then parse the numeric error code in the result, throwing a std::runtime_error if it doesn't parse.
// e.g. "HTTP/2 200"
if (!httpResponse.starts_with("HTTP/"))
{
throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse));
}
size_t codeStart = httpResponse.find(' ');
if (codeStart == std::string::npos)
{
throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse));
}
++codeStart;
size_t codeEnd = httpResponse.find(' ', codeStart);
if (codeEnd == std::string::npos)
{
codeEnd = httpResponse.length();
}
int statusCode = 0;
try
{
statusCode = std::stoi(httpResponse.substr(codeStart, codeEnd - codeStart));
}
catch (const std::exception &)
{
throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse));
}
return statusCode;
}
int CurlGet(
const std::string &url,
std::vector<uint8_t> &output,
std::vector<std::string> *headersOpt
) {
TemporaryFile tempFile { WEB_TEMP_DIR};
int rc = CurlGet(url,tempFile.Path(), headersOpt);
if (rc == 200)
{
std::ifstream inputStream(tempFile.Path(), std::ios::binary);
if (!inputStream)
{
throw std::runtime_error("Failed to open download file.");
}
inputStream.seekg(0, std::ios::end);
std::streamsize size = inputStream.tellg();
inputStream.seekg(0, std::ios::beg);
output.resize(static_cast<size_t>(size));
if (size > 0)
{
inputStream.read(reinterpret_cast<char *>(output.data()), size);
}
}
return rc;
}
int CurlGet(
const std::string&url,
std::vector<std::string> &output,
std::vector<std::string>*headersOpt
)
{
TemporaryFile tempFile {WEB_TEMP_DIR};
int rc = CurlGet(url,tempFile.Path(), headersOpt);
std::ifstream inputStream(tempFile.Path(), std::ios::binary);
if (!inputStream)
{
throw std::runtime_error("Failed to open download file.");
}
output.clear();
std::string line;
while (std::getline(inputStream, line))
{
if (!line.empty() && line.back() == '\r')
{
line.pop_back();
}
output.push_back(line);
}
return rc;
}
int CurlGet(
const std::string &url,
const std::filesystem::path&outputPath,
std::vector<std::string> *headersOpt
)
{
TemporaryFile headersFile { WEB_TEMP_DIR};
std::vector<std::string> defaultHeaders;
if (headersOpt == nullptr)
{
headersOpt = &defaultHeaders;
}
bool bResult = true;
std::string args = SS("-s -L -D " << ShellEscape(headersFile.Path().c_str()) << " " << ShellEscape(url) << " -o " << ShellEscape(outputPath.c_str()) ) ;
auto curlOutput = sysExecForOutput("/usr/bin/curl", args);
if (headersOpt != nullptr)
{
std::ifstream headersStream(headersFile.Path());
if (headersStream)
{
headersOpt->clear();
std::string line;
while (std::getline(headersStream, line))
{
// Remove trailing carriage return if present
if (!line.empty() && line.back() == '\r')
{
line.pop_back();
}
if (!line.empty())
{
headersOpt->push_back(line);
}
}
}
}
logHeaders(*headersOpt);
if (curlOutput.exitCode == 512)
{
throw std::runtime_error("Invalid curl arguments.");
}
if (curlOutput.exitCode != EXIT_SUCCESS && curlOutput.exitCode != 22 * 256)
{
if (WIFEXITED(curlOutput.exitCode))
{
auto exitCode = WEXITSTATUS(curlOutput.exitCode);
throwCurlExitCode(exitCode);
return -99;
}
else if (WIFSIGNALED(curlOutput.exitCode))
{
throw std::runtime_error("No internet access.");
}
else
{
throw std::runtime_error(SS("Unable to exec curl. (exit status = " << curlOutput.exitCode << ")"));
}
}
int errorCode = checkCurlHttpResponse(*headersOpt);
return errorCode;
}
static int curlExec(
const std::string&commandPath, // path to executable.
const std::vector<std::string> &arguments, // commandline arguments.
const std::function<bool (const std::string &string)> &onStdoutLine,
std::string&stdErrOutput
)
{
// Unexpected errors throw std::runtime_error.
// create linux socket pairs for stdout and stderr.
int stdoutPipe[2] = {-1,-1};
int stderrPipe[2] = {-1,-1};
Finally ffPipes {
[&stdoutPipe, &stderrPipe]()
{
if (stdoutPipe[0] != -1) close(stdoutPipe[0]);
if (stdoutPipe[1] != -1) close(stdoutPipe[1]);
if (stderrPipe[0] != -1) close(stdoutPipe[0]);
if (stderrPipe[1] != -1) close(stdoutPipe[1]);
}
};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, stdoutPipe) == -1)
{
throw std::runtime_error("Failed to create stdout socket pair");
}
if (socketpair(AF_UNIX, SOCK_STREAM, 0, stderrPipe) == -1)
{
throw std::runtime_error("Failed to create stderr socket pair");
}
// exec the command with the supplied arguments
pid_t pid = fork();
if (pid == -1)
{
throw std::runtime_error("Failed to fork process");
}
if (pid == 0)
{
// Child process
close(stdoutPipe[0]); // Close read end
close(stderrPipe[0]); // Close read end
// Redirect stdout and stderr
dup2(stdoutPipe[1], STDOUT_FILENO);
dup2(stderrPipe[1], STDERR_FILENO);
close(stdoutPipe[1]);
close(stderrPipe[1]);
// stdin to /dev/null.
int devNull = open("/dev/null", O_RDONLY);
if (devNull == -1)
{
std::cerr << "Failed to open /dev/null" << std::endl;
exit(EXIT_FAILURE);
}
if (dup2(devNull, STDIN_FILENO) == -1)
{
std::cerr << "Failed to redirect stdin" << std::endl;
close(devNull);
exit(EXIT_FAILURE);
}
close(devNull);
// Build argv array
std::vector<char*> argv;
argv.push_back(const_cast<char*>(commandPath.c_str()));
for (const auto& arg : arguments)
{
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
execv(commandPath.c_str(), argv.data());
// If execv returns, it failed
std::cerr << "Failed to execute: " << commandPath << std::endl;
exit(EXIT_FAILURE);
}
// Parent process
close(stdoutPipe[1]); // Close write end
stdoutPipe[1] = -1;
close(stderrPipe[1]); // Close write end
stderrPipe[1] = -1;
// Read from stderr and stdout.
// stderr output gets appended to the stdErrOutput argument.
// stdin output gets read line by line, and onStdOutLine is called for each line of text that's read.
// Set non-blocking mode for both pipes
fcntl(stdoutPipe[0], F_SETFL, O_NONBLOCK);
fcntl(stderrPipe[0], F_SETFL, O_NONBLOCK);
std::string stdoutBuffer;
std::string stderrBuffer;
bool stdoutOpen = true;
bool stderrOpen = true;
while (stdoutOpen || stderrOpen)
{
fd_set readfds;
FD_ZERO(&readfds);
int maxfd = -1;
if (stdoutOpen)
{
FD_SET(stdoutPipe[0], &readfds);
maxfd = std::max(maxfd, stdoutPipe[0]);
}
if (stderrOpen)
{
FD_SET(stderrPipe[0], &readfds);
maxfd = std::max(maxfd, stderrPipe[0]);
}
struct timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int ret = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
if (ret > 0)
{
// Read from stdout
if (stdoutOpen && FD_ISSET(stdoutPipe[0], &readfds))
{
char buffer[4096];
ssize_t n = read(stdoutPipe[0], buffer, sizeof(buffer));
if (n > 0)
{
stdoutBuffer.append(buffer, n);
// Process complete lines
size_t pos;
while ((pos = stdoutBuffer.find('\n')) != std::string::npos)
{
std::string line = stdoutBuffer.substr(0, pos);
if (!line.empty() && line.back() == '\r')
{
line.pop_back();
}
if (!onStdoutLine(line))
{
// Kill the child process and return immediately
// do NOT waitpid, as that will be done in the processing loop.
kill(pid, SIGTERM);
}
stdoutBuffer.erase(0, pos + 1);
}
}
else if (n == 0)
{
stdoutOpen = false;
close(stdoutPipe[0]);
// Process any remaining data
if (!stdoutBuffer.empty())
{
if (!stdoutBuffer.empty() && stdoutBuffer.back() == '\r')
{
stdoutBuffer.pop_back();
}
if (!stdoutBuffer.empty())
{
if (!onStdoutLine(stdoutBuffer))
{
// abort the child process
kill(pid, SIGINT); // Curl: SIGINT= graceful shutdown. SIGTERM=abrupt shutdown.
// but wait for output conforming the cancellation
// i.e. don't waitpid -- that will be done on cleanup, and don't abort the loop.
}
}
}
}
}
// Read from stderr
if (stderrOpen && FD_ISSET(stderrPipe[0], &readfds))
{
char buffer[4096];
ssize_t n = read(stderrPipe[0], buffer, sizeof(buffer));
if (n > 0)
{
stdErrOutput.append(buffer, n);
}
else if (n == 0)
{
stderrOpen = false;
close(stderrPipe[0]);
}
}
}
}
// Return the procesess's exit code.
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
{
auto exitCode = WEXITSTATUS(status);
if (exitCode == EXIT_SUCCESS) {
return EXIT_SUCCESS;
}
throwCurlExitCode(exitCode);
return exitCode;
}
else if (WIFSIGNALED(status))
{
return -1;
}
return -2;
}
int CurlGet(
const std::vector<CurlDownloadRequest> &request,
const std::function<void(size_t completed, size_t total)> &progressCallback,
std::vector<std::string>*headersOpt
)
{
if (request.empty())
{
return 200;
}
size_t currentDownload = 0;
size_t totalDownloads = request.size();
int lastStatusCode = 200;
// Create temporary file for curl config
TemporaryFile configFile{WEB_TEMP_DIR};
// Write curl config file with URL and output pairs
{
std::ofstream configStream(configFile.Path());
if (!configStream)
{
throw std::runtime_error("Failed to create curl config file");
}
for (const auto& req : request)
{
configStream << "url = \"" << req.url << "\"\n";
configStream << "output = \"" << req.outputFile.string() << "\"\n";
}
}
// Build curl arguments
std::vector<std::string> curlArgs;
curlArgs.push_back("-s"); // Silent mode
curlArgs.push_back("-L"); // Follow redirects
// curlArgs.push_back("-w"); // Write format
// curlArgs.push_back("URL: %{url_effective}\\n");
curlArgs.push_back("--parallel-max");
curlArgs.push_back("1");
curlArgs.push_back("-D"); // Dump headers to stdout
curlArgs.push_back("-"); // Write headers to stdout
curlArgs.push_back("--no-progress-meter"); // Suppress progress
curlArgs.push_back("--retry-delay");
curlArgs.push_back("2");
curlArgs.push_back("--retry-max-time");
curlArgs.push_back("6");
curlArgs.push_back("-K");
curlArgs.push_back(configFile.Path().string());
curlArgs.push_back("--fail-early"); // quit as soon as one transfer fails.
std::string stderrOutput;
std::string savedHttpHeader;
// Process stdout lines
auto onStdoutLine = [&](const std::string& line) -> bool {
if (headersOpt != nullptr)
{
headersOpt->push_back(line);
}
// Empty line indicates start of new download or end of headers
if (line.empty())
{
if (!savedHttpHeader.empty())
{
std::string lastHeader = savedHttpHeader;
savedHttpHeader = "";
// Parse status code
size_t codeStart = lastHeader.find(' ');
if (codeStart != std::string::npos)
{
codeStart++; // Skip the space
size_t codeEnd = lastHeader.find(' ', codeStart);
if (codeEnd == std::string::npos)
{
codeEnd = lastHeader.length();
}
std::string codeStr = lastHeader.substr(codeStart, codeEnd - codeStart);
try
{
int statusCode = std::stoi(codeStr);
// Handle different status codes
if (statusCode == 200)
{
// Successful download
currentDownload++;
lastStatusCode = 200;
if (progressCallback)
{
progressCallback(currentDownload, totalDownloads);
}
}
else if (statusCode == 429)
{
// Retry - curl will handle it, just ignore
return true;
}
else if (statusCode == 503)
{
// Throttling - save position and expect exit
lastStatusCode = 503;
Lv2Log::warning("%s","curl: Received 503 response.");
return false;
}
else if (statusCode >= 300 && statusCode < 400)
{
// Redirect, &c - advisory only, ignore, and hope curl will deal with it.
return true;
}
else if (statusCode >= 400)
{
// Error - fatal
lastStatusCode = statusCode;
return false; // Stop processing
}
}
catch (const std::exception&)
{
// Failed to parse status code - continue
return true;
}
}
}
} else if (line.starts_with("HTTP/"))
{
// Delay processing until we get a blank line.
savedHttpHeader = line;
}
// Other lines are headers - ignore unless we need to save them
return true;
};
// Execute curl
int exitCode = curlExec("/usr/bin/curl", curlArgs, onStdoutLine, stderrOutput);
logHeaders(*headersOpt);
if (exitCode != EXIT_SUCCESS && exitCode != -1)
{
throwCurlExitCode(exitCode);
}
// Return appropriate status code
return lastStatusCode;
}
}
+60
View File
@@ -0,0 +1,60 @@
/*
* 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.
*/
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <filesystem>
#include <functional>
namespace pipedal {
extern int CurlGet(
const std::string &url,
const std::filesystem::path&outputFile,
std::vector<std::string> *headersOpt = nullptr
);
extern int CurlGet(
const std::string &url,
std::vector<uint8_t> &output,
std::vector<std::string> *headersOpt = nullptr
);
extern int CurlGet(
const std::string&url,
std::vector<std::string> &output,
std::vector<std::string> *headersOpt = nullptr
);
struct CurlDownloadRequest{
std::string url;
std::filesystem::path outputFile;
};
extern int CurlGet(
const std::vector<CurlDownloadRequest> &request,
const std::function<void(size_t completed, size_t total)> &progressCallback,
std::vector<std::string>*headersOpt
);
}
+12 -3
View File
@@ -234,6 +234,7 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration)
int64_t PiPedalModel::DownloadModelsFromTone3000(
int64_t clientId,
Tone3000DownloadType downloadType,
const std::string &downloadPath,
const std::string &tone3000Url)
{
@@ -261,6 +262,7 @@ int64_t PiPedalModel::DownloadModelsFromTone3000(
tone3000Downloader->SetListener(this);
}
return tone3000Downloader->RequestDownload(
downloadType,
downloadPath,
tone3000Url
);
@@ -508,11 +510,10 @@ void PiPedalModel::OnNotifyLv2StateChanged(uint64_t instanceId)
{
// a sent PATCH_Set, or an explicit state changed notification.
OnNotifyMaybeLv2StateChanged(instanceId);
this->SetPresetChanged(-1, true, false);
}
// The plugin notified us that a path path property changed. The state *purrobably changed.
void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
bool PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
{
// one or more received PATCH_Sets, which MAY change the state.
std::lock_guard<std::recursive_mutex> lock(mutex);
@@ -521,7 +522,7 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
{
if (!audioHost)
{
return;
return false;
}
bool changed = this->audioHost->UpdatePluginState(*item);
if (changed)
@@ -532,8 +533,11 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
Lv2PluginState newState = item->lv2State();
FireLv2StateChanged(instanceId, newState);
this->SetPresetChanged(-1, true, false);
return true;
}
}
return false;
}
void PiPedalModel::SetInputVolume(float value)
@@ -3403,3 +3407,8 @@ int64_t PiPedalModel::CopyPresetsToBank(int64_t bankInstanceId, const std::vecto
return lastAdded;
}
std::string PiPedalModel::Tone3000ThumbnailDirectory()
{
return "/var/pipedal/tone3000_thumbnails";
}
+9 -1
View File
@@ -125,6 +125,7 @@ namespace pipedal
virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override;
virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override;
std::shared_ptr<Tone3000Downloader> tone3000Downloader;
void CancelAudioRetry();
clock::time_point lastRestartTime = clock::time_point::min();
@@ -246,7 +247,7 @@ namespace pipedal
private: // IAudioHostCallbacks
virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override;
virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override;
virtual bool OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override;
virtual void OnNotifyVusSubscription(const std::vector<VuUpdate> &updates) override;
virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) override;
virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) override;
@@ -291,7 +292,13 @@ namespace pipedal
Decrease
};
Storage &GetStorage() { return storage; }
virtual std::string Tone3000ThumbnailDirectory() override;
bool GetHasWifi();
void NextBank(Direction direction = Direction::Increase);
@@ -301,6 +308,7 @@ namespace pipedal
int64_t DownloadModelsFromTone3000(
int64_t clientId,
Tone3000DownloadType downloadType,
const std::string &downloadPath,
const std::string &tone3000Url);
+29
View File
@@ -19,6 +19,7 @@
#include "pch.h"
#include "Curl.hpp"
#include "PiPedalSocket.hpp"
#include "Updater.hpp"
#include "json.hpp"
@@ -51,11 +52,13 @@ class DownloadModelsFromTone3000Body {
public:
std::string downloadPath_;
std::string tone3000Url_;
std::string downloadType_;
DECLARE_JSON_MAP(DownloadModelsFromTone3000Body);
};
JSON_MAP_BEGIN(DownloadModelsFromTone3000Body)
JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadPath)
JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, tone3000Url)
JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadType)
JSON_MAP_END()
@@ -693,6 +696,8 @@ private:
int64_t subscriptionHandle;
int64_t instanceId;
};
bool PingTone3000Server();
std::vector<VuSubscription> activeVuSubscriptions;
struct PortMonitorSubscription
@@ -1904,6 +1909,7 @@ public:
pReader->read(&args);
auto result = this->model.DownloadModelsFromTone3000(
this->clientId,
StringToTone3000DownloadType(args.downloadType_),
args.downloadPath_,
args.tone3000Url_
);
@@ -1913,6 +1919,13 @@ public:
int64_t handle = -1;
pReader->read(&handle);
model.CancelTone3000Download(clientId,handle);
} else if (message == "pingTone3000Server")
{
std::shared_ptr<PiPedalSocketHandler> this_ = shared_from_this();
model.Post([this_, replyTo] () {
bool result = this_->PingTone3000Server();
this_->Reply(replyTo,"pingTone3000Server",result);
});
}
else
{
@@ -2451,3 +2464,19 @@ std::shared_ptr<ISocketFactory> pipedal::MakePiPedalSocketFactory(PiPedalModel &
{
return std::make_shared<PiPedalSocketFactory>(model);
}
bool PiPedalSocketHandler::PingTone3000Server()
{
constexpr const char* TONE3000_PING_URL = "https://www.tone3000.com/robots.txt";
std::vector<std::string> output;
std::vector<std::string> headers;
try {
int result = CurlGet(TONE3000_PING_URL, output, &headers);
return result == 200;
} catch(const std::exception&e)
{
Lv2Log::error("PingTone3000Server: %s",e.what());
return false;
}
}
+5 -5
View File
@@ -2135,13 +2135,13 @@ static void AddFilesToResult(
if (match && !name.starts_with("."))
{
resultFiles.push_back(
FileEntry(path, safeFilenameToString(name), false, false));
FileEntry(path, SafeFilenameToString(name), false, false));
}
}
}
else if (dir_entry.is_directory())
{
resultFiles.push_back(FileEntry{path, safeFilenameToString(name), true, fs::is_symlink(path)});
resultFiles.push_back(FileEntry{path, SafeFilenameToString(name), true, fs::is_symlink(path)});
}
}
}
@@ -2336,7 +2336,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
modDirectoryPath = uploadsDirectory / fileProperty.directory();
result.breadcrumbs_.push_back({
modDirectoryPath.string(),
safeFilenameToString(fs::path(fileProperty.directory()).filename().string())});
SafeFilenameToString(fs::path(fileProperty.directory()).filename().string())});
}
else
{
@@ -2361,7 +2361,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
while (iRp != rp.end())
{
cumulativePath /= (*iRp);
result.breadcrumbs_.push_back({cumulativePath, safeFilenameToString(*iRp)});
result.breadcrumbs_.push_back({cumulativePath, SafeFilenameToString(*iRp)});
++iRp;
}
}
@@ -2451,7 +2451,7 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const
while (iAbsolutePath != fsAbsolutePath.end())
{
cumulativePath /= (*iAbsolutePath);
result.breadcrumbs_.push_back({cumulativePath.string(), safeFilenameToString(iAbsolutePath->string())});
result.breadcrumbs_.push_back({cumulativePath.string(), SafeFilenameToString(iAbsolutePath->string())});
++iAbsolutePath;
}
}
+275 -158
View File
@@ -30,6 +30,8 @@
#include "Tone3000DownloadProgress.hpp"
#include <thread>
#include "HtmlHelper.hpp"
#include "Curl.hpp"
#include "PiPedalModel.hpp"
using namespace pipedal;
using namespace pipedal::tone3000;
@@ -77,28 +79,7 @@ JSON_MAP_REFERENCE(Tone3000DownloadResult, errorMessage)
JSON_MAP_END()
static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
static bool enableLogging = true;
static std::string shellEscape(const std::string &input)
{
// Escape string for safe use in shell commands by using single quotes
// and escaping any single quotes in the input
std::string result = "'";
for (char c : input)
{
if (c == '\'')
{
// End quote, add escaped single quote, start quote again
result += "'\\''";
}
else
{
result += c;
}
}
result += "'";
return result;
}
static const std::filesystem::path TONE3000_THUMBNAIL_PATH{"/var/pipedal/tone3000_thumbnails"};
static std::string getCurlErrorFromExitCode(int exitCode)
{
@@ -139,99 +120,6 @@ static void cancellableSleep(std::chrono::steady_clock::duration duration, std::
}
}
static void downloadFile(
const std::string &url,
const fs::path &path,
std::function<bool()> &isCancelled)
{
// Create temporary file for HTTP status code
TemporaryFile httpCodeFile{WEB_TEMP_DIR};
fs::path httpCodePath = httpCodeFile.Path();
TemporaryFile headersFile{WEB_TEMP_DIR};
fs::path headersFilePath = headersFile.Path();
for (size_t retry = 0; retry < 10; ++retry)
{
// Build curl command with error detection and proper escaping
std::ostringstream os;
os << "/usr/bin/curl -f -s -S -L --max-time 60 --retry 5 --retry-delay 1 --retry-max-time 10 --max-filesize 104857600";
os << " -w '%{http_code}' -o ";
os << shellEscape(path.string());
os << " -D " << shellEscape(headersFilePath);
os << " ";
os << shellEscape(url);
os << " > ";
os << shellEscape(httpCodePath.string());
std::string command = os.str();
// Execute curl command
int exitCode = std::system(command.c_str());
// Read HTTP status code
int httpCode = 0;
if (fs::exists(httpCodePath))
{
std::ifstream httpCodeStream(httpCodePath);
std::string httpCodeStr;
std::getline(httpCodeStream, httpCodeStr);
try
{
httpCode = std::stoi(httpCodeStr);
}
catch (...)
{
// If parsing fails, keep httpCode as 0
}
}
if (exitCode != 0)
{
if (exitCode == 22 * 256) // HTTP error
{
std::ifstream headersStream(headersFilePath);
std::string headerStr;
while (std::getline(headersStream, headerStr))
{
std::cout << headerStr << std::endl;
}
if (httpCode == 429 || httpCode == 403 || httpCode == 503)
{
cancellableSleep(std::chrono::milliseconds(2000), isCancelled);
if (isCancelled())
{
return;
}
continue;
}
throw std::runtime_error(
SS("Server was unabled to download the file. Http error: "
<< HtmlHelper::httpErrorString(httpCode)));
}
else
{
throw std::runtime_error(
SS("Server was unable to download the file. "
<< getCurlErrorFromExitCode(exitCode)
<< " " << "url: " << url));
}
}
else
{
break;
}
}
// Verify file was downloaded
if (!fs::exists(path) || fs::file_size(path) == 0)
{
throw std::runtime_error("Downloaded file is empty or missing");
}
}
static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "")
{
std::ostringstream os;
@@ -254,6 +142,80 @@ static std::string mdSanitize(const std::string &str, const std::string &illegal
return os.str();
}
#ifdef JUNK
static std::string mdDataUrl(const std::string &mimeType, const std::filesystem::path &filePath)
{
// maximum 512k!
size_t fileSize = fs::file_size(filePath);
if (fileSize > 512UL * 1024UL * 1024UL)
{
return "";
}
std::ifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
return "";
}
static const char kBase64Alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string result;
std::ostringstream os;
os << "data:" << mimeType << ";base64,";
result = os.str();
size_t encodedDataSize = ((fileSize + 2) / 3) * 4;
result.reserve(result.length() + encodedDataSize);
constexpr size_t kBufferSize = 32 * 1024;
std::vector<char> buffer;
buffer.resize(kBufferSize);
size_t offset = 0;
while (offset < fileSize)
{
size_t thisTime = std::min(kBufferSize, fileSize - offset);
file.read(buffer.data(), thisTime);
size_t i = 0;
while (i + 2 < thisTime)
{
unsigned int triple = (buffer[i] << 16) | (buffer[i + 1] << 8) | buffer[i + 2];
result.push_back(kBase64Alphabet[(triple >> 18) & 0x3F]);
result.push_back(kBase64Alphabet[(triple >> 12) & 0x3F]);
result.push_back(kBase64Alphabet[(triple >> 6) & 0x3F]);
result.push_back(kBase64Alphabet[triple & 0x3F]);
i += 3;
}
if (i < thisTime)
{
unsigned int triple = buffer[i] << 16;
result.push_back(kBase64Alphabet[(triple >> 18) & 0x3F]);
if (i + 1 < thisTime)
{
triple |= buffer[i + 1] << 8;
result.push_back(kBase64Alphabet[(triple >> 12) & 0x3F]);
result.push_back(kBase64Alphabet[(triple >> 6) & 0x3F]);
result.push_back('=');
}
else
{
result.push_back(kBase64Alphabet[(triple >> 12) & 0x3F]);
result.push_back('=');
result.push_back('=');
}
}
offset += thisTime;
}
return result;
}
#endif
static std::string mdDate(const tone3000_time_point &date_)
{
std::ostringstream os;
@@ -376,7 +338,7 @@ namespace
static std::vector<LicenseInfo> licenses{
{.key = "t3k",
.displayName = "T3K",
.displayName = "T3K",
.url = "licenses/t3k_license.html",
.licenseFlags = LicenseFlags::None},
{.key = "cc-by",
@@ -485,7 +447,7 @@ static std::string mdHref(const std::string &url)
{
std::ostringstream os;
os << '\'';
for (char c: url)
for (char c : url)
{
if (c == '\'')
{
@@ -498,7 +460,7 @@ static std::string mdHref(const std::string &url)
}
static std::string mdLicense(const std::string &license)
{
auto ff = licenseEnum.find(license);
LicenseInfo licenseInfo;
if (ff != licenseEnum.end())
@@ -551,8 +513,7 @@ static std::map<std::string, std::string> platformEnumValues =
{"ir", "I/R"},
{"aida-x", "Aida X"},
{"aa-snapshot", "aa-snapshot"},
{"proteus", "Proteus"}
};
{"proteus", "Proteus"}};
static std::string mdPlatform(const std::string &platform)
{
return mdEnum(platform, platformEnumValues);
@@ -569,37 +530,30 @@ static std::string mdUser(const tone3000::Tone3000User &user)
// clang-format on
}
static void mdLink(std::ostream &f, const std::string &label, const std::string &url)
{
f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")";
}
static void mdImage(std::ostream &f, const std::string url)
{
f << "![" << mdSanitize("Plugin Thumbname", "]") << "](" << mdSanitize(url, "]") << "#tone3000_thumbnail" << ")";
f << "\n\n";
}
static void writeReadme(const fs::path &path, const Tone3000Download &download)
static void writeReadme(const fs::path &path, const std::string &thumbnailUrl, const Tone3000Download &download)
{
std::ofstream of{path};
if (!of.is_open())
{
throw std::runtime_error(SS("Unable to create file " << path));
}
std::string imageLink;
if (download.images().size() > 0)
{
imageLink = download.images()[0];
}
// clang-format off
of <<
"<div id='tone3000_float' >\n"
<< " <img id='tone3000_thumbnail' src='"
<< imageLink << "' alt='' />\n"
<< " <div id='tone3000_float_content' >\n"
"<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"
@@ -607,10 +561,22 @@ static void writeReadme(const fs::path &path, const Tone3000Download &download)
<< " "
<< mdUser(download.user())
<< " <br/>\n"
<< " " << mdSizes(download.sizes()) << ", "
<< mdGear(download.gear()) << ", "
<< mdPlatform(download.platform())
<< "<br/>\n";
<< " ";
if (download.sizes().has_value()) {
of
<< mdSizes(download.sizes().value()) << ", "
;
}
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";
}
@@ -636,6 +602,72 @@ static void writeReadme(const fs::path &path, const Tone3000Download &download)
}
}
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 "";
}
std::string pipedal::tone3000::DownloadTone3000Bundle(
const std::filesystem::path &destinationFolder,
const std::string &downloadUrl,
@@ -645,13 +677,18 @@ std::string pipedal::tone3000::DownloadTone3000Bundle(
{
TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR};
fs::path downloadPath = downloadTemporaryFile.Path();
std::vector<std::string> headers;
std::string resultDirectory;
Tone3000DownloadProgress progress;
progress.handle(handle);
progressCallback(progress);
downloadFile(downloadUrl, downloadPath, isCancelled);
int httpResult = CurlGet(downloadUrl, downloadPath, &headers);
if (httpResult != 200)
{
throw std::runtime_error(SS("Download failed: HTTP " << HtmlHelper::httpErrorString(httpResult)));
}
if (isCancelled())
{
@@ -677,40 +714,120 @@ std::string pipedal::tone3000::DownloadTone3000Bundle(
progress.total(tone3000Download.models().size());
progressCallback(progress);
if (tone3000Download.platform() != "nam")
if (tone3000Download.platform() != "nam" && tone3000Download.platform() != "ir")
{
throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")"));
}
fs::path bundlePath = destinationFolder / stringToSafeFilename(tone3000Download.title());
fs::path bundlePath = destinationFolder / StringToSafeFilename(tone3000Download.title());
resultDirectory = bundlePath;
fs::create_directories(bundlePath);
uint64_t nModel = 0;
std::vector<CurlDownloadRequest> requests;
for (auto &model : tone3000Download.models())
{
if (isCancelled())
fs::path modelPath;
if (tone3000Download.platform() == "ir")
{
return resultDirectory;
modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".wav");
}
fs::path modelPath = bundlePath / SS(stringToSafeFilename(model.name()) << ".nam");
downloadFile(model.model_url(), modelPath, isCancelled);
if (isCancelled())
else
{
return resultDirectory;
modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".nam");
}
progress.progress(++nModel);
progressCallback(progress);
requests.push_back(CurlDownloadRequest{url : model.model_url(), outputFile : modelPath});
}
if (isCancelled())
try
{
return resultDirectory;
int result = CurlGet(
requests,
[&](size_t completed, size_t total)
{
if (isCancelled())
{
return false;
}
progress.progress(completed);
progress.total(total);
progressCallback(progress);
return true;
},
&headers);
if (isCancelled())
{
CleanUpFailedDownload(bundlePath, requests);
return "";
}
if (result != 200)
{
CleanUpFailedDownload(bundlePath, requests);
throw std::runtime_error(SS("Server download failed. HTTP Error " << HtmlHelper::httpErrorString(result)));
}
fs::path readmePath = bundlePath / "README.md";
std::string thumbnailUrl;
if (tone3000Download.images().has_value() && tone3000Download.images().value().size() != 0)
{
TemporaryFile imageFile{TONE3000_THUMBNAIL_PATH};
std::string imageUrl = tone3000Download.images().value()[0];
std::vector<std::string> imageHeaders;
try
{
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(tone3000Download.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=" << tone3000Download.id());
// thumbnailUrl = mdDataUrl("image/jpeg", finalPath);
}
}
}
catch (const std::exception &)
{
// ignore.
}
}
writeReadme(readmePath, thumbnailUrl, tone3000Download);
}
catch (const std::exception &e)
{
CleanUpFailedDownload(bundlePath, requests);
if (isCancelled())
{
return "";
}
throw;
}
fs::path readmePath = bundlePath / "README.md";
writeReadme(readmePath, tone3000Download);
return resultDirectory;
}
Tone3000DownloadResult::Tone3000DownloadResult()
+711
View File
@@ -0,0 +1,711 @@
/*
* 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 "ss.hpp"
#include "Tone3000Download.hpp"
#include "TemporaryFile.hpp"
#include <array>
#include <ctime>
#include "util.hpp"
#include "Tone3000DownloadProgress.hpp"
#include <thread>
#include "HtmlHelper.hpp"
using namespace pipedal;
using namespace pipedal::tone3000;
namespace fs = std::filesystem;
JSON_MAP_BEGIN(Tone3000Model)
JSON_MAP_REFERENCE(Tone3000Model, id)
JSON_MAP_REFERENCE(Tone3000Model, name)
JSON_MAP_REFERENCE(Tone3000Model, model_url)
JSON_MAP_REFERENCE(Tone3000Model, created_at)
JSON_MAP_REFERENCE(Tone3000Model, size)
JSON_MAP_REFERENCE(Tone3000Model, user_id)
JSON_MAP_END()
JSON_MAP_BEGIN(Tone3000User)
JSON_MAP_REFERENCE(Tone3000User, id)
JSON_MAP_REFERENCE(Tone3000User, username)
JSON_MAP_REFERENCE(Tone3000User, avatar_url)
JSON_MAP_REFERENCE(Tone3000User, url)
JSON_MAP_END()
JSON_MAP_BEGIN(Tone3000Download)
JSON_MAP_REFERENCE(Tone3000Download, id)
JSON_MAP_REFERENCE(Tone3000Download, user_id)
JSON_MAP_REFERENCE(Tone3000Download, title)
JSON_MAP_REFERENCE(Tone3000Download, description)
JSON_MAP_REFERENCE(Tone3000Download, created_at)
JSON_MAP_REFERENCE(Tone3000Download, updated_at)
JSON_MAP_REFERENCE(Tone3000Download, platform)
JSON_MAP_REFERENCE(Tone3000Download, gear)
JSON_MAP_REFERENCE(Tone3000Download, images)
JSON_MAP_REFERENCE(Tone3000Download, is_public)
JSON_MAP_REFERENCE(Tone3000Download, links)
JSON_MAP_REFERENCE(Tone3000Download, model_count)
JSON_MAP_REFERENCE(Tone3000Download, favorites_count)
JSON_MAP_REFERENCE(Tone3000Download, license)
JSON_MAP_REFERENCE(Tone3000Download, sizes)
JSON_MAP_REFERENCE(Tone3000Download, user)
JSON_MAP_REFERENCE(Tone3000Download, models)
JSON_MAP_END()
JSON_MAP_BEGIN(Tone3000DownloadResult)
JSON_MAP_REFERENCE(Tone3000DownloadResult, success)
JSON_MAP_REFERENCE(Tone3000DownloadResult, errorMessage)
JSON_MAP_END()
static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
static bool enableLogging = true;
static std::string getCurlErrorFromExitCode(int exitCode)
{
switch (exitCode)
{
case 3 * 256: // malformed URL
return "Invalid URL.";
case 6 * 256: // could not resolve hostname
return "Could not resolve hostname. Check that the PiPedal server is connected to the Internet.";
case 7 * 256: // failed to connect to host
return "Failed to connect to host.";
case 22 * 256: // HTTP error
return "HTTP error downloading file.";
case 28 * 256: // timeout
return "Download timeout.";
case 63 * 256: // file too large
return "File too large.";
default:
return SS("Failed to download file (exit code: " << (exitCode / 256) << ")");
}
}
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 void downloadFile(
const std::string &url,
const fs::path &path,
std::function<bool()> &isCancelled)
{
// Create temporary file for HTTP status code
TemporaryFile httpCodeFile{WEB_TEMP_DIR};
fs::path httpCodePath = httpCodeFile.Path();
TemporaryFile headersFile{WEB_TEMP_DIR};
fs::path headersFilePath = headersFile.Path();
for (size_t retry = 0; retry < 10; ++retry)
{
// Build curl command with error detection and proper escaping
std::ostringstream os;
os << "/usr/bin/curl -f -s -S -L --max-time 60 --retry 5 --retry-delay 1 --retry-max-time 10 --max-filesize 104857600";
os << " -w '%{http_code}' -o ";
os << ShellEscape(path.string());
os << " -D " << ShellEscape(headersFilePath);
os << " ";
os << ShellEscape(url);
os << " > ";
os << ShellEscape(httpCodePath.string());
std::string command = os.str();
// Execute curl command
int exitCode = std::system(command.c_str());
// Read HTTP status code
int httpCode = 0;
if (fs::exists(httpCodePath))
{
std::ifstream httpCodeStream(httpCodePath);
std::string httpCodeStr;
std::getline(httpCodeStream, httpCodeStr);
try
{
httpCode = std::stoi(httpCodeStr);
}
catch (...)
{
// If parsing fails, keep httpCode as 0
}
}
if (exitCode != 0)
{
if (exitCode == 22 * 256) // HTTP error
{
std::ifstream headersStream(headersFilePath);
std::string headerStr;
while (std::getline(headersStream, headerStr))
{
std::cout << headerStr << std::endl;
}
if (httpCode == 429 || httpCode == 403 || httpCode == 503)
{
cancellableSleep(std::chrono::milliseconds(2000), isCancelled);
if (isCancelled())
{
return;
}
continue;
}
throw std::runtime_error(
SS("Server was unabled to download the file. Http error: "
<< HtmlHelper::httpErrorString(httpCode)));
}
else
{
throw std::runtime_error(
SS("Server was unable to download the file. "
<< getCurlErrorFromExitCode(exitCode)
<< " " << "url: " << url));
}
}
else
{
break;
}
}
// Verify file was downloaded
if (!fs::exists(path) || fs::file_size(path) == 0)
{
throw std::runtime_error("Downloaded file is empty or missing");
}
}
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 tone3000::Tone3000User &user)
{
// clang-format off
std::ostringstream os;
os <<
"<a target='_blank' rel='noopener noreferrer' href='"
<< mdSanitize(user.url(),"'")
<< "'>" << mdSanitize(user.username()) << "</a>";
return os.str();
// clang-format on
}
static void mdLink(std::ostream &f, const std::string &label, const std::string &url)
{
f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")";
}
static void mdImage(std::ostream &f, const std::string url)
{
f << "![" << mdSanitize("Plugin Thumbname", "]") << "](" << mdSanitize(url, "]") << "#tone3000_thumbnail" << ")";
f << "\n\n";
}
static void writeReadme(const fs::path &path, const Tone3000Download &download)
{
std::ofstream of{path};
if (!of.is_open())
{
throw std::runtime_error(SS("Unable to create file " << path));
}
std::string imageLink;
if (download.images().size() > 0)
{
imageLink = download.images()[0];
}
// clang-format off
of <<
"<div id='tone3000_float' >\n"
<< " <img id='tone3000_thumbnail' src='"
<< imageLink << "' alt='' />\n"
<< " <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());
if (download.sizes().has_value()) {
of
<< " <br/>\n"
<< " " << mdSizes(download.sizes().value()) << ", "
;
}
of
<< mdGear(download.gear()) << ", "
<< mdPlatform(download.platform())
<< "<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;
}
}
}
std::string pipedal::tone3000::DownloadTone3000Bundle(
const std::filesystem::path &destinationFolder,
const std::string &downloadUrl,
int64_t handle,
std::function<void(const Tone3000DownloadProgress &progress)> progressCallback,
std::function<bool()> isCancelled)
{
TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR};
fs::path downloadPath = downloadTemporaryFile.Path();
std::string resultDirectory;
Tone3000DownloadProgress progress;
progress.handle(handle);
progressCallback(progress);
downloadFile(downloadUrl, downloadPath, isCancelled);
if (isCancelled())
{
return resultDirectory;
}
std::ifstream f{downloadPath};
if (!f.is_open())
{
throw std::runtime_error(SS("Can't open file " << downloadPath));
}
json_reader reader(f);
Tone3000Download tone3000Download;
reader.read(&tone3000Download);
if (isCancelled())
{
return resultDirectory;
}
progress.title(tone3000Download.title());
progress.total(tone3000Download.models().size());
progressCallback(progress);
if (tone3000Download.platform() != "nam" && tone3000Download.platform() != "ir")
{
throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")"));
}
fs::path bundlePath = destinationFolder / StringToSafeFilename(tone3000Download.title());
resultDirectory = bundlePath;
fs::create_directories(bundlePath);
uint64_t nModel = 0;
for (auto &model : tone3000Download.models())
{
if (isCancelled())
{
return resultDirectory;
}
fs::path modelPath;
if (tone3000Download.platform() == "ir")
{
modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".wav");
} else {
modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".nam");
}
downloadFile(model.model_url(), modelPath, isCancelled);
if (isCancelled())
{
return resultDirectory;
}
progress.progress(++nModel);
progressCallback(progress);
}
if (isCancelled())
{
return resultDirectory;
}
fs::path readmePath = bundlePath / "README.md";
writeReadme(readmePath, tone3000Download);
return resultDirectory;
}
Tone3000DownloadResult::Tone3000DownloadResult()
: success_(true)
{
}
Tone3000DownloadResult::Tone3000DownloadResult(const std::exception &e)
: success_(false), errorMessage_(e.what())
{
}
+5 -3
View File
@@ -31,9 +31,11 @@
#include <chrono>
#include <functional>
#include "Tone3000DownloadProgress.hpp"
#include <memory>
namespace pipedal
{
namespace tone3000
{
using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones)
@@ -44,7 +46,7 @@ namespace pipedal
std::string name_;
std::string model_url_;
tone3000_time_point created_at_;
std::string size_;
std::optional<std::string> size_;
std::string user_id_;
public:
@@ -85,13 +87,13 @@ namespace pipedal
tone3000_time_point created_at_;
tone3000_time_point updated_at_;
std::string gear_;
std::vector<std::string> images_;
std::optional<std::vector<std::string>> images_;
bool is_public_ = true;
std::vector<std::string> links_;
int64_t model_count_ = 0;
int64_t favorites_count_ = 0;
std::string license_;
std::vector<std::string> sizes_;
std::optional<std::vector<std::string>> sizes_;
Tone3000User user_;
std::vector<Tone3000Model> models_;
+59
View File
@@ -0,0 +1,59 @@
/*
* 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 "Tone3000DownloadType.hpp"
#include <stdexcept>
#include <unordered_map>
namespace pipedal {
// Must agree with Tone3000DownloadType.tsx
static std::unordered_map<std::string,Tone3000DownloadType> downloadTypeEnums =
{
{"nam", Tone3000DownloadType::Nam},
{"cabir", Tone3000DownloadType::CabIr},
};
Tone3000DownloadType StringToTone3000DownloadType(const std::string &value)
{
auto ff = downloadTypeEnums.find(value);
if (ff == downloadTypeEnums.end())
{
throw std::runtime_error("Invalid argument.");
}
return ff->second;
}
std::string Tone3000DownloadTypeToString(Tone3000DownloadType value)
{
switch (value)
{
case Tone3000DownloadType::Nam:
return "nam";
case Tone3000DownloadType::CabIr:
return "cabir";
default:
throw std::runtime_error("Inalid argument.");
}
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* 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.
*/
#pragma once
#include <string>
namespace pipedal {
enum class Tone3000DownloadType {
Nam,
CabIr
};
Tone3000DownloadType StringToTone3000DownloadType(const std::string &value);
std::string Tone3000DownloadTypeToString(Tone3000DownloadType value);
}
+15 -6
View File
@@ -60,6 +60,7 @@ Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::NextHandle()
return result;
}
Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload(
Tone3000DownloadType downloadType,
const std::string &path,
const std::string &url)
{
@@ -67,7 +68,8 @@ Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload(
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
handle = NextHandle();
std::shared_ptr<DownloadRequest> request = std::make_shared<DownloadRequest>(false, handle, path, url);
std::shared_ptr<DownloadRequest> request =
std::make_shared<DownloadRequest>(downloadType, false, handle, path, url);
this->requestQueue.push_back(request);
request = nullptr;
@@ -144,6 +146,7 @@ Tone3000DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus()
void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress)
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
if (closed)
{
@@ -160,6 +163,7 @@ void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const Tone3000DownloadProg
}
std::string Tone3000DownloaderImpl::PerformDownload(
Tone3000DownloadType downloadType,
const std::string &downloadPath,
const std::string &downloadUrl,
int64_t downloadHandle,
@@ -199,7 +203,7 @@ std::string Tone3000DownloaderImpl::PerformDownload(
if (isCancelled())
{
// maybe there's a partial result.
return downloadedDirectory;
return downloadedDirectory;
}
return downloadedDirectory;
}
@@ -249,14 +253,19 @@ void Tone3000DownloaderImpl::ThreadProc()
try
{
std::string outputPath = PerformDownload(request->downloadPath, request->downloadUrl, request->handle, isCancelled);
std::string outputPath = PerformDownload(
request->downloadType,
request->downloadPath,
request->downloadUrl,
request->handle,
isCancelled);
// Notify completion if not cancelled
// Notify completion
{
std::lock_guard lockGuard{mutex};
if (!request->cancelled.load() && currentListener)
if (currentListener)
{
currentListener->OnTone3000DownloadComplete(request->handle,outputPath);
currentListener->OnTone3000DownloadComplete(request->handle, outputPath);
}
}
}
+4
View File
@@ -26,6 +26,7 @@
#include <memory>
#include <cstdint>
#include "Tone3000DownloadProgress.hpp"
#include "Tone3000DownloadType.hpp"
namespace pipedal {
@@ -57,6 +58,8 @@ namespace pipedal {
const std::string &errorMessage
) = 0;
virtual std::string Tone3000ThumbnailDirectory() = 0;
};
@@ -66,6 +69,7 @@ namespace pipedal {
virtual void SetListener(Listener*listener) = 0;
virtual handle_t RequestDownload(
Tone3000DownloadType downloadType,
const std::string &path,
const std::string &url
) = 0;
+3
View File
@@ -41,6 +41,7 @@ namespace pipedal {
virtual void SetListener(Listener*listener) override;
virtual handle_t RequestDownload(
Tone3000DownloadType downloadType,
const std::string &path,
const std::string &url
) override;
@@ -59,6 +60,7 @@ namespace pipedal {
// Performs the actual download with cancellation support
std::string PerformDownload(
Tone3000DownloadType downloadType,
const std::string &downloadPath,
const std::string &downloadUrl,
int64_t downloadHandle,
@@ -66,6 +68,7 @@ namespace pipedal {
);
struct DownloadRequest {
Tone3000DownloadType downloadType;
std::atomic<bool> cancelled = false;
handle_t handle;
const std::string downloadPath;
+74 -1
View File
@@ -127,6 +127,31 @@ private:
std::vector<std::string> extensions;
};
static fs::path GetTone3000ThumbnailFile(const fs::path &thumbnailDirectory, const std::string & id) {
std::string stem = id;
// find a file in thumbnailDirectory which has a filename of {stem}.*.
if (!fs::exists(thumbnailDirectory) || !fs::is_directory(thumbnailDirectory))
{
return {};
}
for (const auto &entry : fs::directory_iterator(thumbnailDirectory))
{
if (!entry.is_regular_file())
{
continue;
}
const auto &path = entry.path();
if (path.stem() == stem)
{
return path;
}
}
return {};
}
class DownloadIntercept : public RequestHandler
{
PiPedalModel *model;
@@ -146,6 +171,11 @@ public:
}
std::string segment = request_uri.segment(1);
if (segment == "tone3000_thumbnail")
{
return true;
}
if (segment == "downloadMediaFile")
{
return true;
@@ -298,6 +328,30 @@ public:
try
{
std::string segment = request_uri.segment(1);
if (segment == "tone3000_thumbnail")
{
std::string id = request_uri.query("id");
if (id == "")
{
throw PiPedalException("Invalid request.");
}
fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id);
if (!fs::exists(path))
{
throw PiPedalException("File not found.");
}
auto mimeType = GetMimeType(path);
if (mimeType.empty())
{
throw PiPedalException("Can't download files of this type.");
}
res.set(HttpField::content_type, mimeType);
size_t contentLength = std::filesystem::file_size(path);
res.setContentLength(contentLength);
return;
}
if (segment == "downloadMediaFile")
{
fs::path path = request_uri.query("path");
@@ -432,7 +486,26 @@ public:
{
std::string segment = request_uri.segment(1);
if (segment == "downloadMediaFile")
if (segment == "tone3000_thumbnail")
{
std::string id = request_uri.query("id");
fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id);
if (!fs::exists(path))
{
throw PiPedalException("File not found.");
}
auto mimeType = GetMimeType(path);
if (mimeType.empty())
{
throw PiPedalException("Can't download files of this type.");
}
res.set(HttpField::content_type, mimeType);
size_t contentLength = std::filesystem::file_size(path);
res.setContentLength(contentLength);
res.setBodyFile(path, false);
return;
} else if (segment == "downloadMediaFile")
{
fs::path path = request_uri.query("path");