From 92e36332f310a2937803269200a3155fda01da7a Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Sun, 8 Nov 2020 19:24:20 -0500 Subject: [PATCH] Reimplement importation of .aup3 file more simply... ... Breaking dependency of ProjectFileIO on on TimeTrack, and reusing the same functions that implement cross-document copy and paste of tracks. Also changing behavior, so that if a TimeTrack exists but another is imported, then the import quietly replaces the existing completely. --- src/ProjectFileIO.cpp | 352 ------------------------------------- src/ProjectFileIO.h | 1 - src/ProjectFileManager.cpp | 25 ++- src/TimeTrack.cpp | 13 ++ src/TimeTrack.h | 2 + 5 files changed, 39 insertions(+), 354 deletions(-) diff --git a/src/ProjectFileIO.cpp b/src/ProjectFileIO.cpp index 9dc921558..6ffe36f9f 100644 --- a/src/ProjectFileIO.cpp +++ b/src/ProjectFileIO.cpp @@ -27,7 +27,6 @@ Paul Licameli split from AudacityProject.cpp #include "SampleBlock.h" #include "Tags.h" #include "TempDirectory.h" -#include "TimeTrack.h" #include "ViewInfo.h" #include "WaveTrack.h" #include "widgets/AudacityMessageBox.h" @@ -1677,357 +1676,6 @@ bool ProjectFileIO::WriteDoc(const char *table, return true; } -// Importing an AUP3 project into an AUP3 project is a bit different than -// normal importing since we need to copy data from one DB to the other -// while adjusting the sample block IDs to represent the newly assigned -// IDs. -bool ProjectFileIO::ImportProject(const FilePath &fileName) -{ - // Get access to the current project file - auto db = DB(); - - bool success = false; - bool restore = true; - int rc = SQLITE_OK; - - // Ensure the inbound database gets detached - auto detach = finally([&] - { - // If this DETACH fails, subsequent project imports will fail until Audacity - // is relaunched. - auto result = sqlite3_exec(db, "DETACH DATABASE inbound;", nullptr, nullptr, nullptr); - - // Only capture the error if there wasn't a previous error - if (result != SQLITE_OK && (rc == SQLITE_DONE || rc == SQLITE_OK)) - { - SetDBError( - XO("Failed to detach project during import") - ); - } - }); - - // Attach the inbound project file - wxString sql; - sql.Printf("ATTACH DATABASE 'file:%s?immutable=1&mode=ro' AS inbound;", fileName); - - rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to attach %s project file").Format(fileName) - ); - - return false; - } - - // We need either the autosave or project docs from the inbound AUP3 - wxMemoryBuffer buffer; - - // Get the autosave doc, if any - if (!GetBlob("SELECT dict || doc FROM inbound.project WHERE id = 1;", buffer)) - { - // Error already set - return false; - } - - // If we didn't have an autosave doc, load the project doc instead - if (buffer.GetDataLen() == 0) - { - if (!GetBlob("SELECT dict || doc FROM inbound.autosave WHERE id = 1;", buffer)) - { - // Error already set - return false; - } - - // Missing both the autosave and project docs. This can happen if the - // system were to crash before the first autosave into a temporary file. - if (buffer.GetDataLen() == 0) - { - SetError(XO("Unable to load project or autosave documents")); - return false; - } - } - - wxString project; - - project = ProjectSerializer::Decode(buffer); - if (project.size() == 0) - { - SetError(XO("Unable to decode project document")); - - return false; - } - - // Parse the project doc - wxStringInputStream in(project); - wxXmlDocument doc; - if (!doc.Load(in)) - { - SetError(XO("Unable to parse the project document")); - - return false; - } - - // Get the root ("project") node - wxXmlNode *root = doc.GetRoot(); - if (!root->GetName().IsSameAs(wxT("project"))) - { - SetError(XO("Missing root node in project document")); - - return false; - } - - // Soft delete all non-essential attributes to prevent updating the active - // project. This takes advantage of the knowledge that when a project is - // parsed, unrecognized attributes are simply ignored. - // - // This is necessary because we don't want any of the active project settings - // to be modified by the inbound project. - for (wxXmlAttribute *attr = root->GetAttributes(); attr; attr = attr->GetNext()) - { - wxString name = attr->GetName(); - if (!name.IsSameAs(wxT("version")) && !name.IsSameAs(wxT("audacityversion"))) - { - attr->SetName(name + wxT("_deleted")); - } - } - - // Recursively find and collect all waveblock nodes - std::vector blocknodes; - std::function findblocks = [&](wxXmlNode *node) - { - while (node) - { - if (node->GetName().IsSameAs(wxT("waveblock"))) - { - blocknodes.push_back(node); - } - else - { - findblocks(node->GetChildren()); - } - - node = node->GetNext(); - } - }; - - // Get access to the active tracklist - auto pProject = &mProject; - auto &tracklist = TrackList::Get(*pProject); - - // Search for a timetrack and remove it if the project already has one - if (*tracklist.Any().begin()) - { - // Find a timetrack and remove it if it exists - for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext()) - { - if (node->GetName().IsSameAs(wxT("timetrack"))) - { - AudacityMessageBox( - XO("The active project already has a time track and one was encountered in the project being imported, bypassing imported time track."), - XO("Project Import"), - wxOK | wxICON_EXCLAMATION | wxCENTRE, - &GetProjectFrame(*pProject)); - - root->RemoveChild(node); - break; - } - } - } - - // Find all waveblocks in all wavetracks - for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext()) - { - if (node->GetName().IsSameAs(wxT("wavetrack"))) - { - findblocks(node->GetChildren()); - } - } - - { - // Cleanup... - sqlite3_stmt *stmt = nullptr; - auto cleanup = finally([&] - { - // Ensure the prepared statement gets cleaned up - if (stmt) - { - // Ignore the return code since it will have already been captured below. - sqlite3_finalize(stmt); - } - }); - - // Prepare the statement to copy the sample block from the inbound project to the - // active project. All columns other than the blockid column get copied. - wxString columns(wxT("sampleformat, summin, summax, sumrms, summary256, summary64k, samples")); - sql.Printf("INSERT INTO main.sampleblocks (%s)" - " SELECT %s" - " FROM inbound.sampleblocks" - " WHERE blockid = ?;", - columns, - columns); - - rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to prepare project file command:\n\n%s").Format(sql) - ); - return false; - } - - /* i18n-hint: This title appears on a dialog that indicates the progress - in doing something.*/ - ProgressDialog progress(XO("Progress"), XO("Importing project"), pdlgHideStopButton); - ProgressResult result = ProgressResult::Success; - - wxLongLong_t count = 0; - wxLongLong_t total = blocknodes.size(); - - rc = sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to start a transaction during import") - ); - return false; - } - - // Copy all the sample blocks from the inbound project file into - // the active one, while remembering which were copied. - for (auto node : blocknodes) - { - // Find the blockid attribute...it should always be there - wxXmlAttribute *attr = node->GetAttributes(); - while (attr && !attr->GetName().IsSameAs(wxT("blockid"))) - { - attr = attr->GetNext(); - } - wxASSERT(attr != nullptr); - - // And get the blockid - SampleBlockID blockid; - attr->GetValue().ToLongLong(&blockid); - - if ( blockid <= 0 ) - // silent block - continue; - - // Bind statement parameters - rc = sqlite3_bind_int64(stmt, 1, blockid); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Failed to bind SQL parameter") - ); - - break; - } - - // Process it - rc = sqlite3_step(stmt); - if (rc != SQLITE_DONE) - { - SetDBError( - XO("Failed to import sample block.\nThe following command failed:\n\n%s").Format(sql) - ); - - break; - } - - // Replace the original blockid with the new one - attr->SetValue(wxString::Format(wxT("%lld"), sqlite3_last_insert_rowid(db))); - - // Reset the statement for the next iteration - if (sqlite3_reset(stmt) != SQLITE_OK) - { - THROW_INCONSISTENCY_EXCEPTION; - } - - // Remember that we copied this node in case the user cancels - result = progress.Update(++count, total); - if (result != ProgressResult::Success) - { - break; - } - } - - // Bail if the import was cancelled or failed. If the user stopped the - // import or it completed, then we continue on. - if (rc != SQLITE_DONE || result == ProgressResult::Cancelled || result == ProgressResult::Failed) - { - // If this fails (probably due to memory or disk space), the transaction will - // (presumably) stil be active, so further updates to the project file will - // fail as well. Not really much we can do about it except tell the user. - auto result = sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - - // Only capture the error if there wasn't a previous error - if (result != SQLITE_OK && rc == SQLITE_DONE) - { - SetDBError( - XO("Failed to rollback transaction during import") - ); - } - - return false; - } - - // Go ahead and commit now. If this fails (probably due to memory or disk space), - // the transaction will (presumably) stil be active, so further updates to the - // project file will fail as well. Not really much we can do about it except tell - // the user. - rc = sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to commit transaction during import") - ); - - return false; - } - - // Copy over tags...likely to produce duplicates...needs work once used - rc = sqlite3_exec(db, - "INSERT INTO main.tags SELECT * FROM inbound.tags;", - nullptr, - nullptr, - nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Failed to import tags") - ); - - return false; - } - } - - // Recreate the project doc with the revisions we've made above. If this fails - // it's probably due to memory. - wxStringOutputStream output; - if (!doc.Save(output)) - { - SetError( - XO("Unable to recreate project information.") - ); - - return false; - } - - // Now load the document as normal - XMLFileReader xmlFile; - if (!xmlFile.ParseString(this, output.GetString())) - { - SetError( - XO("Unable to parse project information."), xmlFile.GetErrorStr() - ); - - return false; - } - - return true; -} - bool ProjectFileIO::LoadProject(const FilePath &fileName, bool ignoreAutosave) { bool success = false; diff --git a/src/ProjectFileIO.h b/src/ProjectFileIO.h index e99822917..b69cb0b1c 100644 --- a/src/ProjectFileIO.h +++ b/src/ProjectFileIO.h @@ -92,7 +92,6 @@ public: bool CloseProject(); bool ReopenProject(); - bool ImportProject(const FilePath &fileName); bool LoadProject(const FilePath &fileName, bool ignoreAutosave); bool UpdateSaved(const TrackList *tracks = nullptr); bool SaveProject(const FilePath &fileName, const TrackList *lastSaved); diff --git a/src/ProjectFileManager.cpp b/src/ProjectFileManager.cpp index 9df1452cd..c6054fbfc 100644 --- a/src/ProjectFileManager.cpp +++ b/src/ProjectFileManager.cpp @@ -1137,6 +1137,29 @@ ProjectFileManager::AddImportedTracks(const FilePath &fileName, // HandleResize(); } +namespace { +bool ImportProject(AudacityProject &dest, const FilePath &fileName) +{ + InvisibleTemporaryProject temp; + auto &project = temp.Project(); + + auto &projectFileIO = ProjectFileIO::Get(project); + if (!projectFileIO.LoadProject(fileName, false)) + return false; + auto &srcTracks = TrackList::Get(project); + auto &destTracks = TrackList::Get(dest); + for (const Track *pTrack : srcTracks.Any()) { + auto destTrack = pTrack->PasteInto(dest); + Track::FinishCopy(pTrack, destTrack.get()); + if (destTrack.use_count() == 1) + destTracks.Add(destTrack); + } + Tags::Get(dest).Merge(Tags::Get(project)); + + return true; +} +} + // If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks. bool ProjectFileManager::Import( const FilePath &fileName, @@ -1151,7 +1174,7 @@ bool ProjectFileManager::Import( // Handle AUP3 ("project") files directly if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) { - if (projectFileIO.ImportProject(fileName)) { + if (ImportProject(project, fileName)) { auto &history = ProjectHistory::Get(project); // If the project was clean and temporary (not permanently saved), then set diff --git a/src/TimeTrack.cpp b/src/TimeTrack.cpp index 11703b691..4a8eeefb6 100644 --- a/src/TimeTrack.cpp +++ b/src/TimeTrack.cpp @@ -52,6 +52,11 @@ static ProjectFileIORegistry::Entry registerFactory{ TimeTrack::TimeTrack(const ZoomInfo *zoomInfo): Track() , mZoomInfo(zoomInfo) +{ + CleanState(); +} + +void TimeTrack::CleanState() { mEnvelope = std::make_unique(true, TIMETRACK_MIN, TIMETRACK_MAX, 1.0); @@ -144,7 +149,15 @@ Track::Holder TimeTrack::PasteInto( AudacityProject &project ) const pNewTrack = pTrack->SharedPointer(); else pNewTrack = std::make_shared( &ViewInfo::Get( project ) ); + + // Should come here only for .aup3 import, not for paste (because the + // track is skipped in cut/copy commands) + // And for import we agree to replace the track contents completely + pNewTrack->CleanState(); + pNewTrack->Init(*this); pNewTrack->Paste(0.0, this); + pNewTrack->SetRangeLower(this->GetRangeLower()); + pNewTrack->SetRangeUpper(this->GetRangeUpper()); return pNewTrack; } diff --git a/src/TimeTrack.h b/src/TimeTrack.h index 041a2da71..c2c40c434 100644 --- a/src/TimeTrack.h +++ b/src/TimeTrack.h @@ -95,6 +95,8 @@ class TimeTrack final : public Track { Ruler &GetRuler() const { return *mRuler; } private: + void CleanState(); + // Identifying the type of track TrackKind GetKind() const override { return TrackKind::Time; }