mirror of
https://github.com/cookiengineer/audacity
synced 2025-11-23 17:30:17 +01:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user