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.
This commit is contained in:
Paul Licameli 2020-11-08 19:24:20 -05:00
parent 15313a27f7
commit 92e36332f3
5 changed files with 39 additions and 354 deletions

View File

@ -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<wxXmlNode *> blocknodes;
std::function<void(wxXmlNode *)> 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<TimeTrack>().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;

View File

@ -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);

View File

@ -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

View File

@ -52,6 +52,11 @@ static ProjectFileIORegistry::Entry registerFactory{
TimeTrack::TimeTrack(const ZoomInfo *zoomInfo):
Track()
, mZoomInfo(zoomInfo)
{
CleanState();
}
void TimeTrack::CleanState()
{
mEnvelope = std::make_unique<BoundedEnvelope>(true, TIMETRACK_MIN, TIMETRACK_MAX, 1.0);
@ -144,7 +149,15 @@ Track::Holder TimeTrack::PasteInto( AudacityProject &project ) const
pNewTrack = pTrack->SharedPointer<TimeTrack>();
else
pNewTrack = std::make_shared<TimeTrack>( &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;
}

View File

@ -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; }