mirror of
https://github.com/cookiengineer/audacity
synced 2025-04-30 23:59:41 +02: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:
parent
15313a27f7
commit
92e36332f3
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user