diff --git a/libraries/lib-cloud-audiocom/CMakeLists.txt b/libraries/lib-cloud-audiocom/CMakeLists.txt index 6e1ca96b886d..ce3c86d205c8 100644 --- a/libraries/lib-cloud-audiocom/CMakeLists.txt +++ b/libraries/lib-cloud-audiocom/CMakeLists.txt @@ -35,6 +35,8 @@ set( SOURCES sync/LocalProjectSnapshot.h sync/MissingBlocksUploader.cpp sync/MissingBlocksUploader.h + sync/MixdownUploader.cpp + sync/MixdownUploader.h sync/ProjectCloudExtension.cpp sync/ProjectCloudExtension.h sync/RemoteProjectSnapshot.cpp @@ -54,6 +56,7 @@ set ( LIBRARIES PRIVATE lib-sqlite-helpers-interface lib-crypto-interface + lib-import-export-interface wavpack::wavpack # Required for the WavPackCompressor wxwidgets::base # Required to retrieve the OS information ) diff --git a/libraries/lib-cloud-audiocom/CloudSyncService.cpp b/libraries/lib-cloud-audiocom/CloudSyncService.cpp index 6a6261cf333d..74246fff8d7c 100644 --- a/libraries/lib-cloud-audiocom/CloudSyncService.cpp +++ b/libraries/lib-cloud-audiocom/CloudSyncService.cpp @@ -103,6 +103,23 @@ class DefaultCloudSyncUI : public sync::CloudSyncUI { return sync::DownloadConflictResolution::Remote; } + + void OnMixdownStarted() override + { + } + + void SetMixdownProgressMessage(const TranslatableString& message) override + { + } + + bool OnMixdownProgress(double progress) override + { + return true; + } + + void OnMixdownFinished() override + { + } }; // class DefaultCloudSyncUI std::mutex& GetResponsesMutex() @@ -163,7 +180,7 @@ void PerformProjectGetRequest( auto removeRequest = finally([response] { RemovePendingRequest(response); }); - const auto& body = response->readAll(); + auto body = response->readAll(); if (response->getError() != NetworkError::NoError) { @@ -698,7 +715,7 @@ void CloudSyncService::CreateSnapshot(AudacityProject& project) { auto& cloudExtension = sync::ProjectCloudExtension::Get(project); mLocalSnapshots.emplace_back(sync::LocalProjectSnapshot::Create( - GetServiceConfig(), GetOAuthService(), cloudExtension, + GetUI(), GetServiceConfig(), GetOAuthService(), cloudExtension, [this](const auto& update) { UpdateProgress(); diff --git a/libraries/lib-cloud-audiocom/sync/CloudSyncUI.h b/libraries/lib-cloud-audiocom/sync/CloudSyncUI.h index 7d019d7ff5ef..31095db09c30 100644 --- a/libraries/lib-cloud-audiocom/sync/CloudSyncUI.h +++ b/libraries/lib-cloud-audiocom/sync/CloudSyncUI.h @@ -13,6 +13,7 @@ #include class AudacityProject; +class TranslatableString; namespace BasicUI { @@ -74,6 +75,11 @@ class CLOUD_AUDIOCOM_API CloudSyncUI /* not final */ virtual DownloadConflictResolution OnDownloadConflict(const BasicUI::WindowPlacement& placement) = 0; + + virtual void OnMixdownStarted() = 0; + virtual void SetMixdownProgressMessage(const TranslatableString& message) = 0; + virtual bool OnMixdownProgress(double progress) = 0; + virtual void OnMixdownFinished() = 0; }; // class CloudSyncUI } // namespace cloud::audiocom::sync diff --git a/libraries/lib-cloud-audiocom/sync/DataUploader.cpp b/libraries/lib-cloud-audiocom/sync/DataUploader.cpp index 36113efe9968..45900ec28e1a 100644 --- a/libraries/lib-cloud-audiocom/sync/DataUploader.cpp +++ b/libraries/lib-cloud-audiocom/sync/DataUploader.cpp @@ -11,26 +11,38 @@ #include "DataUploader.h" +#include + +#include + +#include "CodeConversions.h" + #include "IResponse.h" #include "Request.h" #include "NetworkManager.h" +#include "RequestPayload.h" + #include "BasicUI.h" using namespace audacity::network_manager; namespace cloud::audiocom::sync { +using UploadData = std::variant, std::string>; + struct DataUploader::Response final { DataUploader& Uploader; UploadUrls Target; std::function Callback; + std::function ProgressCallback; int RetriesCount { 3 }; int RetriesLeft { 3 }; - std::vector Data; + std::string MimeType; + UploadData Data; std::shared_ptr NetworkResponse; bool UploadFailed { false }; @@ -38,12 +50,15 @@ struct DataUploader::Response final UploadResultCode CurrentResult { UploadResultCode::Success }; Response( - DataUploader& uploader, const UploadUrls& target, - std::vector data, std::function callback) + DataUploader& uploader, const UploadUrls& target, UploadData data, + std::string mimeType, std::function callback, + std::function progressCallback) : Uploader { uploader } , Target { target } , Callback { std::move(callback) } + , ProgressCallback { std::move(progressCallback) } , RetriesLeft { RetriesCount } + , MimeType { std::move(mimeType) } , Data { std::move(data) } { PerformUpload(); @@ -54,10 +69,23 @@ struct DataUploader::Response final Request request { Target.UploadUrl }; request.setHeader( common_headers::ContentType, - common_content_types::ApplicationXOctetStream); + MimeType); - NetworkResponse = NetworkManager::GetInstance().doPut ( - request, Data.data(), Data.size()); + if (std::holds_alternative>(Data)) + { + auto data = *std::get_if>(&Data); + + NetworkResponse = NetworkManager::GetInstance().doPut( + request, data.data(), data.size()); + } + else + { + auto filePath = *std::get_if(&Data); + + NetworkResponse = NetworkManager::GetInstance().doPut( + request, CreateRequestPayloadStream(filePath)); + + } NetworkResponse->setRequestFinishedCallback( [this](auto) @@ -67,6 +95,19 @@ struct DataUploader::Response final else OnUploadFailed(); }); + + NetworkResponse->setUploadProgressCallback( + [this](int64_t current, int64_t total) + { + if (total <= 0) + { + total = 1; + current = 0; + } + + if (!ProgressCallback(static_cast(current) / total)) + NetworkResponse->abort(); + }); } void OnUploadSucceeded() @@ -213,12 +254,50 @@ void DataUploader::CancelAll() void DataUploader::Upload( const ServiceConfig&, const UploadUrls& target, std::vector data, - std::function callback) + std::function callback, + std::function progressCallback) { + if (!callback) + callback = [](auto...) {}; + + if (!progressCallback) + progressCallback = [](auto...) { return true; }; + + auto lock = std::lock_guard { mResponseMutex }; + + mResponses.emplace_back(std::make_unique( + *this, target, std::move(data), + audacity::network_manager::common_content_types::ApplicationXOctetStream, + std::move(callback), std::move(progressCallback))); +} + +void DataUploader::Upload( + const ServiceConfig& config, const UploadUrls& target, std::string filePath, + std::function callback, + std::function progressCallback) +{ + if (!callback) + callback = [](auto...) {}; + + if (!progressCallback) + progressCallback = [](auto...) { return true; }; + + if (!wxFileExists(audacity::ToWXString(filePath))) + { + if (callback) + callback(UploadResult { + UploadResultCode::UnknownError, + audacity::ToUTF8(XO("File not found").Translation()) }); + + return; + } + auto lock = std::lock_guard { mResponseMutex }; mResponses.emplace_back(std::make_unique( - *this, target, std::move(data), std::move(callback))); + *this, target, std::move(filePath), + audacity::network_manager::common_content_types::ApplicationXOctetStream, + std::move(callback), std::move(progressCallback))); } void DataUploader::RemoveResponse(Response& response) diff --git a/libraries/lib-cloud-audiocom/sync/DataUploader.h b/libraries/lib-cloud-audiocom/sync/DataUploader.h index 8c3cec61159d..e4780d758cd6 100644 --- a/libraries/lib-cloud-audiocom/sync/DataUploader.h +++ b/libraries/lib-cloud-audiocom/sync/DataUploader.h @@ -58,7 +58,13 @@ class DataUploader final void Upload( const ServiceConfig& config, const UploadUrls& target, - std::vector data, std::function callback); + std::vector data, std::function callback, + std::function progressCallback = {}); + + void Upload( + const ServiceConfig& config, const UploadUrls& target, + std::string filePath, std::function callback, + std::function progressCallback = {}); private: struct Response; diff --git a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp index 40e489642137..5dec18bf3733 100644 --- a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp +++ b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp @@ -16,10 +16,13 @@ #include "../ServiceConfig.h" #include "../OAuthService.h" +#include "BasicUI.h" + #include "BlockHasher.h" #include "CloudProjectsDatabase.h" #include "ProjectCloudExtension.h" #include "DataUploader.h" +#include "MixdownUploader.h" #include "SampleBlock.h" #include "Sequence.h" @@ -202,10 +205,12 @@ struct LocalProjectSnapshot::ProjectBlocksLock final : private BlockHashCache }; LocalProjectSnapshot::LocalProjectSnapshot( - Tag, const ServiceConfig& config, const OAuthService& authService, + Tag, CloudSyncUI& ui, const ServiceConfig& config, + const OAuthService& authService, ProjectCloudExtension& extension, SnapshotOperationUpdated callback) : mProjectCloudExtension { extension } , mWeakProject { extension.GetProject() } + , mCloudSyncUI { ui } , mServiceConfig { config } , mOAuthService { authService } , mUpdateCallback { std::move(callback) } @@ -218,9 +223,9 @@ LocalProjectSnapshot::~LocalProjectSnapshot() } std::shared_ptr LocalProjectSnapshot::Create( - const ServiceConfig& config, const OAuthService& authService, - ProjectCloudExtension& extension, SnapshotOperationUpdated callback, - bool forceCreateNewProject) + CloudSyncUI& ui, const ServiceConfig& config, + const OAuthService& authService, ProjectCloudExtension& extension, + SnapshotOperationUpdated callback, bool forceCreateNewProject) { auto project = extension.GetProject().lock(); @@ -234,7 +239,7 @@ std::shared_ptr LocalProjectSnapshot::Create( } auto snapshot = std::make_shared( - Tag {}, config, authService, extension, std::move(callback)); + Tag {}, ui, config, authService, extension, std::move(callback)); snapshot->mProjectBlocksLock = std::make_unique( extension, *project, @@ -450,6 +455,31 @@ void LocalProjectSnapshot::OnSnapshotCreated( }); } }); + + BasicUI::CallAfter( + [this, mixdownUrls = response.SyncState.MixdownUrls] + { + auto project = mWeakProject.lock(); + + if (!project) + return; + + if (mProjectCloudExtension.NeedsMixdownSync()) + { + mMixdownUploadInProgress.store(true, std::memory_order_release); + mMixdownUploader = MixdownUploader::Upload( + mCloudSyncUI, mServiceConfig, *project, mixdownUrls, + [this](std::string, bool success) + { + if (success) + mProjectCloudExtension.MixdownSynced(); + + mMixdownUploader.reset(); + mMixdownUploadInProgress.store( + false, std::memory_order_release); + }); + } + }); } void LocalProjectSnapshot::MarkSnapshotSynced(int64_t blocksCount) @@ -495,6 +525,10 @@ void LocalProjectSnapshot::MarkSnapshotSynced(int64_t blocksCount) mCompleted.store(true, std::memory_order_release); mProjectCloudExtension.OnSyncCompleted(true); + // Wait for mixdown upload to complete + while (mMixdownUploadInProgress.load(std::memory_order_acquire)) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + mUpdateCallback( { this, blocksCount, blocksCount, true, true, true, {} }); }); diff --git a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h index aca1303c12da..2e05657830d8 100644 --- a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h +++ b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h @@ -31,6 +31,8 @@ constexpr auto UNASSIGNED_PROJECT_ID = -1; class ProjectCloudExtension; class LocalProjectSnapshot; +class MixdownUploader; +class CloudSyncUI; struct SnapshotOperationStatus final { @@ -56,14 +58,15 @@ class LocalProjectSnapshot final : public std::enable_shared_from_this Create( - const ServiceConfig& config, const OAuthService& authService, - ProjectCloudExtension& extension, SnapshotOperationUpdated callback, - bool forceCreateNewProject = false); + CloudSyncUI& ui, const ServiceConfig& config, + const OAuthService& authService, ProjectCloudExtension& extension, + SnapshotOperationUpdated callback, bool forceCreateNewProject = false); bool IsCompleted() const; @@ -84,6 +87,7 @@ class LocalProjectSnapshot final : public std::enable_shared_from_this mWeakProject; + CloudSyncUI& mCloudSyncUI; const ServiceConfig& mServiceConfig; const OAuthService& mOAuthService; @@ -94,11 +98,14 @@ class LocalProjectSnapshot final : public std::enable_shared_from_this mMissingBlockUploader; + std::unique_ptr mMixdownUploader; + std::atomic mUploadedBlocks { 0 }; std::atomic mTotalBlocks { 0 }; std::atomic mProjectUploaded { false }; std::atomic mCompleted { false }; + std::atomic mMixdownUploadInProgress { false }; }; } // namespace cloud::audiocom::sync diff --git a/libraries/lib-cloud-audiocom/sync/MissingBlocksUploader.cpp b/libraries/lib-cloud-audiocom/sync/MissingBlocksUploader.cpp index f1389a3adedf..199fbcb2c4d2 100644 --- a/libraries/lib-cloud-audiocom/sync/MissingBlocksUploader.cpp +++ b/libraries/lib-cloud-audiocom/sync/MissingBlocksUploader.cpp @@ -52,6 +52,10 @@ MissingBlocksUploader::~MissingBlocksUploader() thread.join(); mConsumerThread.join(); + + // mProgressMutex can be held by the consumer thread, so we need to wait + // until it's released. + std::lock_guard lock(mProgressDataMutex); } MissingBlocksUploader::ProducedItem MissingBlocksUploader::ProduceBlock() diff --git a/libraries/lib-cloud-audiocom/sync/MixdownUploader.cpp b/libraries/lib-cloud-audiocom/sync/MixdownUploader.cpp new file mode 100644 index 000000000000..f73f66a43cf3 --- /dev/null +++ b/libraries/lib-cloud-audiocom/sync/MixdownUploader.cpp @@ -0,0 +1,332 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/*!******************************************************************** + + Audacity: A Digital Audio Editor + + MixdownUploader.cpp + + Dmitry Vedenko + +**********************************************************************/ + +#include "MixdownUploader.h" + +#include + +#include "WaveTrack.h" + +#include "CodeConversions.h" + +#include "ServiceConfig.h" + +#include "Export.h" +#include "ExportPluginRegistry.h" +#include "ExportUtils.h" +#include "ExportProgressUI.h" + +#include "ProjectRate.h" + +#include "UploadService.h" +#include "DataUploader.h" + +#include "sync/CloudSyncUI.h" + +namespace cloud::audiocom::sync +{ +namespace +{ +std::string GenerateTempPath(FileExtension extension) +{ + const auto tempPath = GetUploadTempPath(); + + wxFileName fileName( + tempPath, + wxString::Format( + "%lld", std::chrono::system_clock::now().time_since_epoch().count()), + extension); + + fileName.Mkdir(0700, wxPATH_MKDIR_FULL); + + if (fileName.Exists()) + { + if (!wxRemoveFile(fileName.GetFullPath())) + return {}; + } + + return audacity::ToUTF8(fileName.GetFullPath()); +} + +int CalculateChannels(const TrackList& trackList) +{ + auto range = trackList.Any(); + return std::all_of( + range.begin(), range.end(), + [](const WaveTrack* track) + { return IsMono(*track) && track->GetPan() == 0; }) ? + 1 : + 2; +} +} // namespace + + +class MixdownUploader::DataExporter final : + public ExportProcessorDelegate +{ +public: + DataExporter(MixdownUploader& parent, ExportTask task) + : mParent { parent } + , mTask { std::move(task) } + , mExportThread { [this] { ExportTread(); } } + { + } + + ~DataExporter() override + { + mExportThread.join(); + } + + void Cancel() + { + mCancelled.store(true, std::memory_order_release); + } + + ExportResult GetResult() const + { + return mResult; + } + + void OnComplete(ExportResult result) + { + mResult = result; + + if (result == ExportResult::Success) + { + mParent.UploadMixdown(); + } + else + { + mParent.mOnComplete({}, false); + } + } + + void SetStatusString(const TranslatableString& str) override + { + } + + bool IsCancelled() const override + { + return mCancelled.load(std::memory_order_acquire); + } + + bool IsStopped() const override + { + return false; + } + + void OnProgress(double value) override + { + mParent.SetProgress(value); + } + + void ExportTread() + { + try { + auto future = mTask.get_future(); + mTask(*this); + const auto result = future.get(); + + BasicUI::CallAfter([this, result] { OnComplete(result); }); + } + catch (const ExportDiskFullError& error) + { + HandleExportDiskFullError(error); + } + catch (const ExportErrorException& error) + { + HandleExportError(error); + } + catch (const ExportException& error) + { + HandleExportException(error); + } + catch (...) + { + HandleUnkonwnException(); + } + } + + void HandleExportDiskFullError(const ExportDiskFullError& error) + { + BasicUI::CallAfter( + [this, fileName = error.GetFileName()] + { + ShowDiskFullExportErrorDialog(fileName); + mParent.mOnComplete({}, false); + }); + } + + void HandleExportError(const ExportErrorException& error) + { + BasicUI::CallAfter( + [this, message = error.GetMessage(), helpPage = error.GetHelpPageId()] + { + ShowExportErrorDialog(message, XO("Export failed"), helpPage, true); + mParent.mOnComplete({}, false); + }); + } + + void HandleExportException(const ExportException& error) + { + BasicUI::CallAfter( + [this, message = error.What()] + { + ShowExportErrorDialog(Verbatim(message), XO("Export failed"), true); + mParent.mOnComplete({}, false); + }); + } + + void HandleUnkonwnException() + { + BasicUI::CallAfter([] { BasicUI::ShowMessageBox(XO("Export error")); }); + } + +private: + MixdownUploader& mParent; + ExportTask mTask; + std::thread mExportThread; + + std::atomic mCancelled { false }; + ExportResult mResult { ExportResult::Stopped }; +}; + +MixdownUploader::MixdownUploader( + Tag, CloudSyncUI& ui, const ServiceConfig& config, + const AudacityProject& project, const UploadUrls& urls, + MixdownUploaderCompleteCallback onComplete) + : mCloudSyncUI { ui } + , mServiceConfig { config } + , mProject { project } + , mUploadUrls { urls } + , mOnComplete { std::move(onComplete) } +{ + ExportProject(); +} + +MixdownUploader::~MixdownUploader() +{ + if (wxFileExists(mExportedFilePath)) + wxRemoveFile(mExportedFilePath); +} + +std::unique_ptr MixdownUploader::Upload( + CloudSyncUI& ui, const ServiceConfig& config, + const AudacityProject& project, const UploadUrls& urls, + MixdownUploaderCompleteCallback onComplete) +{ + if (!onComplete) + onComplete = [](auto...) {}; + + return std::make_unique( + Tag {}, ui, config, project, urls, std::move(onComplete)); +} + +void MixdownUploader::SetProgress(double progress) +{ + mCurrentProgress.store(progress, std::memory_order_release); + + if (!mProgressUpdateQueued.exchange(true, std::memory_order_acq_rel)) + BasicUI::CallAfter( + [this] + { + if (!mCloudSyncUI.OnMixdownProgress( + mCurrentProgress.load(std::memory_order_acquire))) + { + if (mExporting.load(std::memory_order_acquire) && mDataExporter) + mDataExporter->Cancel(); + else + mUploadCancelled.store(true, std::memory_order_release); + } + + mProgressUpdateQueued.store(false, std::memory_order_release); + }); +} + +void MixdownUploader::ExportProject() +{ + mCloudSyncUI.OnMixdownStarted(); + mCloudSyncUI.SetMixdownProgressMessage(XO("Exporting project...")); + + auto& tracks = TrackList::Get(mProject); + + const double t0 = 0.0; + const double t1 = tracks.GetEndTime(); + + const int nChannels = CalculateChannels(tracks); + + auto hasMimeType = [](const auto&& mimeTypes, const std::string& mimeType) + { + return std::find(mimeTypes.begin(), mimeTypes.end(), mimeType) != + mimeTypes.end(); + }; + + const auto& registry = ExportPluginRegistry::Get(); + + for (const auto& preferredMimeType : + GetServiceConfig().GetPreferredAudioFormats()) + { + auto config = GetServiceConfig().GetExportConfig(preferredMimeType); + ExportProcessor::Parameters parameters; + auto pluginIt = std::find_if( + registry.begin(), registry.end(), + [&](auto t) + { + auto [plugin, formatIndex] = t; + parameters.clear(); + return hasMimeType( + plugin->GetMimeTypes(formatIndex), preferredMimeType) && + plugin->ParseConfig(formatIndex, config, parameters); + }); + + if (pluginIt == registry.end()) + continue; + + const auto [plugin, formatIndex] = *pluginIt; + + const auto formatInfo = plugin->GetFormatInfo(formatIndex); + const auto path = GenerateTempPath(formatInfo.extensions[0]); + + if (path.empty()) + continue; + + auto builder = ExportTaskBuilder {} + .SetParameters(parameters) + .SetNumChannels(nChannels) + .SetSampleRate(ProjectRate::Get(mProject).GetRate()) + .SetPlugin(plugin) + .SetFileName(audacity::ToWXString(path)) + .SetRange(t0, t1, false); + + mExportedFilePath = path; + + mDataExporter = std::make_unique( + *this, builder.Build(const_cast(mProject))); + + return; + } + + if (!mDataExporter) + mOnComplete({}, false); +} +void MixdownUploader::UploadMixdown() +{ + mCloudSyncUI.SetMixdownProgressMessage(XO("Uploading mixdown...")); + DataUploader::Get().Upload( + mServiceConfig, mUploadUrls, mExportedFilePath, + [this](UploadResult result) + { + BasicUI::CallAfter([this, result = std::move(result)] + { mCloudSyncUI.OnMixdownFinished(); }); + }, + {}); +} +} // namespace cloud::audiocom::sync diff --git a/libraries/lib-cloud-audiocom/sync/MixdownUploader.h b/libraries/lib-cloud-audiocom/sync/MixdownUploader.h new file mode 100644 index 000000000000..62e78c6eed96 --- /dev/null +++ b/libraries/lib-cloud-audiocom/sync/MixdownUploader.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/*!******************************************************************** + + Audacity: A Digital Audio Editor + + MixdownUploader.h + + Dmitry Vedenko + +**********************************************************************/ +#pragma once + +#include +#include +#include +#include + +#include "CloudSyncUtils.h" + +class AudacityProject; + +namespace cloud::audiocom +{ +class ServiceConfig; +} + +namespace cloud::audiocom::sync +{ +class CloudSyncUI; + +using MixdownUploaderCompleteCallback = std::function; + +class MixdownUploader final +{ + struct Tag {}; + +public: + MixdownUploader( + Tag, CloudSyncUI& ui, const ServiceConfig& config, + const AudacityProject& project, const UploadUrls& urls, + MixdownUploaderCompleteCallback onComplete); + + ~MixdownUploader(); + + static std::unique_ptr Upload( + CloudSyncUI& ui, const ServiceConfig& config, + const AudacityProject& project, const UploadUrls& urls, + MixdownUploaderCompleteCallback onComplete); + +private: + void SetProgress(double progress); + void ExportProject(); + void UploadMixdown(); + + CloudSyncUI& mCloudSyncUI; + const ServiceConfig& mServiceConfig; + const AudacityProject& mProject; + + UploadUrls mUploadUrls; + + MixdownUploaderCompleteCallback mOnComplete; + + class DataExporter; + std::unique_ptr mDataExporter; + + std::string mExportedFilePath; + + std::atomic mCurrentProgress; + std::atomic mProgressUpdateQueued { false }; + std::atomic mExporting { true }; + std::atomic mUploadCancelled { false }; +}; // class MixdownUploader +} // namespace cloud::audiocom::sync diff --git a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp index 0333ebeef1e6..a1a5dfbd5a1b 100644 --- a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp +++ b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp @@ -15,6 +15,7 @@ #include #include +#include "CloudSettings.h" #include "CloudSyncUtils.h" #include "CodeConversions.h" @@ -182,6 +183,45 @@ bool ProjectCloudExtension::GetAutoDownloadSuppressed() const return mSuppressAutoDownload; } +bool ProjectCloudExtension::NeedsMixdownSync() const +{ + if (!IsCloudProject()) + return false; + + auto& cloudDatabase = CloudProjectsDatabase::Get(); + auto dbData = cloudDatabase.GetProjectData(mProjectId); + + if (!dbData) + return false; + + if (dbData->LastAudioPreview == 0) + return true; + + const auto frequency = MixdownGenerationFrequency.Read(); + + if (frequency == 0) + return false; + + const auto savesSinceLastMixdown = dbData->SavesCount - dbData->LastAudioPreview; + + return savesSinceLastMixdown >= frequency; +} + +void ProjectCloudExtension::MixdownSynced() +{ + if (!IsCloudProject()) + return; + + auto& cloudDatabase = CloudProjectsDatabase::Get(); + auto dbData = cloudDatabase.GetProjectData(mProjectId); + + if (!dbData) + return; + + dbData->LastAudioPreview = dbData->SavesCount; + cloudDatabase.UpdateProjectData(*dbData); +} + void ProjectCloudExtension::UpdateIdFromDatabase() { auto& projectFileIO = ProjectFileIO::Get(mProject); diff --git a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h index 9fcb32f07ea8..6facc2e1e6f6 100644 --- a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h +++ b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h @@ -60,6 +60,9 @@ class CLOUD_AUDIOCOM_API ProjectCloudExtension final : public ClientData::Base bool GetAutoDownloadSuppressed() const; + bool NeedsMixdownSync() const; + void MixdownSynced(); + private: void UpdateIdFromDatabase(); diff --git a/modules/mod-cloud-audiocom/ui/AudioComSyncUI.cpp b/modules/mod-cloud-audiocom/ui/AudioComSyncUI.cpp index f597d61a67b1..b13a06cef70c 100644 --- a/modules/mod-cloud-audiocom/ui/AudioComSyncUI.cpp +++ b/modules/mod-cloud-audiocom/ui/AudioComSyncUI.cpp @@ -11,6 +11,8 @@ #include +#include + #include "CloudSyncService.h" #include "sync/CloudSyncUI.h" @@ -117,6 +119,16 @@ class AudioComShareUI : public CloudSyncUI if (wxID_OK != linkDialog.ShowModal()) return false; + std::atomic waitingForAuth = true; + std::atomic authSuccessful = false; + + auto authSubscription = GetOAuthService().Subscribe( + [&](const AuthStateChangedMessage& message) + { + authSuccessful.store(message.authorised, std::memory_order_relaxed); + waitingForAuth.store(false, std::memory_order_release); + }); + OpenInDefaultBrowser( { audacity::ToWXString(GetServiceConfig().GetOAuthLoginPage()) }); @@ -127,17 +139,18 @@ class AudioComShareUI : public CloudSyncUI auto progress = BasicUI::MakeGenericProgress(placement, XO("Link account"), XO("Waiting for authorization...")); - while (!GetOAuthService ().HasAccessToken ()) + while (waitingForAuth.load(std::memory_order_acquire)) { if (progress->Pulse() != BasicUI::ProgressResult::Success) return false; + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + BasicUI::Yield(); } progress.reset(); GetAuthorizationHandler().PopSuppressDialogs(); - - return true; } else #endif @@ -148,7 +161,7 @@ class AudioComShareUI : public CloudSyncUI dlg.ShowModal(); } - return false; + return authSuccessful.load(std::memory_order_acquire); } bool OnUploadProgress(AudacityProject* project, double progress) override @@ -186,33 +199,33 @@ class AudioComShareUI : public CloudSyncUI void OnDownloadStarted() override { - assert(!mDownloadProgressDialog); + assert(!mProgressDialog); - mDownloadProgressDialog = BasicUI::MakeProgress( + mProgressDialog = BasicUI::MakeProgress( XO("Opening cloud project"), XO("Loading..."), BasicUI::ProgressShowCancel); - mDownloadProgressDialog->Poll(0, 10000); + mProgressDialog->Poll(0, 10000); } bool OnDownloadProgress(double progress) override { - assert(mDownloadProgressDialog); + assert(mProgressDialog); - if (!mDownloadProgressDialog) + if (!mProgressDialog) return true; if (progress < 0.0) progress = 0.0; - return mDownloadProgressDialog->Poll( + return mProgressDialog->Poll( static_cast(progress * 10000), 10000) == BasicUI::ProgressResult::Success; } void OnDownloadFinished() override { - mDownloadProgressDialog.reset(); + mProgressDialog.reset(); } void ShowDownloadError (std::string errorMessage) override @@ -231,8 +244,45 @@ class AudioComShareUI : public CloudSyncUI { return DownloadConflictResolution::Remote; } + + void OnMixdownStarted() override + { + assert(!mProgressDialog); + + mProgressDialog = BasicUI::MakeProgress( + XO("Save to audio.com"), XO("Loading..."), + BasicUI::ProgressShowCancel); + } + + void SetMixdownProgressMessage(const TranslatableString& message) override + { + assert(mProgressDialog); + + if (!mProgressDialog) + return; + + mProgressDialog->SetMessage(message); + } + + bool OnMixdownProgress(double progress) override + { + assert(mProgressDialog); + + if (!mProgressDialog) + return true; + + return mProgressDialog->Poll( + static_cast(progress * 10000), 10000) == + BasicUI::ProgressResult::Success; + } + + void OnMixdownFinished() override + { + mProgressDialog.reset(); + } + private: - std::unique_ptr mDownloadProgressDialog; + std::unique_ptr mProgressDialog; }; CloudSyncService::UI::Scope scope { []() -> CloudSyncUI&