Sync.
This commit is contained in:
@@ -204,6 +204,10 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
|
||||
Tone3000Download.cpp Tone3000Download.hpp
|
||||
Tone3000Downloader.cpp Tone3000Downloader.hpp
|
||||
|
||||
CrashGuard.cpp CrashGuard.hpp
|
||||
ModTemplateGenerator.cpp ModTemplateGenerator.hpp
|
||||
WebServerMod.cpp WebServerMod.hpp
|
||||
@@ -265,6 +269,7 @@ set (PIPEDAL_SOURCES
|
||||
GovernorSettings.cpp GovernorSettings.hpp
|
||||
WebServer.cpp WebServer.hpp pch.h Uri.cpp Uri.hpp
|
||||
|
||||
|
||||
RequestHandler.hpp
|
||||
Scratch.cpp PluginHost.hpp PluginHost.cpp
|
||||
PluginType.hpp PluginType.cpp
|
||||
|
||||
+38
-17
@@ -46,6 +46,7 @@
|
||||
#include "DummyAudioDriver.hpp"
|
||||
#include "AudioFiles.hpp"
|
||||
#include "CrashGuard.hpp"
|
||||
#include "Tone3000Downloader.hpp"
|
||||
|
||||
#ifndef NO_MLOCK
|
||||
#include <sys/mman.h>
|
||||
@@ -135,10 +136,15 @@ void PiPedalModel::Close()
|
||||
std::unique_ptr<AudioHost> oldAudioHost;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
if (closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (tone3000Downloader)
|
||||
{
|
||||
tone3000Downloader->Close();
|
||||
}
|
||||
closed = true;
|
||||
|
||||
CancelAudioRetry();
|
||||
@@ -225,6 +231,38 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
int64_t PiPedalModel::DownloadModelsFromTone3000(
|
||||
int64_t clientId,
|
||||
const std::string &downloadPath,
|
||||
const std::string &tone3000Url)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
if (!tone3000Downloader)
|
||||
{
|
||||
tone3000Downloader = Tone3000Downloader::Create();
|
||||
}
|
||||
return tone3000Downloader->RequestDownload(
|
||||
downloadPath,
|
||||
tone3000Url
|
||||
);
|
||||
}
|
||||
|
||||
void PiPedalModel::CancelTone3000Download(
|
||||
int64_t clientId,
|
||||
int64_t downloadHandle
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
if (tone3000Downloader)
|
||||
{
|
||||
tone3000Downloader->CancelDownload(downloadHandle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PiPedalModel::LoadLv2PluginInfo()
|
||||
{
|
||||
// Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory.
|
||||
@@ -3289,23 +3327,6 @@ void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string
|
||||
this->FirePedalboardChanged(-1, false);
|
||||
}
|
||||
|
||||
void PiPedalModel::SetTone3000Auth(const std::string &apiKey)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
storage.SetTone3000Auth(apiKey);
|
||||
|
||||
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
|
||||
bool hasAuth = apiKey != "";
|
||||
for (auto &subscriber : t)
|
||||
{
|
||||
subscriber->OnTone3000AuthChanged(hasAuth);
|
||||
}
|
||||
}
|
||||
bool PiPedalModel::HasTone3000Auth() const
|
||||
{
|
||||
return storage.GetTone3000Auth() != "";
|
||||
}
|
||||
|
||||
std::vector<PresetIndexEntry> PiPedalModel::RequestBankPresets(int64_t bankInstanceId)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
+25
-20
@@ -51,12 +51,13 @@ namespace pipedal
|
||||
class Updater;
|
||||
class AvahiService;
|
||||
class Lv2PluginState;
|
||||
class Tone3000Downloader;
|
||||
|
||||
class IPiPedalModelSubscriber
|
||||
{
|
||||
public:
|
||||
using ptr = std::shared_ptr<IPiPedalModelSubscriber>;
|
||||
|
||||
|
||||
virtual int64_t GetClientId() = 0;
|
||||
virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
|
||||
virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
|
||||
@@ -97,8 +98,6 @@ namespace pipedal
|
||||
virtual void OnHasWifiChanged(bool hasWifi) = 0;
|
||||
virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
|
||||
virtual void Close() = 0;
|
||||
virtual void OnTone3000AuthChanged(bool value) = 0;
|
||||
|
||||
};
|
||||
|
||||
class HotspotManager;
|
||||
@@ -113,8 +112,9 @@ namespace pipedal
|
||||
using NetworkChangedListener = std::function<void(void)>;
|
||||
|
||||
private:
|
||||
PedalboardItem* GetPedalboardItemForFileProperty(const UiFileProperty& fileProperty);
|
||||
PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty);
|
||||
|
||||
std::shared_ptr<Tone3000Downloader> tone3000Downloader;
|
||||
void CancelAudioRetry();
|
||||
clock::time_point lastRestartTime = clock::time_point::min();
|
||||
int audioRestartRetries = 0;
|
||||
@@ -207,7 +207,7 @@ namespace pipedal
|
||||
void FireBanksChanged(int64_t clientId);
|
||||
void FireJackConfigurationChanged(const JackConfiguration &jackConfiguration);
|
||||
void FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State);
|
||||
void UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardItem*pedalboardItem);
|
||||
void UpdateDefaults(SnapshotValue &snapshotValue, const PedalboardItem *pedalboardItem);
|
||||
void UpdateDefaults(Snapshot *snapshot, std::unordered_map<int64_t, PedalboardItem *> &itemMap);
|
||||
void UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered_map<int64_t, PedalboardItem *> &itemMap);
|
||||
void UpdateDefaults(Pedalboard *pedalboard);
|
||||
@@ -232,6 +232,7 @@ namespace pipedal
|
||||
IPiPedalModelSubscriber *GetNotificationSubscriber(int64_t clientId);
|
||||
std::atomic<bool> closed = false;
|
||||
bool SyncLv2State();
|
||||
|
||||
private: // IAudioHostCallbacks
|
||||
virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override;
|
||||
virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override;
|
||||
@@ -241,7 +242,7 @@ namespace pipedal
|
||||
virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) override;
|
||||
virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) override;
|
||||
virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override;
|
||||
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override;
|
||||
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex, int64_t snapshotRequestId) override;
|
||||
virtual void OnAlsaDriverTerminatedAbnormally() override;
|
||||
virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override;
|
||||
virtual void OnAlsaSequencerDeviceRemoved(int client) override;
|
||||
@@ -267,8 +268,7 @@ namespace pipedal
|
||||
PiPedalConfiguration configuration;
|
||||
|
||||
void CheckForResourceInitialization(Pedalboard &pedalboard);
|
||||
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri);
|
||||
|
||||
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri);
|
||||
|
||||
public:
|
||||
PiPedalModel();
|
||||
@@ -280,7 +280,7 @@ namespace pipedal
|
||||
Decrease
|
||||
};
|
||||
|
||||
Storage&GetStorage() { return storage; }
|
||||
Storage &GetStorage() { return storage; }
|
||||
bool GetHasWifi();
|
||||
|
||||
void NextBank(Direction direction = Direction::Increase);
|
||||
@@ -288,6 +288,15 @@ namespace pipedal
|
||||
void NextPreset(Direction direction = Direction::Increase);
|
||||
void PreviousPreset() { NextPreset(Direction::Decrease); }
|
||||
|
||||
int64_t DownloadModelsFromTone3000(
|
||||
int64_t clientId,
|
||||
const std::string &downloadPath,
|
||||
const std::string &tone3000Url);
|
||||
|
||||
void CancelTone3000Download(
|
||||
int64_t clientId,
|
||||
int64_t downloadHandle
|
||||
);
|
||||
void RequestShutdown(bool restart);
|
||||
|
||||
virtual PostHandle Post(PostCallback &&fn);
|
||||
@@ -323,7 +332,7 @@ namespace pipedal
|
||||
void SetRestartListener(std::function<void(void)> &&listener);
|
||||
void OnLv2PluginsChanged();
|
||||
void SetOnboarding(bool value);
|
||||
std::map<std::string,std::string> GetWifiRegulatoryDomains();
|
||||
std::map<std::string, std::string> GetWifiRegulatoryDomains();
|
||||
|
||||
void UpdateDnsSd();
|
||||
|
||||
@@ -336,7 +345,7 @@ namespace pipedal
|
||||
|
||||
const PluginHost &GetPluginHost() const { return pluginHost; }
|
||||
PluginHost &GetPluginHost() { return pluginHost; }
|
||||
|
||||
|
||||
Pedalboard GetCurrentPedalboardCopy()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex);
|
||||
@@ -381,7 +390,6 @@ namespace pipedal
|
||||
int64_t SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId = -1);
|
||||
int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name);
|
||||
|
||||
|
||||
void LoadPreset(int64_t clientId, int64_t instanceId);
|
||||
bool UpdatePresets(int64_t clientId, const PresetIndex &presets);
|
||||
void UpdatePluginPresets(const PluginUiPresets &pluginPresets);
|
||||
@@ -472,8 +480,8 @@ namespace pipedal
|
||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
||||
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
|
||||
std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty);
|
||||
std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty,bool overwrite);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath);
|
||||
std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty, bool overwrite);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::string &selectedPath);
|
||||
|
||||
bool IsInUploadsDirectory(const std::string &path);
|
||||
|
||||
@@ -483,14 +491,11 @@ namespace pipedal
|
||||
bool LoadCurrentPedalboard();
|
||||
|
||||
void MoveAudioFile(
|
||||
const std::string & path,
|
||||
int32_t from,
|
||||
const std::string &path,
|
||||
int32_t from,
|
||||
int32_t to);
|
||||
|
||||
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string&iconColor);
|
||||
|
||||
void SetTone3000Auth(const std::string &apiKey);
|
||||
bool HasTone3000Auth() const;
|
||||
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string &iconColor);
|
||||
|
||||
void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId);
|
||||
|
||||
|
||||
+30
-8
@@ -78,6 +78,21 @@ JSON_MAP_BEGIN(RequestBankPresetsBody)
|
||||
JSON_MAP_REFERENCE(RequestBankPresetsBody, bankInstanceId)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
class DownloadTone3000Body {
|
||||
public:
|
||||
int64_t handle_;
|
||||
std::string tone3000DownloadUrl_;
|
||||
|
||||
DECLARE_JSON_MAP(DownloadTone3000Body);
|
||||
};
|
||||
|
||||
JSON_MAP_BEGIN(DownloadTone3000Body)
|
||||
JSON_MAP_REFERENCE(DownloadTone3000Body, handle)
|
||||
JSON_MAP_REFERENCE(DownloadTone3000Body, tone3000DownloadUrl)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
class PathPatchPropertyChangedBody
|
||||
{
|
||||
public:
|
||||
@@ -1494,10 +1509,6 @@ public:
|
||||
pReader->read(&instanceId);
|
||||
uint64_t result = model.DeleteBank(this->clientId, instanceId);
|
||||
this->Reply(replyTo, "deleteBankItem", result);
|
||||
} else if (message == "getHasTone3000Auth")
|
||||
{
|
||||
bool result = model.HasTone3000Auth();
|
||||
this->Reply(replyTo, "getHasTone3000Auth", result);
|
||||
}
|
||||
else if (message == "renameBank")
|
||||
{
|
||||
@@ -1848,6 +1859,21 @@ public:
|
||||
auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_);
|
||||
this->Reply(replyTo,"copyPresetsToBank",result);
|
||||
}
|
||||
else if (message == "downloadModelsFromTone3000")
|
||||
{
|
||||
struct DownloadModelsFromTone3000Body {
|
||||
std::string downloadPath_;
|
||||
std::string tone3000Url_;
|
||||
};
|
||||
DownloadModelsFromTone3000Body args;
|
||||
pReader->read(&args);
|
||||
auto result = this->model.DownloadModelsFromTone3000(
|
||||
this->clientId,
|
||||
args.downloadPath_,
|
||||
args.tone3000Url_
|
||||
);
|
||||
this->Reply(replyTo,"downloadModelsFromTone3000",result);
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::error("Unknown message received: %s", message.c_str());
|
||||
@@ -1964,10 +1990,6 @@ private:
|
||||
{
|
||||
}
|
||||
}
|
||||
virtual void OnTone3000AuthChanged(bool value)
|
||||
{
|
||||
Send("onTone3000AuthChanged", value);
|
||||
}
|
||||
|
||||
virtual void OnErrorMessage(const std::string &message)
|
||||
{
|
||||
|
||||
+12
-50
@@ -44,6 +44,7 @@
|
||||
#include "AtomConverter.hpp"
|
||||
#include "FileBrowserFilesFeature.hpp"
|
||||
#include <string.h>
|
||||
#include "util.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
@@ -375,7 +376,6 @@ void Storage::Initialize()
|
||||
LoadPluginPresetIndex();
|
||||
LoadBankIndex();
|
||||
LoadCurrentBank();
|
||||
LoadTone3000Auth();
|
||||
try
|
||||
{
|
||||
LoadChannelSelection();
|
||||
@@ -434,10 +434,6 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
|
||||
return this->dataRoot / "currentPreset.json";
|
||||
}
|
||||
|
||||
std::filesystem::path Storage::GetTone3000AuthPath() const
|
||||
{
|
||||
return this->dataRoot / "tone3000.json";
|
||||
}
|
||||
|
||||
std::filesystem::path Storage::GetChannelSelectionFileName()
|
||||
{
|
||||
@@ -2139,13 +2135,13 @@ static void AddFilesToResult(
|
||||
if (match && !name.starts_with("."))
|
||||
{
|
||||
resultFiles.push_back(
|
||||
FileEntry(path, name, false, false));
|
||||
FileEntry(path, safeFilenameToString(name), false, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dir_entry.is_directory())
|
||||
{
|
||||
resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)});
|
||||
resultFiles.push_back(FileEntry{path, safeFilenameToString(name), true, fs::is_symlink(path)});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2325,7 +2321,10 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
{
|
||||
rootModDirectory = modDirectoryInfo;
|
||||
modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath;
|
||||
result.breadcrumbs_.push_back({modDirectoryPath.string(), modDirectoryInfo->displayName});
|
||||
result.breadcrumbs_.push_back({
|
||||
modDirectoryPath.string(),
|
||||
(modDirectoryInfo->displayName)}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2335,7 +2334,9 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
if (IsSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory()))
|
||||
{
|
||||
modDirectoryPath = uploadsDirectory / fileProperty.directory();
|
||||
result.breadcrumbs_.push_back({modDirectoryPath.string(), fs::path(fileProperty.directory()).filename().string()});
|
||||
result.breadcrumbs_.push_back({
|
||||
modDirectoryPath.string(),
|
||||
safeFilenameToString(fs::path(fileProperty.directory()).filename().string())});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2360,7 +2361,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
while (iRp != rp.end())
|
||||
{
|
||||
cumulativePath /= (*iRp);
|
||||
result.breadcrumbs_.push_back({cumulativePath, *iRp});
|
||||
result.breadcrumbs_.push_back({cumulativePath, safeFilenameToString(*iRp)});
|
||||
++iRp;
|
||||
}
|
||||
}
|
||||
@@ -2450,7 +2451,7 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const
|
||||
while (iAbsolutePath != fsAbsolutePath.end())
|
||||
{
|
||||
cumulativePath /= (*iAbsolutePath);
|
||||
result.breadcrumbs_.push_back({cumulativePath.string(), iAbsolutePath->string()});
|
||||
result.breadcrumbs_.push_back({cumulativePath.string(), safeFilenameToString(iAbsolutePath->string())});
|
||||
++iAbsolutePath;
|
||||
}
|
||||
}
|
||||
@@ -2978,46 +2979,7 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex()
|
||||
return pluginPresetIndex;
|
||||
}
|
||||
|
||||
void Storage::LoadTone3000Auth()
|
||||
{
|
||||
fs::path path = GetTone3000AuthPath();
|
||||
try
|
||||
{
|
||||
if (!fs::exists(path))
|
||||
{
|
||||
this->tone3000Auth = "";
|
||||
return;
|
||||
}
|
||||
std::ifstream s(path);
|
||||
json_reader reader(s);
|
||||
reader.read(&(this->tone3000Auth));
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error("Failed to load tone3000Auth: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void Storage::SetTone3000Auth(const std::string &apiKey)
|
||||
{
|
||||
if (tone3000Auth != apiKey)
|
||||
{
|
||||
tone3000Auth = apiKey;
|
||||
|
||||
pipedal::ofstream_synced os(this->GetTone3000AuthPath());
|
||||
if (!os.is_open())
|
||||
{
|
||||
Lv2Log::error("Failed to open Tone3000 auth file for writing.");
|
||||
return;
|
||||
}
|
||||
json_writer writer(os);
|
||||
writer.write(apiKey);
|
||||
}
|
||||
}
|
||||
std::string Storage::GetTone3000Auth() const
|
||||
{
|
||||
return tone3000Auth;
|
||||
}
|
||||
|
||||
std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath)
|
||||
{
|
||||
|
||||
@@ -75,7 +75,6 @@ private:
|
||||
BankIndex bankIndex;
|
||||
BankFile currentBank;
|
||||
PluginPresetIndex pluginPresetIndex;
|
||||
std::string tone3000Auth;
|
||||
|
||||
private:
|
||||
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
|
||||
@@ -90,7 +89,6 @@ private:
|
||||
std::filesystem::path GetChannelSelectionFileName();
|
||||
std::filesystem::path GetAlsaSequencerConfigurationFileName();
|
||||
std::filesystem::path GetCurrentPresetPath() const;
|
||||
std::filesystem::path GetTone3000AuthPath() const;
|
||||
|
||||
void LoadBankIndex();
|
||||
void SaveBankIndex();
|
||||
@@ -267,9 +265,6 @@ public:
|
||||
bool overwrite = false);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath);
|
||||
|
||||
void LoadTone3000Auth();
|
||||
void SetTone3000Auth(const std::string&apiKey);
|
||||
std::string GetTone3000Auth() const;
|
||||
|
||||
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
|
||||
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
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 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 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 downloadFile(const std::string &url, const fs::path &path)
|
||||
{
|
||||
// Create temporary file for HTTP status code
|
||||
TemporaryFile httpCodeFile{WEB_TEMP_DIR};
|
||||
fs::path httpCodePath = httpCodeFile.Path();
|
||||
|
||||
// Build curl command with error detection and proper escaping
|
||||
std::ostringstream os;
|
||||
os << "/usr/bin/curl -f -s -S -L --max-time 300 --max-filesize 104857600";
|
||||
os << " -w '%{http_code}' -o ";
|
||||
os << shellEscape(path.string());
|
||||
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
|
||||
{
|
||||
throw std::runtime_error(
|
||||
SS("Server was unabled to download the file. Http error: " << httpCode << " url: " << url));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error(
|
||||
SS("Server was unable to download the file. "
|
||||
<< getCurlErrorFromExitCode(exitCode)
|
||||
<< " " << "url: " << url));
|
||||
}
|
||||
}
|
||||
|
||||
// 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 << "<";
|
||||
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 = "",
|
||||
.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",
|
||||
.url = "https://creativecommons.org/licenses/by-nd/4.0/",
|
||||
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd
|
||||
},
|
||||
{
|
||||
.key = "cc-by-nc-nd",
|
||||
.displayName = "CC BY-NC-ND",
|
||||
.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 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 target='_blank' rel='noopener noreferrer' href='"
|
||||
<< licenseInfo.url
|
||||
<< "'>" << mdSanitize(licenseInfo.displayName) << "</a>";
|
||||
}
|
||||
else
|
||||
{
|
||||
os << mdSanitize(licenseInfo.displayName);
|
||||
}
|
||||
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 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 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())
|
||||
<< " <br/>\n"
|
||||
<< " " << mdSizes(download.sizes()) << ", "
|
||||
<< mdSizes(download.sizes()) << ", "
|
||||
<< mdGear(download.gear()) << ", "
|
||||
<< mdPlatform(download.platform())
|
||||
<< "<br/>\n"
|
||||
<< " License: xxx" << mdLicense(download.license()) << "\n"
|
||||
<< " </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pipedal::tone3000::DownloadTone3000Bundle(
|
||||
const std::filesystem::path &destinationFolder,
|
||||
const std::string &downloadUrl)
|
||||
{
|
||||
TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR};
|
||||
fs::path downloadPath = downloadTemporaryFile.Path();
|
||||
|
||||
downloadFile(downloadUrl, downloadPath);
|
||||
|
||||
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 (tone3000Download.platform() != "nam")
|
||||
{
|
||||
throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")"));
|
||||
}
|
||||
fs::path bundlePath = destinationFolder / stringToSafeFilename(tone3000Download.title());
|
||||
|
||||
fs::create_directories(bundlePath);
|
||||
|
||||
for (auto &model : tone3000Download.models())
|
||||
{
|
||||
fs::path modelPath = bundlePath / SS(stringToSafeFilename(model.name()) << ".nam");
|
||||
downloadFile(model.model_url(), modelPath);
|
||||
}
|
||||
fs::path readmePath = bundlePath / "README.md";
|
||||
writeReadme(readmePath, tone3000Download);
|
||||
}
|
||||
|
||||
Tone3000DownloadResult::Tone3000DownloadResult()
|
||||
: success_(true)
|
||||
{
|
||||
}
|
||||
Tone3000DownloadResult::Tone3000DownloadResult(const std::exception &e)
|
||||
: success_(false), errorMessage_(e.what())
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "json.hpp"
|
||||
#include <filesystem>
|
||||
#include <chrono>
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
namespace tone3000
|
||||
{
|
||||
using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones)
|
||||
class Tone3000Model
|
||||
{
|
||||
private:
|
||||
int64_t id_ = -1;
|
||||
std::string name_;
|
||||
std::string model_url_;
|
||||
tone3000_time_point created_at_;
|
||||
std::string size_;
|
||||
std::string user_id_;
|
||||
|
||||
public:
|
||||
JSON_GETTER_SETTER(id)
|
||||
JSON_GETTER_SETTER_REF(name)
|
||||
JSON_GETTER_SETTER_REF(model_url)
|
||||
JSON_GETTER_SETTER_REF(created_at)
|
||||
JSON_GETTER_SETTER_REF(size)
|
||||
JSON_GETTER_SETTER_REF(user_id)
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000Model);
|
||||
};
|
||||
|
||||
class Tone3000User
|
||||
{
|
||||
private:
|
||||
std::string id_;
|
||||
std::string avatar_url_;
|
||||
std::string username_;
|
||||
std::string url_;
|
||||
|
||||
public:
|
||||
JSON_GETTER_SETTER_REF(id)
|
||||
JSON_GETTER_SETTER(username)
|
||||
JSON_GETTER_SETTER(url)
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000User);
|
||||
};
|
||||
|
||||
class Tone3000Download
|
||||
{
|
||||
private:
|
||||
int64_t id_ = -1;
|
||||
std::string user_id_;
|
||||
std::string title_;
|
||||
std::string platform_;
|
||||
std::string description_;
|
||||
tone3000_time_point created_at_;
|
||||
tone3000_time_point updated_at_;
|
||||
std::string gear_;
|
||||
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_;
|
||||
Tone3000User user_;
|
||||
std::vector<Tone3000Model> models_;
|
||||
|
||||
public:
|
||||
JSON_GETTER_SETTER(id)
|
||||
JSON_GETTER_SETTER_REF(user_id)
|
||||
JSON_GETTER_SETTER_REF(title)
|
||||
JSON_GETTER_SETTER_REF(platform)
|
||||
JSON_GETTER_SETTER_REF(description)
|
||||
JSON_GETTER_SETTER_REF(created_at)
|
||||
JSON_GETTER_SETTER_REF(updated_at)
|
||||
JSON_GETTER_SETTER_REF(gear)
|
||||
JSON_GETTER_SETTER_REF(images)
|
||||
JSON_GETTER_SETTER(is_public)
|
||||
JSON_GETTER_SETTER_REF(links)
|
||||
JSON_GETTER_SETTER(model_count)
|
||||
JSON_GETTER_SETTER(favorites_count)
|
||||
JSON_GETTER_SETTER_REF(license)
|
||||
JSON_GETTER_SETTER_REF(sizes)
|
||||
JSON_GETTER_SETTER_REF(user)
|
||||
JSON_GETTER_SETTER_REF(models)
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000Download);
|
||||
};
|
||||
|
||||
class Tone3000DownloadResult {
|
||||
private:
|
||||
bool success_;
|
||||
std::string errorMessage_;
|
||||
public:
|
||||
JSON_GETTER_SETTER(success);
|
||||
JSON_GETTER_SETTER_REF(errorMessage);
|
||||
|
||||
Tone3000DownloadResult();
|
||||
Tone3000DownloadResult(const std::exception &e);
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000DownloadResult);
|
||||
};
|
||||
|
||||
extern void DownloadTone3000Bundle
|
||||
(
|
||||
const std::filesystem::path&destinationFolder,
|
||||
const std::string &downloadUrl
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
std::shared_ptr<Tone3000Downloader> Tone3000Downloader::Create()
|
||||
{
|
||||
return std::make_shared<Tone3000DownloaderImpl>();
|
||||
}
|
||||
|
||||
Tone3000Downloader::Tone3000Downloader()
|
||||
{
|
||||
}
|
||||
|
||||
Tone3000Downloader::~Tone3000Downloader()
|
||||
{
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload(
|
||||
const std::string &path,
|
||||
const std::string &url)
|
||||
{
|
||||
handle_t handle;
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
handle = NextHandle();
|
||||
std::shared_ptr<DownloadRequest> request = std::make_shared<DownloadRequest>(false, handle, path, url);
|
||||
|
||||
this->requestQueue.push_back(request);
|
||||
request = nullptr;
|
||||
if (!this->thread)
|
||||
{
|
||||
this->thread = std::make_unique<std::jthread>([this]{ this->ThreadProc(); });
|
||||
}
|
||||
}
|
||||
this->thread_cv.notify_one();
|
||||
return handle;
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::CancelDownload(
|
||||
handle_t handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
for (auto it = requestQueue.begin(); it != requestQueue.end(); ++it)
|
||||
{
|
||||
if ((*it)->handle == handle)
|
||||
{
|
||||
requestQueue.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
this->requestQueue.resize(0);
|
||||
|
||||
DownloadProgress progress;
|
||||
fgDownloadProgress = progress;
|
||||
|
||||
if (listener)
|
||||
{
|
||||
listener->OnTone3000Progress(progress);
|
||||
}
|
||||
if (this->activeRequest != nullptr)
|
||||
{
|
||||
activeRequest->cancelled = true;
|
||||
}
|
||||
}
|
||||
if (notify) {
|
||||
thread_cv.notify_one();
|
||||
}
|
||||
}
|
||||
Tone3000DownloaderImpl::DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus() {
|
||||
DownloadProgress result;
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
result = fgDownloadProgress;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const DownloadProgress &progress)
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
if (closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->fgDownloadProgress = progress;
|
||||
if (listener)
|
||||
{
|
||||
listener->OnTone3000Progress(this->fgDownloadProgress);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::ThreadProc()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
std::shared_ptr<DownloadRequest> downloadRequest;
|
||||
{
|
||||
std::unique_lock lock { this->mutex};
|
||||
this->activeRequest = nullptr;
|
||||
|
||||
this->thread_cv.wait(lock,[this] { return this->closed || this->requestQueue.size() != 0;});
|
||||
|
||||
if (this->closed) {
|
||||
return;
|
||||
}
|
||||
if (this->requestQueue.size() != 0)
|
||||
{
|
||||
this->activeRequest = this->requestQueue[0];
|
||||
this->requestQueue.erase(requestQueue.begin());
|
||||
}
|
||||
downloadRequest = this->activeRequest;
|
||||
}
|
||||
if (!downloadRequest)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 <memory>
|
||||
#include <cstdint>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class Tone3000Downloader {
|
||||
protected:
|
||||
Tone3000Downloader();
|
||||
public:
|
||||
using handle_t = int64_t;
|
||||
static constexpr handle_t INVALID_HANDLE = (handle_t)(int64_t)-1;
|
||||
|
||||
|
||||
virtual ~Tone3000Downloader();
|
||||
|
||||
class DownloadProgress {
|
||||
public:
|
||||
private:
|
||||
handle_t handle_ = INVALID_HANDLE;
|
||||
std::string title_;
|
||||
uint64_t progress_ = 0;
|
||||
uint64_t total_ = 0;
|
||||
};
|
||||
|
||||
class Listener {
|
||||
public:
|
||||
virtual void OnStartTone3000Download
|
||||
(
|
||||
handle_t handle,
|
||||
const std::string &title
|
||||
) = 0;
|
||||
virtual void OnTone3000Progress(
|
||||
const DownloadProgress& downloadProgress
|
||||
) = 0;
|
||||
virtual void OnTone3000DownloadComplete(
|
||||
handle_t handle
|
||||
) = 0;
|
||||
|
||||
};
|
||||
|
||||
|
||||
using self = Tone3000Downloader;
|
||||
static std::shared_ptr<self> Create();
|
||||
|
||||
virtual void SetListener(Listener*listener) = 0;
|
||||
|
||||
virtual handle_t RequestDownload(
|
||||
const std::string &path,
|
||||
const std::string &url
|
||||
) = 0;
|
||||
|
||||
virtual void CancelDownload(
|
||||
handle_t handle
|
||||
) = 0;
|
||||
|
||||
virtual void Close() = 0;
|
||||
virtual DownloadProgress GetDownloadStatus() = 0;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 <memory>
|
||||
#include <cstdint>
|
||||
#include "Tone3000Downloader.hpp"
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <vector>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class Tone3000DownloaderImpl: public Tone3000Downloader {
|
||||
public:
|
||||
virtual ~Tone3000DownloaderImpl();
|
||||
|
||||
virtual void SetListener(Listener*listener) override;
|
||||
|
||||
virtual handle_t RequestDownload(
|
||||
const std::string &path,
|
||||
const std::string &url
|
||||
) override;
|
||||
|
||||
virtual void CancelDownload(
|
||||
handle_t handle
|
||||
) override;
|
||||
|
||||
virtual void Close() override;
|
||||
virtual DownloadProgress GetDownloadStatus() override;
|
||||
|
||||
private:
|
||||
Listener *listener = nullptr;
|
||||
|
||||
void ThreadProc();
|
||||
struct DownloadRequest {
|
||||
std::atomic<bool> cancelled = false;
|
||||
handle_t handle;
|
||||
const std::string downloadPath;
|
||||
const std::string downloadUrl;
|
||||
};
|
||||
|
||||
std::atomic<bool> closed = false;
|
||||
handle_t nextHandle = 1;
|
||||
handle_t NextHandle();
|
||||
|
||||
std::vector<std::shared_ptr<DownloadRequest>> requestQueue;
|
||||
|
||||
DownloadProgress fgDownloadProgress;
|
||||
void bgUpdateDownloadProgress(const DownloadProgress &progress);
|
||||
|
||||
std::shared_ptr<DownloadRequest> activeRequest;
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable thread_cv;
|
||||
|
||||
|
||||
std::unique_ptr<std::jthread> thread;
|
||||
};
|
||||
}
|
||||
+47
-19
@@ -40,6 +40,7 @@
|
||||
#include "util.hpp"
|
||||
#include "HtmlHelper.hpp"
|
||||
#include "WebServerMod.hpp"
|
||||
#include "Tone3000Download.hpp"
|
||||
|
||||
#define OLD_PRESET_EXTENSION ".piPreset"
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
@@ -145,6 +146,11 @@ public:
|
||||
return false;
|
||||
}
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "tone3000Upload")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadMediaFile")
|
||||
{
|
||||
return true;
|
||||
@@ -189,10 +195,6 @@ public:
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "Tone3000Auth")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "PluginPresets")
|
||||
{
|
||||
return true;
|
||||
@@ -418,11 +420,11 @@ public:
|
||||
return true;
|
||||
}
|
||||
auto filename = path.stem();
|
||||
if (filename == "LICENSE" || filename == "README") {
|
||||
if (filename == "LICENSE" || filename == "README")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
@@ -435,6 +437,44 @@ public:
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "tone3000Upload")
|
||||
{
|
||||
|
||||
tone3000::Tone3000DownloadResult result;
|
||||
try
|
||||
{
|
||||
fs::path downloadPath = request_uri.query("path");
|
||||
if (downloadPath.empty())
|
||||
{
|
||||
throw std::runtime_error("Invalid query parameters.");
|
||||
}
|
||||
if (!this->model->IsInUploadsDirectory(downloadPath) || HasDotDot(downloadPath))
|
||||
{
|
||||
throw std::runtime_error("Invalid path.");
|
||||
}
|
||||
std::string downloadUrl = request_uri.query("url");
|
||||
uri downloadUri{downloadUrl};
|
||||
if (!downloadUri.authority().ends_with(".tone3000.com"))
|
||||
{
|
||||
throw std::runtime_error("Invalid Tone3000 URL address.");
|
||||
}
|
||||
tone3000::DownloadTone3000Bundle(downloadPath, downloadUrl);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
result = tone3000::Tone3000DownloadResult(e);
|
||||
}
|
||||
std::ostringstream os;
|
||||
json_writer writer(os);
|
||||
writer.write(&result);
|
||||
std::string strResult = os.str();
|
||||
|
||||
res.set(HttpField::content_type,"application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setBody(strResult);
|
||||
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadMediaFile")
|
||||
{
|
||||
fs::path path = request_uri.query("path");
|
||||
@@ -765,19 +805,7 @@ public:
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "Tone3000Auth")
|
||||
{
|
||||
// https://www.tone3000.com/api/v1/auth?redirect_url=http://10.0.0.151:8080/var/Tone3000Auth&otp_only=true
|
||||
std::string apiKey = request_uri.query("api_key");
|
||||
|
||||
model->SetTone3000Auth(apiKey);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
|
||||
res.setBody("\"OK\"");
|
||||
}
|
||||
else if (segment == "uploadPluginPresets")
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
PluginPresets presets;
|
||||
fs::path filePath = req.get_body_temporary_file();
|
||||
|
||||
Reference in New Issue
Block a user