From 182c319ee7992f8a913cb2d854d52d1330bdc156 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 3 Mar 2016 17:33:11 +0100 Subject: [PATCH 01/52] Encapsulate location and id of tracks in TrackRef --- build/depends.py | 1 + src/library/scanner/importfilestask.cpp | 15 ++-- src/test/trackreftest.cpp | 83 ++++++++++++++++++ src/track/track.cpp | 5 +- src/track/trackref.cpp | 28 ++++++ src/track/trackref.h | 108 ++++++++++++++++++++++++ src/widget/wtracktableview.cpp | 4 +- 7 files changed, 233 insertions(+), 11 deletions(-) create mode 100644 src/test/trackreftest.cpp create mode 100644 src/track/trackref.cpp create mode 100644 src/track/trackref.h diff --git a/build/depends.py b/build/depends.py index 9b203a36181c..2fcd7f494465 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1086,6 +1086,7 @@ def sources(self, build): "track/albuminfo.cpp", "track/trackinfo.cpp", "track/trackrecord.cpp", + "track/trackref.cpp", "mixer/auxiliary.cpp", "mixer/baseplayer.cpp", diff --git a/src/library/scanner/importfilestask.cpp b/src/library/scanner/importfilestask.cpp index 1370c2be4da4..df770c250c21 100644 --- a/src/library/scanner/importfilestask.cpp +++ b/src/library/scanner/importfilestask.cpp @@ -1,6 +1,7 @@ #include "library/scanner/importfilestask.h" #include "library/scanner/libraryscanner.h" +#include "track/trackref.h" #include "util/timer.h" ImportFilesTask::ImportFilesTask(LibraryScanner* pScanner, @@ -29,27 +30,27 @@ void ImportFilesTask::run() { return; } - const QString filePath(fileInfo.filePath()); - //qDebug() << "ImportFilesTask::run" << filePath; + const QString trackLocation(TrackRef::location(fileInfo)); + //qDebug() << "ImportFilesTask::run" << trackLocation; // If the file does not exist in the database then add it. If it // does then it is either in the user's library OR the user has // "removed" the track via "Right-Click -> Remove". These tracks // stay in the library, but their mixxx_deleted column is 1. - if (m_scannerGlobal->trackExistsInDatabase(filePath)) { + if (m_scannerGlobal->trackExistsInDatabase(trackLocation)) { // If the track is in the database, mark it as existing. This code gets // executed when other files in the same directory have changed (the // directory hash has changed). - emit(trackExists(filePath)); + emit(trackExists(trackLocation)); } else { if (!fileInfo.exists()) { qWarning() << "ImportFilesTask: Skipping inaccessible file" - << filePath; + << trackLocation; continue; } - qDebug() << "Importing track" << filePath; + qDebug() << "Importing track" << trackLocation; - emit(addNewTrack(filePath)); + emit(addNewTrack(trackLocation)); } } // Insert or update the hash in the database. diff --git a/src/test/trackreftest.cpp b/src/test/trackreftest.cpp new file mode 100644 index 000000000000..42fbf7d7e5f7 --- /dev/null +++ b/src/test/trackreftest.cpp @@ -0,0 +1,83 @@ +#include + +#include +#include +#include + +#include "track/trackref.h" + +namespace { + +class TrackRefTest : public testing::Test { + protected: + + // NOTE(uklotzde): Explicitly initialize all const members, even + // if only the default constructor is used. This workaround was + // needed for LLVM version 7.0.2 (clang-700.1.81). + TrackRefTest() + : m_tempFile("TrackRefTest.tmp"), + m_tempFileDir(QDir::tempPath()), + m_tempFileName(m_tempFile.fileName()), + m_tempFileInfo(m_tempFileDir, m_tempFileName), + m_validTrackId(123), + m_invalidTrackId() { + } + + virtual void SetUp() { + ASSERT_TRUE(m_validTrackId.isValid()); + ASSERT_FALSE(m_invalidTrackId.isValid()); + } + + virtual void TearDown() { + } + + static void verifyFileInfo(const TrackRef& actual, const QFileInfo& fileInfo) { + EXPECT_TRUE(actual.hasLocation()); + EXPECT_EQ(TrackRef::location(fileInfo), actual.getLocation()); + EXPECT_TRUE(actual.hasCanonicalLocation()); + EXPECT_EQ(TrackRef::canonicalLocation(fileInfo), actual.getCanonicalLocation()); + } + + const QTemporaryFile m_tempFile; + const QDir m_tempFileDir; + const QString m_tempFileName; + const QFileInfo m_tempFileInfo; + const TrackId m_validTrackId; + const TrackId m_invalidTrackId; +}; + +TEST_F(TrackRefTest, DefaultConstructor) { + TrackRef actual; + + EXPECT_FALSE(actual.hasLocation()); + EXPECT_FALSE(actual.hasCanonicalLocation()); + EXPECT_FALSE(actual.hasId()); +} + +TEST_F(TrackRefTest, FromFileInfoWithId) { + const TrackRef actual(m_tempFileInfo, m_validTrackId); + + verifyFileInfo(actual, m_tempFileInfo); + EXPECT_TRUE(actual.hasId()); + EXPECT_EQ(m_validTrackId, actual.getId()); +} + +TEST_F(TrackRefTest, FromFileInfoWithoutId) { + const TrackRef actual(m_tempFileInfo); + + verifyFileInfo(actual, m_tempFileInfo); + EXPECT_FALSE(actual.hasId()); + EXPECT_EQ(m_invalidTrackId, actual.getId()); +} + +TEST_F(TrackRefTest, CopyAndSetId) { + const TrackRef withoutId(m_tempFileInfo); + + const TrackRef actual(withoutId, m_validTrackId); + + verifyFileInfo(actual, m_tempFileInfo); + EXPECT_TRUE(actual.hasId()); + EXPECT_EQ(m_validTrackId, actual.getId()); +} + +} // namespace diff --git a/src/track/track.cpp b/src/track/track.cpp index e684d3e43e45..7a7f3e1caeda 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -4,6 +4,7 @@ #include #include "track/track.h" +#include "track/trackref.h" #include "track/beatfactory.h" #include "util/assert.h" @@ -173,7 +174,7 @@ QString Track::getLocation() const { // (copy-on write). But operating on a single instance of QFileInfo // might not be thread-safe due to internal caching! QMutexLocker lock(&m_qMutex); - return m_fileInfo.absoluteFilePath(); + return TrackRef::location(m_fileInfo); } QString Track::getCanonicalLocation() const { @@ -181,7 +182,7 @@ QString Track::getCanonicalLocation() const { // (copy-on write). But operating on a single instance of QFileInfo // might not be thread-safe due to internal caching! QMutexLocker lock(&m_qMutex); - return m_fileInfo.canonicalFilePath(); + return TrackRef::canonicalLocation(m_fileInfo); } QString Track::getDirectory() const { diff --git a/src/track/trackref.cpp b/src/track/trackref.cpp new file mode 100644 index 000000000000..b71feb84ba42 --- /dev/null +++ b/src/track/trackref.cpp @@ -0,0 +1,28 @@ +#include "track/trackref.h" + + +bool TrackRef::verifyConsistency() const { + VERIFY_OR_DEBUG_ASSERT(hasLocation() || !hasCanonicalLocation()) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(hasLocation() || !hasId()) { + return false; + } + return true; +} + +std::ostream& operator<<(std::ostream& os, const TrackRef& trackRef) { + return os << '[' << trackRef.getLocation().toStdString() + << " | " << trackRef.getCanonicalLocation().toStdString() + << " | " << trackRef.getId() + << ']'; + +} + +QDebug operator<<(QDebug debug, const TrackRef& trackRef) { + debug.nospace() << '[' << trackRef.getLocation() + << " | " << trackRef.getCanonicalLocation() + << " | " << trackRef.getId() + << ']'; + return debug.space(); +} diff --git a/src/track/trackref.h b/src/track/trackref.h new file mode 100644 index 000000000000..a281d0067a48 --- /dev/null +++ b/src/track/trackref.h @@ -0,0 +1,108 @@ +#ifndef TRACKREF_H +#define TRACKREF_H + + +#include + +#include "track/trackid.h" + + +// A track in the library is identified by a location and an id. +// The location is mandatory to identify the file, whereas the id +// only exists after the track has been inserted into the database. +// +// This class is intended to be used as a simple, almost immutable +// value object. Only the id can be set once. +class TrackRef { +public: + static QString location(const QFileInfo& fileInfo) { + return fileInfo.absoluteFilePath(); + } + static QString canonicalLocation(const QFileInfo& fileInfo) { + return fileInfo.canonicalFilePath(); + } + + TrackRef() { + DEBUG_ASSERT(verifyConsistency()); + } + // Conversion from QFileInfo with an optional TrackId + explicit TrackRef( + const QFileInfo& fileInfo, + TrackId id = TrackId()) + : m_location(location(fileInfo)), + m_canonicalLocation(canonicalLocation(fileInfo)), + m_id(id) { + DEBUG_ASSERT(verifyConsistency()); + } + // Create a copy of an existing TrackRef, but overwrite the + // TrackId with a custom value. + TrackRef( + const TrackRef& other, + TrackId id) + : m_location(other.m_location), + m_canonicalLocation(other.m_canonicalLocation), + m_id(id) { + DEBUG_ASSERT(verifyConsistency()); + } + + // The human-readable identifier of a track in Mixxx. The location is + // immutable and the starting point for accessing a track's file. + const QString& getLocation() const { + return m_location; + } + bool hasLocation() const { + return !getLocation().isEmpty(); + } + + // The unique identifier of a track's file at runtime and used + // for caching. The canonical location is empty for inexistent + // files. + const QString& getCanonicalLocation() const { + return m_canonicalLocation; + } + bool hasCanonicalLocation() const { + return !getCanonicalLocation().isEmpty(); + } + + // The primary key of a track in the Mixxx library. The id must only + // be set once after inserting into or after loading from the database. + // Tracks that have not been stored in the database don't have an id. + const TrackId& getId() const { + return m_id; + } + bool hasId() const { + return getId().isValid(); + } + + bool isValid() const { + return hasId() || hasCanonicalLocation(); + } + +private: + bool verifyConsistency() const; + + QString m_location; + QString m_canonicalLocation; + TrackId m_id; +}; + +inline +bool operator==(const TrackRef& lhs, const TrackRef& rhs) { + return (lhs.getId() == rhs.getId()) && + (lhs.getLocation() == rhs.getLocation()) && + (lhs.getCanonicalLocation() == rhs.getCanonicalLocation()); +} + +inline +bool operator!=(const TrackRef& lhs, const TrackRef& rhs) { + return !(lhs == rhs); +} + +Q_DECLARE_METATYPE(TrackRef) + +std::ostream& operator<<(std::ostream& os, const TrackRef& trackRef); + +QDebug operator<<(QDebug debug, const TrackRef& trackRef); + + +#endif // TRACKREF_H diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index ef39f9cec209..71fa1197dd95 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -19,6 +19,7 @@ #include "control/controlobject.h" #include "control/controlproxy.h" #include "track/track.h" +#include "track/trackref.h" #include "sources/soundsourceproxy.h" #include "mixer/playermanager.h" #include "preferences/dialog/dlgpreflibrary.h" @@ -1232,8 +1233,7 @@ void WTrackTableView::dropEvent(QDropEvent * event) { QList fileLocationList; for (const QFileInfo& fileInfo : fileList) { - // TODO(uklotzde): Replace with TrackRef::location() - fileLocationList.append(fileInfo.absoluteFilePath()); + fileLocationList.append(TrackRef::location(fileInfo)); } // Drag-and-drop from an external application From 30c302a7c5e5e3938941f4dc0f48d5ba83597924 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 5 Mar 2016 12:10:50 +0100 Subject: [PATCH 02/52] Move construction from QFileInfo into a separate factory function ...because construction from QFileInfo is costly and should be used consciously. --- src/test/trackreftest.cpp | 9 +++++--- src/track/trackref.h | 46 +++++++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/test/trackreftest.cpp b/src/test/trackreftest.cpp index 42fbf7d7e5f7..ab15fbd5099c 100644 --- a/src/test/trackreftest.cpp +++ b/src/test/trackreftest.cpp @@ -55,7 +55,8 @@ TEST_F(TrackRefTest, DefaultConstructor) { } TEST_F(TrackRefTest, FromFileInfoWithId) { - const TrackRef actual(m_tempFileInfo, m_validTrackId); + const TrackRef actual( + TrackRef::fromFileInfo(m_tempFileInfo, m_validTrackId)); verifyFileInfo(actual, m_tempFileInfo); EXPECT_TRUE(actual.hasId()); @@ -63,7 +64,8 @@ TEST_F(TrackRefTest, FromFileInfoWithId) { } TEST_F(TrackRefTest, FromFileInfoWithoutId) { - const TrackRef actual(m_tempFileInfo); + const TrackRef actual( + TrackRef::fromFileInfo(m_tempFileInfo)); verifyFileInfo(actual, m_tempFileInfo); EXPECT_FALSE(actual.hasId()); @@ -71,7 +73,8 @@ TEST_F(TrackRefTest, FromFileInfoWithoutId) { } TEST_F(TrackRefTest, CopyAndSetId) { - const TrackRef withoutId(m_tempFileInfo); + const TrackRef withoutId( + TrackRef::fromFileInfo(m_tempFileInfo)); const TrackRef actual(withoutId, m_validTrackId); diff --git a/src/track/trackref.h b/src/track/trackref.h index a281d0067a48..13ee290fd492 100644 --- a/src/track/trackref.h +++ b/src/track/trackref.h @@ -15,6 +15,13 @@ // value object. Only the id can be set once. class TrackRef { public: + // All file-related track properties are snapshots from the provided + // QFileInfo. Obtaining them might involve accessing the file system + // and should be used consciously! The QFileInfo class does some + // caching behind the scenes. + // Please note that the canonical location of QFileInfo may change at + // any time, when the underlying file system structure is modified. + // It becomes empty if the file is deleted. static QString location(const QFileInfo& fileInfo) { return fileInfo.absoluteFilePath(); } @@ -22,20 +29,26 @@ class TrackRef { return fileInfo.canonicalFilePath(); } - TrackRef() { - DEBUG_ASSERT(verifyConsistency()); - } - // Conversion from QFileInfo with an optional TrackId - explicit TrackRef( + // Converts a QFileInfo and an optional TrackId into a TrackRef. This + // involves obtaining the file-related track properties from QFileInfo + // (see above) and should used consciously! + static TrackRef fromFileInfo( const QFileInfo& fileInfo, - TrackId id = TrackId()) - : m_location(location(fileInfo)), - m_canonicalLocation(canonicalLocation(fileInfo)), - m_id(id) { + TrackId id = TrackId()) { + return TrackRef( + location(fileInfo), + canonicalLocation(fileInfo), + id); + } + + // Default constructor + TrackRef() { DEBUG_ASSERT(verifyConsistency()); } - // Create a copy of an existing TrackRef, but overwrite the - // TrackId with a custom value. + // Regular copy constructor + TrackRef(const TrackRef& other) = default; + // Custom copy constructor: Creates a copy of an existing TrackRef, + // but overwrite the TrackId with a custom value. TrackRef( const TrackRef& other, TrackId id) @@ -77,6 +90,17 @@ class TrackRef { bool isValid() const { return hasId() || hasCanonicalLocation(); } +protected: + // Initializing constructor + TrackRef( + const QString& location, + const QString& canonicalLocation, + TrackId id = TrackId()) + : m_location(location), + m_canonicalLocation(canonicalLocation), + m_id(id) { + DEBUG_ASSERT(verifyConsistency()); + } private: bool verifyConsistency() const; From 92d18208c14ceba756308354fd85e561d18e0098 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 15 Jun 2017 13:26:43 +0200 Subject: [PATCH 03/52] Introduce TrackCache for managing Track objects Ensures that for each file only a single TrackInfoObject at the same time exists in Mixxx. --- build/depends.py | 1 + src/library/browse/browsethread.cpp | 239 +++++++------ src/library/dao/trackdao.cpp | 230 ++++-------- src/library/dao/trackdao.h | 34 +- src/library/library.cpp | 4 + src/library/library.h | 7 +- src/library/trackcollection.cpp | 9 +- src/library/trackcollection.h | 2 + src/mixxx.cpp | 12 + src/test/coverartcache_test.cpp | 4 +- src/test/librarytest.h | 13 +- src/track/track.cpp | 19 - src/track/track.h | 18 +- src/track/trackcache.cpp | 524 ++++++++++++++++++++++++++++ src/track/trackcache.h | 219 ++++++++++++ 15 files changed, 993 insertions(+), 342 deletions(-) create mode 100644 src/track/trackcache.cpp create mode 100644 src/track/trackcache.h diff --git a/build/depends.py b/build/depends.py index 2fcd7f494465..b89760a27067 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1080,6 +1080,7 @@ def sources(self, build): "track/playcounter.cpp", "track/replaygain.cpp", "track/track.cpp", + "track/trackcache.cpp", "track/trackmetadata.cpp", "track/trackmetadatataglib.cpp", "track/tracknumbers.cpp", diff --git a/src/library/browse/browsethread.cpp b/src/library/browse/browsethread.cpp index f465f39bdccf..65f76fbccd4a 100644 --- a/src/library/browse/browsethread.cpp +++ b/src/library/browse/browsethread.cpp @@ -8,8 +8,9 @@ #include #include "library/browse/browsetablemodel.h" + #include "sources/soundsourceproxy.h" -#include "track/trackmetadata.h" +#include "track/trackcache.h" #include "util/trace.h" @@ -145,124 +146,136 @@ void BrowseThread::populateModel() { return populateModel(); } - QString filepath = fileIt.next(); - auto pTrack = Track::newTemporary(filepath, thisPath.token()); - SoundSourceProxy(pTrack).updateTrackFromSource(); - QList row_data; QStandardItem* item = new QStandardItem("0"); item->setData("0", Qt::UserRole); row_data.insert(COLUMN_PREVIEW, item); - item = new QStandardItem(pTrack->getFileName()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_FILENAME, item); - - item = new QStandardItem(pTrack->getArtist()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_ARTIST, item); - - item = new QStandardItem(pTrack->getTitle()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_TITLE, item); - - item = new QStandardItem(pTrack->getAlbum()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_ALBUM, item); - - item = new QStandardItem(pTrack->getAlbumArtist()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_ALBUMARTIST, item); - - item = new QStandardItem(pTrack->getTrackNumber()); - item->setToolTip(item->text()); - item->setData(item->text().toInt(), Qt::UserRole); - row_data.insert(COLUMN_TRACK_NUMBER, item); - - const QString year(pTrack->getYear()); - item = new YearItem(year); - item->setToolTip(year); - // The year column is sorted according to the numeric calendar year - item->setData(mixxx::TrackMetadata::parseCalendarYear(year), Qt::UserRole); - row_data.insert(COLUMN_YEAR, item); - - item = new QStandardItem(pTrack->getGenre()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_GENRE, item); - - item = new QStandardItem(pTrack->getComposer()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_COMPOSER, item); - - item = new QStandardItem(pTrack->getGrouping()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_GROUPING, item); - - item = new QStandardItem(pTrack->getComment()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_COMMENT, item); - - QString duration = pTrack->getDurationText(mixxx::Duration::Precision::SECONDS); - item = new QStandardItem(duration); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_DURATION, item); - - item = new QStandardItem(pTrack->getBpmText()); - item->setToolTip(item->text()); - item->setData(pTrack->getBpm(), Qt::UserRole); - row_data.insert(COLUMN_BPM, item); - - item = new QStandardItem(pTrack->getKeyText()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_KEY, item); - - item = new QStandardItem(pTrack->getType()); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_TYPE, item); - - item = new QStandardItem(pTrack->getBitrateText()); - item->setToolTip(item->text()); - item->setData(pTrack->getBitrate(), Qt::UserRole); - row_data.insert(COLUMN_BITRATE, item); - - QString location = pTrack->getLocation(); - QString nativeLocation = QDir::toNativeSeparators(location); - item = new QStandardItem(nativeLocation); - item->setToolTip(nativeLocation); - item->setData(location, Qt::UserRole); - row_data.insert(COLUMN_NATIVELOCATION, item); - - QDateTime modifiedTime = pTrack->getFileModifiedTime().toLocalTime(); - item = new QStandardItem(modifiedTime.toString(Qt::DefaultLocaleShortDate)); - item->setToolTip(item->text()); - item->setData(modifiedTime, Qt::UserRole); - row_data.insert(COLUMN_FILE_MODIFIED_TIME, item); - - QDateTime creationTime = pTrack->getFileCreationTime().toLocalTime(); - item = new QStandardItem(creationTime.toString(Qt::DefaultLocaleShortDate)); - item->setToolTip(item->text()); - item->setData(creationTime, Qt::UserRole); - row_data.insert(COLUMN_FILE_CREATION_TIME, item); - - const mixxx::ReplayGain replayGain(pTrack->getReplayGain()); - item = new QStandardItem( - mixxx::ReplayGain::ratioToString(replayGain.getRatio())); - item->setToolTip(item->text()); - item->setData(item->text(), Qt::UserRole); - row_data.insert(COLUMN_REPLAYGAIN, item); + const QString filepath = fileIt.next(); + { + TrackCacheLocker cacheLocker( + TrackCache::instance().lookupOrCreateTemporaryForFile( + filepath, thisPath.token())); + auto pTrack = cacheLocker.getTrack(); + if (!pTrack) { + qWarning() << "Skipping inaccessible file" + << filepath; + continue; + } + + // Import metadata from file + SoundSourceProxy(pTrack).updateTrackFromSource(); + + item = new QStandardItem(pTrack->getFileName()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_FILENAME, item); + + item = new QStandardItem(pTrack->getArtist()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_ARTIST, item); + + item = new QStandardItem(pTrack->getTitle()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_TITLE, item); + + item = new QStandardItem(pTrack->getAlbum()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_ALBUM, item); + + item = new QStandardItem(pTrack->getAlbumArtist()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_ALBUMARTIST, item); + + item = new QStandardItem(pTrack->getTrackNumber()); + item->setToolTip(item->text()); + item->setData(item->text().toInt(), Qt::UserRole); + row_data.insert(COLUMN_TRACK_NUMBER, item); + + const QString year(pTrack->getYear()); + item = new YearItem(year); + item->setToolTip(year); + // The year column is sorted according to the numeric calendar year + item->setData(mixxx::TrackMetadata::parseCalendarYear(year), Qt::UserRole); + row_data.insert(COLUMN_YEAR, item); + + item = new QStandardItem(pTrack->getGenre()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_GENRE, item); + + item = new QStandardItem(pTrack->getComposer()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_COMPOSER, item); + + item = new QStandardItem(pTrack->getGrouping()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_GROUPING, item); + + item = new QStandardItem(pTrack->getComment()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_COMMENT, item); + + QString duration = pTrack->getDurationText(mixxx::Duration::Precision::SECONDS); + item = new QStandardItem(duration); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_DURATION, item); + + item = new QStandardItem(pTrack->getBpmText()); + item->setToolTip(item->text()); + item->setData(pTrack->getBpm(), Qt::UserRole); + row_data.insert(COLUMN_BPM, item); + + item = new QStandardItem(pTrack->getKeyText()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_KEY, item); + + item = new QStandardItem(pTrack->getType()); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_TYPE, item); + + item = new QStandardItem(pTrack->getBitrateText()); + item->setToolTip(item->text()); + item->setData(pTrack->getBitrate(), Qt::UserRole); + row_data.insert(COLUMN_BITRATE, item); + + QString location = pTrack->getLocation(); + QString nativeLocation = QDir::toNativeSeparators(location); + item = new QStandardItem(nativeLocation); + item->setToolTip(nativeLocation); + item->setData(location, Qt::UserRole); + row_data.insert(COLUMN_NATIVELOCATION, item); + + QDateTime modifiedTime = pTrack->getFileModifiedTime().toLocalTime(); + item = new QStandardItem(modifiedTime.toString(Qt::DefaultLocaleShortDate)); + item->setToolTip(item->text()); + item->setData(modifiedTime, Qt::UserRole); + row_data.insert(COLUMN_FILE_MODIFIED_TIME, item); + + QDateTime creationTime = pTrack->getFileCreationTime().toLocalTime(); + item = new QStandardItem(creationTime.toString(Qt::DefaultLocaleShortDate)); + item->setToolTip(item->text()); + item->setData(creationTime, Qt::UserRole); + row_data.insert(COLUMN_FILE_CREATION_TIME, item); + + const mixxx::ReplayGain replayGain(pTrack->getReplayGain()); + item = new QStandardItem( + mixxx::ReplayGain::ratioToString(replayGain.getRatio())); + item->setToolTip(item->text()); + item->setData(item->text(), Qt::UserRole); + row_data.insert(COLUMN_REPLAYGAIN, item); + } // implicitly release track pointer and unlock cache rows.append(row_data); ++row; diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index b65d52455c40..823b617845ab 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -30,15 +30,13 @@ #include "track/beats.h" #include "track/keyfactory.h" #include "track/keyutils.h" +#include "track/trackcache.h" #include "track/tracknumbers.h" #include "util/assert.h" #include "util/file.h" #include "util/timer.h" #include "util/math.h" -QHash TrackDAO::m_sTracks; -QMutex TrackDAO::m_sTracksMutex; - enum { UndefinedRecordIndex = -2 }; RecentTrackCacheItem::RecentTrackCacheItem( @@ -47,22 +45,9 @@ RecentTrackCacheItem::RecentTrackCacheItem( DEBUG_ASSERT(m_pTrack); } -RecentTrackCacheItem::~RecentTrackCacheItem() { - // qDebug() << "~RecentTrackCacheItem" << m_pTrack << "ID" - // << m_pTrack->getId() << m_pTrack->getInfo(); - - // Signal to TrackDAO::saveTrack(TrackPointer) to save the track if it is - // dirty. Does not drop the strong reference to the track until saveTrack - // completes. - if (m_pTrack->isDirty()) { - emit(saveTrack(m_pTrack)); - } -} - // The number of recently used tracks to cache strong references to at -// once. Once the n+1'th track is created, the TrackDAO's QCache deletes its -// RecentTrackCacheItem which signals to TrackDAO::saveTrack(TrackPointer) to save the -// track and drop the strong reference. The recent tracks cache basically +// once. Once the n+1'th track is created, the TrackDAO's QCache deletes it +// and drops the strong reference. The recent tracks cache basically // functions to prevent repeated getTrack() calls for the same track from // repeatedly deserializing / serializing a track to the database since this is // expensive. @@ -91,38 +76,11 @@ TrackDAO::~TrackDAO() { } void TrackDAO::finish() { - // Save all tracks that haven't been saved yet. - QMutexLocker locker(&m_sTracksMutex); - QHashIterator it(m_sTracks); - while (it.hasNext()) { - it.next(); - // Cast from TrackWeakPointer to TrackPointer. If the track still exists - // then pTrack will be non-nullptr. If the track is dirty then save it. - TrackPointer pTrack = TrackPointer(it.value()); - if (pTrack) { - if (pTrack->isDirty()) { - saveTrack(pTrack); - } - - // When this reference expires, tell the track to delete itself - // rather than signalling to TrackDAO. - pTrack->setDeleteOnReferenceExpiration(true); - } - } - m_sTracks.clear(); - locker.unlock(); + qDebug() << "TrackDAO::finish()"; - // Clear cache, so all cached tracks without other references are deleted. - // Will queue a bunch of saveTrack calls for every RecentTrackCacheItem in - // m_recentTracksCache. The event loop is already stopped at this point so - // the queued signals won't be processed. + // Drop all strong references of recently loaded tracks. clearCache(); - // Clearing the cache should queue a bunch of save events for tracks still - // in the recent tracks cache. Deliver those events manually since we don't - // have an event loop running anymore. - QCoreApplication::sendPostedEvents(this, 0); - // clear out played information on exit // crash prevention: if mixxx crashes, played information will be maintained qDebug() << "Clearing played information for this session"; @@ -248,12 +206,6 @@ bool TrackDAO::trackExistsInDatabase(const QString& absoluteFilePath) { return getTrackId(absoluteFilePath).isValid(); } -void TrackDAO::saveTrack(const TrackPointer& pTrack) { - if (pTrack) { - saveTrack(pTrack.get()); - } -} - void TrackDAO::saveTrack(Track* pTrack) { DEBUG_ASSERT(nullptr != pTrack); @@ -655,32 +607,39 @@ TrackId TrackDAO::addTracksAddTrack(const TrackPointer& pTrack, bool unremove) { DEBUG_ASSERT(!m_tracksAddedSet.contains(trackId)); m_tracksAddedSet.insert(trackId); } + + return trackId; +} + +TrackPointer TrackDAO::addTracksAddTrack(TrackCacheResolver&& cacheResolver, bool unremove) { + const TrackPointer pTrack(cacheResolver.getTrack()); + if (!pTrack) { + qWarning() << "TrackDAO::addTracksAddTrack: Track not found" + << cacheResolver.getTrackRef(); + return TrackPointer(); + } + + const TrackId trackId(addTracksAddTrack(pTrack, unremove)); if (trackId.isValid()) { - pTrack->initId(trackId); + cacheResolver.updateTrackId(trackId); + cacheResolver.unlockCache(); + // Only newly inserted tracks must be marked as clean! + // Existing or unremoved tracks have not been added to + // m_tracksAddedSet and will keep their dirty flag unchanged. + if (m_tracksAddedSet.contains(trackId)) { + pTrack->markClean(); + } + return pTrack; } else { qWarning() << "TrackDAO::addTracksAddTrack:" << "Failed to add track to database" << pTrack->getLocation(); + // TrackCache will be unlocked implicitly + return TrackPointer(); } - // When exiting this function the trackId obtained from the - // database must match the id of the track, no matter if the - // track has been inserted or updated. If the trackId is still - // invalid adding the track to the library must have failed - // and the id of the track should also still be invalid. - DEBUG_ASSERT(pTrack->getId() == trackId); - // Only newly inserted tracks must be marked as clean! - // Existing or unremoved tracks have not been added to - // m_tracksAddedSet and will keep their dirty flag unchanged. - if (m_tracksAddedSet.contains(trackId)) { - pTrack->markClean(); - } - return trackId; } TrackPointer TrackDAO::addTracksAddFile(const QFileInfo& fileInfo, bool unremove) { - // TODO(uklotzde): Resolve Track through TrackCache - TrackPointer pTrack(Track::newTemporary(fileInfo)); - // Check that track is a supported extension. // TODO(uklotzde): The following check can be skipped if // the track is already in the library. A refactoring is @@ -688,7 +647,17 @@ TrackPointer TrackDAO::addTracksAddFile(const QFileInfo& fileInfo, bool unremove if (!SoundSourceProxy::isFileSupported(fileInfo)) { qWarning() << "TrackDAO::addTracksAddFile:" << "Unsupported file type" - << pTrack->getLocation(); + << TrackRef::location(fileInfo); + return TrackPointer(); + } + + TrackCacheResolver cacheResolver( + TrackCache::instance().resolve(fileInfo)); + TrackPointer pTrack(cacheResolver.getTrack()); + if (!pTrack) { + qWarning() << "TrackDAO::addTracksAddFile:" + << "File not found" + << TrackRef::location(fileInfo); return TrackPointer(); } @@ -703,16 +672,7 @@ TrackPointer TrackDAO::addTracksAddFile(const QFileInfo& fileInfo, bool unremove // if parsing the metadata from file succeeded or failed. } - const TrackId trackId(addTracksAddTrack(pTrack, unremove)); - - // TODO(uklotzde): Update TrackCache with trackId - - if (trackId.isValid()) { - DEBUG_ASSERT(pTrack->getId() == trackId); - return pTrack; - } else { - return TrackPointer(); - } + return addTracksAddTrack(std::move(cacheResolver), unremove); } TrackPointer TrackDAO::addSingleTrack(const QFileInfo& fileInfo, bool unremove) { @@ -971,33 +931,6 @@ void TrackDAO::afterPurgingTracks( emit(forceModelUpdate()); } -void TrackDAO::slotTrackReferenceExpired(Track* pTrack) { - // Should not be possible. - VERIFY_OR_DEBUG_ASSERT(pTrack != nullptr) { - return; - } - - // qDebug() << "TrackDAO::slotTrackReferenceExpired" << pTrack << "ID" - // << pTrack->getId() << pTrack->getInfo(); - // This is a private slot that is connected to TIO's created by this - // TrackDAO. It is a way for the track to notify us once its reference count - // has dropped to zero. This is invoked via a QueuedConnection so even - // though the track reference count can drop to zero in any thread this - // handler runs in the main thread. - - // Save the track if it is dirty. - if (pTrack->isDirty()) { - saveTrack(pTrack); - } - - // Delete Track from weak reference cache - m_sTracksMutex.lock(); - m_sTracks.remove(pTrack->getId()); - m_sTracksMutex.unlock(); - - delete pTrack; -} - namespace { typedef bool (*TrackPopulatorFn)(const QSqlRecord& record, const int column, @@ -1233,14 +1166,7 @@ void TrackDAO::cacheRecentTrack( std::unique_ptr pCacheItem = std::make_unique(pTrack); m_recentTracksCache.insert(trackId, pCacheItem.get()); - RecentTrackCacheItem* pCachedItem = pCacheItem.release(); // m_recentTracksCache has taken ownership - - // Queued connection. We are not in a rush to process cache - // expirations and it can produce dangerous signal loops. - // See: https://bugs.launchpad.net/mixxx/+bug/1365708 - connect(pCachedItem, SIGNAL(saveTrack(TrackPointer)), - this, SLOT(saveTrack(TrackPointer)), - Qt::QueuedConnection); + pCacheItem.release(); // m_recentTracksCache has taken ownership } TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const { @@ -1338,13 +1264,17 @@ TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const { const QString trackLocation(queryRecord.value(0).toString()); const QFileInfo fileInfo(trackLocation); - // TODO(uklotzde): Resolve through TrackCache - TrackPointer pTrack( - new Track(fileInfo, SecurityTokenPointer(), trackId), - Track::onTrackReferenceExpired); - - // TIO already stats the file to see if it exists, what its length is, - // etc. So don't bother setting it. + TrackCacheResolver cacheResolver( + TrackCache::instance().resolve(trackId, fileInfo)); + TrackPointer pTrack(cacheResolver.getTrack()); + VERIFY_OR_DEBUG_ASSERT(pTrack) { + return pTrack; + } + if (cacheResolver.getTrackCacheLookupResult() == TrackCacheLookupResult::HIT) { + // Must not reload cached tracks from database! + return pTrack; + } + DEBUG_ASSERT(cacheResolver.getTrackCacheLookupResult() == TrackCacheLookupResult::MISS); // For every column run its populator to fill the track in with the data. bool shouldDirty = false; @@ -1352,7 +1282,9 @@ TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const { TrackPopulatorFn populator = columns[i].populator; if (populator != nullptr) { // If any populator says the track should be dirty then we dirty it. - shouldDirty = (*populator)(queryRecord, i, pTrack) || shouldDirty; + if ((*populator)(queryRecord, i, pTrack)) { + shouldDirty = true; + } } } @@ -1410,23 +1342,14 @@ TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const { connect(pTrack.get(), SIGNAL(changed(Track*)), this, SLOT(slotTrackChanged(Track*)), Qt::DirectConnection); - // Queued connection. We are not in a rush to process reference - // count expirations and it can produce dangerous signal loops. - // See: https://bugs.launchpad.net/mixxx/+bug/1365708 - connect(pTrack.get(), SIGNAL(referenceExpired(Track*)), - this, SLOT(slotTrackReferenceExpired(Track*)), - Qt::QueuedConnection); - - m_sTracksMutex.lock(); - // Automatic conversion to a weak pointer - m_sTracks[trackId] = pTrack; - //int trackCount = m_sTracks.count(); - m_sTracksMutex.unlock(); - //qDebug() << "TrackDAO::m_sTracks.count() =" << trackCount; // Insert the loaded track into the recent tracks cache cacheRecentTrack(trackId, pTrack); + // Finish the caching operation to release all locks before + // emitting any signals. + cacheResolver.unlockCache(); + // BaseTrackCache cares about track trackDirty/trackClean notifications // from TrackDAO that are triggered by the track itself. But the preceding // track modifications above have been sent before the TrackDAO has been @@ -1451,34 +1374,18 @@ TrackPointer TrackDAO::getTrack(TrackId trackId, const bool cacheOnly) const { return pTrackCacheItem->getTrack(); } - TrackPointer pTrack; - // Next, check the weak-reference cache to see if the track still has a - // strong reference somewhere. It's possible that something is currently - // using this track so its reference count is non-zero despite it not being - // present in the track cache. - QMutexLocker locker(&m_sTracksMutex); - QHash::iterator it = m_sTracks.find(trackId); - if (it != m_sTracks.end()) { - //qDebug() << "Returning cached TIO for track" << id; - pTrack = TrackPointer(it.value()); - } - // Unlock the track cache mutex. Otherwise we can deadlock. - locker.unlock(); - - // If we were able to convert a weak reference to a strong reference then - // re-insert it into the recent tracks cache so that its least-recently-used - // tracking is accurate. - if (pTrack) { - // If we were able to convert a weak reference to a strong reference then - // re-insert it into the recent tracks cache so that its least-recently-used - // tracking is accurate. - cacheRecentTrack(trackId, pTrack); - } else { + // strong reference somewhere. + TrackCacheLocker cacheLocker( + TrackCache::instance().lookupById(trackId)); + TrackPointer pTrack(cacheLocker.getTrack()); + if (!pTrack) { // Cache miss if (!cacheOnly) { // Deserialize the track from the database. pTrack = getTrackFromDB(trackId); + // The loaded track has already been inserted into + // the recent tracks cache. } } return pTrack; @@ -1802,8 +1709,7 @@ bool TrackDAO::detectMovedTracks(QSet* pTracksMovedSetOld, } void TrackDAO::clearCache() { - // Triggers a deletion of all the RecentTrackCacheItems which in turn calls - // saveTrack(TrackPointer) for all of the tracks in the recent tracks cache. + // Drops all strong references from the recent tracks cache. m_recentTracksCache.clear(); } diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 6cc4e5996ab1..6237a9925ddd 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -4,11 +4,8 @@ #include #include #include -#include #include #include -#include -#include #include #include @@ -23,28 +20,20 @@ class PlaylistDAO; class AnalysisDao; class CueDAO; class LibraryHashDAO; +class TrackCollection; +class TrackCacheResolver; // Holds a strong reference to a track while it is in the "recent tracks" -// cache. Once it expires from the cache it signals to -// TrackDAO::saveTrack(TrackPointer) to save the track if it is dirty and then -// drops the strong reference. This prevents a race condition caused by caching -// TrackPointers themselves within the QCache by holding a strong reference to -// TrackPointer (and thereby serving it out of the weak pointer track cache) up -// until the track has been saved to the database. -class RecentTrackCacheItem : public QObject { - Q_OBJECT +// cache. +class RecentTrackCacheItem final { public: explicit RecentTrackCacheItem( const TrackPointer& pTrack); - virtual ~RecentTrackCacheItem(); const TrackPointer& getTrack() const { return m_pTrack; } - signals: - void saveTrack(TrackPointer pTrack); - private: TrackPointer m_pTrack; }; @@ -85,6 +74,7 @@ class TrackDAO : public QObject, public virtual DAO { void addTracksPrepare(); TrackPointer addTracksAddFile(const QFileInfo& fileInfo, bool unremove); + TrackPointer addTracksAddTrack(TrackCacheResolver&& /*r-value ref*/ cacheResolver, bool unremove); TrackId addTracksAddTrack(const TrackPointer& pTrack, bool unremove); void addTracksFinish(bool rollback = false); @@ -145,13 +135,7 @@ class TrackDAO : public QObject, public virtual DAO { void forceModelUpdate(); public slots: - // The public interface to the TrackDAO requires a TrackPointer so that we - // have a guarantee that the track will not be deleted while we are working - // on it. However, private parts of TrackDAO can use the raw saveTrack(TIO*) - // call. - void saveTrack(const TrackPointer& pTrack); - - // Clears the cached Tracks, which can be useful when the + // Clears the cached TrackInfoObjects, which can be useful when the // underlying database tables change (eg. during a library rescan, // we might detect that a track has been moved and modify the update // the tables directly.) @@ -165,11 +149,11 @@ class TrackDAO : public QObject, public virtual DAO { void slotTrackDirty(Track* pTrack); void slotTrackChanged(Track* pTrack); void slotTrackClean(Track* pTrack); - void slotTrackReferenceExpired(Track* pTrack); private: TrackPointer getTrackFromDB(TrackId trackId) const; + friend class TrackCollection; void saveTrack(Track* pTrack); bool updateTrack(Track* pTrack); @@ -181,10 +165,6 @@ class TrackDAO : public QObject, public virtual DAO { LibraryHashDAO& m_libraryHashDao; UserSettingsPointer m_pConfig; - // Mutex that protects m_sTracks. - static QMutex m_sTracksMutex; - // Weak pointer cache of active tracks. - static QHash m_sTracks; void cacheRecentTrack( TrackId trackId, diff --git a/src/library/library.cpp b/src/library/library.cpp index b02dd49e28a5..2c5599a31451 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -421,3 +421,7 @@ void Library::slotSetTrackTableRowHeight(int rowHeight) { m_iTrackTableRowHeight = rowHeight; emit(setTrackTableRowHeight(rowHeight)); } + +void Library::evictTrack(Track* pTrack) { + m_pTrackCollection->saveTrack(pTrack); +} diff --git a/src/library/library.h b/src/library/library.h index 6737528b76af..3c6f1cb9440b 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -13,7 +13,7 @@ #include #include "preferences/usersettings.h" -#include "track/track.h" +#include "track/trackcache.h" #include "recording/recordingmanager.h" #include "analysisfeature.h" #include "library/coverartcache.h" @@ -36,7 +36,8 @@ class LibraryControl; class KeyboardEventFilter; class PlayerManagerInterface; -class Library : public QObject { +class Library: public QObject, + public virtual /*implements*/ TrackCacheEvictor { Q_OBJECT public: @@ -80,6 +81,8 @@ class Library : public QObject { static const int kDefaultRowHeightPx; + void evictTrack(Track* pTrack) override; + public slots: void slotShowTrackModel(QAbstractItemModel* model); void slotSwitchToView(const QString& view); diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index 73dbcc2929dd..96ae7a0bb310 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -2,7 +2,7 @@ #include "library/trackcollection.h" -#include "track/track.h" +#include "track/trackcache.h" #include "util/logger.h" #include "util/db/sqltransaction.h" @@ -67,6 +67,9 @@ void TrackCollection::relocateDirectory(QString oldDir, QString newDir) { QSet movedIds( m_directoryDao.relocateDirectory(oldDir, newDir)); + // Discard all cached tracks + TrackCache::instance().evictAll(); + // Clear cache to that all TIO with the old dir information get updated m_trackDao.clearCache(); m_trackDao.databaseTracksMoved(std::move(movedIds), QSet()); @@ -327,3 +330,7 @@ bool TrackCollection::updateAutoDjCrate( crate.setAutoDjSource(isAutoDjSource); return updateCrate(crate); } + +void TrackCollection::saveTrack(Track* pTrack) { + m_trackDao.saveTrack(pTrack); +} diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index 97b2c769b44b..f98f98c6907e 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -80,6 +80,8 @@ class TrackCollection : public QObject, bool updateAutoDjCrate(CrateId crateId, bool isAutoDjSource); + void saveTrack(Track* pTrack); + signals: void crateInserted(CrateId id); void crateUpdated(CrateId id); diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 8b1af5e1d7f2..7c41dfbfb95c 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -288,6 +288,9 @@ void MixxxMainWindow::initialize(QApplication* pApp, const CmdlineArgs& args) { m_pRecordingManager); m_pPlayerManager->bindToLibrary(m_pLibrary); + // Create the singular TrackCache instance + TrackCache::createInstance(m_pLibrary); + launchProgress(40); // Get Music dir @@ -567,12 +570,21 @@ void MixxxMainWindow::finalize() { qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting PlayerManager"; delete m_pPlayerManager; + // Evict all remaining tracks from the cache to trigger + // updating of modified tracks. + TrackCache::instance().evictAll(); + // Delete the library after the view so there are no dangling pointers to // the data models. // Depends on RecordingManager and PlayerManager qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting Library"; delete m_pLibrary; + // The TrackCache singleton must be destroyed immediately + // after the library has been destroyed! + qDebug() << "Destroying TrackCache" << t.elapsed(false); + TrackCache::destroyInstance(); + qDebug() << t.elapsed(false).debugMillisWithUnit() << "closing database connection(s)"; m_pDbConnectionPool->destroyThreadLocalConnection(); m_pDbConnectionPool.reset(); // should drop the last reference diff --git a/src/test/coverartcache_test.cpp b/src/test/coverartcache_test.cpp index 960f2fc92c0d..c2602df532d1 100644 --- a/src/test/coverartcache_test.cpp +++ b/src/test/coverartcache_test.cpp @@ -5,12 +5,12 @@ #include "library/coverartcache.h" #include "library/coverartutils.h" #include "library/trackcollection.h" -#include "test/mixxxtest.h" +#include "test/librarytest.h" #include "sources/soundsourceproxy.h" // first inherit from MixxxTest to construct a QApplication to be able to // construct the default QPixmap in CoverArtCache -class CoverArtCacheTest : public MixxxTest, public CoverArtCache { +class CoverArtCacheTest : public LibraryTest, public CoverArtCache { protected: void loadCoverFromMetadata(QString trackLocation) { CoverInfo info; diff --git a/src/test/librarytest.h b/src/test/librarytest.h index 96ce5cd377d8..cf4dbc31e292 100644 --- a/src/test/librarytest.h +++ b/src/test/librarytest.h @@ -7,9 +7,17 @@ #include "library/trackcollection.h" #include "util/db/dbconnectionpooler.h" #include "util/db/dbconnectionpooled.h" +#include "track/trackcache.h" -class LibraryTest : public MixxxTest { +class LibraryTest : public MixxxTest, + public virtual /*implements*/ TrackCacheEvictor { + + public: + void evictTrack(Track* pTrack) override { + m_trackCollection.saveTrack(pTrack); + } + protected: LibraryTest() : m_mixxxDb(config()), @@ -18,8 +26,11 @@ class LibraryTest : public MixxxTest { m_trackCollection(config()) { MixxxDb::initDatabaseSchema(m_dbConnection); m_trackCollection.connectDatabase(m_dbConnection); + TrackCache::createInstance(this); } ~LibraryTest() override { + TrackCache::instance().evictAll(); + TrackCache::destroyInstance(); m_trackCollection.disconnectDatabase(); } diff --git a/src/track/track.cpp b/src/track/track.cpp index 7a7f3e1caeda..6bd7643e60a9 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -45,7 +45,6 @@ Track::Track( TrackId trackId) : m_fileInfo(fileInfo), m_pSecurityToken(openSecurityToken(m_fileInfo, pSecurityToken)), - m_bDeleteOnReferenceExpiration(false), m_qMutex(QMutex::Recursive), m_record(trackId), m_bDirty(false), @@ -77,24 +76,6 @@ TrackPointer Track::newDummy( return TrackPointer(pTrack); } -// static -void Track::onTrackReferenceExpired(Track* pTrack) { - VERIFY_OR_DEBUG_ASSERT(pTrack != nullptr) { - return; - } - //qDebug() << "Track::onTrackReferenceExpired" - // << pTrack << pTrack->getId() << pTrack->getInfo(); - if (pTrack->m_bDeleteOnReferenceExpiration) { - delete pTrack; - } else { - emit(pTrack->referenceExpired(pTrack)); - } -} - -void Track::setDeleteOnReferenceExpiration(bool deleteOnReferenceExpiration) { - m_bDeleteOnReferenceExpiration = deleteOnReferenceExpiration; -} - void Track::setTrackMetadata( mixxx::TrackMetadata trackMetadata, QDateTime metadataSynchronized) { diff --git a/src/track/track.h b/src/track/track.h index b870a62162b8..6035e99cd000 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -298,15 +298,6 @@ class Track : public QObject { // files that are still opened for reading. void markForMetadataExport(); - // Called when the shared pointer reference count for a library TrackPointer - // drops to zero. - static void onTrackReferenceExpired(Track* pTrack); - - // Set whether the track should delete itself when its reference count drops - // to zero. This happens during shutdown when TrackDAO has already been - // destroyed. - void setDeleteOnReferenceExpiration(bool deleteOnReferenceExpiration); - public slots: void slotCueUpdated(); @@ -324,7 +315,6 @@ class Track : public QObject { void changed(Track* pTrack); void dirty(Track* pTrack); void clean(Track* pTrack); - void referenceExpired(Track* pTrack); private slots: void slotBeatsUpdated(); @@ -335,7 +325,7 @@ class Track : public QObject { TrackId trackId); // Set a unique identifier for the track. Only used by - // TrackDAO! + // TrackCacheResolver! void initId(TrackId id); // write-once // Set whether the TIO is dirty or not and unlock before emitting @@ -367,10 +357,6 @@ class Track : public QObject { const SecurityTokenPointer m_pSecurityToken; - // Whether the track should delete itself when its reference count drops to - // zero. Used for cleaning up after shutdown. - volatile bool m_bDeleteOnReferenceExpiration; - // Mutex protecting access to object mutable QMutex m_qMutex; @@ -397,6 +383,8 @@ class Track : public QObject { QAtomicInt m_analyzerProgress; // in 0.1% friend class TrackDAO; + friend class TrackCache; + friend class TrackCacheResolver; friend class SoundSourceProxy; }; diff --git a/src/track/trackcache.cpp b/src/track/trackcache.cpp new file mode 100644 index 000000000000..91c4a24985d7 --- /dev/null +++ b/src/track/trackcache.cpp @@ -0,0 +1,524 @@ +#include "track/trackcache.h" + +#include "util/assert.h" + + +namespace { + +inline +TrackRef createTrackRef(const Track& track) { + return TrackRef::fromFileInfo(track.getFileInfo(), track.getId()); +} + +} // anonymous namespace + +TrackCacheLocker::TrackCacheLocker() + : m_pCacheMutex(nullptr), + m_lookupResult(TrackCacheLookupResult::NONE) { + lockCache(); + // Verify consistency after the cache has been locked + DEBUG_ASSERT(TrackCache::instance().verifyConsistency()); +} + +TrackCacheLocker::TrackCacheLocker( + TrackCacheLocker&& moveable) + : m_pCacheMutex(std::move(moveable.m_pCacheMutex)), + m_lookupResult(std::move(moveable.m_lookupResult)), + m_trackRef(std::move(moveable.m_trackRef)), + m_pTrack(std::move(moveable.m_pTrack)) { + moveable.m_pCacheMutex = nullptr; +} + +TrackCacheLocker& TrackCacheLocker::operator=( + TrackCacheLocker&& moveable) { + if (this != &moveable) { + m_pCacheMutex = std::move(moveable.m_pCacheMutex); + moveable.m_pCacheMutex = nullptr; + m_lookupResult = std::move(moveable.m_lookupResult); + m_trackRef = std::move(moveable.m_trackRef); + m_pTrack = std::move(moveable.m_pTrack); + } + return *this; +} + +TrackCacheLocker::TrackCacheLocker( + TrackCacheLocker&& moveable, + TrackCacheLookupResult lookupResult, + TrackRef trackRef, + TrackPointer pTrack) + : m_pCacheMutex(moveable.m_pCacheMutex), + m_lookupResult(lookupResult), + m_trackRef(std::move(trackRef)), + m_pTrack(std::move(pTrack)) { + moveable.m_pCacheMutex = nullptr; + // Class invariants + DEBUG_ASSERT((TrackCacheLookupResult::NONE != m_lookupResult) || !m_pTrack); +} + +TrackCacheLocker::~TrackCacheLocker() { + unlockCache(); +} + +void TrackCacheLocker::lockCache() { + DEBUG_ASSERT(nullptr == m_pCacheMutex); + QMutex* pCacheMutex = &TrackCache::instance().m_mutex; + pCacheMutex->lock(); + m_pCacheMutex = pCacheMutex; +} + +void TrackCacheLocker::unlockCache() { + if (nullptr != m_pCacheMutex) { + // Verify consistency before unlocking the cache + DEBUG_ASSERT(TrackCache::instance().verifyConsistency()); + m_pCacheMutex->unlock(); + m_pCacheMutex = nullptr; + } +} + +TrackCacheResolver::TrackCacheResolver() { +} + +TrackCacheResolver::TrackCacheResolver( + TrackCacheResolver&& moveable, + TrackCacheLookupResult lookupResult, + TrackRef trackRef, + TrackPointer pTrack) + : TrackCacheLocker( + std::move(moveable), + std::move(lookupResult), + std::move(trackRef), + std::move(pTrack)) { +} + +void TrackCacheResolver::updateTrackId(TrackId trackId) { + DEBUG_ASSERT(nullptr != m_pCacheMutex); // cache is still locked + DEBUG_ASSERT(TrackCacheLookupResult::NONE != m_lookupResult); + DEBUG_ASSERT(m_pTrack); + DEBUG_ASSERT(trackId.isValid()); + m_trackRef = TrackCache::instance().updateTrackIdInternal( + m_pTrack, + m_trackRef, + trackId); + DEBUG_ASSERT(m_trackRef.getId() == trackId); + m_pTrack->initId(trackId); + DEBUG_ASSERT(createTrackRef(*m_pTrack) == m_trackRef); +} + + +//static +TrackCache* volatile TrackCache::s_pInstance = nullptr; + +//static +void TrackCache::createInstance(TrackCacheEvictor* pEvictor) { + DEBUG_ASSERT(s_pInstance == nullptr); + s_pInstance = new TrackCache(pEvictor); +} + +//static +void TrackCache::destroyInstance() { + DEBUG_ASSERT(s_pInstance != nullptr); + TrackCache* pInstance = s_pInstance; + s_pInstance = nullptr; + delete pInstance; +} + +//static +void TrackCache::deleter(Track* pTrack) { + if (s_pInstance != nullptr) { + s_pInstance->evict(pTrack); + } + delete pTrack; +} + +TrackCache::TrackCache(TrackCacheEvictor* pEvictor) + : m_pEvictor(pEvictor), + m_mutex(QMutex::Recursive) { + DEBUG_ASSERT(m_pEvictor != nullptr); + DEBUG_ASSERT(verifyConsistency()); +} + +TrackCache::~TrackCache() { + DEBUG_ASSERT(verifyConsistency()); + // Verify that the cache is empty upon destruction + DEBUG_ASSERT(m_tracksById.empty()); + DEBUG_ASSERT(m_tracksByCanonicalLocation.empty()); +} + +bool TrackCache::verifyConsistency() const { + VERIFY_OR_DEBUG_ASSERT(m_tracksById.keys().size() == m_tracksById.uniqueKeys().size()) { + return false; + } + for (TracksById::const_iterator i(m_tracksById.begin()); i != m_tracksById.end(); ++i) { + const TrackRef trackRef((*i).ref); + const TrackId trackId(trackRef.getId()); + VERIFY_OR_DEBUG_ASSERT(trackId.isValid()) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(i.key() == trackId) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(createTrackRef(*(*i).plainPtr) == trackRef) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(1 == m_tracksById.count(trackId)) { + return false; + } + const QString canonicalLocation(trackRef.getCanonicalLocation()); + if (!canonicalLocation.isEmpty()) { + VERIFY_OR_DEBUG_ASSERT( + 1 == m_tracksByCanonicalLocation.count(canonicalLocation)) { + return false; + } + TracksByCanonicalLocation::const_iterator j( + m_tracksByCanonicalLocation.find(canonicalLocation)); + VERIFY_OR_DEBUG_ASSERT(m_tracksByCanonicalLocation.end() != j) { + return false; + } + VERIFY_OR_DEBUG_ASSERT((*j).ref == trackRef) { + return false; + } + } + } + for (TracksByCanonicalLocation::const_iterator i(m_tracksByCanonicalLocation.begin()); i != m_tracksByCanonicalLocation.end(); ++i) { + const TrackRef trackRef((*i).ref); + const TrackId trackId(trackRef.getId()); + const QString canonicalLocation(trackRef.getCanonicalLocation()); + VERIFY_OR_DEBUG_ASSERT(!canonicalLocation.isEmpty()) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(i.key() == canonicalLocation) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(createTrackRef(*(*i).plainPtr) == trackRef) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(1 == m_tracksByCanonicalLocation.count(canonicalLocation)) { + return false; + } + TracksById::const_iterator j( + m_tracksById.find(trackId)); + VERIFY_OR_DEBUG_ASSERT( + (m_tracksById.end() == j) || ((*j).ref == trackRef)) { + return false; + } + } + return true; +} + +TrackCacheLocker TrackCache::lookupById( + const TrackId& trackId) const { + TrackCacheLocker cacheLocker; + if (trackId.isValid()) { + const TrackPointer pTrack(lookupInternal(trackId)); + if (pTrack) { + cacheLocker.m_lookupResult = TrackCacheLookupResult::HIT; + cacheLocker.m_trackRef = createTrackRef(*pTrack); + cacheLocker.m_pTrack = pTrack; + } else { + cacheLocker.m_lookupResult = TrackCacheLookupResult::MISS; + } + } + return std::move(cacheLocker); +} + +TrackCacheLocker TrackCache::lookupOrCreateTemporaryForFile( + const QFileInfo& fileInfo, + const SecurityTokenPointer& pSecurityToken) const { + const TrackRef trackRef(TrackRef::fromFileInfo(fileInfo)); + TrackCacheLocker cacheLocker; + if (trackRef.isValid()) { + const TrackPointer pTrack(lookupInternal(trackRef)); + if (pTrack) { + cacheLocker.m_lookupResult = TrackCacheLookupResult::HIT; + cacheLocker.m_trackRef = createTrackRef(*pTrack); + cacheLocker.m_pTrack = pTrack; + } else { + cacheLocker.m_lookupResult = TrackCacheLookupResult::MISS; + cacheLocker.m_trackRef = trackRef; + cacheLocker.m_pTrack = + Track::newTemporary(fileInfo, pSecurityToken); + } + } + return std::move(cacheLocker); +} + +TrackPointer TrackCache::lookupInternal( + const TrackId& trackId) const { + const auto trackById(m_tracksById.find(trackId)); + if (m_tracksById.end() != trackById) { + // Cache hit + return TrackPointer((*trackById).weakPtr); + } else { + // Cache miss + return TrackPointer(); + } +} + +TrackPointer TrackCache::lookupInternal( + const TrackRef& trackRef) const { + if (trackRef.hasId()) { + return lookupInternal(trackRef.getId()); + } else { + const auto trackByCanonicalLocation( + m_tracksByCanonicalLocation.find(trackRef.getCanonicalLocation())); + if (m_tracksByCanonicalLocation.end() != trackByCanonicalLocation) { + // Cache hit + return TrackPointer((*trackByCanonicalLocation).weakPtr); + } else { + // Cache miss + return TrackPointer(); + } + } +} + +bool TrackCache::resolveInternal( + TrackCacheResolver* /*in/out*/ pCacheResolver, + TrackRef* /*out*/ pTrackRef, + const TrackId& /*in*/ trackId, + const QFileInfo& /*in*/ fileInfo) { + DEBUG_ASSERT(nullptr != pCacheResolver); + // Primary lookup by id (if available) + if (trackId.isValid()) { + qDebug() << "TrackCache:" + << "Resolving track by id" + << trackId; + const auto trackById(m_tracksById.find(trackId)); + if (m_tracksById.end() != trackById) { + // Cache hit + TrackRef resolvedTrackRef((*trackById).ref); + TrackPointer pResolvedTrack((*trackById).weakPtr); + if (pResolvedTrack) { + qDebug() << "TrackCache:" + << "Cache hit - found track by id" + << resolvedTrackRef; + *pCacheResolver = TrackCacheResolver( + std::move(*pCacheResolver), + TrackCacheLookupResult::HIT, + resolvedTrackRef, + pResolvedTrack); + return true; + } else { + // Explicitly evict the cached track before the deleter does it + qDebug() << "TrackCache:" + << "Cache hit - evicting zombie track" + << resolvedTrackRef; + Track* pEvictedTrack = evictInternal(resolvedTrackRef); + DEBUG_ASSERT((nullptr == pEvictedTrack) || + (pEvictedTrack == (*trackById).plainPtr)); + } + } + } + // Secondary lookup by canonical location + // The TrackRef is constructed now after the lookup by ID failed to + // avoid calculating the canonical file path if it is not needed. + TrackRef trackRef(TrackRef::fromFileInfo(fileInfo, trackId)); + if (trackRef.hasCanonicalLocation()) { + qDebug() << "TrackCache:" + << "Resolving track by canonical location" + << trackRef.getCanonicalLocation(); + const auto trackByCanonicalLocation( + m_tracksByCanonicalLocation.find( + trackRef.getCanonicalLocation())); + if (m_tracksByCanonicalLocation.end() != trackByCanonicalLocation) { + // Cache hit + TrackRef resolvedTrackRef((*trackByCanonicalLocation).ref); + TrackPointer pResolvedTrack((*trackByCanonicalLocation).weakPtr); + if (pResolvedTrack) { + qDebug() << "TrackCache:" + << "Cache hit - found track by canonical location" + << resolvedTrackRef; + // Consistency: Resolving by id must return the same result! + DEBUG_ASSERT(!resolvedTrackRef.hasId() || + (lookupInternal(resolvedTrackRef.getId()) == pResolvedTrack)); + *pCacheResolver = TrackCacheResolver( + std::move(*pCacheResolver), + TrackCacheLookupResult::HIT, + resolvedTrackRef, + pResolvedTrack); + return true; + } else { + // Explicitly evict the cached track before the deleter does it + qDebug() << "TrackCache:" + << "Cache hit - evicting zombie track" + << resolvedTrackRef; + Track* pEvictedTrack = evictInternal(resolvedTrackRef); + DEBUG_ASSERT((nullptr == pEvictedTrack) || + (pEvictedTrack == (*trackByCanonicalLocation).plainPtr)); + } + } + } + if (nullptr != pTrackRef) { + *pTrackRef = trackRef; + } + return false; +} + +TrackCacheResolver TrackCache::resolve( + const TrackId& trackId, + const QFileInfo& fileInfo, + const SecurityTokenPointer& pSecurityToken) { + TrackRef trackRef; + TrackCacheResolver cacheResolver; + if (resolveInternal(&cacheResolver, &trackRef, trackId, fileInfo)) { + DEBUG_ASSERT(cacheResolver.getTrackCacheLookupResult() == TrackCacheLookupResult::HIT); + return cacheResolver; + } + if (!trackRef.isValid()) { + DEBUG_ASSERT(cacheResolver.getTrackCacheLookupResult() == TrackCacheLookupResult::NONE); + qWarning() << "TrackCache:" + << "Cache miss - ignoring invalid track" + << trackRef; + return cacheResolver; + } + qDebug() << "TrackCache:" + << "Cache miss - inserting new track into cache" + << trackRef; + TrackPointer pTrack( + new Track( + std::move(fileInfo), + std::move(pSecurityToken), + std::move(trackId)), + deleter); + DEBUG_ASSERT(createTrackRef(*pTrack) == trackRef); + const Item item(trackRef, pTrack); + if (trackRef.hasId()) { + m_tracksById.insert( + trackRef.getId(), + item); + } + if (trackRef.hasCanonicalLocation()) { + m_tracksByCanonicalLocation.insert( + trackRef.getCanonicalLocation(), + item); + } + return TrackCacheResolver( + std::move(cacheResolver), + TrackCacheLookupResult::MISS, + std::move(trackRef), + std::move(pTrack)); +} + +TrackRef TrackCache::updateTrackIdInternal( + const TrackPointer& pTrack, + const TrackRef& trackRef, + TrackId trackId) { + DEBUG_ASSERT(trackId.isValid()); + if (trackRef.getId() != trackId) { + DEBUG_ASSERT(m_tracksById.end() == m_tracksById.find(trackId)); + TrackRef trackRefWithId(trackRef, trackId); + Item item(trackRefWithId, pTrack); + if (trackRef.hasId()) + m_tracksById.insert( + item.ref.getId(), + item); + m_tracksByCanonicalLocation.insert( + item.ref.getCanonicalLocation(), + item); + return trackRefWithId; + } + return trackRef; +} + +void TrackCache::evict( + Track* pTrack) { + DEBUG_ASSERT(pTrack != nullptr); + const TrackCacheLocker cacheLocker; + Track* pEvictedTrack = evictInternal( + TrackRef::fromFileInfo(pTrack->m_fileInfo, pTrack->m_record.getId())); + DEBUG_ASSERT((nullptr == pEvictedTrack) || (pEvictedTrack == pTrack)); + DEBUG_ASSERT(verifyConsistency()); +} + +TrackCache::Item TrackCache::purgeInternal( + const TrackRef& trackRef) { + qDebug() << "TrackCache:" + << "Purging track" + << trackRef; + + Item purgedItem; + DEBUG_ASSERT(!purgedItem.ref.isValid()); + if (trackRef.hasId()) { + const auto trackById(m_tracksById.find(trackRef.getId())); + if (m_tracksById.end() != trackById) { + purgedItem = *trackById; + m_tracksById.erase(trackById); + } + } + DEBUG_ASSERT( + !trackRef.hasCanonicalLocation() || + !purgedItem.ref.hasCanonicalLocation() || + (trackRef.getCanonicalLocation() == purgedItem.ref.getCanonicalLocation())); + const QString canonicalLocation( + purgedItem.ref.hasCanonicalLocation() ? + purgedItem.ref.getCanonicalLocation() : + trackRef.getCanonicalLocation()); + const auto trackByCanonicalLocation( + m_tracksByCanonicalLocation.find(canonicalLocation)); + if (m_tracksByCanonicalLocation.end() != trackByCanonicalLocation) { + if (purgedItem.ref.hasCanonicalLocation()) { + DEBUG_ASSERT(purgedItem == *trackByCanonicalLocation); + } else { + purgedItem = *trackByCanonicalLocation; + // Even if given trackRef does not have an id the found + // item might have one. The corresponding entry must be + // removed, otherwise we end up with an inconsistent + // cache! + DEBUG_ASSERT( + !trackRef.hasId() || + !purgedItem.ref.hasId() || + (trackRef.getId() == purgedItem.ref.getId())); + if (!trackRef.hasId() && purgedItem.ref.hasId()) { + m_tracksById.remove(purgedItem.ref.getId()); + } + } + m_tracksByCanonicalLocation.erase(trackByCanonicalLocation); + } + return purgedItem; +} + +Track* TrackCache::evictInternal( + const TrackRef& trackRef) { + qDebug() << "TrackCache:" + << "Evicting track" + << trackRef; + + const Item purgedItem(purgeInternal(trackRef)); + if (nullptr != purgedItem.plainPtr) { + // It can produce dangerous signal loops if the track is still + // sending signals while being saved! All references to this + // track have been dropped at this point, so there is no need + // to send any signals. + // See: https://bugs.launchpad.net/mixxx/+bug/1365708 + purgedItem.plainPtr->blockSignals(true); + + // Keep the cache locked while evicting the track object! + m_pEvictor->evictTrack(purgedItem.plainPtr); + } else { + qDebug() << "TrackCache:" + << "Uncached track cannot be evicted" + << trackRef; + } + return purgedItem.plainPtr; +} + +QList TrackCache::lookupAll() const { + QList allTracks; + TrackCacheLocker cacheLocker; + QList cacheItems( + m_tracksByCanonicalLocation.values()); + allTracks.reserve(cacheItems.size()); + for (Item cacheItem : cacheItems) { + TrackPointer pTrack(cacheItem.weakPtr); + if (pTrack) { + allTracks.append(pTrack); + } + } + return allTracks; +} + +void TrackCache::evictAll() { + QList allTracks(lookupAll()); + for (const TrackPointer& pTrack : allTracks) { + evict(pTrack.get()); + } +} diff --git a/src/track/trackcache.h b/src/track/trackcache.h new file mode 100644 index 000000000000..2a1fffa7a33d --- /dev/null +++ b/src/track/trackcache.h @@ -0,0 +1,219 @@ +#ifndef TRACKCACHE_H_ +#define TRACKCACHE_H_ + + +#include +#include +#include +#include + +#include "track/track.h" +#include "track/trackref.h" + + +class /*interface*/ TrackCacheEvictor { +public: + virtual void evictTrack(Track* pTrack) = 0; + +protected: + virtual ~TrackCacheEvictor() {} +}; + +enum class TrackCacheLookupResult { + NONE, + HIT, + MISS +}; + +class TrackCacheLocker { +public: + TrackCacheLocker(const TrackCacheLocker&) = delete; + TrackCacheLocker(TrackCacheLocker&& moveable); + virtual ~TrackCacheLocker(); + + TrackCacheLookupResult getTrackCacheLookupResult() const { + return m_lookupResult; + } + + const TrackRef& getTrackRef() const { + return m_trackRef; + } + + const TrackPointer& getTrack() const { + return m_pTrack; + } + + void updateResolvedTrackId(TrackId trackId); + + void unlockCache(); + + TrackCacheLocker& operator=(const TrackCacheLocker&) = delete; + +private: + friend class TrackCache; + + void lockCache(); + +protected: + TrackCacheLocker(); + TrackCacheLocker( + TrackCacheLocker&& moveable, + TrackCacheLookupResult lookupResult, + TrackRef trackRef, + TrackPointer pTrack); + + TrackCacheLocker& operator=(TrackCacheLocker&&); + + QMutex* m_pCacheMutex; + + TrackCacheLookupResult m_lookupResult; + + TrackRef m_trackRef; + TrackPointer m_pTrack; +}; + +class TrackCacheResolver final: public TrackCacheLocker { +public: + TrackCacheResolver(const TrackCacheResolver&) = delete; + TrackCacheResolver(TrackCacheResolver&&) = default; + + void updateTrackId(TrackId trackId); + + TrackCacheResolver& operator=(const TrackCacheResolver&) = delete; + +private: + friend class TrackCache; + TrackCacheResolver(); + TrackCacheResolver( + TrackCacheResolver&& moveable, + TrackCacheLookupResult lookupResult, + TrackRef trackRef, + TrackPointer pTrack); + + TrackCacheResolver& operator=(TrackCacheResolver&&) = default; +}; + +class TrackCache { +public: + static void createInstance(TrackCacheEvictor* pEvictor); + static void destroyInstance(); + + // Access the singular instance (singleton) + static TrackCache& instance() { + DEBUG_ASSERT(s_pInstance != nullptr); + return *s_pInstance; + } + + // Lookup an existing Track object in the cache. + TrackCacheLocker lookupById( + const TrackId& trackId) const; + + // Lookup an existing Track object in the cache or create + // a temporary object on cache miss. The temporary object for + // the file should be released before the cache is unlocked + // to prevent concurrent file access. + TrackCacheLocker lookupOrCreateTemporaryForFile( + const QFileInfo& fileInfo, + const SecurityTokenPointer& pSecurityToken = SecurityTokenPointer()) const; + + QList lookupAll() const; + + // Lookup an existing or create a new Track object + TrackCacheResolver resolve( + const QFileInfo& fileInfo, + const SecurityTokenPointer& pSecurityToken = SecurityTokenPointer()) { + return resolve(TrackId(), fileInfo, pSecurityToken); + } + TrackCacheResolver resolve( + const TrackId& trackId, + const QFileInfo& fileInfo, + const SecurityTokenPointer& pSecurityToken = SecurityTokenPointer()); + + void evictAll(); + +private: + friend class TrackCacheLocker; + friend class TrackCacheResolver; + + static TrackCache* volatile s_pInstance; + + static void deleter(Track* pTrack); + + class Item final { + public: + Item() + : plainPtr(nullptr) { + } + Item(const TrackRef trackRef, + const TrackPointer& pTrack) + : ref(trackRef), + weakPtr(pTrack), + plainPtr(pTrack.get()) { + } + + friend bool operator==( + const Item& lhs, + const Item& rhs) { + return (lhs.ref.getId() == rhs.ref.getId()) && + (lhs.ref.getCanonicalLocation() == rhs.ref.getCanonicalLocation()) && + (lhs.plainPtr == rhs.plainPtr) && + // std::weak_ptr does not provide operator==() so we need + // to implement it in terms of bidirectional owner_before() + // comparisons + !lhs.weakPtr.owner_before(rhs.weakPtr) && + !rhs.weakPtr.owner_before(lhs.weakPtr); + } + friend bool operator!=( + const Item& lhs, + const Item& rhs) { + return !(lhs == rhs); + } + + TrackRef ref; + TrackWeakPointer weakPtr; + Track* plainPtr; + }; + + explicit TrackCache(TrackCacheEvictor* pEvictor); + ~TrackCache(); + + // This function should only be called DEBUG_ASSERT statements + // to verify the class invariants during development. + bool verifyConsistency() const; + + TrackPointer lookupInternal( + const TrackId& trackId) const; + TrackPointer lookupInternal( + const TrackRef& trackRef) const; + + bool resolveInternal( + TrackCacheResolver* pCacheResolver, + TrackRef* pTrackRef, + const TrackId& trackId, + const QFileInfo& fileInfo); + + TrackRef updateTrackIdInternal( + const TrackPointer& pTrack, + const TrackRef& trackRef, + TrackId trackId); + + Item purgeInternal( + const TrackRef& trackRef); + + void evict( + Track* pTrack); + Track* evictInternal( + const TrackRef& trackRef); + + TrackCacheEvictor* m_pEvictor; + + mutable QMutex m_mutex; + + typedef QHash TracksById; + TracksById m_tracksById; + typedef QMap TracksByCanonicalLocation; + TracksByCanonicalLocation m_tracksByCanonicalLocation; +}; + + +#endif // TRACKCACHE_H_ From 14f1e1fb3fc2c429dbdaeff61224aba3bf6ea5a8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 15 Jun 2017 13:28:32 +0200 Subject: [PATCH 04/52] Workaround for missing defaulted move ctors/operators in VS 2015 https://msdn.microsoft.com/en-us/library/dn457344.aspx: "Visual Studio does not support defaulted move constructors or move-assignment operators as the C++11 standard mandates." --- src/track/trackcache.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/track/trackcache.h b/src/track/trackcache.h index 2a1fffa7a33d..20328867e3c1 100644 --- a/src/track/trackcache.h +++ b/src/track/trackcache.h @@ -75,7 +75,14 @@ class TrackCacheLocker { class TrackCacheResolver final: public TrackCacheLocker { public: TrackCacheResolver(const TrackCacheResolver&) = delete; +#if defined(_MSC_VER) && (_MSC_VER <= 1900) + // Visual Studio 2015 does not support default generated move constructors + TrackCacheResolver(TrackCacheResolver&& moveable) + : TrackCacheLocker(std::move(moveable)) { + } +#else TrackCacheResolver(TrackCacheResolver&&) = default; +#endif void updateTrackId(TrackId trackId); @@ -90,7 +97,15 @@ class TrackCacheResolver final: public TrackCacheLocker { TrackRef trackRef, TrackPointer pTrack); +#if defined(_MSC_VER) && (_MSC_VER <= 1900) + // Visual Studio 2015 does not support default generated move assignment operators + TrackCacheResolver& operator=(TrackCacheResolver&& moveable) { + TrackCacheLocker::operator=(std::move(moveable)); + return *this; + } +#else TrackCacheResolver& operator=(TrackCacheResolver&&) = default; +#endif }; class TrackCache { From d2ca2ca634436c6520517cb63e6597943885c874 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 27 Dec 2015 23:38:13 +0100 Subject: [PATCH 05/52] Browse view: Modify Track object instead of writing directly into files --- src/library/browse/browsetablemodel.cpp | 173 +++++++++++------------- src/library/browse/browsetablemodel.h | 6 +- 2 files changed, 86 insertions(+), 93 deletions(-) diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index 0f1318ce14c1..14d9212a6567 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -11,7 +11,6 @@ #include "mixer/playerinfo.h" #include "control/controlobject.h" #include "library/dao/trackdao.h" -#include "sources/metadatasourcetaglib.h" #include "util/dnd.h" BrowseTableModel::BrowseTableModel(QObject* parent, @@ -109,6 +108,14 @@ TrackPointer BrowseTableModel::getTrack(const QModelIndex& index) const { + "\n" +track_location); return TrackPointer(); } + // NOTE(uklotzde, 2015-12-08): Accessing tracks from the browse view + // will implicitly add them to the library. Is this really what we + // want here?? + // NOTE(rryan, 2015-12-27): This was intentional at the time since + // some people use Browse instead of the library and we want to let + // them edit the tracks in a way that persists across sessions + // and we didn't want to edit the files on disk by default + // unless the user opts in to that. return m_pTrackCollection->getTrackDAO() .getOrAddTrack(track_location, true, NULL); } @@ -233,108 +240,93 @@ Qt::ItemFlags BrowseTableModel::flags(const QModelIndex &index) const { QString track_location = getTrackLocation(index); int column = index.column(); - if (isTrackInUse(track_location) || - column == COLUMN_FILENAME || - column == COLUMN_BITRATE || - column == COLUMN_DURATION || - column == COLUMN_TYPE || - column == COLUMN_FILE_MODIFIED_TIME || - column == COLUMN_FILE_CREATION_TIME || - column == COLUMN_REPLAYGAIN) { + switch (column) { + case COLUMN_FILENAME: + case COLUMN_BITRATE: + case COLUMN_DURATION: + case COLUMN_TYPE: + case COLUMN_FILE_MODIFIED_TIME: + case COLUMN_FILE_CREATION_TIME: + case COLUMN_REPLAYGAIN: + // read-only return defaultFlags; - } else { + default: + // editable return defaultFlags | Qt::ItemIsEditable; } } -bool BrowseTableModel::isTrackInUse(const QString& track_location) const { - if (PlayerInfo::instance().isFileLoaded(track_location)) { - return true; - } - - if (m_pRecordingManager->getRecordingLocation() == track_location) { - return true; - } - - return false; -} - -bool BrowseTableModel::setData(const QModelIndex &index, const QVariant &value, - int role) { +bool BrowseTableModel::setData( + const QModelIndex& index, + const QVariant& value, + int role) { Q_UNUSED(role); - if (!index.isValid()) { + QStandardItem* item = itemFromIndex(index); + DEBUG_ASSERT(nullptr != item); + + TrackPointer pTrack(getTrack(index)); + if (!pTrack) { + qWarning() << "BrowseTableModel::setData():" + << "Failed to resolve track" + << getTrackLocation(index); + // restore previous item content + item->setText(index.data().toString()); + item->setToolTip(item->text()); return false; } - qDebug() << "BrowseTableModel::setData(" << index.data() << ")"; - int row = index.row(); - int col = index.column(); - - mixxx::TrackMetadata trackMetadata; - - // set tagger information - trackMetadata.refAlbumInfo().setTitle(this->index(row, COLUMN_ALBUM).data().toString()); - trackMetadata.refAlbumInfo().setArtist(this->index(row, COLUMN_ALBUMARTIST).data().toString()); - - trackMetadata.refTrackInfo().setArtist(this->index(row, COLUMN_ARTIST).data().toString()); - trackMetadata.refTrackInfo().setTitle(this->index(row, COLUMN_TITLE).data().toString()); - trackMetadata.refTrackInfo().setKey(this->index(row, COLUMN_KEY).data().toString()); - trackMetadata.refTrackInfo().setBpm(mixxx::Bpm(this->index(row, COLUMN_BPM).data().toDouble())); - trackMetadata.refTrackInfo().setComment(this->index(row, COLUMN_COMMENT).data().toString()); - trackMetadata.refTrackInfo().setTrackNumber(this->index(row, COLUMN_TRACK_NUMBER).data().toString()); - trackMetadata.refTrackInfo().setYear(this->index(row, COLUMN_YEAR).data().toString()); - trackMetadata.refTrackInfo().setGenre(this->index(row, COLUMN_GENRE).data().toString()); - trackMetadata.refTrackInfo().setComposer(this->index(row, COLUMN_COMPOSER).data().toString()); - trackMetadata.refTrackInfo().setGrouping(this->index(row, COLUMN_GROUPING).data().toString()); // check if one the item were edited - if (col == COLUMN_ARTIST) { - trackMetadata.refTrackInfo().setArtist(value.toString()); - } else if (col == COLUMN_TITLE) { - trackMetadata.refTrackInfo().setTitle(value.toString()); - } else if (col == COLUMN_ALBUM) { - trackMetadata.refAlbumInfo().setTitle(value.toString()); - } else if (col == COLUMN_BPM) { - trackMetadata.refTrackInfo().setBpm(mixxx::Bpm(value.toDouble())); - } else if (col == COLUMN_KEY) { - trackMetadata.refTrackInfo().setKey(value.toString()); - } else if (col == COLUMN_TRACK_NUMBER) { - trackMetadata.refTrackInfo().setTrackNumber(value.toString()); - } else if (col == COLUMN_COMMENT) { - trackMetadata.refTrackInfo().setComment(value.toString()); - } else if (col == COLUMN_GENRE) { - trackMetadata.refTrackInfo().setGenre(value.toString()); - } else if (col == COLUMN_COMPOSER) { - trackMetadata.refTrackInfo().setComposer(value.toString()); - } else if (col == COLUMN_YEAR) { - trackMetadata.refTrackInfo().setYear(value.toString()); - } else if (col == COLUMN_ALBUMARTIST) { - trackMetadata.refAlbumInfo().setArtist(value.toString()); - } else if (col == COLUMN_GROUPING) { - trackMetadata.refTrackInfo().setGrouping(value.toString()); - } else { - qWarning() << "BrowseTableModel::setData(): no tagger column"; - return false; - } - - QStandardItem* item = itemFromIndex(index); - QString track_location(getTrackLocation(index)); - if (mixxx::MetadataSource::ExportResult::Succeeded == - mixxx::MetadataSourceTagLib(track_location).exportTrackMetadata(trackMetadata).first) { - // Modify underlying interalPointer object - item->setText(value.toString()); - item->setToolTip(item->text()); - return true; - } else { - // reset to old value in error + int col = index.column(); + switch (col) { + case COLUMN_ARTIST: + pTrack->setArtist(value.toString()); + break; + case COLUMN_TITLE: + pTrack->setTitle(value.toString()); + break; + case COLUMN_ALBUM: + pTrack->setAlbum(value.toString()); + break; + case COLUMN_BPM: + pTrack->setBpm(value.toDouble()); + break; + case COLUMN_KEY: + pTrack->setKeyText(value.toString()); + break; + case COLUMN_TRACK_NUMBER: + pTrack->setTrackNumber(value.toString()); + break; + case COLUMN_COMMENT: + pTrack->setComment(value.toString()); + break; + case COLUMN_GENRE: + pTrack->setGenre(value.toString()); + break; + case COLUMN_COMPOSER: + pTrack->setComposer(value.toString()); + break; + case COLUMN_YEAR: + pTrack->setYear(value.toString()); + break; + case COLUMN_ALBUMARTIST: + pTrack->setAlbumArtist(value.toString()); + break; + case COLUMN_GROUPING: + pTrack->setGrouping(value.toString()); + break; + default: + qWarning() << "BrowseTableModel::setData():" + << "No tagger column"; + // restore previous item context item->setText(index.data().toString()); item->setToolTip(item->text()); - QMessageBox::critical( - 0, tr("Mixxx Library"), - tr("Could not update file metadata.") - + "\n" +track_location); return false; } + + item->setText(value.toString()); + item->setToolTip(item->text()); + return true; } void BrowseTableModel::trackLoaded(QString group, TrackPointer pTrack) { @@ -361,10 +353,7 @@ void BrowseTableModel::trackLoaded(QString group, TrackPointer pTrack) { } bool BrowseTableModel::isColumnSortable(int column) { - if (COLUMN_PREVIEW == column) { - return false; - } - return true; + return COLUMN_PREVIEW != column; } QAbstractItemDelegate* BrowseTableModel::delegateForColumn(const int i, QObject* pParent) { diff --git a/src/library/browse/browsetablemodel.h b/src/library/browse/browsetablemodel.h index e5c693c7b906..128b268aff83 100644 --- a/src/library/browse/browsetablemodel.h +++ b/src/library/browse/browsetablemodel.h @@ -37,6 +37,10 @@ const int COLUMN_REPLAYGAIN = 20; // The BrowseTable models displays tracks // of given directory on the HDD. // Usage: Recording and Browse feature. +// +// TODO(XXX): Editing track metadata outside of the table view +// (e.g. in the property dialog) does not update the table view! +// Editing single fields in the table view works as expected. class BrowseTableModel : public QStandardItemModel, public virtual TrackModel { Q_OBJECT @@ -73,7 +77,7 @@ class BrowseTableModel : public QStandardItemModel, public virtual TrackModel { private: void addSearchColumn(int index); - bool isTrackInUse(const QString& file) const; + QList m_searchColumns; MDir m_current_directory; TrackCollection* m_pTrackCollection; From b0712795bd27fd8f65b4d1bc661b15daeafaed06 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 22 Jun 2016 10:56:27 +0200 Subject: [PATCH 06/52] Re-enable export of track metadata into files --- src/library/dao/trackdao.cpp | 2 +- src/preferences/dialog/dlgpreflibrary.cpp | 22 ++--- src/preferences/dialog/dlgpreflibrarydlg.ui | 90 +++++++++++---------- src/preferences/settingsmanager.cpp | 7 -- src/widget/wtracktableview.cpp | 31 +++++++ src/widget/wtracktableview.h | 4 + 6 files changed, 97 insertions(+), 59 deletions(-) diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 823b617845ab..a48282d4be3f 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -216,7 +216,7 @@ void TrackDAO::saveTrack(Track* pTrack) { // a timestamp is used to keep track of when metadata has been // last synchronized. Exporting metadata will update this time // stamp on the track object! - if (m_pConfig && m_pConfig->getValueString(ConfigKey("[Library]","WriteAudioTags")).toInt() == 1) { + if (m_pConfig && m_pConfig->getValueString(ConfigKey("[Library]","SyncTrackMetadataExport")).toInt() == 1) { SoundSourceProxy::exportTrackMetadataBeforeSaving(pTrack); } diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 7c91f84eb676..9e9e6475ff64 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -38,8 +38,6 @@ DlgPrefLibrary::DlgPrefLibrary(QWidget * parent, m_baddedDirectory(false), m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx) { setupUi(this); - slotUpdate(); - checkbox_ID3_sync->setVisible(false); connect(this, SIGNAL(requestAddDir(QString)), m_pLibrary, SLOT(slotRequestAddDir(QString))); @@ -89,6 +87,12 @@ DlgPrefLibrary::DlgPrefLibrary(QWidget * parent, builtInFormatsStr += ", ModPlug"; #endif builtInFormats->setText(builtInFormatsStr); + + connect(checkBox_SyncTrackMetadataExport, SIGNAL(toggled(bool)), + this, SLOT(slotSyncTrackMetadataExportToggled())); + + // Initialize the controls after all slots have been connected + slotUpdate(); } DlgPrefLibrary::~DlgPrefLibrary() { @@ -148,7 +152,7 @@ void DlgPrefLibrary::slotExtraPlugins() { void DlgPrefLibrary::slotResetToDefaults() { checkBox_library_scan->setChecked(false); - checkbox_ID3_sync->setChecked(false); + checkBox_SyncTrackMetadataExport->setChecked(false); checkBox_use_relative_path->setChecked(false); checkBox_show_rhythmbox->setChecked(true); checkBox_show_banshee->setChecked(true); @@ -165,9 +169,9 @@ void DlgPrefLibrary::slotUpdate() { initializeDirList(); checkBox_library_scan->setChecked(m_pconfig->getValue( ConfigKey("[Library]","RescanOnStartup"), false)); - checkbox_ID3_sync->setChecked(m_pconfig->getValue( - ConfigKey("[Library]","WriteAudioTags"), false)); - checkBox_use_relative_path->setChecked(m_pconfig->getValue( + checkBox_SyncTrackMetadataExport->setChecked(m_pConfig->getValue( + ConfigKey("[Library]","SyncTrackMetadataExport"), false)); + checkBox_use_relative_path->setChecked(m_pConfig->getValue( ConfigKey("[Library]","UseRelativePathOnExport"), false)); checkBox_show_rhythmbox->setChecked(m_pconfig->getValue( ConfigKey("[Library]","ShowRhythmboxLibrary"), true)); @@ -298,9 +302,9 @@ void DlgPrefLibrary::slotRelocateDir() { void DlgPrefLibrary::slotApply() { m_pconfig->set(ConfigKey("[Library]","RescanOnStartup"), ConfigValue((int)checkBox_library_scan->isChecked())); - m_pconfig->set(ConfigKey("[Library]","WriteAudioTags"), - ConfigValue((int)checkbox_ID3_sync->isChecked())); - m_pconfig->set(ConfigKey("[Library]","UseRelativePathOnExport"), + m_pConfig->set(ConfigKey("[Library]","SyncTrackMetadataExport"), + ConfigValue((int)checkBox_SyncTrackMetadataExport->isChecked())); + m_pConfig->set(ConfigKey("[Library]","UseRelativePathOnExport"), ConfigValue((int)checkBox_use_relative_path->isChecked())); m_pconfig->set(ConfigKey("[Library]","ShowRhythmboxLibrary"), ConfigValue((int)checkBox_show_rhythmbox->isChecked())); diff --git a/src/preferences/dialog/dlgpreflibrarydlg.ui b/src/preferences/dialog/dlgpreflibrarydlg.ui index 508bdcb03f79..ab7cedd8b375 100644 --- a/src/preferences/dialog/dlgpreflibrarydlg.ui +++ b/src/preferences/dialog/dlgpreflibrarydlg.ui @@ -101,7 +101,7 @@ - + Audio File Formats @@ -161,66 +161,55 @@ - + - Miscellaneous + Track Metadata Synchronization - - - + + + - Library Font: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Export: Write back modified track metadata from the library when closing audio files - - + + + + + + + Miscellaneous + + + + - Library Row Height: + Rescan library on start-up - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + true - - - false - - - Synchronize ID3 tags on track modifications - - - - Use relative paths for playlist export if possible - - + + - Rescan library on start-up - - - true + Library Row Height: - - - - - - ... + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + px @@ -236,13 +225,30 @@ - + + + + Library Font: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + true + + + + ... + + + @@ -254,7 +260,7 @@ Track Load Action - + @@ -287,7 +293,7 @@ External Libraries - + @@ -376,7 +382,7 @@ pushButton pushButtonExtraPlugins checkBox_library_scan - checkbox_ID3_sync + checkBox_SyncTrackMetadataExport checkBox_use_relative_path checkBox_show_rhythmbox checkBox_show_banshee diff --git a/src/preferences/settingsmanager.cpp b/src/preferences/settingsmanager.cpp index a131a44e9666..4ee0a2ed23dc 100644 --- a/src/preferences/settingsmanager.cpp +++ b/src/preferences/settingsmanager.cpp @@ -44,13 +44,6 @@ void SettingsManager::initializeDefaults() { // TODO(rryan): this looks unused. m_pSettings->set(ConfigKey("[Config]", "Path"), ConfigValue(resourcePath)); - // Do not write meta data back to ID3 when meta data has changed - // Because multiple TrackDao objects can exists for a particular track - // writing meta data may ruin your MP3 file if done simultaneously. - // see Bug #728197 - // For safety reasons, we deactivate this feature. - m_pSettings->set(ConfigKey("[Library]","WriteAudioTags"), ConfigValue(0)); - // Intialize default BPM system values. // NOTE(rryan): These should be in a better place but they've always been in // MixxxMainWindow. diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 71fa1197dd95..8339edd03c31 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -118,6 +118,7 @@ WTrackTableView::~WTrackTableView() { delete m_pImportMetadataFromFileAct; delete m_pImportMetadataFromMusicBrainzAct; + delete m_pExportMetadataAct; delete m_pAddToPreviewDeck; delete m_pAutoDJBottomAct; delete m_pAutoDJTopAct; @@ -409,6 +410,10 @@ void WTrackTableView::createActions() { connect(m_pImportMetadataFromMusicBrainzAct, SIGNAL(triggered()), this, SLOT(slotShowDlgTagFetcher())); + m_pExportMetadataAct = new QAction(tr("Export Metadata into File"), this); + connect(m_pExportMetadataAct, SIGNAL(triggered()), + this, SLOT(slotExportTrackMetadata())); + m_pAddToPreviewDeck = new QAction(tr("Load to Preview Deck"), this); // currently there is only one preview deck so just map it here. QString previewDeckGroup = PlayerManager::groupForPreviewDeck(0); @@ -946,6 +951,7 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { m_pMenu->addAction(m_pImportMetadataFromFileAct); m_pImportMetadataFromMusicBrainzAct->setEnabled(oneSongSelected); m_pMenu->addAction(m_pImportMetadataFromMusicBrainzAct); + m_pMenu->addAction(m_pExportMetadataAct); } // Cover art menu only applies if at least one track is selected. @@ -1398,6 +1404,31 @@ void WTrackTableView::slotImportTrackMetadata() { } } +void WTrackTableView::slotExportTrackMetadata() { + if (!modelHasCapabilities(TrackModel::TRACKMODELCAPS_IMPORTMETADATA)) { + return; + } + + QModelIndexList indices = selectionModel()->selectedRows(); + + TrackModel* trackModel = getTrackModel(); + + if (trackModel == NULL) { + return; + } + + foreach (QModelIndex index, indices) { + TrackPointer pTrack = trackModel->getTrack(index); + if (pTrack) { + // Export of metadata is deferred until all references to the + // corresponding track object have been dropped. Otherwise + // writing to files that are still used for playback might + // cause crashes or at least audible glitches! + pTrack->markForMetadataExport(); + } + } +} + //slot for reset played count, sets count to 0 of one or more tracks void WTrackTableView::slotResetPlayed() { QModelIndexList indices = selectionModel()->selectedRows(); diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index d605864b3b27..83aae49e23dc 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -61,6 +61,7 @@ class WTrackTableView : public WLibraryTableView { void slotPrevDlgTagFetcher(); void slotShowTrackInTagFetcher(TrackPointer track); void slotImportTrackMetadata(); + void slotExportTrackMetadata(); void slotResetPlayed(); void addSelectionToPlaylist(int iPlaylistId); void addSelectionToCrate(int iCrateId); @@ -130,6 +131,9 @@ class WTrackTableView : public WLibraryTableView { QAction *m_pImportMetadataFromFileAct; QAction *m_pImportMetadataFromMusicBrainzAct; + // Save Track Metadata Action: + QAction *m_pExportMetadataAct; + // Load Track to PreviewDeck QAction* m_pAddToPreviewDeck; From ec6c94dcc1f7de073393a3eeca35c70ca9439343 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Jan 2017 10:17:24 +0100 Subject: [PATCH 07/52] Rename members and reformat code --- src/preferences/dialog/dlgpreflibrary.cpp | 68 +++++++++-------------- src/preferences/dialog/dlgpreflibrary.h | 29 +++------- 2 files changed, 32 insertions(+), 65 deletions(-) diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 9e9e6475ff64..b33b09aaa878 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -1,20 +1,3 @@ -/*************************************************************************** - dlgpreflibrary.cpp - description - ------------------- - begin : Thu Apr 17 2003 - copyright : (C) 2003 by Tue & Ken Haste Andersen - email : haste@diku.dk -***************************************************************************/ - -/*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ - #include #include #include @@ -29,13 +12,15 @@ #define MIXXX_ADDONS_URL "http://www.mixxx.org/wiki/doku.php/add-ons" -DlgPrefLibrary::DlgPrefLibrary(QWidget * parent, - UserSettingsPointer config, Library *pLibrary) - : DlgPreferencePage(parent), +DlgPrefLibrary::DlgPrefLibrary( + QWidget* pParent, + UserSettingsPointer pConfig, + Library* pLibrary) + : DlgPreferencePage(pParent), m_dirListModel(), - m_pconfig(config), + m_pConfig(pConfig), m_pLibrary(pLibrary), - m_baddedDirectory(false), + m_bAddedDirectory(false), m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx) { setupUi(this); @@ -95,15 +80,12 @@ DlgPrefLibrary::DlgPrefLibrary(QWidget * parent, slotUpdate(); } -DlgPrefLibrary::~DlgPrefLibrary() { -} - void DlgPrefLibrary::slotShow() { - m_baddedDirectory = false; + m_bAddedDirectory = false; } void DlgPrefLibrary::slotHide() { - if (!m_baddedDirectory) { + if (!m_bAddedDirectory) { return; } @@ -167,22 +149,22 @@ void DlgPrefLibrary::slotResetToDefaults() { void DlgPrefLibrary::slotUpdate() { initializeDirList(); - checkBox_library_scan->setChecked(m_pconfig->getValue( + checkBox_library_scan->setChecked(m_pConfig->getValue( ConfigKey("[Library]","RescanOnStartup"), false)); checkBox_SyncTrackMetadataExport->setChecked(m_pConfig->getValue( ConfigKey("[Library]","SyncTrackMetadataExport"), false)); checkBox_use_relative_path->setChecked(m_pConfig->getValue( ConfigKey("[Library]","UseRelativePathOnExport"), false)); - checkBox_show_rhythmbox->setChecked(m_pconfig->getValue( + checkBox_show_rhythmbox->setChecked(m_pConfig->getValue( ConfigKey("[Library]","ShowRhythmboxLibrary"), true)); - checkBox_show_banshee->setChecked(m_pconfig->getValue( + checkBox_show_banshee->setChecked(m_pConfig->getValue( ConfigKey("[Library]","ShowBansheeLibrary"), true)); - checkBox_show_itunes->setChecked(m_pconfig->getValue( + checkBox_show_itunes->setChecked(m_pConfig->getValue( ConfigKey("[Library]","ShowITunesLibrary"), true)); - checkBox_show_traktor->setChecked(m_pconfig->getValue( + checkBox_show_traktor->setChecked(m_pConfig->getValue( ConfigKey("[Library]","ShowTraktorLibrary"), true)); - switch (m_pconfig->getValue( + switch (m_pConfig->getValue( ConfigKey("[Library]","TrackLoadAction"), LOAD_TRACK_DECK)) { case ADD_TRACK_BOTTOM: radioButton_dbclick_bottom->setChecked(true); @@ -214,7 +196,7 @@ void DlgPrefLibrary::slotAddDir() { if (!fd.isEmpty()) { emit(requestAddDir(fd)); slotUpdate(); - m_baddedDirectory = true; + m_bAddedDirectory = true; } } @@ -300,19 +282,19 @@ void DlgPrefLibrary::slotRelocateDir() { } void DlgPrefLibrary::slotApply() { - m_pconfig->set(ConfigKey("[Library]","RescanOnStartup"), + m_pConfig->set(ConfigKey("[Library]","RescanOnStartup"), ConfigValue((int)checkBox_library_scan->isChecked())); m_pConfig->set(ConfigKey("[Library]","SyncTrackMetadataExport"), ConfigValue((int)checkBox_SyncTrackMetadataExport->isChecked())); m_pConfig->set(ConfigKey("[Library]","UseRelativePathOnExport"), ConfigValue((int)checkBox_use_relative_path->isChecked())); - m_pconfig->set(ConfigKey("[Library]","ShowRhythmboxLibrary"), + m_pConfig->set(ConfigKey("[Library]","ShowRhythmboxLibrary"), ConfigValue((int)checkBox_show_rhythmbox->isChecked())); - m_pconfig->set(ConfigKey("[Library]","ShowBansheeLibrary"), + m_pConfig->set(ConfigKey("[Library]","ShowBansheeLibrary"), ConfigValue((int)checkBox_show_banshee->isChecked())); - m_pconfig->set(ConfigKey("[Library]","ShowITunesLibrary"), + m_pConfig->set(ConfigKey("[Library]","ShowITunesLibrary"), ConfigValue((int)checkBox_show_itunes->isChecked())); - m_pconfig->set(ConfigKey("[Library]","ShowTraktorLibrary"), + m_pConfig->set(ConfigKey("[Library]","ShowTraktorLibrary"), ConfigValue((int)checkBox_show_traktor->isChecked())); int dbclick_status; if (radioButton_dbclick_bottom->isChecked()) { @@ -322,23 +304,23 @@ void DlgPrefLibrary::slotApply() { } else { dbclick_status = LOAD_TRACK_DECK; } - m_pconfig->set(ConfigKey("[Library]","TrackLoadAction"), + m_pConfig->set(ConfigKey("[Library]","TrackLoadAction"), ConfigValue(dbclick_status)); QFont font = m_pLibrary->getTrackTableFont(); if (m_originalTrackTableFont != font) { - m_pconfig->set(ConfigKey("[Library]", "Font"), + m_pConfig->set(ConfigKey("[Library]", "Font"), ConfigValue(font.toString())); } int rowHeight = spinBoxRowHeight->value(); if (m_iOriginalTrackTableRowHeight != rowHeight) { - m_pconfig->set(ConfigKey("[Library]","RowHeight"), + m_pConfig->set(ConfigKey("[Library]","RowHeight"), ConfigValue(rowHeight)); } // TODO(rryan): Don't save here. - m_pconfig->save(); + m_pConfig->save(); } void DlgPrefLibrary::slotRowHeightValueChanged(int height) { diff --git a/src/preferences/dialog/dlgpreflibrary.h b/src/preferences/dialog/dlgpreflibrary.h index 339a203cf055..fc296eda5600 100644 --- a/src/preferences/dialog/dlgpreflibrary.h +++ b/src/preferences/dialog/dlgpreflibrary.h @@ -1,20 +1,3 @@ -/*************************************************************************** - dlgpreflibrary.h - description - ------------------- - begin : Thu Apr 17 2003 - copyright : (C) 2003 by Tue & Ken Haste Andersen - email : haste@diku.dk - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef DLGPREFLIBRARY_H #define DLGPREFLIBRARY_H @@ -40,9 +23,11 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { ADD_TRACK_TOP // Add track to Auto-DJ Queue (top). }; - DlgPrefLibrary(QWidget *parent, UserSettingsPointer config, - Library *pLibrary); - virtual ~DlgPrefLibrary(); + DlgPrefLibrary( + QWidget* pParent, + UserSettingsPointer pConfig, + Library* pLibrary); + ~DlgPrefLibrary() override {} public slots: // Common preference page slots. @@ -77,9 +62,9 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { void setLibraryFont(const QFont& font); QStandardItemModel m_dirListModel; - UserSettingsPointer m_pconfig; + UserSettingsPointer m_pConfig; Library* m_pLibrary; - bool m_baddedDirectory; + bool m_bAddedDirectory; QFont m_originalTrackTableFont; int m_iOriginalTrackTableRowHeight; }; From 7e434ac708d9d5cfd86d93c663dbc2da1e6382e7 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 11 Nov 2017 17:40:28 +0100 Subject: [PATCH 08/52] Safely write tags by using a temporary file --- src/sources/metadatasourcetaglib.cpp | 89 +++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index 150595e07732..2b330f3f7ca9 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -5,6 +5,7 @@ #include "util/logger.h" #include "util/memory.h" +#include #include #include @@ -22,6 +23,8 @@ namespace { Logger kLogger("MetadataSourceTagLib"); +const QString kSafelyWritableFileSuffix = "_writable"; + // Workaround for missing functionality in TagLib 1.11.x that // doesn't support to read text chunks from AIFF files. // See also: @@ -581,6 +584,72 @@ class AiffTagSaver: public TagSaver { bool m_modifiedTags; }; +class SafelyWritableFile final { + public: + explicit SafelyWritableFile(QString origFileName) + : m_origFileName(std::move(origFileName)) { + DEBUG_ASSERT(m_tempFileName.isNull()); + QString tempFileName = m_origFileName + kSafelyWritableFileSuffix; + if (QFile::copy(m_origFileName, tempFileName)) { + m_tempFileName = std::move(tempFileName); + } else { + kLogger.warning() + << "Failed to copy original into temporary file before writing:" + << m_origFileName + << "->" + << tempFileName; + } + } + ~SafelyWritableFile() { + cancel(); + } + + const QString& fileName() const { + return m_tempFileName; + } + + bool commit() { + if (m_tempFileName.isNull() || + !QFile::exists(m_tempFileName)) { + return false; // nothing to do + } + if (!QFile::remove(m_origFileName)) { + kLogger.warning() + << "Failed to remove original file after writing:" + << m_origFileName; + return false; + } + if (!QFile::rename(m_tempFileName, m_origFileName)) { + kLogger.critical() + << "Failed to rename temporary file after writing:" + << m_tempFileName + << "->" + << m_origFileName; + return false; + } + m_tempFileName = QString(); + return true; + } + + void cancel() { + if (m_tempFileName.isNull() || + !QFile::exists(m_tempFileName)) { + return; // nothing to do + } + if (!QFile::remove(m_tempFileName)) { + kLogger.warning() + << "Failed to remove temporary file:" + << m_tempFileName; + } + // Only try once to remove the temporary file + m_tempFileName = QString(); + } + + private: + QString m_origFileName; + QString m_tempFileName; +}; + } // anonymous namespace std::pair @@ -590,48 +659,50 @@ MetadataSourceTagLib::exportTrackMetadata( << "into file" << m_fileName << "with type" << m_fileType; + SafelyWritableFile safelyWritableFile(m_fileName); + std::unique_ptr pTagSaver; switch (m_fileType) { case taglib::FileType::MP3: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } case taglib::FileType::MP4: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } case taglib::FileType::FLAC: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } case taglib::FileType::OGG: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } #if (TAGLIB_HAS_OPUSFILE) case taglib::FileType::OPUS: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } #endif // TAGLIB_HAS_OPUSFILE case taglib::FileType::WV: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } case taglib::FileType::WAV: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } case taglib::FileType::AIFF: { - pTagSaver = std::make_unique(m_fileName, trackMetadata); + pTagSaver = std::make_unique(safelyWritableFile.fileName(), trackMetadata); break; } default: @@ -644,7 +715,7 @@ MetadataSourceTagLib::exportTrackMetadata( } if (pTagSaver->hasModifiedTags()) { - if (pTagSaver->saveModifiedTags()) { + if (pTagSaver->saveModifiedTags() && safelyWritableFile.commit()) { return afterExportSucceeded(); } else { kLogger.warning() << "Failed to save tags of file" << m_fileName; From 3bf38d534b171df6efac6473f5bd769367dd913d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 11 Nov 2017 18:01:36 +0100 Subject: [PATCH 09/52] Explain the purpose of SafelyWritableFile --- src/sources/metadatasourcetaglib.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index 2b330f3f7ca9..aedf7e1fd02e 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -584,6 +584,21 @@ class AiffTagSaver: public TagSaver { bool m_modifiedTags; }; +/** + * When writing the tags in-place directly into the original file + * an intermediate failure might corrupt this precious file. For + * example this might occur if the application crashes or is quit + * unexpectedly, if the original file becomes unavailable while + * writing by disconnecting a drive, if the file system is running + * out of free space, or if an unexpected driver or hardware failure + * occurs. + * + * To reduce the risk of corrupting the original file all write + * operations are performed on a temporary file that is created + * as an exact copy of the original file. Only after all write + * operations have finished successfully the original file is + * replaced with the temporary file. + */ class SafelyWritableFile final { public: explicit SafelyWritableFile(QString origFileName) From 2dbf1ec8c7c45bca55a52319935528ab9370e28e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 26 Nov 2017 12:04:55 +0100 Subject: [PATCH 10/52] Improve safety when writing tags even more ...and try to find out why a tests fails on Windows. --- src/sources/metadatasourcetaglib.cpp | 44 +++++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index aedf7e1fd02e..3e4a2a20ac0d 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -23,7 +23,13 @@ namespace { Logger kLogger("MetadataSourceTagLib"); -const QString kSafelyWritableFileSuffix = "_writable"; +// Appended to the original file name of the temporary file used for writing +const QString kSafelyWritableTempFileSuffix = "_temp"; + +// Appended to the original file name for renaming and before deleting this +// file. Should not be longer than kSafelyWritableTempFileSuffix to avoid +// potential failures caused by exceeded path length. +const QString kSafelyWritableOrigFileSuffix = "_orig"; // Workaround for missing functionality in TagLib 1.11.x that // doesn't support to read text chunks from AIFF files. @@ -564,7 +570,8 @@ class AiffTagSaver: public TagSaver { : m_file(TAGLIB_FILENAME_FROM_QSTRING(fileName)), m_modifiedTags(exportTrackMetadata(&m_file, trackMetadata)) { } - ~AiffTagSaver() override {} + ~AiffTagSaver() override { + } bool hasModifiedTags() const override { return m_modifiedTags; @@ -604,7 +611,7 @@ class SafelyWritableFile final { explicit SafelyWritableFile(QString origFileName) : m_origFileName(std::move(origFileName)) { DEBUG_ASSERT(m_tempFileName.isNull()); - QString tempFileName = m_origFileName + kSafelyWritableFileSuffix; + QString tempFileName = m_origFileName + kSafelyWritableTempFileSuffix; if (QFile::copy(m_origFileName, tempFileName)) { m_tempFileName = std::move(tempFileName); } else { @@ -628,18 +635,31 @@ class SafelyWritableFile final { !QFile::exists(m_tempFileName)) { return false; // nothing to do } - if (!QFile::remove(m_origFileName)) { - kLogger.warning() - << "Failed to remove original file after writing:" - << m_origFileName; + QString backupFileName = m_origFileName + kSafelyWritableOrigFileSuffix; + if (!QFile::rename(m_origFileName, backupFileName)) { + kLogger.critical() + << "Failed to rename the original file for backup after writing:" + << m_origFileName + << "->" + << backupFileName; return false; } if (!QFile::rename(m_tempFileName, m_origFileName)) { kLogger.critical() - << "Failed to rename temporary file after writing:" - << m_tempFileName - << "->" - << m_origFileName; + << "Failed to rename temporary file after writing:" + << m_tempFileName + << "->" + << m_origFileName; + kLogger.warning() + << "Both the original an the updated file are still available here:" + << backupFileName + << m_tempFileName; + return false; + } + if (!QFile::remove(backupFileName)) { + kLogger.warning() + << "Failed to remove backup file after writing:" + << backupFileName; return false; } m_tempFileName = QString(); @@ -740,6 +760,8 @@ MetadataSourceTagLib::exportTrackMetadata( kLogger.warning() << "Failed to modify tags of file" << m_fileName; return std::make_pair(ExportResult::Failed, QDateTime()); } + + // pTagSaver will be destroyed and all open files closed before safelyWritableFile } } // namespace mixxx From abaf7eaa6550b41c2c432f65fb611d79ea8406cd Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 26 Nov 2017 20:45:20 +0100 Subject: [PATCH 11/52] Improve temporary file handling when writing tags --- src/sources/metadatasourcetaglib.cpp | 60 ++++++++++++++++++---------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index 3e4a2a20ac0d..3433fbd45c23 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -631,36 +631,52 @@ class SafelyWritableFile final { } bool commit() { - if (m_tempFileName.isNull() || - !QFile::exists(m_tempFileName)) { + if (m_tempFileName.isNull()) { return false; // nothing to do } - QString backupFileName = m_origFileName + kSafelyWritableOrigFileSuffix; - if (!QFile::rename(m_origFileName, backupFileName)) { - kLogger.critical() - << "Failed to rename the original file for backup after writing:" - << m_origFileName - << "->" - << backupFileName; - return false; + QFile newFile(m_tempFileName); + if (!newFile.exists()) { + return false; // nothing to do + } + QFile oldFile(m_origFileName); + if (oldFile.exists()) { + QString backupFileName = m_origFileName + kSafelyWritableOrigFileSuffix; + DEBUG_ASSERT(!QFile::exists(backupFileName)); // very unlikely, otherwise renaming fails + if (!oldFile.rename(backupFileName)) { + kLogger.critical() + << "Failed to rename the original file for backup before writing:" + << oldFile.fileName() + << "->" + << backupFileName; + return false; + } } - if (!QFile::rename(m_tempFileName, m_origFileName)) { + DEBUG_ASSERT(!QFile::exists(m_origFileName)); + if (!newFile.rename(m_origFileName)) { kLogger.critical() << "Failed to rename temporary file after writing:" - << m_tempFileName + << newFile.fileName() << "->" << m_origFileName; - kLogger.warning() - << "Both the original an the updated file are still available here:" - << backupFileName - << m_tempFileName; - return false; + if (oldFile.exists()) { + // Try to restore the original file + if (!oldFile.rename(m_origFileName)) { + // Undo operation failed + kLogger.warning() + << "Both the original and the temporary file are still available:" + << oldFile.fileName() + << newFile.fileName(); + } + return false; + } } - if (!QFile::remove(backupFileName)) { - kLogger.warning() - << "Failed to remove backup file after writing:" - << backupFileName; - return false; + if (oldFile.exists()) { + if (!oldFile.remove()) { + kLogger.warning() + << "Failed to remove backup file after writing:" + << oldFile.fileName(); + return false; + } } m_tempFileName = QString(); return true; From ef3d25d3e4ebd80254854665f7a787b7cf80caba Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 26 Nov 2017 13:34:29 +0100 Subject: [PATCH 12/52] Try to fix tag writing tests on AppVeyor --- src/test/searchqueryparsertest.cpp | 1 - src/test/taglibtest.cpp | 14 +++++++++----- src/test/trackexport_test.cpp | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/test/searchqueryparsertest.cpp b/src/test/searchqueryparsertest.cpp index 392974a6bba4..4844901cda20 100644 --- a/src/test/searchqueryparsertest.cpp +++ b/src/test/searchqueryparsertest.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include "test/librarytest.h" diff --git a/src/test/taglibtest.cpp b/src/test/taglibtest.cpp index 3d63522ef8bb..cdee2fe1ed4e 100644 --- a/src/test/taglibtest.cpp +++ b/src/test/taglibtest.cpp @@ -29,8 +29,8 @@ QString generateTemporaryFileName(const QString& fileNameTemplate) { QTemporaryFile tmpFile(fileNameTemplate); tmpFile.open(); DEBUG_ASSERT(tmpFile.exists()); - const QString tmpFileName(tmpFile.fileName()); - FileRemover tmpFileRemover(tmpFileName); + const QString tmpFileName = tmpFile.fileName(); + DEBUG_ASSERT(tmpFile.remove()); return tmpFileName; } @@ -40,8 +40,8 @@ void copyFile(const QString& srcFileName, const QString& dstFileName) { qDebug() << "Copying file" << srcFileName << "->" - < only an ID3v2 tag should be added mixxx::TrackMetadata trackMetadata; trackMetadata.refTrackInfo().setTitle("title"); @@ -84,6 +86,8 @@ TEST_F(TagLibTest, WriteID3v2Tag) { EXPECT_FALSE(mixxx::taglib::hasAPETag(mpegFile)); } + qDebug() << "Updating track title"; + // Write metadata again -> only the ID3v2 tag should be modified trackMetadata.refTrackInfo().setTitle("title2"); const auto exported2 = diff --git a/src/test/trackexport_test.cpp b/src/test/trackexport_test.cpp index f3b22000e44a..7877315fb6c3 100644 --- a/src/test/trackexport_test.cpp +++ b/src/test/trackexport_test.cpp @@ -5,7 +5,6 @@ #include #include -#include FakeOverwriteAnswerer::~FakeOverwriteAnswerer() { } From 35ce52b269b35be15c5df2b3d759b99aef9d6342 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 1 Dec 2017 07:32:03 +0100 Subject: [PATCH 13/52] Improve documentation of TrackRef --- src/track/trackref.cpp | 8 ++++++-- src/track/trackref.h | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/track/trackref.cpp b/src/track/trackref.cpp index b71feb84ba42..663a2f9790ac 100644 --- a/src/track/trackref.cpp +++ b/src/track/trackref.cpp @@ -2,10 +2,14 @@ bool TrackRef::verifyConsistency() const { - VERIFY_OR_DEBUG_ASSERT(hasLocation() || !hasCanonicalLocation()) { + // Class invariant: The location can only be set together with + // at least one of the other members! + VERIFY_OR_DEBUG_ASSERT(!hasCanonicalLocation() || hasLocation()) { + // Condition violated: hasCanonicalLocation() => hasLocation() return false; } - VERIFY_OR_DEBUG_ASSERT(hasLocation() || !hasId()) { + VERIFY_OR_DEBUG_ASSERT(!hasId() || hasLocation()) { + // Condition violated: hasId() => hasLocation() return false; } return true; diff --git a/src/track/trackref.h b/src/track/trackref.h index 13ee290fd492..c39974f44b24 100644 --- a/src/track/trackref.h +++ b/src/track/trackref.h @@ -103,6 +103,7 @@ class TrackRef { } private: + // Checks if all class invariants are met bool verifyConsistency() const; QString m_location; From 2d8941693197ce4710673d1ec6723d0f521ce844 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 1 Dec 2017 09:24:43 +0100 Subject: [PATCH 14/52] Rename slots to indicate source/target of metadata import/export --- src/widget/wtracktableview.cpp | 8 ++++---- src/widget/wtracktableview.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 8339edd03c31..335439a45e56 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -404,7 +404,7 @@ void WTrackTableView::createActions() { m_pImportMetadataFromFileAct = new QAction(tr("Import Metadata from File"), this); connect(m_pImportMetadataFromFileAct, SIGNAL(triggered()), - this, SLOT(slotImportTrackMetadata())); + this, SLOT(slotImportTrackMetadataFromFileTags())); m_pImportMetadataFromMusicBrainzAct = new QAction(tr("Import Metadata from MusicBrainz"),this); connect(m_pImportMetadataFromMusicBrainzAct, SIGNAL(triggered()), @@ -412,7 +412,7 @@ void WTrackTableView::createActions() { m_pExportMetadataAct = new QAction(tr("Export Metadata into File"), this); connect(m_pExportMetadataAct, SIGNAL(triggered()), - this, SLOT(slotExportTrackMetadata())); + this, SLOT(slotExportTrackMetadataIntoFileTags())); m_pAddToPreviewDeck = new QAction(tr("Load to Preview Deck"), this); // currently there is only one preview deck so just map it here. @@ -1379,7 +1379,7 @@ void WTrackTableView::sendToAutoDJ(PlaylistDAO::AutoDJSendLoc loc) { playlistDao.sendToAutoDJ(trackIds, loc); } -void WTrackTableView::slotImportTrackMetadata() { +void WTrackTableView::slotImportTrackMetadataFromFileTags() { if (!modelHasCapabilities(TrackModel::TRACKMODELCAPS_IMPORTMETADATA)) { return; } @@ -1404,7 +1404,7 @@ void WTrackTableView::slotImportTrackMetadata() { } } -void WTrackTableView::slotExportTrackMetadata() { +void WTrackTableView::slotExportTrackMetadataIntoFileTags() { if (!modelHasCapabilities(TrackModel::TRACKMODELCAPS_IMPORTMETADATA)) { return; } diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index 83aae49e23dc..6f9f9b19bd5f 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -60,8 +60,8 @@ class WTrackTableView : public WLibraryTableView { void slotPrevTrackInfo(); void slotPrevDlgTagFetcher(); void slotShowTrackInTagFetcher(TrackPointer track); - void slotImportTrackMetadata(); - void slotExportTrackMetadata(); + void slotImportTrackMetadataFromFileTags(); + void slotExportTrackMetadataIntoFileTags(); void slotResetPlayed(); void addSelectionToPlaylist(int iPlaylistId); void addSelectionToCrate(int iCrateId); From 6c92735b9e10da95cd9884a93d95a9fd1801ed16 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 1 Dec 2017 17:10:30 +0100 Subject: [PATCH 15/52] Use the terms "evict" and "cache" in conjunction ...to illustrate their relationship. --- src/library/library.cpp | 2 +- src/library/library.h | 2 +- src/test/librarytest.h | 2 +- src/track/trackcache.cpp | 2 +- src/track/trackcache.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/library/library.cpp b/src/library/library.cpp index 2c5599a31451..17fa3e8b73d7 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -422,6 +422,6 @@ void Library::slotSetTrackTableRowHeight(int rowHeight) { emit(setTrackTableRowHeight(rowHeight)); } -void Library::evictTrack(Track* pTrack) { +void Library::onEvictingTrackFromCache(Track* pTrack) { m_pTrackCollection->saveTrack(pTrack); } diff --git a/src/library/library.h b/src/library/library.h index 3c6f1cb9440b..88265aeb5262 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -81,7 +81,7 @@ class Library: public QObject, static const int kDefaultRowHeightPx; - void evictTrack(Track* pTrack) override; + void onEvictingTrackFromCache(Track* pTrack) override; public slots: void slotShowTrackModel(QAbstractItemModel* model); diff --git a/src/test/librarytest.h b/src/test/librarytest.h index cf4dbc31e292..6e0846ebc554 100644 --- a/src/test/librarytest.h +++ b/src/test/librarytest.h @@ -14,7 +14,7 @@ class LibraryTest : public MixxxTest, public virtual /*implements*/ TrackCacheEvictor { public: - void evictTrack(Track* pTrack) override { + void onEvictingTrackFromCache(Track* pTrack) override { m_trackCollection.saveTrack(pTrack); } diff --git a/src/track/trackcache.cpp b/src/track/trackcache.cpp index 91c4a24985d7..a5857018c48d 100644 --- a/src/track/trackcache.cpp +++ b/src/track/trackcache.cpp @@ -492,7 +492,7 @@ Track* TrackCache::evictInternal( purgedItem.plainPtr->blockSignals(true); // Keep the cache locked while evicting the track object! - m_pEvictor->evictTrack(purgedItem.plainPtr); + m_pEvictor->onEvictingTrackFromCache(purgedItem.plainPtr); } else { qDebug() << "TrackCache:" << "Uncached track cannot be evicted" diff --git a/src/track/trackcache.h b/src/track/trackcache.h index 20328867e3c1..80a9985a799d 100644 --- a/src/track/trackcache.h +++ b/src/track/trackcache.h @@ -13,7 +13,7 @@ class /*interface*/ TrackCacheEvictor { public: - virtual void evictTrack(Track* pTrack) = 0; + virtual void onEvictingTrackFromCache(Track* pTrack) = 0; protected: virtual ~TrackCacheEvictor() {} From 9a4cc91d607b0ac0fcfbf3fbf7d1e60d154bd4b2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 1 Dec 2017 17:45:58 +0100 Subject: [PATCH 16/52] Display message box before exporting track metadata --- src/widget/wtracktableview.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 335439a45e56..458f01e87920 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -1417,6 +1417,15 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { return; } + if (QMessageBox::Apply != QMessageBox::question( + nullptr, + tr("Export Track Metadata"), + tr("Write track metadata back into the underlying audio file(s)?\n\nFile modifications are deferred and changes may not appear immediately! Close Mixxx to enforce writing of all pending exports."), + QMessageBox::Apply | QMessageBox::Cancel, + QMessageBox::Apply)) { + return; + } + foreach (QModelIndex index, indices) { TrackPointer pTrack = trackModel->getTrack(index); if (pTrack) { From ceae6c23c1ebd1e7ad2948af1a8429f9ea3885bf Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 00:33:34 +0100 Subject: [PATCH 17/52] Inform the user how exporting of track metadata works ...without getting into details. --- src/preferences/dialog/dlgpreflibrary.cpp | 11 +++++++++++ src/preferences/dialog/dlgpreflibrary.h | 1 + src/preferences/dialog/dlgpreflibrarydlg.ui | 2 +- src/widget/wtracktableview.cpp | 4 +++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index b33b09aaa878..753a59d41fe3 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "preferences/dialog/dlgpreflibrary.h" #include "sources/soundsourceproxy.h" @@ -350,3 +351,13 @@ void DlgPrefLibrary::slotSelectFont() { setLibraryFont(font); } } + +void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() { + if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked()) { + QMessageBox::information( + nullptr, + tr("Export Modified Track Metadata"), + tr("File modifications are deferred until considered safe and changes may not appear instantly." + " Close Mixxx if you need to ensure that all pending write operations are finished.")); + } +} diff --git a/src/preferences/dialog/dlgpreflibrary.h b/src/preferences/dialog/dlgpreflibrary.h index fc296eda5600..58b76631890a 100644 --- a/src/preferences/dialog/dlgpreflibrary.h +++ b/src/preferences/dialog/dlgpreflibrary.h @@ -56,6 +56,7 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { private slots: void slotRowHeightValueChanged(int); void slotSelectFont(); + void slotSyncTrackMetadataExportToggled(); private: void initializeDirList(); diff --git a/src/preferences/dialog/dlgpreflibrarydlg.ui b/src/preferences/dialog/dlgpreflibrarydlg.ui index ab7cedd8b375..0b6a940aa077 100644 --- a/src/preferences/dialog/dlgpreflibrarydlg.ui +++ b/src/preferences/dialog/dlgpreflibrarydlg.ui @@ -169,7 +169,7 @@ - Export: Write back modified track metadata from the library when closing audio files + Export: Write back modified track metadata from the library into file tags diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 458f01e87920..e216ed49b94c 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -1420,7 +1420,9 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { if (QMessageBox::Apply != QMessageBox::question( nullptr, tr("Export Track Metadata"), - tr("Write track metadata back into the underlying audio file(s)?\n\nFile modifications are deferred and changes may not appear immediately! Close Mixxx to enforce writing of all pending exports."), + tr("Write track metadata back into the underlying audio file(s)?\n\n" + "File modifications are deferred until considered safe and changes may not appear instantly!" + " Close Mixxx if you need to ensure that all pending write operations are finished."), QMessageBox::Apply | QMessageBox::Cancel, QMessageBox::Apply)) { return; From f970405778af96e120985a5a9afbdcb333ec9554 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 01:30:04 +0100 Subject: [PATCH 18/52] Allow re-import of metadata for tracks with unsaved changes --- src/sources/soundsourceproxy.cpp | 5 ++--- src/test/trackupdate_test.cpp | 4 ++-- src/track/track.cpp | 6 +----- src/track/track.h | 3 +-- src/widget/wtracktableview.cpp | 22 ++++++++++++++++++++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/sources/soundsourceproxy.cpp b/src/sources/soundsourceproxy.cpp index bf6c8f7f526c..2b28741eb750 100644 --- a/src/sources/soundsourceproxy.cpp +++ b/src/sources/soundsourceproxy.cpp @@ -427,8 +427,7 @@ void SoundSourceProxy::updateTrackFromSource( // not be supported at all and those would get lost! mixxx::TrackMetadata trackMetadata; bool metadataSynchronized = false; - bool isDirty = false; - m_pTrack->getTrackMetadata(&trackMetadata, &metadataSynchronized, &isDirty); + m_pTrack->getTrackMetadata(&trackMetadata, &metadataSynchronized); // Cast away the enriched track location by explicitly slicing the // returned CoverInfo to CoverInfoRelative const CoverInfoRelative coverInfo(m_pTrack->getCoverInfo()); @@ -438,7 +437,7 @@ void SoundSourceProxy::updateTrackFromSource( // If the file tags have already been parsed once, both track metadata // and cover art should not be updated implicitly. if (metadataSynchronized) { - if (isDirty || (importTrackMetadataMode == ImportTrackMetadataMode::Once)) { + if (importTrackMetadataMode == ImportTrackMetadataMode::Once) { kLogger.info() << "Skip parsing of track metadata and cover art from file" << getUrl().toString(); return; // abort diff --git a/src/test/trackupdate_test.cpp b/src/test/trackupdate_test.cpp index 82c54dd809bf..6c6175e721b1 100644 --- a/src/test/trackupdate_test.cpp +++ b/src/test/trackupdate_test.cpp @@ -136,9 +136,9 @@ TEST_F(TrackUpdateTest, parseModifiedDirtyAgain) { pTrack->getTrackMetadata(&trackMetadataAfter); auto coverInfoAfter = pTrack->getCoverInfo(); - // Not updated + // Updated EXPECT_TRUE(pTrack->isMetadataSynchronized()); EXPECT_TRUE(pTrack->isDirty()); - EXPECT_EQ(trackMetadataBefore, trackMetadataAfter); + EXPECT_NE(trackMetadataBefore, trackMetadataAfter); EXPECT_EQ(coverInfoBefore, coverInfoAfter); } diff --git a/src/track/track.cpp b/src/track/track.cpp index 6bd7643e60a9..954bce42eb3e 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -126,17 +126,13 @@ void Track::setTrackMetadata( void Track::getTrackMetadata( mixxx::TrackMetadata* pTrackMetadata, - bool* pMetadataSynchronized, - bool* pDirty) const { + bool* pMetadataSynchronized) const { DEBUG_ASSERT(pTrackMetadata); QMutexLocker lock(&m_qMutex); *pTrackMetadata = m_record.getMetadata(); if (pMetadataSynchronized != nullptr) { *pMetadataSynchronized = m_record.getMetadataSynchronized(); } - if (pDirty != nullptr) { - *pDirty = m_bDirty; - } } void Track::getTrackRecord( diff --git a/src/track/track.h b/src/track/track.h index 6035e99cd000..a36e67268b4e 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -281,8 +281,7 @@ class Track : public QObject { QDateTime metadataSynchronized); void getTrackMetadata( mixxx::TrackMetadata* pTrackMetadata, - bool* pMetadataSynchronized = nullptr, - bool* pDirty = nullptr) const; + bool* pMetadataSynchronized = nullptr) const; void getTrackRecord( mixxx::TrackRecord* pTrackRecord, diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index e216ed49b94c..0fdf4f4871c4 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -1392,12 +1392,30 @@ void WTrackTableView::slotImportTrackMetadataFromFileTags() { return; } + for (const QModelIndex& index : indices) { + TrackPointer pTrack = trackModel->getTrack(index); + if (pTrack && pTrack->isDirty()) { + if (QMessageBox::Apply == QMessageBox::question( + nullptr, + tr("Import Track Metadata"), + tr("One or more selected tracks have unsaved changes. Continue anyway?"), + QMessageBox::Apply | QMessageBox::Cancel, + QMessageBox::Apply)) { + // Continue with import + break; + } else { + // Abort import + return; + } + } + } + for (const QModelIndex& index : indices) { TrackPointer pTrack = trackModel->getTrack(index); if (pTrack) { // The user has explicitly requested to reload metadata from the file - // to override the information within Mixxx! Cover art is reloaded - // separately. + // to override the information within Mixxx! Custom cover art must be + // reloaded separately. SoundSourceProxy(pTrack).updateTrackFromSource( SoundSourceProxy::ImportTrackMetadataMode::Again); } From 40ff03ff3067099eaf2776887e9f11e7213d3d4e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 10:27:02 +0100 Subject: [PATCH 19/52] Simply overwrite unsaved metadata when importing from file The user decided to overwrite what they see in Mixxx, so just do it. --- src/widget/wtracktableview.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 0fdf4f4871c4..a0688fbcd3a0 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -1392,24 +1392,6 @@ void WTrackTableView::slotImportTrackMetadataFromFileTags() { return; } - for (const QModelIndex& index : indices) { - TrackPointer pTrack = trackModel->getTrack(index); - if (pTrack && pTrack->isDirty()) { - if (QMessageBox::Apply == QMessageBox::question( - nullptr, - tr("Import Track Metadata"), - tr("One or more selected tracks have unsaved changes. Continue anyway?"), - QMessageBox::Apply | QMessageBox::Cancel, - QMessageBox::Apply)) { - // Continue with import - break; - } else { - // Abort import - return; - } - } - } - for (const QModelIndex& index : indices) { TrackPointer pTrack = trackModel->getTrack(index); if (pTrack) { From 0567e31abaf894a87e0f8a67c9483c07cc8aafbe Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 11:54:23 +0100 Subject: [PATCH 20/52] Change wording of message box when exporting track metadata --- src/preferences/dialog/dlgpreflibrary.cpp | 5 +++-- src/preferences/dialog/dlgpreflibrarydlg.ui | 2 +- src/widget/wtracktableview.cpp | 9 +++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 753a59d41fe3..477f75cd6de6 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -357,7 +357,8 @@ void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() { QMessageBox::information( nullptr, tr("Export Modified Track Metadata"), - tr("File modifications are deferred until considered safe and changes may not appear instantly." - " Close Mixxx if you need to ensure that all pending write operations are finished.")); + tr("File modifications are deferred and may not appear at once! " + "If you do not see changed metadata in other programs, " + "close Mixxx to modify those files immediately.")); } } diff --git a/src/preferences/dialog/dlgpreflibrarydlg.ui b/src/preferences/dialog/dlgpreflibrarydlg.ui index 0b6a940aa077..cf1644e92507 100644 --- a/src/preferences/dialog/dlgpreflibrarydlg.ui +++ b/src/preferences/dialog/dlgpreflibrarydlg.ui @@ -169,7 +169,7 @@ - Export: Write back modified track metadata from the library into file tags + Export: Write modified track metadata from the library into file tags diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index a0688fbcd3a0..ca729531226b 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -1420,15 +1420,16 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { if (QMessageBox::Apply != QMessageBox::question( nullptr, tr("Export Track Metadata"), - tr("Write track metadata back into the underlying audio file(s)?\n\n" - "File modifications are deferred until considered safe and changes may not appear instantly!" - " Close Mixxx if you need to ensure that all pending write operations are finished."), + tr("Write track metadata into the corresponding audio file(s)?\n\n" + "File modifications are deferred and may not appear at once! " + "If you do not see changed metadata in other programs, " + "close Mixxx to modify those files immediately."), QMessageBox::Apply | QMessageBox::Cancel, QMessageBox::Apply)) { return; } - foreach (QModelIndex index, indices) { + for (const QModelIndex& index : indices) { TrackPointer pTrack = trackModel->getTrack(index); if (pTrack) { // Export of metadata is deferred until all references to the From d94d536e2f4cfb74e7f1e52ee8eabe7f96e39d0f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 12:11:39 +0100 Subject: [PATCH 21/52] Show information about track metadata export only once per session Now both import and export of metadata don't require an explicit confirmation. The informational message when exporting metadata is only shown once per session. --- src/preferences/dialog/dlgpreflibrary.cpp | 6 ++++-- src/preferences/dialog/dlgpreflibrary.h | 2 ++ src/widget/wtracktableview.cpp | 17 +++++++++-------- src/widget/wtracktableview.h | 2 ++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 477f75cd6de6..1cd39696b2f0 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -22,7 +22,8 @@ DlgPrefLibrary::DlgPrefLibrary( m_pConfig(pConfig), m_pLibrary(pLibrary), m_bAddedDirectory(false), - m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx) { + m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx), + m_bShowTrackMetadataExportInfo(true) { setupUi(this); connect(this, SIGNAL(requestAddDir(QString)), @@ -353,12 +354,13 @@ void DlgPrefLibrary::slotSelectFont() { } void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() { - if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked()) { + if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked() && m_bShowTrackMetadataExportInfo) { QMessageBox::information( nullptr, tr("Export Modified Track Metadata"), tr("File modifications are deferred and may not appear at once! " "If you do not see changed metadata in other programs, " "close Mixxx to modify those files immediately.")); + m_bShowTrackMetadataExportInfo = false; } } diff --git a/src/preferences/dialog/dlgpreflibrary.h b/src/preferences/dialog/dlgpreflibrary.h index 58b76631890a..fa3d974eafa8 100644 --- a/src/preferences/dialog/dlgpreflibrary.h +++ b/src/preferences/dialog/dlgpreflibrary.h @@ -68,6 +68,8 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { bool m_bAddedDirectory; QFont m_originalTrackTableFont; int m_iOriginalTrackTableRowHeight; + + bool m_bShowTrackMetadataExportInfo; }; #endif diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index ca729531226b..5aab34308764 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -43,7 +43,8 @@ WTrackTableView::WTrackTableView(QWidget * parent, m_iCoverHashColumn(-1), m_iCoverColumn(-1), m_selectionChangedSinceLastGuiTick(true), - m_loadCachedOnly(false) { + m_loadCachedOnly(false), + m_bShowTrackMetadataExportInfo(true) { connect(&m_loadTrackMapper, SIGNAL(mapped(QString)), @@ -1417,16 +1418,16 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { return; } - if (QMessageBox::Apply != QMessageBox::question( + if (m_bShowTrackMetadataExportInfo) { + // Inform the user once per session that the corresponding files + // will not be modified at once and changes may appear later. + QMessageBox::information( nullptr, tr("Export Track Metadata"), - tr("Write track metadata into the corresponding audio file(s)?\n\n" - "File modifications are deferred and may not appear at once! " + tr("File modifications are deferred and may not appear at once! " "If you do not see changed metadata in other programs, " - "close Mixxx to modify those files immediately."), - QMessageBox::Apply | QMessageBox::Cancel, - QMessageBox::Apply)) { - return; + "close Mixxx to modify those files immediately.")); + m_bShowTrackMetadataExportInfo = false; } for (const QModelIndex& index : indices) { diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index 6f9f9b19bd5f..daa9dae279b7 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -190,6 +190,8 @@ class WTrackTableView : public WLibraryTableView { bool m_selectionChangedSinceLastGuiTick; bool m_loadCachedOnly; ControlProxy* m_pCOTGuiTick; + + bool m_bShowTrackMetadataExportInfo; }; #endif From cd52a1ff784815915c8c22063c6ecd12a8f65b41 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 13:44:09 +0100 Subject: [PATCH 22/52] Temporarily disable tag writing test on Windows builds --- src/test/taglibtest.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/taglibtest.cpp b/src/test/taglibtest.cpp index cdee2fe1ed4e..8f251110761e 100644 --- a/src/test/taglibtest.cpp +++ b/src/test/taglibtest.cpp @@ -47,6 +47,9 @@ void copyFile(const QString& srcFileName, const QString& dstFileName) { DEBUG_ASSERT(srcFile.size() == dstFile.size()); } +// TODO(XXX): The following test has been disabled on Windows until +// file system issues with temporary files have been solved. +#ifndef __WINDOWS__ TEST_F(TagLibTest, WriteID3v2Tag) { // Generate a file name for the temporary file const QString tmpFileName = generateTemporaryFileName("no_id3v1_mp3"); @@ -105,5 +108,6 @@ TEST_F(TagLibTest, WriteID3v2Tag) { EXPECT_FALSE(mixxx::taglib::hasAPETag(mpegFile)); } } +#endif // __WINDOWS__ } // anonymous namespace From 183c8d324b31c8b9ad197921f674639b3dc3e017 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 15:56:35 +0100 Subject: [PATCH 23/52] Revert "Temporarily disable tag writing test on Windows builds" This reverts commit cd52a1ff784815915c8c22063c6ecd12a8f65b41. --- src/test/taglibtest.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/taglibtest.cpp b/src/test/taglibtest.cpp index 8f251110761e..cdee2fe1ed4e 100644 --- a/src/test/taglibtest.cpp +++ b/src/test/taglibtest.cpp @@ -47,9 +47,6 @@ void copyFile(const QString& srcFileName, const QString& dstFileName) { DEBUG_ASSERT(srcFile.size() == dstFile.size()); } -// TODO(XXX): The following test has been disabled on Windows until -// file system issues with temporary files have been solved. -#ifndef __WINDOWS__ TEST_F(TagLibTest, WriteID3v2Tag) { // Generate a file name for the temporary file const QString tmpFileName = generateTemporaryFileName("no_id3v1_mp3"); @@ -108,6 +105,5 @@ TEST_F(TagLibTest, WriteID3v2Tag) { EXPECT_FALSE(mixxx::taglib::hasAPETag(mpegFile)); } } -#endif // __WINDOWS__ } // anonymous namespace From 612f691e8a8841499215869b36163e9e8f6f2059 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 15:56:06 +0100 Subject: [PATCH 24/52] Close the audio source before releasing the track pointer --- src/sources/audiosourcetrackproxy.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sources/audiosourcetrackproxy.h b/src/sources/audiosourcetrackproxy.h index 1564a04a8eeb..0e422f93499b 100644 --- a/src/sources/audiosourcetrackproxy.h +++ b/src/sources/audiosourcetrackproxy.h @@ -49,8 +49,12 @@ class AudioSourceTrackProxy: public AudioSource { } private: - AudioSourcePointer m_pAudioSource; TrackPointer m_pTrack; + // The audio source must be closed before releasing the track + // pointer to close any open file handles. Otherwise exporting + // track metadata into the same file may not work, because the + // file is still locked by the OS! + AudioSourcePointer m_pAudioSource; }; } // namespace mixxx From b6a24aff67a8d6d673c2f22adc0b536d063d18cc Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 17:25:59 +0100 Subject: [PATCH 25/52] Log cause after file operations failed --- src/sources/metadatasourcetaglib.cpp | 32 ++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index 3433fbd45c23..bafd5f4eeca3 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -612,12 +612,14 @@ class SafelyWritableFile final { : m_origFileName(std::move(origFileName)) { DEBUG_ASSERT(m_tempFileName.isNull()); QString tempFileName = m_origFileName + kSafelyWritableTempFileSuffix; - if (QFile::copy(m_origFileName, tempFileName)) { + QFile origFile(m_origFileName); + if (origFile.copy(tempFileName)) { m_tempFileName = std::move(tempFileName); } else { kLogger.warning() - << "Failed to copy original into temporary file before writing:" - << m_origFileName + << origFile.errorString() + << "- Failed to copy original into temporary file before writing:" + << origFile.fileName() << "->" << tempFileName; } @@ -644,7 +646,8 @@ class SafelyWritableFile final { DEBUG_ASSERT(!QFile::exists(backupFileName)); // very unlikely, otherwise renaming fails if (!oldFile.rename(backupFileName)) { kLogger.critical() - << "Failed to rename the original file for backup before writing:" + << oldFile.errorString() + << "- Failed to rename the original file for backup before writing:" << oldFile.fileName() << "->" << backupFileName; @@ -654,7 +657,8 @@ class SafelyWritableFile final { DEBUG_ASSERT(!QFile::exists(m_origFileName)); if (!newFile.rename(m_origFileName)) { kLogger.critical() - << "Failed to rename temporary file after writing:" + << newFile.errorString() + << "- Failed to rename temporary file after writing:" << newFile.fileName() << "->" << m_origFileName; @@ -663,7 +667,8 @@ class SafelyWritableFile final { if (!oldFile.rename(m_origFileName)) { // Undo operation failed kLogger.warning() - << "Both the original and the temporary file are still available:" + << oldFile.errorString() + << "- Both the original and the temporary file are still available:" << oldFile.fileName() << newFile.fileName(); } @@ -673,7 +678,8 @@ class SafelyWritableFile final { if (oldFile.exists()) { if (!oldFile.remove()) { kLogger.warning() - << "Failed to remove backup file after writing:" + << oldFile.errorString() + << "- Failed to remove backup file after writing:" << oldFile.fileName(); return false; } @@ -683,13 +689,17 @@ class SafelyWritableFile final { } void cancel() { - if (m_tempFileName.isNull() || - !QFile::exists(m_tempFileName)) { + if (m_tempFileName.isNull()) { + return; // nothing to do + } + QFile tempFile(m_tempFileName); + if (!tempFile.exists()) { return; // nothing to do } - if (!QFile::remove(m_tempFileName)) { + if (!tempFile.remove()) { kLogger.warning() - << "Failed to remove temporary file:" + << tempFile.errorString() + << "- Failed to remove temporary file:" << m_tempFileName; } // Only try once to remove the temporary file From f1b458d15a349e691bb63353fdef9d8f521e872f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 18:24:58 +0100 Subject: [PATCH 26/52] Don't call function with side effects in DBEUG_ASSERT --- src/test/taglibtest.cpp | 52 +++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/test/taglibtest.cpp b/src/test/taglibtest.cpp index cdee2fe1ed4e..68ba8063306a 100644 --- a/src/test/taglibtest.cpp +++ b/src/test/taglibtest.cpp @@ -11,7 +11,35 @@ namespace { const QDir kTestDir(QDir::current().absoluteFilePath("src/test/id3-test-data")); -class TagLibTest : public testing::Test {}; +class TagLibTest : public testing::Test { + protected: + static QString generateTemporaryFileName(const QString& fileNameTemplate) { + QTemporaryFile tmpFile(fileNameTemplate); + tmpFile.setAutoRemove(true); + tmpFile.open(); + DEBUG_ASSERT(tmpFile.exists()); + const QString tmpFileName = tmpFile.fileName(); + return tmpFileName; + } + + static bool copyFile(const QString& srcFileName, const QString& dstFileName) { + QFile srcFile(srcFileName); + DEBUG_ASSERT(srcFile.exists()); + if (!srcFile.copy(dstFileName)) { + qWarning() + << srcFile.errorString() + << "- Failed to copy file" + << srcFileName + << "->" + << dstFileName; + return false; + } + QFile dstFile(dstFileName); + DEBUG_ASSERT(dstFile.exists()); + DEBUG_ASSERT(srcFile.size() == dstFile.size()); + return true; + } +}; class FileRemover final { public: @@ -25,28 +53,6 @@ class FileRemover final { QString m_fileName; }; -QString generateTemporaryFileName(const QString& fileNameTemplate) { - QTemporaryFile tmpFile(fileNameTemplate); - tmpFile.open(); - DEBUG_ASSERT(tmpFile.exists()); - const QString tmpFileName = tmpFile.fileName(); - DEBUG_ASSERT(tmpFile.remove()); - return tmpFileName; -} - -void copyFile(const QString& srcFileName, const QString& dstFileName) { - QFile srcFile(srcFileName); - DEBUG_ASSERT(srcFile.exists()); - qDebug() << "Copying file" - << srcFileName - << "->" - << dstFileName; - DEBUG_ASSERT(srcFile.copy(dstFileName)); - QFile dstFile(dstFileName); - DEBUG_ASSERT(dstFile.exists()); - DEBUG_ASSERT(srcFile.size() == dstFile.size()); -} - TEST_F(TagLibTest, WriteID3v2Tag) { // Generate a file name for the temporary file const QString tmpFileName = generateTemporaryFileName("no_id3v1_mp3"); From 19041803193900532368f3a261913a22f7717a18 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 20:31:47 +0100 Subject: [PATCH 27/52] Close temporary file before renaming --- src/sources/metadatasourcetaglib.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index bafd5f4eeca3..b41c7ab16ef7 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -776,18 +776,19 @@ MetadataSourceTagLib::exportTrackMetadata( } if (pTagSaver->hasModifiedTags()) { - if (pTagSaver->saveModifiedTags() && safelyWritableFile.commit()) { - return afterExportSucceeded(); - } else { - kLogger.warning() << "Failed to save tags of file" << m_fileName; - return std::make_pair(ExportResult::Failed, QDateTime()); + if (pTagSaver->saveModifiedTags()) { + // Close all file handles after modified tags have been saved into the temporary file! + pTagSaver.reset(); + // Now we can safely replace the original file with the temporary file + if (safelyWritableFile.commit()) { + return afterExportSucceeded(); + } } + kLogger.warning() << "Failed to save tags of file" << m_fileName; } else { kLogger.warning() << "Failed to modify tags of file" << m_fileName; - return std::make_pair(ExportResult::Failed, QDateTime()); } - - // pTagSaver will be destroyed and all open files closed before safelyWritableFile + return std::make_pair(ExportResult::Failed, QDateTime()); } } // namespace mixxx From ba5cbff07aab410198e052b9fba202de272ceafc Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 20:35:52 +0100 Subject: [PATCH 28/52] Change wording of informational messages again --- src/preferences/dialog/dlgpreflibrary.cpp | 5 ++--- src/widget/wtracktableview.cpp | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 1cd39696b2f0..fe4c520f7128 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -358,9 +358,8 @@ void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() { QMessageBox::information( nullptr, tr("Export Modified Track Metadata"), - tr("File modifications are deferred and may not appear at once! " - "If you do not see changed metadata in other programs, " - "close Mixxx to modify those files immediately.")); + tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches! " + "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); m_bShowTrackMetadataExportInfo = false; } } diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 5aab34308764..81e1e8e1d4a4 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -1424,9 +1424,8 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { QMessageBox::information( nullptr, tr("Export Track Metadata"), - tr("File modifications are deferred and may not appear at once! " - "If you do not see changed metadata in other programs, " - "close Mixxx to modify those files immediately.")); + tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches! " + "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); m_bShowTrackMetadataExportInfo = false; } From 69bd2bf6d36946841274b266ebdcdaf47d8ab6c7 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 22:03:36 +0100 Subject: [PATCH 29/52] Add a quick hack for avoiding redundant message strings --- build/depends.py | 1 + src/library/exporttrackmetadatainfo.cpp | 16 ++++++++++++++++ src/library/exporttrackmetadatainfo.h | 13 +++++++++++++ src/preferences/dialog/dlgpreflibrary.cpp | 7 ++----- src/widget/wtracktableview.cpp | 7 ++----- 5 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 src/library/exporttrackmetadatainfo.cpp create mode 100644 src/library/exporttrackmetadatainfo.h diff --git a/build/depends.py b/build/depends.py index b89760a27067..bf7ade5826a9 100644 --- a/build/depends.py +++ b/build/depends.py @@ -942,6 +942,7 @@ def sources(self, build): "library/dlgmissing.cpp", "library/dlgtagfetcher.cpp", "library/dlgtrackinfo.cpp", + "library/exporttrackmetadatainfo.cpp", "library/browse/browsetablemodel.cpp", "library/browse/browsethread.cpp", diff --git a/src/library/exporttrackmetadatainfo.cpp b/src/library/exporttrackmetadatainfo.cpp new file mode 100644 index 000000000000..b7e5136d6336 --- /dev/null +++ b/src/library/exporttrackmetadatainfo.cpp @@ -0,0 +1,16 @@ +#include "library/exporttrackmetadatainfo.h" + +#include + + +namespace mixxx { + +void ExportTrackMetadataInfo::showMessageBox() { + QMessageBox::information( + nullptr, + tr("Export Modified Track Metadata"), + tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches. " + "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); +} + +} // namespace mixxx diff --git a/src/library/exporttrackmetadatainfo.h b/src/library/exporttrackmetadatainfo.h new file mode 100644 index 000000000000..30211f8b3226 --- /dev/null +++ b/src/library/exporttrackmetadatainfo.h @@ -0,0 +1,13 @@ +#pragma once + +#include + + +namespace mixxx { + +class ExportTrackMetadataInfo: private QDialog { + public: + static void showMessageBox(); +}; + +} // namespace mixxx diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index fe4c520f7128..f782c14b3adf 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -9,6 +9,7 @@ #include #include "preferences/dialog/dlgpreflibrary.h" +#include "library/exporttrackmetadatainfo.h" #include "sources/soundsourceproxy.h" #define MIXXX_ADDONS_URL "http://www.mixxx.org/wiki/doku.php/add-ons" @@ -355,11 +356,7 @@ void DlgPrefLibrary::slotSelectFont() { void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() { if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked() && m_bShowTrackMetadataExportInfo) { - QMessageBox::information( - nullptr, - tr("Export Modified Track Metadata"), - tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches! " - "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); + mixxx::ExportTrackMetadataInfo::showMessageBox(); m_bShowTrackMetadataExportInfo = false; } } diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 81e1e8e1d4a4..4cb8e06e7dc1 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -16,6 +16,7 @@ #include "library/librarytablemodel.h" #include "library/crate/cratefeaturehelper.h" #include "library/dao/trackschema.h" +#include "library/exporttrackmetadatainfo.h" #include "control/controlobject.h" #include "control/controlproxy.h" #include "track/track.h" @@ -1421,11 +1422,7 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { if (m_bShowTrackMetadataExportInfo) { // Inform the user once per session that the corresponding files // will not be modified at once and changes may appear later. - QMessageBox::information( - nullptr, - tr("Export Track Metadata"), - tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches! " - "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); + mixxx::ExportTrackMetadataInfo::showMessageBox(); m_bShowTrackMetadataExportInfo = false; } From 36e3cf98f3f210661f0468b5dc2be73ef44d19ce Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 2 Dec 2017 22:28:51 +0100 Subject: [PATCH 30/52] Reduce ugliness of the translabtable string hack --- src/library/exporttrackmetadatainfo.cpp | 16 +++++++++++----- src/library/exporttrackmetadatainfo.h | 5 +++++ src/preferences/dialog/dlgpreflibrary.cpp | 6 ++---- src/preferences/dialog/dlgpreflibrary.h | 2 -- src/widget/wtracktableview.cpp | 12 ++++-------- src/widget/wtracktableview.h | 2 -- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/library/exporttrackmetadatainfo.cpp b/src/library/exporttrackmetadatainfo.cpp index b7e5136d6336..52fb17119a4f 100644 --- a/src/library/exporttrackmetadatainfo.cpp +++ b/src/library/exporttrackmetadatainfo.cpp @@ -5,12 +5,18 @@ namespace mixxx { +//static +bool ExportTrackMetadataInfo::s_bShownOncePerSession = false; + void ExportTrackMetadataInfo::showMessageBox() { - QMessageBox::information( - nullptr, - tr("Export Modified Track Metadata"), - tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches. " - "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); + if (!s_bShownOncePerSession) { + QMessageBox::information( + nullptr, + tr("Export Modified Track Metadata"), + tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches. " + "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); + s_bShownOncePerSession = true; + } } } // namespace mixxx diff --git a/src/library/exporttrackmetadatainfo.h b/src/library/exporttrackmetadatainfo.h index 30211f8b3226..b787bdf98472 100644 --- a/src/library/exporttrackmetadatainfo.h +++ b/src/library/exporttrackmetadatainfo.h @@ -5,9 +5,14 @@ namespace mixxx { +// This is just an ugly hack to avoid duplicate code and to define +// translatable strings only once. class ExportTrackMetadataInfo: private QDialog { public: static void showMessageBox(); + + private: + static bool s_bShownOncePerSession; }; } // namespace mixxx diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index f782c14b3adf..95bfe6b01ad9 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -23,8 +23,7 @@ DlgPrefLibrary::DlgPrefLibrary( m_pConfig(pConfig), m_pLibrary(pLibrary), m_bAddedDirectory(false), - m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx), - m_bShowTrackMetadataExportInfo(true) { + m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx) { setupUi(this); connect(this, SIGNAL(requestAddDir(QString)), @@ -355,8 +354,7 @@ void DlgPrefLibrary::slotSelectFont() { } void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() { - if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked() && m_bShowTrackMetadataExportInfo) { + if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked()) { mixxx::ExportTrackMetadataInfo::showMessageBox(); - m_bShowTrackMetadataExportInfo = false; } } diff --git a/src/preferences/dialog/dlgpreflibrary.h b/src/preferences/dialog/dlgpreflibrary.h index fa3d974eafa8..58b76631890a 100644 --- a/src/preferences/dialog/dlgpreflibrary.h +++ b/src/preferences/dialog/dlgpreflibrary.h @@ -68,8 +68,6 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { bool m_bAddedDirectory; QFont m_originalTrackTableFont; int m_iOriginalTrackTableRowHeight; - - bool m_bShowTrackMetadataExportInfo; }; #endif diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 4cb8e06e7dc1..2f76afe1f474 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -44,8 +44,7 @@ WTrackTableView::WTrackTableView(QWidget * parent, m_iCoverHashColumn(-1), m_iCoverColumn(-1), m_selectionChangedSinceLastGuiTick(true), - m_loadCachedOnly(false), - m_bShowTrackMetadataExportInfo(true) { + m_loadCachedOnly(false) { connect(&m_loadTrackMapper, SIGNAL(mapped(QString)), @@ -1419,12 +1418,9 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { return; } - if (m_bShowTrackMetadataExportInfo) { - // Inform the user once per session that the corresponding files - // will not be modified at once and changes may appear later. - mixxx::ExportTrackMetadataInfo::showMessageBox(); - m_bShowTrackMetadataExportInfo = false; - } + // Inform the user once per session that the corresponding files + // will not be modified at once and changes may appear later. + mixxx::ExportTrackMetadataInfo::showMessageBox(); for (const QModelIndex& index : indices) { TrackPointer pTrack = trackModel->getTrack(index); diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index daa9dae279b7..6f9f9b19bd5f 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -190,8 +190,6 @@ class WTrackTableView : public WLibraryTableView { bool m_selectionChangedSinceLastGuiTick; bool m_loadCachedOnly; ControlProxy* m_pCOTGuiTick; - - bool m_bShowTrackMetadataExportInfo; }; #endif From 6822a608adacacaab99133e0afeb5af474161401 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Dec 2017 12:05:25 +0100 Subject: [PATCH 31/52] VorbisComment: Handle "DESCRIPTION"/"COMMENT" fields like TagLib Aligned with TagLib 1.11 + bug fix when writing into those fields --- src/track/trackmetadatataglib.cpp | 46 ++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp index a43ba661c676..3c8a89832729 100644 --- a/src/track/trackmetadatataglib.cpp +++ b/src/track/trackmetadatataglib.cpp @@ -20,6 +20,10 @@ #define TAGLIB_HAS_VORBIS_COMMENT_PICTURES \ (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 11)) +// TagLib correctly distinguishes between "DESCRIPTION" and "COMMENT" fields since version 1.11 (at least for reading) +#define TAGLIB_HAS_VORBIS_COMMENT_READ_DESCRIPTION \ + (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 11)) + #include #include #include @@ -760,7 +764,7 @@ void writeAPEItem( bool readXiphCommentField( const TagLib::Ogg::XiphComment& tag, const TagLib::String& key, - QString* pValue = nullptr) { + QString* pValue) { const TagLib::Ogg::FieldListMap::ConstIterator it( tag.fieldListMap().find(key)); if (it != tag.fieldListMap().end() && !(*it).second.isEmpty()) { @@ -773,6 +777,13 @@ bool readXiphCommentField( } } +inline +bool hasXiphCommentField( + const TagLib::Ogg::XiphComment& tag, + const TagLib::String& key) { + return readXiphCommentField(tag, key, nullptr); +} + // Unconditionally write the field void writeXiphCommentField( TagLib::Ogg::XiphComment* pTag, @@ -792,7 +803,7 @@ void updateXiphCommentField( TagLib::Ogg::XiphComment* pTag, const TagLib::String& key, const TagLib::String& value) { - if (readXiphCommentField(*pTag, key)) { + if (hasXiphCommentField(*pTag, key)) { writeXiphCommentField(pTag, key, value); } } @@ -1388,15 +1399,15 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, importTrackMetadataFromTag(pTrackMetadata, tag); - // Some applications (like puddletag up to version 1.0.5) write - // "COMMENT" instead "DESCRIPTION". +#ifndef TAGLIB_HAS_VORBIS_COMMENT_READ_DESCRIPTION // Reference: http://www.xiph.org/vorbis/doc/v-comment.html - if (!readXiphCommentField(tag, "DESCRIPTION")) { // recommended field (already read by TagLib) - QString comment; - if (readXiphCommentField(tag, "COMMENT", &comment)) { // alternative field - pTrackMetadata->refTrackInfo().setComment(comment); - } + // Workaround for TagLib < 1.11, see also xiphcomment.cpp / Ogg::XiphComment::comment() + QString comment; + if (!readXiphCommentField(tag, "DESCRIPTION", &comment) || comment.isEmpty()) { + readXiphCommentField(tag, "COMMENT", &comment); } + pTrackMetadata->refTrackInfo().setComment(comment); +#endif QString albumArtist; if (readXiphCommentField(tag, "ALBUMARTIST", &albumArtist) || // recommended field @@ -2023,7 +2034,22 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, } exportTrackMetadataIntoTag(pTag, trackMetadata, - WRITE_TAG_OMIT_TRACK_NUMBER | WRITE_TAG_OMIT_YEAR); + WRITE_TAG_OMIT_TRACK_NUMBER | + WRITE_TAG_OMIT_YEAR | + WRITE_TAG_OMIT_COMMENT); + + // Reference: http://www.xiph.org/vorbis/doc/v-comment.html + // NOTE(uklotzde, 2017-12-03): TagLib is doing it wrong up to the current version 1.11.1! + if (hasXiphCommentField(*pTag, "DESCRIPTION") || !hasXiphCommentField(*pTag, "COMMENT")) { + // Update or add the correct field for comments + writeXiphCommentField(pTag, "DESCRIPTION", + toTagLibString(trackMetadata.getTrackInfo().getComment())); + } else { + // Update the "wrong" field for comments only if it already exists exclusively + DEBUG_ASSERT(hasXiphCommentField(*pTag, "COMMENT")); + writeXiphCommentField(pTag, "COMMENT", + toTagLibString(trackMetadata.getTrackInfo().getComment())); + } // Write unambiguous fields writeXiphCommentField(pTag, "DATE", From 8f53cc8053bbad3626b428d9b232dc73f6a6c337 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Dec 2017 12:06:40 +0100 Subject: [PATCH 32/52] VorbisComment: Use MusicBrainz recommendation "BPM" instead of "TEMPO" --- src/track/trackmetadatataglib.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp index 3c8a89832729..a0ac95ec1841 100644 --- a/src/track/trackmetadatataglib.cpp +++ b/src/track/trackmetadatataglib.cpp @@ -1452,10 +1452,13 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, pTrackMetadata->refTrackInfo().setYear(date); } + // MusicBrainz recommends "BPM": https://picard.musicbrainz.org/docs/mappings + // Mixxx (<= 2.0) favored "TEMPO": https://www.mixxx.org/wiki/doku.php/library_metadata_rewrite_using_taglib QString bpm; - if (readXiphCommentField(tag, "TEMPO", &bpm) || // recommended field - readXiphCommentField(tag, "BPM", &bpm)) { // alternative field - parseBpm(pTrackMetadata, bpm); + if (!readXiphCommentField(tag, "BPM", &bpm) || !parseBpm(pTrackMetadata, bpm)) { + if (readXiphCommentField(tag, "TEMPO", &bpm)) { + parseBpm(pTrackMetadata, bpm); + } } // Only read track gain (not album gain) @@ -2115,8 +2118,16 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, const TagLib::String bpm( toTagLibString(formatBpm(trackMetadata))); - writeXiphCommentField(pTag, "TEMPO", bpm); // recommended field - updateXiphCommentField(pTag, "BPM", bpm); // alternative field + // MusicBrainz recommends "BPM": https://picard.musicbrainz.org/docs/mappings + // Mixxx (<= 2.0) favored "TEMPO": https://www.mixxx.org/wiki/doku.php/library_metadata_rewrite_using_taglib + if (hasXiphCommentField(*pTag, "BPM") || !hasXiphCommentField(*pTag, "TEMPO")) { + // Update or add the recommended field for BPM values + writeXiphCommentField(pTag, "BPM", bpm); + } else { + // Update the legacy field for BPM values only if it already exists exclusively + DEBUG_ASSERT(hasXiphCommentField(*pTag, "TEMPO")); + writeXiphCommentField(pTag, "TEMPO", bpm); + } // Write both INITIALKEY and KEY const TagLib::String key( From 9e864722fcbd0c30f935ff5045a19f98c290de57 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Dec 2017 13:29:51 +0100 Subject: [PATCH 33/52] Fix initialization order of members --- src/sources/audiosourcetrackproxy.h | 16 ++++++++-------- src/sources/soundsourceproxy.cpp | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sources/audiosourcetrackproxy.h b/src/sources/audiosourcetrackproxy.h index 0e422f93499b..d532865cc3bc 100644 --- a/src/sources/audiosourcetrackproxy.h +++ b/src/sources/audiosourcetrackproxy.h @@ -17,19 +17,19 @@ namespace mixxx { class AudioSourceTrackProxy: public AudioSource { public: static AudioSourcePointer create( - AudioSourcePointer pAudioSource, - TrackPointer pTrack) { + TrackPointer pTrack, + AudioSourcePointer pAudioSource) { return std::make_shared( - std::move(pAudioSource), - std::move(pTrack)); + std::move(pTrack), + std::move(pAudioSource)); } AudioSourceTrackProxy( - AudioSourcePointer pAudioSource, - TrackPointer pTrack) + TrackPointer pTrack, + AudioSourcePointer pAudioSource) : AudioSource(*pAudioSource), - m_pAudioSource(std::move(pAudioSource)), - m_pTrack(std::move(pTrack)) { + m_pTrack(std::move(pTrack)), + m_pAudioSource(std::move(pAudioSource)) { } void close() override { diff --git a/src/sources/soundsourceproxy.cpp b/src/sources/soundsourceproxy.cpp index 2b28741eb750..015f4d479bab 100644 --- a/src/sources/soundsourceproxy.cpp +++ b/src/sources/soundsourceproxy.cpp @@ -443,7 +443,7 @@ void SoundSourceProxy::updateTrackFromSource( return; // abort } // Only parse and update cover art from file tags if the track has - // no cover art or if cover art has already been loaded file tags. + // no cover art or if cover art has already been loaded from file tags. if (((coverInfo.type == CoverInfo::METADATA) || (coverInfo.type == CoverInfo::NONE)) && (coverInfo.source != CoverInfo::USER_SELECTED)) { @@ -584,7 +584,7 @@ mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource(const mixxx::AudioSo continue; // try again } if ((openResult == mixxx::SoundSource::OpenResult::Succeeded) && m_pSoundSource->verifyReadable()) { - m_pAudioSource = mixxx::AudioSourceTrackProxy::create(m_pSoundSource, m_pTrack); + m_pAudioSource = mixxx::AudioSourceTrackProxy::create(m_pTrack, m_pSoundSource); DEBUG_ASSERT(m_pAudioSource); if (m_pAudioSource->frameIndexRange().empty()) { kLogger.warning() << "File is empty" From d0177a5179349282fa74178cf135573527949466 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Dec 2017 14:41:15 +0100 Subject: [PATCH 34/52] Read file modification dates safely --- src/sources/metadatasourcetaglib.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index b41c7ab16ef7..07ae8838ab90 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -73,17 +73,24 @@ class AiffFile: public TagLib::RIFF::AIFF::File { } }; -} // anonymous namespace +QDateTime getMetadataSynchronized(QFileInfo fileInfo) { + const QDateTime metadataSynchronized = fileInfo.lastModified(); + VERIFY_OR_DEBUG_ASSERT(!metadataSynchronized.isNull()) { + return QDateTime::currentDateTime(); + } + return metadataSynchronized; +} +} // anonymous namespace std::pair MetadataSourceTagLib::afterImportSucceeded() const { - return std::make_pair(ImportResult::Succeeded, QFileInfo(m_fileName).lastModified()); + return std::make_pair(ImportResult::Succeeded, getMetadataSynchronized(QFileInfo(m_fileName))); } std::pair MetadataSourceTagLib::afterExportSucceeded() const { - return std::make_pair(ExportResult::Succeeded, QFileInfo(m_fileName).lastModified()); + return std::make_pair(ExportResult::Succeeded, getMetadataSynchronized(QFileInfo(m_fileName))); } std::pair From 2513d9448803d0f2f586ff048085763423a2e56d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Dec 2017 15:17:25 +0100 Subject: [PATCH 35/52] Prepare to configure safe/unsafe file access when exporting track metadata --- src/sources/metadatasourcetaglib.cpp | 38 +++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index 07ae8838ab90..27d0412e6cb6 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -23,6 +23,9 @@ namespace { Logger kLogger("MetadataSourceTagLib"); +// TODO(uklotzde): Add a configurable option in the user settings +const bool kExportTrackMetadataIntoTemporaryFile = true; + // Appended to the original file name of the temporary file used for writing const QString kSafelyWritableTempFileSuffix = "_temp"; @@ -615,20 +618,22 @@ class AiffTagSaver: public TagSaver { */ class SafelyWritableFile final { public: - explicit SafelyWritableFile(QString origFileName) + SafelyWritableFile(QString origFileName, bool useTemporaryFile) : m_origFileName(std::move(origFileName)) { DEBUG_ASSERT(m_tempFileName.isNull()); - QString tempFileName = m_origFileName + kSafelyWritableTempFileSuffix; - QFile origFile(m_origFileName); - if (origFile.copy(tempFileName)) { - m_tempFileName = std::move(tempFileName); - } else { - kLogger.warning() - << origFile.errorString() - << "- Failed to copy original into temporary file before writing:" - << origFile.fileName() - << "->" - << tempFileName; + if (useTemporaryFile) { + QString tempFileName = m_origFileName + kSafelyWritableTempFileSuffix; + QFile origFile(m_origFileName); + if (origFile.copy(tempFileName)) { + m_tempFileName = std::move(tempFileName); + } else { + kLogger.warning() + << origFile.errorString() + << "- Failed to copy original into temporary file before writing:" + << origFile.fileName() + << "->" + << tempFileName; + } } } ~SafelyWritableFile() { @@ -641,11 +646,14 @@ class SafelyWritableFile final { bool commit() { if (m_tempFileName.isNull()) { - return false; // nothing to do + return true; // nothing to do } QFile newFile(m_tempFileName); if (!newFile.exists()) { - return false; // nothing to do + kLogger.warning() + << "Temporary file not found:" + << newFile.fileName(); + return false; } QFile oldFile(m_origFileName); if (oldFile.exists()) { @@ -727,7 +735,7 @@ MetadataSourceTagLib::exportTrackMetadata( << "into file" << m_fileName << "with type" << m_fileType; - SafelyWritableFile safelyWritableFile(m_fileName); + SafelyWritableFile safelyWritableFile(m_fileName, kExportTrackMetadataIntoTemporaryFile); std::unique_ptr pTagSaver; switch (m_fileType) { From f36c7dd3e24170313884474217c07b0d3eb1997a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Dec 2017 15:17:25 +0100 Subject: [PATCH 36/52] Follow-up of previous commit --- src/sources/metadatasourcetaglib.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index 27d0412e6cb6..82e0d63680b8 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -641,7 +641,11 @@ class SafelyWritableFile final { } const QString& fileName() const { - return m_tempFileName; + if (m_tempFileName.isNull()) { + return m_origFileName; + } else { + return m_tempFileName; + } } bool commit() { @@ -699,6 +703,8 @@ class SafelyWritableFile final { return false; } } + // Prevent any further interaction and file access + m_origFileName = QString(); m_tempFileName = QString(); return true; } @@ -717,7 +723,8 @@ class SafelyWritableFile final { << "- Failed to remove temporary file:" << m_tempFileName; } - // Only try once to remove the temporary file + // Prevent any further interaction and file access + m_origFileName = QString(); m_tempFileName = QString(); } From 56a73e852c09fa1c7a5712e3208126461ed4a977 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Dec 2017 17:42:25 +0100 Subject: [PATCH 37/52] VorbisComments: Prefer "COMMENT" (MusicBrainz) over "DESCRIPTION" ...and do not rely on TagLib here! --- src/track/trackmetadatataglib.cpp | 42 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp index a0ac95ec1841..02da185033a8 100644 --- a/src/track/trackmetadatataglib.cpp +++ b/src/track/trackmetadatataglib.cpp @@ -20,10 +20,6 @@ #define TAGLIB_HAS_VORBIS_COMMENT_PICTURES \ (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 11)) -// TagLib correctly distinguishes between "DESCRIPTION" and "COMMENT" fields since version 1.11 (at least for reading) -#define TAGLIB_HAS_VORBIS_COMMENT_READ_DESCRIPTION \ - (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 11)) - #include #include #include @@ -1399,15 +1395,22 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, importTrackMetadataFromTag(pTrackMetadata, tag); -#ifndef TAGLIB_HAS_VORBIS_COMMENT_READ_DESCRIPTION - // Reference: http://www.xiph.org/vorbis/doc/v-comment.html - // Workaround for TagLib < 1.11, see also xiphcomment.cpp / Ogg::XiphComment::comment() + // The original specification only defines a "DESCRIPTION" field, + // while MusicBrainz recommends to use "COMMENT". Mixxx follows + // MusicBrainz. + // http://www.xiph.org/vorbis/doc/v-comment.html + // https://picard.musicbrainz.org/docs/mappings + // + // We are not relying on TagLib (1.11.1) with a somehow inconsistent + // handling. It prefers "DECSCRIPTION" for reading, but adds a "COMMENT" + // fields upon writing when no "DESCRIPTION" field exists. QString comment; - if (!readXiphCommentField(tag, "DESCRIPTION", &comment) || comment.isEmpty()) { - readXiphCommentField(tag, "COMMENT", &comment); + if (!readXiphCommentField(tag, "COMMENT", &comment) || comment.isEmpty()) { + // Fallback to the the original "DESCRIPTION" field only if the + // "COMMENT" field is either missing or empty + readXiphCommentField(tag, "DESCRIPTION", &comment); } pTrackMetadata->refTrackInfo().setComment(comment); -#endif QString albumArtist; if (readXiphCommentField(tag, "ALBUMARTIST", &albumArtist) || // recommended field @@ -2041,16 +2044,19 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, WRITE_TAG_OMIT_YEAR | WRITE_TAG_OMIT_COMMENT); - // Reference: http://www.xiph.org/vorbis/doc/v-comment.html - // NOTE(uklotzde, 2017-12-03): TagLib is doing it wrong up to the current version 1.11.1! - if (hasXiphCommentField(*pTag, "DESCRIPTION") || !hasXiphCommentField(*pTag, "COMMENT")) { - // Update or add the correct field for comments - writeXiphCommentField(pTag, "DESCRIPTION", + // The original specification only defines a "DESCRIPTION" field, + // while MusicBrainz recommends to use "COMMENT". Mixxx follows + // MusicBrainz. + // http://www.xiph.org/vorbis/doc/v-comment.html + // https://picard.musicbrainz.org/docs/mappings + if (hasXiphCommentField(*pTag, "COMMENT") || !hasXiphCommentField(*pTag, "DESCRIPTION")) { + // MusicBrainz-style + writeXiphCommentField(pTag, "COMMENT", toTagLibString(trackMetadata.getTrackInfo().getComment())); } else { - // Update the "wrong" field for comments only if it already exists exclusively - DEBUG_ASSERT(hasXiphCommentField(*pTag, "COMMENT")); - writeXiphCommentField(pTag, "COMMENT", + // Preserve and update the "DESCRIPTION" field only if it already exists + DEBUG_ASSERT(hasXiphCommentField(*pTag, "DESCRIPTION")); + writeXiphCommentField(pTag, "DESCRIPTION", toTagLibString(trackMetadata.getTrackInfo().getComment())); } From 92a7daba93ceac4c3612c56cfc0ece211f90819c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 4 Dec 2017 07:51:04 +0100 Subject: [PATCH 38/52] Remove the recent tracks cache from TrackDAO --- src/library/basetrackcache.cpp | 15 +++++--- src/library/basetrackcache.h | 6 ++- src/library/dao/trackdao.cpp | 63 ++----------------------------- src/library/dao/trackdao.h | 36 +----------------- src/library/trackcollection.cpp | 2 - src/test/autodjprocessor_test.cpp | 2 +- 6 files changed, 20 insertions(+), 104 deletions(-) diff --git a/src/library/basetrackcache.cpp b/src/library/basetrackcache.cpp index b87a27062859..cd129097a0b6 100644 --- a/src/library/basetrackcache.cpp +++ b/src/library/basetrackcache.cpp @@ -10,6 +10,7 @@ #include "library/searchqueryparser.h" #include "library/queryutil.h" #include "track/keyutils.h" +#include "track/trackcache.h" #include "util/performancetimer.h" namespace { @@ -144,12 +145,16 @@ void BaseTrackCache::setSearchColumns(const QStringList& columns) { } TrackPointer BaseTrackCache::lookupCachedTrack(TrackId trackId) const { - // Only get the track from the TrackDAO if it's in the cache and marked as - // dirty. - if (m_bIsCaching && m_dirtyTracks.contains(trackId)) { - return m_trackDAO.getTrack(trackId, true); + TrackCacheLocker cacheLocker( + TrackCache::instance().lookupById(trackId)); + auto pTrack = cacheLocker.getTrack(); + if (pTrack && pTrack->isDirty()) { + m_dirtyTracks.insert(trackId); + return pTrack; + } else { + m_dirtyTracks.remove(trackId); + return TrackPointer(); } - return TrackPointer(); } bool BaseTrackCache::updateIndexWithTrackpointer(TrackPointer pTrack) { diff --git a/src/library/basetrackcache.h b/src/library/basetrackcache.h index a8eb2453f54f..4d7c796b2fca 100644 --- a/src/library/basetrackcache.h +++ b/src/library/basetrackcache.h @@ -126,7 +126,11 @@ class BaseTrackCache : public QObject { QVector m_trackOrder; - QSet m_dirtyTracks; + // This set is updated by signals from the Track object. It might contain + // false positives, i.e. track ids of tracks that are neither cached nor + // dirty. Each invocation of lookupCachedTrack() will take care of + // updating this set by inserting and removing entries as required. + mutable QSet m_dirtyTracks; bool m_bIndexBuilt; bool m_bIsCaching; diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index a48282d4be3f..10ac1fec5217 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -39,20 +39,6 @@ enum { UndefinedRecordIndex = -2 }; -RecentTrackCacheItem::RecentTrackCacheItem( - const TrackPointer& pTrack) - : m_pTrack(pTrack) { - DEBUG_ASSERT(m_pTrack); -} - -// The number of recently used tracks to cache strong references to at -// once. Once the n+1'th track is created, the TrackDAO's QCache deletes it -// and drops the strong reference. The recent tracks cache basically -// functions to prevent repeated getTrack() calls for the same track from -// repeatedly deserializing / serializing a track to the database since this is -// expensive. -const int kRecentTracksCacheSize = 5; - TrackDAO::TrackDAO(CueDAO& cueDao, PlaylistDAO& playlistDao, AnalysisDao& analysisDao, @@ -63,7 +49,6 @@ TrackDAO::TrackDAO(CueDAO& cueDao, m_analysisDao(analysisDao), m_libraryHashDao(libraryHashDao), m_pConfig(pConfig), - m_recentTracksCache(kRecentTracksCacheSize), m_trackLocationIdColumn(UndefinedRecordIndex), m_queryLibraryIdColumn(UndefinedRecordIndex), m_queryLibraryMixxxDeletedColumn(UndefinedRecordIndex) { @@ -78,9 +63,6 @@ TrackDAO::~TrackDAO() { void TrackDAO::finish() { qDebug() << "TrackDAO::finish()"; - // Drop all strong references of recently loaded tracks. - clearCache(); - // clear out played information on exit // crash prevention: if mixxx crashes, played information will be maintained qDebug() << "Clearing played information for this session"; @@ -679,9 +661,6 @@ TrackPointer TrackDAO::addSingleTrack(const QFileInfo& fileInfo, bool unremove) addTracksPrepare(); TrackPointer pTrack(addTracksAddFile(fileInfo, unremove)); addTracksFinish(!pTrack); - if (pTrack) { - cacheRecentTrack(pTrack->getId(), pTrack); - } return pTrack; } @@ -1160,15 +1139,6 @@ struct ColumnPopulator { #define ARRAYLENGTH(x) (sizeof(x) / sizeof(*x)) -void TrackDAO::cacheRecentTrack( - TrackId trackId, - const TrackPointer& pTrack) const { - std::unique_ptr pCacheItem = - std::make_unique(pTrack); - m_recentTracksCache.insert(trackId, pCacheItem.get()); - pCacheItem.release(); // m_recentTracksCache has taken ownership -} - TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const { if (!trackId.isValid()) { return TrackPointer(); @@ -1343,9 +1313,6 @@ TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const { this, SLOT(slotTrackChanged(Track*)), Qt::DirectConnection); - // Insert the loaded track into the recent tracks cache - cacheRecentTrack(trackId, pTrack); - // Finish the caching operation to release all locks before // emitting any signals. cacheResolver.unlockCache(); @@ -1363,32 +1330,13 @@ TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const { return pTrack; } -TrackPointer TrackDAO::getTrack(TrackId trackId, const bool cacheOnly) const { +TrackPointer TrackDAO::getTrack(TrackId trackId) const { //qDebug() << "TrackDAO::getTrack" << QThread::currentThread() << m_database.connectionName(); - // If the track cache contains the track ID, use it to get a strong - // reference to the track. We do this first so that the QCache keeps track - // of the least-recently-used track so that it expires them intelligently. - RecentTrackCacheItem* pTrackCacheItem = m_recentTracksCache.object(trackId); - if (pTrackCacheItem != nullptr) { - return pTrackCacheItem->getTrack(); - } - - // Next, check the weak-reference cache to see if the track still has a - // strong reference somewhere. TrackCacheLocker cacheLocker( TrackCache::instance().lookupById(trackId)); - TrackPointer pTrack(cacheLocker.getTrack()); - if (!pTrack) { - // Cache miss - if (!cacheOnly) { - // Deserialize the track from the database. - pTrack = getTrackFromDB(trackId); - // The loaded track has already been inserted into - // the recent tracks cache. - } - } - return pTrack; + TrackPointer pTrack = cacheLocker.getTrack(); + return pTrack ? pTrack : getTrackFromDB(trackId); } // Saves a track's info back to the database @@ -1708,11 +1656,6 @@ bool TrackDAO::detectMovedTracks(QSet* pTracksMovedSetOld, return true; } -void TrackDAO::clearCache() { - // Drops all strong references from the recent tracks cache. - m_recentTracksCache.clear(); -} - void TrackDAO::markTracksAsMixxxDeleted(const QString& dir) { // Capture entries that start with the directory prefix dir. // dir needs to end in a slash otherwise we might match other diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 6237a9925ddd..39c06a2947eb 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -6,7 +6,6 @@ #include #include #include -#include #include #include "preferences/usersettings.h" @@ -23,20 +22,6 @@ class LibraryHashDAO; class TrackCollection; class TrackCacheResolver; -// Holds a strong reference to a track while it is in the "recent tracks" -// cache. -class RecentTrackCacheItem final { - public: - explicit RecentTrackCacheItem( - const TrackPointer& pTrack); - - const TrackPointer& getTrack() const { - return m_pTrack; - } - - private: - TrackPointer m_pTrack; -}; class TrackDAO : public QObject, public virtual DAO { Q_OBJECT @@ -63,7 +48,7 @@ class TrackDAO : public QObject, public virtual DAO { bool trackExistsInDatabase(const QString& absoluteFilePath); // WARNING: Only call this from the main thread instance of TrackDAO. - TrackPointer getTrack(TrackId trackId, const bool cacheOnly=false) const; + TrackPointer getTrack(TrackId trackId) const; // Returns a set of all track locations in the library. QSet getTrackLocations(); @@ -135,12 +120,6 @@ class TrackDAO : public QObject, public virtual DAO { void forceModelUpdate(); public slots: - // Clears the cached TrackInfoObjects, which can be useful when the - // underlying database tables change (eg. during a library rescan, - // we might detect that a track has been moved and modify the update - // the tables directly.) - void clearCache(); - void databaseTrackAdded(TrackPointer pTrack); void databaseTracksMoved(QSet tracksMovedSetOld, QSet tracksMovedSetNew); void databaseTracksChanged(QSet tracksChanged); @@ -166,19 +145,6 @@ class TrackDAO : public QObject, public virtual DAO { UserSettingsPointer m_pConfig; - void cacheRecentTrack( - TrackId trackId, - const TrackPointer& pTrack) const; - - // "Recent tracks" cache -- holds strong references to recently used - // tracks. When a track is expired, calls saveTrack(TrackPointer) without - // dropping the strong reference to the track. This prevents a race - // condition where the strong reference is dropped and therefore cache-only - // getTrack calls made by BaseSqlTableModel return null and serve stale - // results from BaseTrackCache before the newly expired TrackPointer has - // been saved to the database. - mutable QCache m_recentTracksCache; - std::unique_ptr m_pQueryTrackLocationInsert; std::unique_ptr m_pQueryTrackLocationSelect; std::unique_ptr m_pQueryLibraryInsert; diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index 96ae7a0bb310..9fdbe1597520 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -70,8 +70,6 @@ void TrackCollection::relocateDirectory(QString oldDir, QString newDir) { // Discard all cached tracks TrackCache::instance().evictAll(); - // Clear cache to that all TIO with the old dir information get updated - m_trackDao.clearCache(); m_trackDao.databaseTracksMoved(std::move(movedIds), QSet()); } diff --git a/src/test/autodjprocessor_test.cpp b/src/test/autodjprocessor_test.cpp index c8fdb5c921a6..7a130ace28fb 100644 --- a/src/test/autodjprocessor_test.cpp +++ b/src/test/autodjprocessor_test.cpp @@ -279,7 +279,7 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_DecksStopped) { // Load the track and mark it playing (as the loadTrackToPlayer signal would // have connected to this eventually). - TrackPointer pTrack = collection()->getTrackDAO().getTrack(testId, false); + TrackPointer pTrack = collection()->getTrackDAO().getTrack(testId); deck1.slotLoadTrack(pTrack, true); // Signal that the request to load pTrack succeeded. From ef1c20b95411f67292f2ccf33ccf856003f2d5a7 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 19:05:35 +0100 Subject: [PATCH 39/52] Delete unused code from Duration class --- src/test/duration_test.cpp | 3 --- src/util/duration.h | 19 ------------------- 2 files changed, 22 deletions(-) diff --git a/src/test/duration_test.cpp b/src/test/duration_test.cpp index 43b8a200c252..0f39f462c8e8 100644 --- a/src/test/duration_test.cpp +++ b/src/test/duration_test.cpp @@ -157,16 +157,13 @@ TEST(DurationTest, GreaterThan) { TEST(DurationTest, Format) { Duration d = Duration::fromNanos(255); - EXPECT_QSTRING_EQ("0x00000000000000ff", d.formatHex()); EXPECT_QSTRING_EQ("255 ns", d.formatNanosWithUnit()); d = Duration::fromNanos(-255); // Formatted as -255 in two's-complement. - EXPECT_QSTRING_EQ("0xffffffffffffff01", d.formatHex()); EXPECT_QSTRING_EQ("-255 ns", d.formatNanosWithUnit()); d = Duration::fromNanos(1e9); - EXPECT_QSTRING_EQ("0x000000003b9aca00", d.formatHex()); EXPECT_QSTRING_EQ("1000000000 ns", d.formatNanosWithUnit()); } diff --git a/src/util/duration.h b/src/util/duration.h index 01391ce61876..ebec1c00668b 100644 --- a/src/util/duration.h +++ b/src/util/duration.h @@ -15,7 +15,6 @@ class DurationBase { public: enum Units { - HEX, SECONDS, MILLIS, MICROS, @@ -98,13 +97,6 @@ class DurationDebug : public DurationBase { friend QDebug operator<<(QDebug debug, const DurationDebug& dd) { switch (dd.m_unit) { - case HEX: - { - QByteArray ret("0x0000000000000000"); - QByteArray hex = QByteArray::number(dd.m_durationNanos, 16); - ret.replace(18 - hex.size(), hex.size(), hex); - return debug << ret; - } case SECONDS: return debug << dd.toIntegerSeconds() << "s"; case MILLIS: @@ -221,17 +213,6 @@ class Duration : public DurationBase { return debug << duration.m_durationNanos << "ns"; } - // Formats the duration as a two's-complement hexadecimal string. - QString formatHex() const { - // Format as fixed-width (8 digits). - return QString("0x%1").arg(m_durationNanos, 16, 16, QLatin1Char('0')); - } - - // Formats the duration as a two's-complement hexadecimal string. - inline DurationDebug debugHex() const { - return debug(HEX); - } - QString formatNanosWithUnit() const { return QString("%1 ns").arg(toIntegerNanos()); } From 84778143a6267e9648ee569ae3e4fceb13463968 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 19:06:37 +0100 Subject: [PATCH 40/52] Fix minor oddities in Duration class --- src/soundio/sounddevicenetwork.cpp | 2 +- src/util/duration.h | 38 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/soundio/sounddevicenetwork.cpp b/src/soundio/sounddevicenetwork.cpp index c968db825407..07433260c68a 100644 --- a/src/soundio/sounddevicenetwork.cpp +++ b/src/soundio/sounddevicenetwork.cpp @@ -481,7 +481,7 @@ void SoundDeviceNetwork::updateAudioLatencyUsage() { double secInAudioCb = m_timeInAudioCallback.toDoubleSeconds(); m_pMasterAudioLatencyUsage->set(secInAudioCb / (m_framesSinceAudioLatencyUsageUpdate / m_dSampleRate)); - m_timeInAudioCallback.reset(); + m_timeInAudioCallback = mixxx::Duration::empty(); m_framesSinceAudioLatencyUsageUpdate = 0; //qDebug() << m_pMasterAudioLatencyUsage->get(); } diff --git a/src/util/duration.h b/src/util/duration.h index ebec1c00668b..075a30607bee 100644 --- a/src/util/duration.h +++ b/src/util/duration.h @@ -22,43 +22,43 @@ class DurationBase { }; // Returns the duration as an integer number of seconds (rounded-down). - inline qint64 toIntegerSeconds() const { + qint64 toIntegerSeconds() const { return m_durationNanos / kNanosPerSecond; } // Returns the duration as a floating point number of seconds. - inline double toDoubleSeconds() const { + double toDoubleSeconds() const { return static_cast(m_durationNanos) / kNanosPerSecond; } // Returns the duration as an integer number of milliseconds (rounded-down). - inline qint64 toIntegerMillis() const { + qint64 toIntegerMillis() const { return m_durationNanos / kNanosPerMilli; } // Returns the duration as a floating point number of milliseconds. - inline qint64 toDoubleMillis() const { + double toDoubleMillis() const { return static_cast(m_durationNanos) / kNanosPerMilli; } // Returns the duration as an integer number of microseconds (rounded-down). - inline qint64 toIntegerMicros() const { + qint64 toIntegerMicros() const { return m_durationNanos / kNanosPerMicro; } // Returns the duration as a floating point number of microseconds. - inline qint64 toDoubleMicros() const { + double toDoubleMicros() const { return static_cast(m_durationNanos) / kNanosPerMicro; } // Returns the duration as an integer number of nanoseconds. The duration is // represented internally as nanoseconds so no rounding occurs. - inline qint64 toIntegerNanos() const { + qint64 toIntegerNanos() const { return m_durationNanos; } // Returns the duration as an integer number of nanoseconds. - inline qint64 toDoubleNanos() const { + double toDoubleNanos() const { return static_cast(m_durationNanos); } @@ -74,14 +74,14 @@ class DurationBase { double dSeconds, Precision precision = Precision::SECONDS); - static const qint64 kMillisPerSecond = 1000; - static const qint64 kMicrosPerSecond = kMillisPerSecond * 1000; - static const qint64 kNanosPerSecond = kMicrosPerSecond * 1000; - static const qint64 kNanosPerMilli = kNanosPerSecond / 1000; - static const qint64 kNanosPerMicro = kNanosPerMilli / 1000; + static constexpr qint64 kMillisPerSecond = 1000; + static constexpr qint64 kMicrosPerSecond = kMillisPerSecond * 1000; + static constexpr qint64 kNanosPerSecond = kMicrosPerSecond * 1000; + static constexpr qint64 kNanosPerMilli = kNanosPerSecond / 1000; + static constexpr qint64 kNanosPerMicro = kNanosPerMilli / 1000; protected: - DurationBase(qint64 durationNanos) + explicit DurationBase(qint64 durationNanos) : m_durationNanos(durationNanos) { } @@ -140,12 +140,12 @@ class Duration : public DurationBase { return Duration(nanos); } - Duration() - : DurationBase(0) { + static Duration empty() { + return Duration(); } - void reset() { - m_durationNanos = 0; + Duration() + : DurationBase(0) { } const Duration operator+(const Duration& other) const { @@ -250,7 +250,7 @@ class Duration : public DurationBase { } private: - Duration(qint64 durationNanos) + explicit Duration(qint64 durationNanos) : DurationBase(durationNanos) { } }; From e8fc77afe55910aaab379125212c2445048a501f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 19:10:19 +0100 Subject: [PATCH 41/52] Do not consider audio properties when exporting metadata --- src/sources/soundsourcemodplug.cpp | 6 ++++-- src/sources/soundsourceopus.cpp | 10 +++++----- src/track/track.cpp | 26 +++++++++++++------------- src/track/trackinfo.cpp | 4 ---- src/track/trackinfo.h | 6 ------ src/track/trackmetadata.cpp | 9 +++++++++ src/track/trackmetadata.h | 28 +++++++++++++++++++++++----- src/track/trackmetadatataglib.cpp | 8 ++++---- 8 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 8f918de7a9ba..4938e8b9ac87 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -82,8 +82,10 @@ SoundSourceModPlug::importTrackMetadataAndCoverImage( pTrackMetadata->refTrackInfo().setComment(QString(ModPlug::ModPlug_GetMessage(pModFile))); pTrackMetadata->refTrackInfo().setTitle(QString(ModPlug::ModPlug_GetName(pModFile))); - pTrackMetadata->refTrackInfo().setDuration(Duration::fromMillis(ModPlug::ModPlug_GetLength(pModFile))); - pTrackMetadata->refTrackInfo().setBitrate(Bitrate(8)); // not really, but fill in something... + pTrackMetadata->setChannels(ChannelCount(kChannelCount)); + pTrackMetadata->setSampleRate(SampleRate(kSampleRate)); + pTrackMetadata->setDuration(Duration::fromMillis(ModPlug::ModPlug_GetLength(pModFile))); + pTrackMetadata->setBitrate(Bitrate(8)); // not really, but fill in something... ModPlug::ModPlug_Unload(pModFile); return std::make_pair(ImportResult::Succeeded, QFileInfo(modFile).lastModified()); diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 93c760ecf344..c87e964d9c73 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -117,13 +117,13 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( int i = 0; const OpusTags *l_ptrOpusTags = op_tags(pOggOpusFile, -1); - pTrackMetadata->refTrackInfo().setChannels(ChannelCount(op_channel_count(pOggOpusFile, -1))); - pTrackMetadata->refTrackInfo().setSampleRate(kSampleRate); - pTrackMetadata->refTrackInfo().setBitrate(Bitrate(op_bitrate(pOggOpusFile, -1) / 1000)); + pTrackMetadata->setChannels(ChannelCount(op_channel_count(pOggOpusFile, -1))); + pTrackMetadata->setSampleRate(kSampleRate); + pTrackMetadata->setBitrate(Bitrate(op_bitrate(pOggOpusFile, -1) / 1000)); // Cast to double is required for duration with sub-second precision const double dTotalFrames = op_pcm_total(pOggOpusFile, -1); - pTrackMetadata->refTrackInfo().setDuration(Duration::fromMicros( - 1000000 * dTotalFrames / pTrackMetadata->getTrackInfo().getSampleRate())); + pTrackMetadata->setDuration(Duration::fromMicros( + 1000000 * dTotalFrames / pTrackMetadata->getSampleRate())); bool hasDate = false; for (i = 0; i < l_ptrOpusTags->comments; ++i) { diff --git a/src/track/track.cpp b/src/track/track.cpp index 954bce42eb3e..271dd6b75336 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -373,22 +373,22 @@ void Track::setDateAdded(const QDateTime& dateAdded) { void Track::setDuration(mixxx::Duration duration) { QMutexLocker lock(&m_qMutex); - if (compareAndSet(&m_record.refMetadata().refTrackInfo().refDuration(), duration)) { + if (compareAndSet(&m_record.refMetadata().refDuration(), duration)) { markDirtyAndUnlock(&lock); } } void Track::setDuration(double duration) { - setDuration(mixxx::Duration::fromNanos(duration * 1000000000L)); + setDuration(mixxx::Duration::fromSeconds(duration)); } double Track::getDuration(DurationRounding rounding) const { QMutexLocker lock(&m_qMutex); switch (rounding) { case DurationRounding::SECONDS: - return std::round(m_record.getMetadata().getTrackInfo().getDuration().toDoubleSeconds()); + return std::round(m_record.getMetadata().getDuration().toDoubleSeconds()); default: - return m_record.getMetadata().getTrackInfo().getDuration().toDoubleSeconds(); + return m_record.getMetadata().getDuration().toDoubleSeconds(); } } @@ -582,31 +582,31 @@ void Track::setType(const QString& sType) { void Track::setSampleRate(int iSampleRate) { QMutexLocker lock(&m_qMutex); - if (compareAndSet(&m_record.refMetadata().refTrackInfo().refSampleRate(), mixxx::AudioSignal::SampleRate(iSampleRate))) { + if (compareAndSet(&m_record.refMetadata().refSampleRate(), mixxx::AudioSignal::SampleRate(iSampleRate))) { markDirtyAndUnlock(&lock); } } int Track::getSampleRate() const { QMutexLocker lock(&m_qMutex); - return m_record.getMetadata().getTrackInfo().getSampleRate(); + return m_record.getMetadata().getSampleRate(); } void Track::setChannels(int iChannels) { QMutexLocker lock(&m_qMutex); - if (compareAndSet(&m_record.refMetadata().refTrackInfo().refChannels(), mixxx::AudioSignal::ChannelCount(iChannels))) { + if (compareAndSet(&m_record.refMetadata().refChannels(), mixxx::AudioSignal::ChannelCount(iChannels))) { markDirtyAndUnlock(&lock); } } int Track::getChannels() const { QMutexLocker lock(&m_qMutex); - return m_record.getMetadata().getTrackInfo().getChannels(); + return m_record.getMetadata().getChannels(); } int Track::getBitrate() const { QMutexLocker lock(&m_qMutex); - return m_record.getMetadata().getTrackInfo().getBitrate(); + return m_record.getMetadata().getBitrate(); } QString Track::getBitrateText() const { @@ -615,7 +615,7 @@ QString Track::getBitrateText() const { void Track::setBitrate(int iBitrate) { QMutexLocker lock(&m_qMutex); - if (compareAndSet(&m_record.refMetadata().refTrackInfo().refBitrate(), mixxx::AudioSource::Bitrate(iBitrate))) { + if (compareAndSet(&m_record.refMetadata().refBitrate(), mixxx::AudioSource::Bitrate(iBitrate))) { markDirtyAndUnlock(&lock); } } @@ -949,10 +949,10 @@ Track::ExportMetadataResult Track::exportMetadata( // if the file has been modified by another program since we have imported // the metadata? But this is expected to happen if files have been copied // or after upgrading the column 'header_parsed' from bool to QDateTime. - mixxx::TrackMetadata trackMetadata; - if ((pMetadataSource->importTrackMetadataAndCoverImage(&trackMetadata, nullptr).first == + mixxx::TrackMetadata importedFromFile; + if ((pMetadataSource->importTrackMetadataAndCoverImage(&importedFromFile, nullptr).first == mixxx::MetadataSource::ImportResult::Succeeded) && - (m_record.getMetadata() == trackMetadata)) { + !m_record.getMetadata().hasBeenModifiedAfterImport(importedFromFile)) { kLogger.debug() << "Skip exporting of unmodified track metadata:" << getLocation(); diff --git a/src/track/trackinfo.cpp b/src/track/trackinfo.cpp index 2885cdbb3ab7..b88b52c15000 100644 --- a/src/track/trackinfo.cpp +++ b/src/track/trackinfo.cpp @@ -5,13 +5,10 @@ namespace mixxx { bool operator==(const TrackInfo& lhs, const TrackInfo& rhs) { return (lhs.getArtist() == rhs.getArtist()) && - (lhs.getBitrate() == rhs.getBitrate()) && (lhs.getBpm() == rhs.getBpm()) && - (lhs.getChannels() == rhs.getChannels()) && (lhs.getComment() == rhs.getComment()) && (lhs.getComposer() == rhs.getComposer()) && (lhs.getConductor() == rhs.getConductor()) && - (lhs.getDuration() == rhs.getDuration()) && (lhs.getGrouping() == rhs.getGrouping()) && (lhs.getGenre() == rhs.getGenre()) && (lhs.getISRC() == rhs.getISRC()) && @@ -24,7 +21,6 @@ bool operator==(const TrackInfo& lhs, const TrackInfo& rhs) { (lhs.getRecordLabel() == rhs.getRecordLabel()) && (lhs.getRemixer() == rhs.getRemixer()) && (lhs.getReplayGain() == rhs.getReplayGain()) && - (lhs.getSampleRate() == rhs.getSampleRate()) && (lhs.getSubtitle() == rhs.getSubtitle()) && (lhs.getTitle() == rhs.getTitle()) && (lhs.getTrackNumber() == rhs.getTrackNumber()) && diff --git a/src/track/trackinfo.h b/src/track/trackinfo.h index 690eda308617..491b61b03c66 100644 --- a/src/track/trackinfo.h +++ b/src/track/trackinfo.h @@ -21,7 +21,6 @@ class TrackInfo final { PROPERTY_SET_BYVAL_GET_BYREF(QString, comment, Comment) PROPERTY_SET_BYVAL_GET_BYREF(QString, composer, Composer) PROPERTY_SET_BYVAL_GET_BYREF(QString, conductor, Conductor) - PROPERTY_SET_BYVAL_GET_BYREF(Duration, duration, Duration) PROPERTY_SET_BYVAL_GET_BYREF(QString, genre, Genre) PROPERTY_SET_BYVAL_GET_BYREF(QString, grouping, Grouping) PROPERTY_SET_BYVAL_GET_BYREF(QString, isrc, ISRC) @@ -40,11 +39,6 @@ class TrackInfo final { PROPERTY_SET_BYVAL_GET_BYREF(QString, trackTotal, TrackTotal) PROPERTY_SET_BYVAL_GET_BYREF(QString, year, Year) // = release date - // Audio properties (in alphabetical order) - PROPERTY_SET_BYVAL_GET_BYREF(AudioSource::Bitrate, bitrate, Bitrate) - PROPERTY_SET_BYVAL_GET_BYREF(AudioSignal::ChannelCount, channels, Channels) - PROPERTY_SET_BYVAL_GET_BYREF(AudioSignal::SampleRate, sampleRate, SampleRate) - public: TrackInfo() = default; TrackInfo(TrackInfo&&) = default; diff --git a/src/track/trackmetadata.cpp b/src/track/trackmetadata.cpp index dfe7eb9c728d..d2866da9eea0 100644 --- a/src/track/trackmetadata.cpp +++ b/src/track/trackmetadata.cpp @@ -66,4 +66,13 @@ QString TrackMetadata::reformatYear(QString year) { return year.simplified(); } +bool operator==(const TrackMetadata& lhs, const TrackMetadata& rhs) { + return (lhs.getAlbumInfo() == rhs.getAlbumInfo()) && + (lhs.getTrackInfo() == rhs.getTrackInfo()) && + (lhs.getBitrate() == rhs.getBitrate()) && + (lhs.getChannels() == rhs.getChannels()) && + (lhs.getDuration() == rhs.getDuration()) && + (lhs.getSampleRate() == rhs.getSampleRate()); +} + } //namespace mixxx diff --git a/src/track/trackmetadata.h b/src/track/trackmetadata.h index 5983d2884536..7241e5d7f08b 100644 --- a/src/track/trackmetadata.h +++ b/src/track/trackmetadata.h @@ -9,6 +9,18 @@ namespace mixxx { class TrackMetadata final { + // Audio properties + // - read-only + // - stored file tags + // - adjusted by audio decoder AFTER import from file tags + PROPERTY_SET_BYVAL_GET_BYREF(AudioSource::Bitrate, bitrate, Bitrate) + PROPERTY_SET_BYVAL_GET_BYREF(AudioSignal::ChannelCount, channels, Channels) + PROPERTY_SET_BYVAL_GET_BYREF(Duration, duration, Duration) + PROPERTY_SET_BYVAL_GET_BYREF(AudioSignal::SampleRate, sampleRate, SampleRate) + + // Track properties + // - read-write + // - stored in file tags PROPERTY_SET_BYVAL_GET_BYREF(AlbumInfo, albumInfo, AlbumInfo) PROPERTY_SET_BYVAL_GET_BYREF(TrackInfo, trackInfo, TrackInfo) @@ -21,6 +33,16 @@ class TrackMetadata final { TrackMetadata& operator=(TrackMetadata&&) = default; TrackMetadata& operator=(const TrackMetadata&) = default; + // Compares the contents with metadata that has been freshly imported + // from a file. + bool hasBeenModifiedAfterImport(const TrackMetadata& importedFromFile) const { + // NOTE(uklotzde): The read-only audio properties might differ after + // they have been updated while decoding audio data. They are read-only + // and must not be considered when exporting metadata. + return (getAlbumInfo() != importedFromFile.getAlbumInfo()) || + (getTrackInfo() != importedFromFile.getTrackInfo()); + } + // Parse an format date/time values according to ISO 8601 static QDate parseDate(QString str) { return QDate::fromString(str.trimmed().replace(" ", ""), Qt::ISODate); @@ -43,11 +65,7 @@ class TrackMetadata final { static QString reformatYear(QString year); }; -inline -bool operator==(const TrackMetadata& lhs, const TrackMetadata& rhs) { - return (lhs.getAlbumInfo() == rhs.getAlbumInfo()) && - (lhs.getTrackInfo() == rhs.getTrackInfo()); -} +bool operator==(const TrackMetadata& lhs, const TrackMetadata& rhs); inline bool operator!=(const TrackMetadata& lhs, const TrackMetadata& rhs) { diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp index 02da185033a8..f3a345de720c 100644 --- a/src/track/trackmetadatataglib.cpp +++ b/src/track/trackmetadatataglib.cpp @@ -369,15 +369,15 @@ void readAudioProperties( // the audio data for this track. Often those properties // stored in tags don't match with the corresponding // audio data in the file. - pTrackMetadata->refTrackInfo().setChannels(AudioSignal::ChannelCount(audioProperties.channels())); - pTrackMetadata->refTrackInfo().setSampleRate(AudioSignal::SampleRate(audioProperties.sampleRate())); - pTrackMetadata->refTrackInfo().setBitrate(AudioSource::Bitrate(audioProperties.bitrate())); + pTrackMetadata->setChannels(AudioSignal::ChannelCount(audioProperties.channels())); + pTrackMetadata->setSampleRate(AudioSignal::SampleRate(audioProperties.sampleRate())); + pTrackMetadata->setBitrate(AudioSource::Bitrate(audioProperties.bitrate())); #if (TAGLIB_HAS_LENGTH_IN_MILLISECONDS) const auto duration = Duration::fromMillis(audioProperties.lengthInMilliseconds()); #else const auto duration = Duration::fromSeconds(audioProperties.length()); #endif - pTrackMetadata->refTrackInfo().setDuration(duration); + pTrackMetadata->setDuration(duration); } // Workaround for missing const member function in TagLib From 029622eb81a90ebc9ae78ab291e57487e22c4bca Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 19:12:49 +0100 Subject: [PATCH 42/52] Add QDebug operators --- src/track/albuminfo.cpp | 12 ++++++++++++ src/track/albuminfo.h | 2 ++ src/track/bpm.h | 5 +++++ src/track/playcounter.h | 7 ++++++- src/track/replaygain.h | 5 +++++ src/track/trackinfo.cpp | 28 ++++++++++++++++++++++++++++ src/track/trackinfo.h | 2 ++ src/track/trackmetadata.cpp | 12 ++++++++++++ src/track/trackmetadata.h | 2 ++ src/util/macros.h | 3 +++ 10 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/track/albuminfo.cpp b/src/track/albuminfo.cpp index c75dcfaef07a..6fae0aff638d 100644 --- a/src/track/albuminfo.cpp +++ b/src/track/albuminfo.cpp @@ -12,4 +12,16 @@ bool operator==(const AlbumInfo& lhs, const AlbumInfo& rhs) { (lhs.getTitle() == rhs.getTitle()); } +QDebug operator<<(QDebug dbg, const AlbumInfo& arg) { + dbg << '{'; + arg.dbgArtist(dbg); + arg.dbgMusicBrainzArtistId(dbg); + arg.dbgMusicBrainzReleaseId(dbg); + arg.dbgMusicBrainzReleaseGroupId(dbg); + arg.dbgReplayGain(dbg); + arg.dbgTitle(dbg); + dbg << '}'; + return dbg; +} + } // namespace mixxx diff --git a/src/track/albuminfo.h b/src/track/albuminfo.h index 6682d33a3210..6ba36960fb68 100644 --- a/src/track/albuminfo.h +++ b/src/track/albuminfo.h @@ -36,6 +36,8 @@ bool operator!=(const AlbumInfo& lhs, const AlbumInfo& rhs) { return !(lhs == rhs); } +QDebug operator<<(QDebug dbg, const AlbumInfo& arg); + } // namespace mixxx Q_DECLARE_METATYPE(mixxx::AlbumInfo) diff --git a/src/track/bpm.h b/src/track/bpm.h index 2c31d0312541..6629cfa3ab2e 100644 --- a/src/track/bpm.h +++ b/src/track/bpm.h @@ -59,6 +59,11 @@ bool operator!=(const Bpm& lhs, const Bpm& rhs) { return !(lhs == rhs); } +inline +QDebug operator<<(QDebug dbg, const Bpm& arg) { + return dbg << arg.getValue(); +} + } Q_DECLARE_TYPEINFO(mixxx::Bpm, Q_MOVABLE_TYPE); diff --git a/src/track/playcounter.h b/src/track/playcounter.h index 1d614fe3ad74..5bf4a0398e31 100644 --- a/src/track/playcounter.h +++ b/src/track/playcounter.h @@ -44,8 +44,13 @@ class PlayCounter { bool operator==(const PlayCounter& lhs, const PlayCounter& rhs); -inline bool operator!=(const PlayCounter& lhs, const PlayCounter& rhs) { +inline +bool operator!=(const PlayCounter& lhs, const PlayCounter& rhs) { return !(lhs == rhs); } +inline +QDebug operator<<(QDebug dbg, const PlayCounter& arg) { + return dbg << "played =" << arg.isPlayed() << "/" << "count =" << arg.getTimesPlayed(); +} #endif // MIXXX_PLAYCOUNTER_H diff --git a/src/track/replaygain.h b/src/track/replaygain.h index a4ef2d9ba787..541d823553c1 100644 --- a/src/track/replaygain.h +++ b/src/track/replaygain.h @@ -107,6 +107,11 @@ bool operator!=(const ReplayGain& lhs, const ReplayGain& rhs) { return !(lhs == rhs); } +inline +QDebug operator<<(QDebug dbg, const ReplayGain& arg) { + return dbg << "ratio =" << arg.getRatio() << "/" << "peak =" << arg.getPeak(); +} + } Q_DECLARE_TYPEINFO(mixxx::ReplayGain, Q_MOVABLE_TYPE); diff --git a/src/track/trackinfo.cpp b/src/track/trackinfo.cpp index b88b52c15000..d2bdd00f49b2 100644 --- a/src/track/trackinfo.cpp +++ b/src/track/trackinfo.cpp @@ -28,4 +28,32 @@ bool operator==(const TrackInfo& lhs, const TrackInfo& rhs) { (lhs.getYear() == rhs.getYear()); } +QDebug operator<<(QDebug dbg, const TrackInfo& arg) { + dbg << '{'; + arg.dbgArtist(dbg); + arg.dbgBpm(dbg); + arg.dbgComment(dbg); + arg.dbgComposer(dbg); + arg.dbgConductor(dbg); + arg.dbgGrouping(dbg); + arg.dbgGenre(dbg); + arg.dbgISRC(dbg); + arg.dbgKey(dbg); + arg.dbgLanguage(dbg); + arg.dbgLyricist(dbg); + arg.dbgMood(dbg); + arg.dbgMusicBrainzArtistId(dbg); + arg.dbgMusicBrainzReleaseId(dbg); + arg.dbgRecordLabel(dbg); + arg.dbgRemixer(dbg); + arg.dbgReplayGain(dbg); + arg.dbgSubtitle(dbg); + arg.dbgTitle(dbg); + arg.dbgTrackNumber(dbg); + arg.dbgTrackTotal(dbg); + arg.dbgYear(dbg); + dbg << '}'; + return dbg; +} + } // namespace mixxx diff --git a/src/track/trackinfo.h b/src/track/trackinfo.h index 491b61b03c66..610cff3fa7a8 100644 --- a/src/track/trackinfo.h +++ b/src/track/trackinfo.h @@ -56,6 +56,8 @@ bool operator!=(const TrackInfo& lhs, const TrackInfo& rhs) { return !(lhs == rhs); } +QDebug operator<<(QDebug dbg, const TrackInfo& arg); + } // namespace mixxx Q_DECLARE_METATYPE(mixxx::TrackInfo) diff --git a/src/track/trackmetadata.cpp b/src/track/trackmetadata.cpp index d2866da9eea0..e3fc9a9202ba 100644 --- a/src/track/trackmetadata.cpp +++ b/src/track/trackmetadata.cpp @@ -75,4 +75,16 @@ bool operator==(const TrackMetadata& lhs, const TrackMetadata& rhs) { (lhs.getSampleRate() == rhs.getSampleRate()); } +QDebug operator<<(QDebug dbg, const TrackMetadata& arg) { + dbg << '{'; + arg.dbgTrackInfo(dbg); + arg.dbgAlbumInfo(dbg); + arg.dbgBitrate(dbg); + arg.dbgChannels(dbg); + arg.dbgDuration(dbg); + arg.dbgSampleRate(dbg); + dbg << '}'; + return dbg; +} + } //namespace mixxx diff --git a/src/track/trackmetadata.h b/src/track/trackmetadata.h index 7241e5d7f08b..b250fbfefff7 100644 --- a/src/track/trackmetadata.h +++ b/src/track/trackmetadata.h @@ -72,6 +72,8 @@ bool operator!=(const TrackMetadata& lhs, const TrackMetadata& rhs) { return !(lhs == rhs); } +QDebug operator<<(QDebug dbg, const TrackMetadata& arg); + } // namespace mixxx Q_DECLARE_METATYPE(mixxx::TrackMetadata) diff --git a/src/util/macros.h b/src/util/macros.h index ebbbb17c30c3..accda62304a2 100644 --- a/src/util/macros.h +++ b/src/util/macros.h @@ -1,5 +1,7 @@ #pragma once +#include + // Helper for defining simple properties with setters and getters that are // passed by value using move assignment in the setter. The getter returns @@ -14,4 +16,5 @@ public: void set##CAP_NAME(TYPE NAME) { m_##NAME = std::move(NAME); } \ public: TYPE const& get##CAP_NAME() const { return m_##NAME; } \ public: TYPE& ref##CAP_NAME() { return m_##NAME; } \ +public: QDebug dbg##CAP_NAME(QDebug dbg) const { return dbg << #NAME ":" << m_##NAME; } \ private: TYPE m_##NAME; From 86e14abeee009f689a320e8418701da52cd0115f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 20:36:15 +0100 Subject: [PATCH 43/52] Fix plugin builds --- plugins/soundsourcem4a/SConscript | 2 ++ plugins/soundsourcemediafoundation/SConscript | 2 ++ plugins/soundsourcewv/SConscript | 2 ++ 3 files changed, 6 insertions(+) diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index aaad74f0bb6b..9f9a74cbca81 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -22,6 +22,8 @@ m4a_sources = [ "util/sample.cpp", "util/logger.cpp", "util/indexrange.cpp", + "track/albuminfo.cpp", + "track/trackinfo.cpp", "track/trackmetadata.cpp", "track/trackmetadatataglib.cpp", "track/tracknumbers.cpp", diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index 21f9a55736df..59f2d2395464 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -30,6 +30,8 @@ if int(build.flags['mediafoundation']): 'util/sample.cpp', 'util/logger.cpp', 'util/indexrange.cpp', + 'track/albuminfo.cpp', + 'track/trackinfo.cpp', 'track/trackmetadata.cpp', 'track/trackmetadatataglib.cpp', 'track/tracknumbers.cpp', diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index bf3a712e888e..b19f7370194b 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -22,6 +22,8 @@ wv_sources = [ "util/sample.cpp", "util/logger.cpp", "util/indexrange.cpp", + "track/albuminfo.cpp", + "track/trackinfo.cpp", "track/trackmetadata.cpp", "track/trackmetadatataglib.cpp", "track/tracknumbers.cpp", From 8b01e12352b84be7dc860e8d8f345788eebd6cfc Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 23:43:27 +0100 Subject: [PATCH 44/52] Temporarily disable unused track metadata properties ...to avoid repeated export of metadata --- src/sources/soundsourceopus.cpp | 2 + src/track/albuminfo.cpp | 18 ++- src/track/albuminfo.h | 10 +- src/track/trackinfo.cpp | 42 +++--- src/track/trackinfo.h | 21 +-- src/track/trackmetadatataglib.cpp | 235 +++++++++++++++++------------- src/track/trackmetadatataglib.h | 6 + 7 files changed, 192 insertions(+), 142 deletions(-) diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index c87e964d9c73..5b66288d93b1 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -162,6 +162,7 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( trackGain.setRatio(gainRatio); pTrackMetadata->refTrackInfo().setReplayGain(trackGain); } +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { bool gainRatioValid = false; double gainRatio = ReplayGain::ratioFromString(l_SPayload, &gainRatioValid); @@ -170,6 +171,7 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( albumGain.setRatio(gainRatio); pTrackMetadata->refAlbumInfo().setReplayGain(albumGain); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } } diff --git a/src/track/albuminfo.cpp b/src/track/albuminfo.cpp index 6fae0aff638d..e314a63fa333 100644 --- a/src/track/albuminfo.cpp +++ b/src/track/albuminfo.cpp @@ -3,22 +3,24 @@ namespace mixxx { +// TODO(XXX): Add the commented out properties to the Mixxx library bool operator==(const AlbumInfo& lhs, const AlbumInfo& rhs) { return (lhs.getArtist() == rhs.getArtist()) && - (lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && - (lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && - (lhs.getMusicBrainzReleaseGroupId() == rhs.getMusicBrainzReleaseGroupId()) && - (lhs.getReplayGain() == rhs.getReplayGain()) && + //(lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && + //(lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && + //(lhs.getMusicBrainzReleaseGroupId() == rhs.getMusicBrainzReleaseGroupId()) && + //(lhs.getReplayGain() == rhs.getReplayGain()) && (lhs.getTitle() == rhs.getTitle()); } +// TODO(XXX): Add the commented out properties to the Mixxx library QDebug operator<<(QDebug dbg, const AlbumInfo& arg) { dbg << '{'; arg.dbgArtist(dbg); - arg.dbgMusicBrainzArtistId(dbg); - arg.dbgMusicBrainzReleaseId(dbg); - arg.dbgMusicBrainzReleaseGroupId(dbg); - arg.dbgReplayGain(dbg); + //arg.dbgMusicBrainzArtistId(dbg); + //arg.dbgMusicBrainzReleaseId(dbg); + //arg.dbgMusicBrainzReleaseGroupId(dbg); + //arg.dbgReplayGain(dbg); arg.dbgTitle(dbg); dbg << '}'; return dbg; diff --git a/src/track/albuminfo.h b/src/track/albuminfo.h index 6ba36960fb68..4db1a5101e5a 100644 --- a/src/track/albuminfo.h +++ b/src/track/albuminfo.h @@ -12,12 +12,14 @@ namespace mixxx { class AlbumInfo final { // Album properties (in alphabetical order) + // TODO(XXX): Add the commented out properties to the Mixxx library PROPERTY_SET_BYVAL_GET_BYREF(QString, artist, Artist) - PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) - PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) - PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseGroupId, MusicBrainzReleaseGroupId) + //PROPERTY_SET_BYVAL_GET_BYREF(ReplayGain, replayGain, ReplayGain) + //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) + //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) + //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseGroupId, MusicBrainzReleaseGroupId) PROPERTY_SET_BYVAL_GET_BYREF(QString, title, Title) - PROPERTY_SET_BYVAL_GET_BYREF(ReplayGain, replayGain, ReplayGain) + public: AlbumInfo() = default; diff --git a/src/track/trackinfo.cpp b/src/track/trackinfo.cpp index d2bdd00f49b2..71d9b041efa1 100644 --- a/src/track/trackinfo.cpp +++ b/src/track/trackinfo.cpp @@ -3,51 +3,53 @@ namespace mixxx { +// TODO(XXX): Add the commented out properties to the Mixxx library bool operator==(const TrackInfo& lhs, const TrackInfo& rhs) { return (lhs.getArtist() == rhs.getArtist()) && (lhs.getBpm() == rhs.getBpm()) && (lhs.getComment() == rhs.getComment()) && (lhs.getComposer() == rhs.getComposer()) && - (lhs.getConductor() == rhs.getConductor()) && + //(lhs.getConductor() == rhs.getConductor()) && (lhs.getGrouping() == rhs.getGrouping()) && (lhs.getGenre() == rhs.getGenre()) && - (lhs.getISRC() == rhs.getISRC()) && + //(lhs.getISRC() == rhs.getISRC()) && (lhs.getKey() == rhs.getKey()) && - (lhs.getLanguage() == rhs.getLanguage()) && - (lhs.getLyricist() == rhs.getLyricist()) && - (lhs.getMood() == rhs.getMood()) && - (lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && - (lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && - (lhs.getRecordLabel() == rhs.getRecordLabel()) && - (lhs.getRemixer() == rhs.getRemixer()) && + //(lhs.getLanguage() == rhs.getLanguage()) && + //(lhs.getLyricist() == rhs.getLyricist()) && + //(lhs.getMood() == rhs.getMood()) && + //(lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && + //(lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && + //(lhs.getRecordLabel() == rhs.getRecordLabel()) && + //(lhs.getRemixer() == rhs.getRemixer()) && (lhs.getReplayGain() == rhs.getReplayGain()) && - (lhs.getSubtitle() == rhs.getSubtitle()) && + //(lhs.getSubtitle() == rhs.getSubtitle()) && (lhs.getTitle() == rhs.getTitle()) && (lhs.getTrackNumber() == rhs.getTrackNumber()) && (lhs.getTrackTotal() == rhs.getTrackTotal()) && (lhs.getYear() == rhs.getYear()); } +// TODO(XXX): Add the commented out properties to the Mixxx library QDebug operator<<(QDebug dbg, const TrackInfo& arg) { dbg << '{'; arg.dbgArtist(dbg); arg.dbgBpm(dbg); arg.dbgComment(dbg); arg.dbgComposer(dbg); - arg.dbgConductor(dbg); + //arg.dbgConductor(dbg); arg.dbgGrouping(dbg); arg.dbgGenre(dbg); - arg.dbgISRC(dbg); + //arg.dbgISRC(dbg); arg.dbgKey(dbg); - arg.dbgLanguage(dbg); - arg.dbgLyricist(dbg); - arg.dbgMood(dbg); - arg.dbgMusicBrainzArtistId(dbg); - arg.dbgMusicBrainzReleaseId(dbg); - arg.dbgRecordLabel(dbg); - arg.dbgRemixer(dbg); + //arg.dbgLanguage(dbg); + //arg.dbgLyricist(dbg); + //arg.dbgMood(dbg); + //arg.dbgMusicBrainzArtistId(dbg); + //arg.dbgMusicBrainzReleaseId(dbg); + //arg.dbgRecordLabel(dbg); + //arg.dbgRemixer(dbg); arg.dbgReplayGain(dbg); - arg.dbgSubtitle(dbg); + //arg.dbgSubtitle(dbg); arg.dbgTitle(dbg); arg.dbgTrackNumber(dbg); arg.dbgTrackTotal(dbg); diff --git a/src/track/trackinfo.h b/src/track/trackinfo.h index 610cff3fa7a8..5ff0a9b02965 100644 --- a/src/track/trackinfo.h +++ b/src/track/trackinfo.h @@ -16,24 +16,25 @@ namespace mixxx { class TrackInfo final { // Track properties (in alphabetical order) + // TODO(XXX): Add the commented out properties to the Mixxx library PROPERTY_SET_BYVAL_GET_BYREF(QString, artist, Artist) PROPERTY_SET_BYVAL_GET_BYREF(Bpm, bpm, Bpm) PROPERTY_SET_BYVAL_GET_BYREF(QString, comment, Comment) PROPERTY_SET_BYVAL_GET_BYREF(QString, composer, Composer) - PROPERTY_SET_BYVAL_GET_BYREF(QString, conductor, Conductor) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, conductor, Conductor) PROPERTY_SET_BYVAL_GET_BYREF(QString, genre, Genre) PROPERTY_SET_BYVAL_GET_BYREF(QString, grouping, Grouping) - PROPERTY_SET_BYVAL_GET_BYREF(QString, isrc, ISRC) PROPERTY_SET_BYVAL_GET_BYREF(QString, key, Key) - PROPERTY_SET_BYVAL_GET_BYREF(QString, language, Language) - PROPERTY_SET_BYVAL_GET_BYREF(QString, lyricist, Lyricist) - PROPERTY_SET_BYVAL_GET_BYREF(QString, mood, Mood) - PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) - PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) - PROPERTY_SET_BYVAL_GET_BYREF(QString, recordLabel, RecordLabel) - PROPERTY_SET_BYVAL_GET_BYREF(QString, remixer, Remixer) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, isrc, ISRC) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, language, Language) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, lyricist, Lyricist) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, mood, Mood) + //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) + //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, recordLabel, RecordLabel) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, remixer, Remixer) PROPERTY_SET_BYVAL_GET_BYREF(ReplayGain, replayGain, ReplayGain) - PROPERTY_SET_BYVAL_GET_BYREF(QString, subtitle, Subtitle) + //PROPERTY_SET_BYVAL_GET_BYREF(QString, subtitle, Subtitle) PROPERTY_SET_BYVAL_GET_BYREF(QString, title, Title) PROPERTY_SET_BYVAL_GET_BYREF(QString, trackNumber, TrackNumber) PROPERTY_SET_BYVAL_GET_BYREF(QString, trackTotal, TrackTotal) diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp index f3a345de720c..059ddb82be9b 100644 --- a/src/track/trackmetadatataglib.cpp +++ b/src/track/trackmetadatataglib.cpp @@ -237,16 +237,6 @@ QString formatTrackGain(const TrackMetadata& trackMetadata) { return formatReplayGainGain(trackMetadata.getTrackInfo().getReplayGain()); } -inline -bool hasAlbumGain(const TrackMetadata& trackMetadata) { - return trackMetadata.getAlbumInfo().getReplayGain().hasRatio(); -} - -inline -QString formatAlbumGain(const TrackMetadata& trackMetadata) { - return formatReplayGainGain(trackMetadata.getAlbumInfo().getReplayGain()); -} - bool parseReplayGainGain( ReplayGain* pReplayGain, const QString& dbGain) { @@ -282,19 +272,6 @@ bool parseTrackGain( return isRatioValid; } -bool parseAlbumGain( - TrackMetadata* pTrackMetadata, - const QString& dbGain) { - DEBUG_ASSERT(pTrackMetadata); - - ReplayGain replayGain(pTrackMetadata->getAlbumInfo().getReplayGain()); - bool isRatioValid = parseReplayGainGain(&replayGain, dbGain); - if (isRatioValid) { - pTrackMetadata->refAlbumInfo().setReplayGain(replayGain); - } - return isRatioValid; -} - inline QString formatReplayGainPeak(const ReplayGain& replayGain) { return ReplayGain::peakToString(replayGain.getPeak()); @@ -310,16 +287,6 @@ QString formatTrackPeak(const TrackMetadata& trackMetadata) { return formatReplayGainPeak(trackMetadata.getTrackInfo().getReplayGain()); } -inline -bool hasAlbumPeak(const TrackMetadata& trackMetadata) { - return trackMetadata.getAlbumInfo().getReplayGain().hasPeak(); -} - -inline -QString formatAlbumPeak(const TrackMetadata& trackMetadata) { - return formatReplayGainPeak(trackMetadata.getAlbumInfo().getReplayGain()); -} - bool parseReplayGainPeak( ReplayGain* pReplayGain, const QString& strPeak) { @@ -346,6 +313,41 @@ bool parseTrackPeak( return isPeakValid; } +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES + +inline +bool hasAlbumGain(const TrackMetadata& trackMetadata) { + return trackMetadata.getAlbumInfo().getReplayGain().hasRatio(); +} + +inline +QString formatAlbumGain(const TrackMetadata& trackMetadata) { + return formatReplayGainGain(trackMetadata.getAlbumInfo().getReplayGain()); +} + +bool parseAlbumGain( + TrackMetadata* pTrackMetadata, + const QString& dbGain) { + DEBUG_ASSERT(pTrackMetadata); + + ReplayGain replayGain(pTrackMetadata->getAlbumInfo().getReplayGain()); + bool isRatioValid = parseReplayGainGain(&replayGain, dbGain); + if (isRatioValid) { + pTrackMetadata->refAlbumInfo().setReplayGain(replayGain); + } + return isRatioValid; +} + +inline +bool hasAlbumPeak(const TrackMetadata& trackMetadata) { + return trackMetadata.getAlbumInfo().getReplayGain().hasPeak(); +} + +inline +QString formatAlbumPeak(const TrackMetadata& trackMetadata) { + return formatReplayGainPeak(trackMetadata.getAlbumInfo().getReplayGain()); +} + bool parseAlbumPeak( TrackMetadata* pTrackMetadata, const QString& strPeak) { @@ -359,6 +361,8 @@ bool parseAlbumPeak( return isPeakValid; } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES + void readAudioProperties( TrackMetadata* pTrackMetadata, const TagLib::AudioProperties& audioProperties) { @@ -600,7 +604,7 @@ void writeID3v2TextIdentificationFrame( } } - +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES bool writeID3v2TextIdentificationFrameStringIfNotNull( TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, @@ -612,6 +616,7 @@ bool writeID3v2TextIdentificationFrameStringIfNotNull( return true; } } +#endif void writeID3v2CommentsFrame( TagLib::ID3v2::Tag* pTag, @@ -1195,6 +1200,8 @@ void importTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, if (!trackPeak.isEmpty()) { parseTrackPeak(pTrackMetadata, trackPeak); } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain = readFirstUserTextIdentificationFrame(tag, "REPLAYGAIN_ALBUM_GAIN"); if (!albumGain.isEmpty()) { @@ -1266,6 +1273,7 @@ void importTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, if (!subtitleFrame.isEmpty()) { pTrackMetadata->refTrackInfo().setSubtitle(toQStringFirstNotEmpty(subtitleFrame)); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { @@ -1323,6 +1331,8 @@ void importTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib:: if (readAPEItem(tag, "REPLAYGAIN_TRACK_PEAK", &trackPeak)) { parseTrackPeak(pTrackMetadata, trackPeak); } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain; if (readAPEItem(tag, "REPLAYGAIN_ALBUM_GAIN", &albumGain)) { parseTrackGain(pTrackMetadata, albumGain); @@ -1385,6 +1395,7 @@ void importTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib:: if (readAPEItem(tag, "Subtitle", &subtitle)) { pTrackMetadata->refTrackInfo().setSubtitle(subtitle); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, @@ -1464,6 +1475,18 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, } } + // Reading key code information + // Unlike, ID3 tags, there's no standard or recommendation on how to store 'key' code + // + // Luckily, there are only a few tools for that, e.g., Rapid Evolution (RE). + // Assuming no distinction between start and end key, RE uses a "INITIALKEY" + // or a "KEY" vorbis comment. + QString key; + if (readXiphCommentField(tag, "INITIALKEY", &key) || // recommended field + readXiphCommentField(tag, "KEY", &key)) { // alternative field + pTrackMetadata->refTrackInfo().setKey(key); + } + // Only read track gain (not album gain) QString trackGain; if (readXiphCommentField(tag, "REPLAYGAIN_TRACK_GAIN", &trackGain)) { @@ -1473,6 +1496,8 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, if (readXiphCommentField(tag, "REPLAYGAIN_TRACK_PEAK", &trackPeak)) { parseTrackPeak(pTrackMetadata, trackPeak); } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain; if (readXiphCommentField(tag, "REPLAYGAIN_ALBUM_GAIN", &albumGain)) { parseAlbumGain(pTrackMetadata, albumGain); @@ -1503,18 +1528,6 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, pTrackMetadata->refAlbumInfo().setMusicBrainzReleaseGroupId(QUuid(albumReleaseGroupId)); } - // Reading key code information - // Unlike, ID3 tags, there's no standard or recommendation on how to store 'key' code - // - // Luckily, there are only a few tools for that, e.g., Rapid Evolution (RE). - // Assuming no distinction between start and end key, RE uses a "INITIALKEY" - // or a "KEY" vorbis comment. - QString key; - if (readXiphCommentField(tag, "INITIALKEY", &key) || // recommended field - readXiphCommentField(tag, "KEY", &key)) { // alternative field - pTrackMetadata->refTrackInfo().setKey(key); - } - QString conductor; if (readXiphCommentField(tag, "CONDUCTOR", &conductor)) { pTrackMetadata->refTrackInfo().setConductor(conductor); @@ -1547,6 +1560,7 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, if (readXiphCommentField(tag, "SUBTITLE", &subtitle)) { pTrackMetadata->refTrackInfo().setSubtitle(subtitle); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP4::Tag& tag) { @@ -1605,6 +1619,12 @@ void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib:: } } + QString key; + if (readMP4Atom(tag, "----:com.apple.iTunes:initialkey", &key) || // preferred (conforms to MixedInKey, Serato, Traktor) + readMP4Atom(tag, "----:com.apple.iTunes:KEY", &key)) { // alternative (conforms to Rapid Evolution) + pTrackMetadata->refTrackInfo().setKey(key); + } + QString trackGain; if (readMP4Atom(tag, "----:com.apple.iTunes:replaygain_track_gain", &trackGain)) { parseTrackGain(pTrackMetadata, trackGain); @@ -1613,6 +1633,8 @@ void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib:: if (readMP4Atom(tag, "----:com.apple.iTunes:replaygain_track_peak", &trackPeak)) { parseTrackPeak(pTrackMetadata, trackPeak); } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain; if (readMP4Atom(tag, "----:com.apple.iTunes:replaygain_album_gain", &albumGain)) { parseAlbumGain(pTrackMetadata, albumGain); @@ -1643,12 +1665,6 @@ void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib:: pTrackMetadata->refAlbumInfo().setMusicBrainzReleaseGroupId(QUuid(albumReleaseGroupId)); } - QString key; - if (readMP4Atom(tag, "----:com.apple.iTunes:initialkey", &key) || // preferred (conforms to MixedInKey, Serato, Traktor) - readMP4Atom(tag, "----:com.apple.iTunes:KEY", &key)) { // alternative (conforms to Rapid Evolution) - pTrackMetadata->refTrackInfo().setKey(key); - } - QString conductor; if (readMP4Atom(tag, "----:com.apple.iTunes:CONDUCTOR", &conductor)) { pTrackMetadata->refTrackInfo().setConductor(conductor); @@ -1681,6 +1697,7 @@ void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib:: if (readMP4Atom(tag, "----:com.apple.iTunes:SUBTITLE", &subtitle)) { pTrackMetadata->refTrackInfo().setSubtitle(subtitle); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromRIFFTag(TrackMetadata* pTrackMetadata, const TagLib::RIFF::Info::Tag& tag) { @@ -1836,6 +1853,11 @@ bool exportTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, formatTrackPeak(trackMetadata), true); } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES + // TODO(XXX): The following tags are currently not stored in the + // Mixxx library. Only write properties that have non-null values + // to prevent deleting existing tags! if (hasAlbumGain(trackMetadata)) { writeID3v2UserTextIdentificationFrame( pTag, @@ -1924,6 +1946,7 @@ bool exportTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, pTag, "TIT3", trackMetadata.getTrackInfo().getSubtitle()); +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } @@ -1956,6 +1979,10 @@ bool exportTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& writeAPEItem(pTag, "BPM", toTagLibString(formatBpm(trackMetadata))); + + writeAPEItem(pTag, "INITIALKEY", + toTagLibString(trackMetadata.getTrackInfo().getKey())); + if (hasTrackGain(trackMetadata)) { writeAPEItem(pTag, "REPLAYGAIN_TRACK_GAIN", toTagLibString(formatTrackGain(trackMetadata))); @@ -1964,6 +1991,11 @@ bool exportTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& writeAPEItem(pTag, "REPLAYGAIN_TRACK_PEAK", toTagLibString(formatTrackPeak(trackMetadata))); } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES + // TODO(XXX): The following tags are currently not stored in the + // Mixxx library. Only write properties that have non-null values + // to prevent deleting existing tags! if (hasAlbumGain(trackMetadata)) { writeAPEItem(pTag, "REPLAYGAIN_ALBUM_GAIN", toTagLibString(formatAlbumGain(trackMetadata))); @@ -2029,6 +2061,7 @@ bool exportTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& writeAPEItem(pTag, "Subtitle", toTagLibString(trackMetadata.getTrackInfo().getSubtitle())); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } @@ -2070,44 +2103,6 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, writeXiphCommentField(pTag, "TRACKNUMBER", toTagLibString(trackMetadata.getTrackInfo().getTrackNumber())); - if (hasTrackGain(trackMetadata)) { - writeXiphCommentField(pTag, "REPLAYGAIN_TRACK_GAIN", - toTagLibString(formatTrackGain(trackMetadata))); - } - if (hasTrackPeak(trackMetadata)) { - writeXiphCommentField(pTag, "REPLAYGAIN_TRACK_PEAK", - toTagLibString(formatTrackPeak(trackMetadata))); - } - if (hasAlbumGain(trackMetadata)) { - writeXiphCommentField(pTag, "REPLAYGAIN_ALBUM_GAIN", - toTagLibString(formatAlbumGain(trackMetadata))); - } - if (hasAlbumPeak(trackMetadata)) { - writeXiphCommentField(pTag, "REPLAYGAIN_ALBUM_PEAK", - toTagLibString(formatAlbumPeak(trackMetadata))); - } - - if (!trackMetadata.getTrackInfo().getMusicBrainzArtistId().isNull()) { - writeXiphCommentField(pTag, "MUSICBRAINZ_ARTISTID", - toTagLibString(trackMetadata.getTrackInfo().getMusicBrainzArtistId().toString())); - } - if (!trackMetadata.getTrackInfo().getMusicBrainzReleaseId().isNull()) { - writeXiphCommentField(pTag, "MUSICBRAINZ_RELEASETRACKID", - toTagLibString(trackMetadata.getTrackInfo().getMusicBrainzReleaseId().toString())); - } - if (!trackMetadata.getAlbumInfo().getMusicBrainzArtistId().isNull()) { - writeXiphCommentField(pTag, "MUSICBRAINZ_ALBUMARTISTID", - toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzArtistId().toString())); - } - if (!trackMetadata.getAlbumInfo().getMusicBrainzReleaseId().isNull()) { - writeXiphCommentField(pTag, "MUSICBRAINZ_ALBUMID", - toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzReleaseId().toString())); - } - if (!trackMetadata.getAlbumInfo().getMusicBrainzReleaseGroupId().isNull()) { - writeXiphCommentField(pTag, "MUSICBRAINZ_RELEASEGROUPID", - toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzReleaseGroupId().toString())); - } - // According to https://wiki.xiph.org/Field_names "TRACKTOTAL" is // the proposed field name, but some applications use "TOTALTRACKS". const TagLib::String trackTotal( @@ -2141,9 +2136,47 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, writeXiphCommentField(pTag, "INITIALKEY", key); // recommended field updateXiphCommentField(pTag, "KEY", key); // alternative field + if (hasTrackGain(trackMetadata)) { + writeXiphCommentField(pTag, "REPLAYGAIN_TRACK_GAIN", + toTagLibString(formatTrackGain(trackMetadata))); + } + if (hasTrackPeak(trackMetadata)) { + writeXiphCommentField(pTag, "REPLAYGAIN_TRACK_PEAK", + toTagLibString(formatTrackPeak(trackMetadata))); + } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES // TODO(XXX): The following tags are currently not stored in the // Mixxx library. Only write properties that have non-null values // to prevent deleting existing tags! + if (hasAlbumGain(trackMetadata)) { + writeXiphCommentField(pTag, "REPLAYGAIN_ALBUM_GAIN", + toTagLibString(formatAlbumGain(trackMetadata))); + } + if (hasAlbumPeak(trackMetadata)) { + writeXiphCommentField(pTag, "REPLAYGAIN_ALBUM_PEAK", + toTagLibString(formatAlbumPeak(trackMetadata))); + } + if (!trackMetadata.getTrackInfo().getMusicBrainzArtistId().isNull()) { + writeXiphCommentField(pTag, "MUSICBRAINZ_ARTISTID", + toTagLibString(trackMetadata.getTrackInfo().getMusicBrainzArtistId().toString())); + } + if (!trackMetadata.getTrackInfo().getMusicBrainzReleaseId().isNull()) { + writeXiphCommentField(pTag, "MUSICBRAINZ_RELEASETRACKID", + toTagLibString(trackMetadata.getTrackInfo().getMusicBrainzReleaseId().toString())); + } + if (!trackMetadata.getAlbumInfo().getMusicBrainzArtistId().isNull()) { + writeXiphCommentField(pTag, "MUSICBRAINZ_ALBUMARTISTID", + toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzArtistId().toString())); + } + if (!trackMetadata.getAlbumInfo().getMusicBrainzReleaseId().isNull()) { + writeXiphCommentField(pTag, "MUSICBRAINZ_ALBUMID", + toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzReleaseId().toString())); + } + if (!trackMetadata.getAlbumInfo().getMusicBrainzReleaseGroupId().isNull()) { + writeXiphCommentField(pTag, "MUSICBRAINZ_RELEASEGROUPID", + toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzReleaseGroupId().toString())); + } if (!trackMetadata.getTrackInfo().getConductor().isNull()) { writeXiphCommentField(pTag, "CONDUCTOR", toTagLibString(trackMetadata.getTrackInfo().getConductor())); @@ -2176,6 +2209,7 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, writeXiphCommentField(pTag, "SUBTITLE", toTagLibString(trackMetadata.getTrackInfo().getSubtitle())); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } @@ -2231,6 +2265,11 @@ bool exportTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", toTagLibString(formatBpm(trackMetadata))); + const TagLib::String key = + toTagLibString(trackMetadata.getTrackInfo().getKey()); + writeMP4Atom(pTag, "----:com.apple.iTunes:initialkey", key); // preferred + updateMP4Atom(pTag, "----:com.apple.iTunes:KEY", key); // alternative + if (hasTrackGain(trackMetadata)) { writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_track_gain", toTagLibString(formatTrackGain(trackMetadata))); @@ -2239,6 +2278,11 @@ bool exportTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_track_peak", toTagLibString(formatTrackPeak(trackMetadata))); } + +#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES + // TODO(XXX): The following tags are currently not stored in the + // Mixxx library. Only write properties that have non-null values + // to prevent deleting existing tags! if (hasAlbumGain(trackMetadata)) { writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_album_gain", toTagLibString(formatAlbumGain(trackMetadata))); @@ -2247,7 +2291,6 @@ bool exportTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_album_peak", toTagLibString(formatAlbumPeak(trackMetadata))); } - if (!trackMetadata.getTrackInfo().getMusicBrainzArtistId().isNull()) { writeMP4Atom(pTag, "----:com.apple.iTunes:MusicBrainz Artist Id", toTagLibString(trackMetadata.getTrackInfo().getMusicBrainzArtistId().toString())); @@ -2268,15 +2311,6 @@ bool exportTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& writeMP4Atom(pTag, "----:com.apple.iTunes:MusicBrainz Release Group Id", toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzReleaseGroupId().toString())); } - - const TagLib::String key = - toTagLibString(trackMetadata.getTrackInfo().getKey()); - writeMP4Atom(pTag, "----:com.apple.iTunes:initialkey", key); // preferred - updateMP4Atom(pTag, "----:com.apple.iTunes:KEY", key); // alternative - - // TODO(XXX): The following tags are currently not stored in the - // Mixxx library. Only write properties that have non-null values - // to prevent deleting existing tags! if (!trackMetadata.getTrackInfo().getConductor().isNull()) { writeMP4Atom(pTag, "----:com.apple.iTunes:CONDUCTOR", toTagLibString(trackMetadata.getTrackInfo().getConductor())); @@ -2309,6 +2343,7 @@ bool exportTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& writeMP4Atom(pTag, "----:com.apple.iTunes:SUBTITLE", toTagLibString(trackMetadata.getTrackInfo().getSubtitle())); } +#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } diff --git a/src/track/trackmetadatataglib.h b/src/track/trackmetadatataglib.h index f1ec48a4ee83..7f4ae67d424c 100644 --- a/src/track/trackmetadatataglib.h +++ b/src/track/trackmetadatataglib.h @@ -22,6 +22,12 @@ #define TAGLIB_HAS_AIFF_HAS_ID3V2TAG \ (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 10)) +// TODO(XXX): Add those fields to the Mixxx library and remove the #define +// If those properties are imported but not stored in the database +// Mixxx would detect metadata as modified and export it again although +// it nothing has changed. +#define EXCLUDE_EXTRA_METADATA_PROPERTIES + #include #include "track/trackmetadata.h" From 26de42da979a88aadc2a246055426bf36a8afeac Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 23:44:30 +0100 Subject: [PATCH 45/52] Use TagLib instead of opusfile ...has been lost on the way somehow. --- src/sources/soundsourceopus.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 5b66288d93b1..734dec2e2626 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -114,9 +114,6 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( return imported; } - int i = 0; - const OpusTags *l_ptrOpusTags = op_tags(pOggOpusFile, -1); - pTrackMetadata->setChannels(ChannelCount(op_channel_count(pOggOpusFile, -1))); pTrackMetadata->setSampleRate(kSampleRate); pTrackMetadata->setBitrate(Bitrate(op_bitrate(pOggOpusFile, -1) / 1000)); @@ -125,8 +122,10 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( pTrackMetadata->setDuration(Duration::fromMicros( 1000000 * dTotalFrames / pTrackMetadata->getSampleRate())); +#ifndef TAGLIB_HAS_OPUSFILE + const OpusTags *l_ptrOpusTags = op_tags(pOggOpusFile, -1); bool hasDate = false; - for (i = 0; i < l_ptrOpusTags->comments; ++i) { + for (int i = 0; i < l_ptrOpusTags->comments; ++i) { QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]); QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("=")); QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1); @@ -174,6 +173,7 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( #endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } } +#endif // TAGLIB_HAS_OPUSFILE return std::make_pair( ImportResult::Succeeded, From 6e05e5312122ebf6be488a3a0f89f52ad8b9e858 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 5 Dec 2017 23:44:53 +0100 Subject: [PATCH 46/52] Fix normalization of bpm values --- src/test/metadatatest.cpp | 4 +-- src/track/bpm.cpp | 10 +++--- src/track/bpm.h | 3 +- src/track/track.cpp | 64 +++++++++++++++++++++++---------------- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 821b73e3a14d..ea46f58139e4 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -28,8 +28,8 @@ class MetadataTest : public testing::Test { } void normalizeBpm(double normalizedValue) { - mixxx::Bpm normalizedBpm(normalizedValue); - normalizedBpm.normalizeValue(); // re-normalize + // Re-normalize + auto normalizedBpm = mixxx::Bpm::fromValue(normalizedValue); // Expected: Re-normalization does not change the value // that should already be normalized. EXPECT_EQ(normalizedBpm.getValue(), normalizedValue); diff --git a/src/track/bpm.cpp b/src/track/bpm.cpp index 6e6d3719eb2c..951fb91c7355 100644 --- a/src/track/bpm.cpp +++ b/src/track/bpm.cpp @@ -45,13 +45,15 @@ QString Bpm::valueToString(double value) { } } -void Bpm::normalizeValue() { - if (isValidValue(m_value)) { - const double normalizedValue = valueFromString(valueToString(m_value)); +Bpm Bpm::fromValue(double value) { + if (isValidValue(value)) { + const double normalizedValue = valueFromString(valueToString(value)); // NOTE(uklotzde): Subsequently formatting and parsing the // normalized value should not alter it anymore! DEBUG_ASSERT(normalizedValue == valueFromString(valueToString(normalizedValue))); - m_value = normalizedValue; + return Bpm(normalizedValue); + } else { + return Bpm(value); } } diff --git a/src/track/bpm.h b/src/track/bpm.h index 6629cfa3ab2e..c1a2288e7dab 100644 --- a/src/track/bpm.h +++ b/src/track/bpm.h @@ -24,6 +24,8 @@ class Bpm final { static bool isValidValue(double value) { return kValueMin < value; } + // Convert and normalize from a value + static Bpm fromValue(double value); bool hasValue() const { return isValidValue(m_value); @@ -37,7 +39,6 @@ class Bpm final { void resetValue() { m_value = kValueUndefined; } - void normalizeValue(); static double valueFromString(const QString& str, bool* pValid = nullptr); static QString valueToString(double value); diff --git a/src/track/track.cpp b/src/track/track.cpp index 271dd6b75336..1f1d1edd77bb 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -37,6 +37,21 @@ bool compareAndSet(T* pField, const T& value) { } } +mixxx::Bpm getActualFromImportedBpm( + mixxx::Bpm importedBpm, + BeatsPointer pBeats = BeatsPointer()) { + // Only use the imported BPM if the beat grid is not valid! + // Reason: The BPM value in the metadata might be normalized + // or rounded, e.g. ID3v2 only supports integer values. + if (pBeats) { + const double beatsBpm = pBeats->getBpm(); + if (mixxx::Bpm::isValidValue(beatsBpm)) { + return mixxx::Bpm::fromValue(beatsBpm); + } + } + return importedBpm; +} + } // anonymous namespace Track::Track( @@ -111,12 +126,9 @@ void Track::setTrackMetadata( // Need to set BPM after sample rate since beat grid creation depends on // knowing the sample rate. Bug #1020438. - if (newBpm.hasValue() && - ((nullptr == m_pBeats) || !mixxx::Bpm::isValidValue(m_pBeats->getBpm()))) { - // Only (re-)set the BPM to the new value if the beat grid is not valid. - // Reason: The BPM value in the metadata might be normalized or rounded, - // e.g. ID3v2 only supports integer values! - setBpm(newBpm.getValue()); + const auto actualBpm = getActualFromImportedBpm(newBpm, m_pBeats); + if (actualBpm.hasValue()) { + setBpm(actualBpm.getValue()); } if (!newKey.isEmpty()) { @@ -249,9 +261,6 @@ double Track::setBpm(double bpmValue) { return bpmValue; } - mixxx::Bpm normalizedBpm(bpmValue); - normalizedBpm.normalizeValue(); - QMutexLocker lock(&m_qMutex); if (!m_pBeats) { @@ -303,24 +312,19 @@ void Track::setBeatsAndUnlock(QMutexLocker* pLock, BeatsPointer pBeats) { } mixxx::Bpm bpm; - double bpmValue = bpm.getValue(); - m_pBeats = pBeats; if (m_pBeats) { - bpmValue = m_pBeats->getBpm(); - bpm.setValue(bpmValue); - bpm.normalizeValue(); + bpm = mixxx::Bpm::fromValue(m_pBeats->getBpm()); pObject = dynamic_cast(m_pBeats.data()); if (pObject) { connect(pObject, SIGNAL(updated()), this, SLOT(slotBeatsUpdated())); } } - m_record.refMetadata().refTrackInfo().setBpm(bpm); markDirtyAndUnlock(pLock); - emit(bpmUpdated(bpmValue)); + emit(bpmUpdated(bpm.getValue())); emit(beatsUpdated()); } @@ -331,12 +335,10 @@ BeatsPointer Track::getBeats() const { void Track::slotBeatsUpdated() { QMutexLocker lock(&m_qMutex); - double bpmValue = m_pBeats->getBpm(); - mixxx::Bpm bpm(bpmValue); - bpm.normalizeValue(); + const auto bpm = mixxx::Bpm::fromValue(m_pBeats->getBpm()); m_record.refMetadata().refTrackInfo().setBpm(bpm); markDirtyAndUnlock(&lock); - emit(bpmUpdated(bpmValue)); + emit(bpmUpdated(bpm.getValue())); emit(beatsUpdated()); } @@ -951,13 +953,23 @@ Track::ExportMetadataResult Track::exportMetadata( // or after upgrading the column 'header_parsed' from bool to QDateTime. mixxx::TrackMetadata importedFromFile; if ((pMetadataSource->importTrackMetadataAndCoverImage(&importedFromFile, nullptr).first == - mixxx::MetadataSource::ImportResult::Succeeded) && - !m_record.getMetadata().hasBeenModifiedAfterImport(importedFromFile)) { - kLogger.debug() - << "Skip exporting of unmodified track metadata:" - << getLocation(); - return ExportMetadataResult::Skipped; + mixxx::MetadataSource::ImportResult::Succeeded)) { + // Before comparison: Adjust imported bpm values that might be imprecise, + // e.g. integer values from ID3v2 + importedFromFile.refTrackInfo().setBpm( + getActualFromImportedBpm(importedFromFile.getTrackInfo().getBpm(), m_pBeats)); + if (!m_record.getMetadata().hasBeenModifiedAfterImport(importedFromFile)) { + kLogger.debug() + << "Skip exporting of unmodified track metadata:" + << getLocation(); + return ExportMetadataResult::Skipped; + } } + kLogger.debug() + << "m_record.getMetadata()" + << m_record.getMetadata() + << "importedFromFile" + << importedFromFile; } m_bExportMetadata = false; // reset flag const auto trackMetadataExported = From 0a46bdc3d6a41c9e92dd8a2f3e741a8c712446f1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 6 Dec 2017 00:22:08 +0100 Subject: [PATCH 47/52] Remove verbose debug output --- src/track/track.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/track/track.cpp b/src/track/track.cpp index 1f1d1edd77bb..398cafba65b5 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -965,11 +965,6 @@ Track::ExportMetadataResult Track::exportMetadata( return ExportMetadataResult::Skipped; } } - kLogger.debug() - << "m_record.getMetadata()" - << m_record.getMetadata() - << "importedFromFile" - << importedFromFile; } m_bExportMetadata = false; // reset flag const auto trackMetadataExported = From 64489db3e60a42e919e393b50f0a9853a53308d4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 6 Dec 2017 07:14:18 +0100 Subject: [PATCH 48/52] Fix typo --- src/track/trackmetadatataglib.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/track/trackmetadatataglib.h b/src/track/trackmetadatataglib.h index 7f4ae67d424c..1535eaa68767 100644 --- a/src/track/trackmetadatataglib.h +++ b/src/track/trackmetadatataglib.h @@ -25,7 +25,7 @@ // TODO(XXX): Add those fields to the Mixxx library and remove the #define // If those properties are imported but not stored in the database // Mixxx would detect metadata as modified and export it again although -// it nothing has changed. +// nothing has changed. #define EXCLUDE_EXTRA_METADATA_PROPERTIES #include From 1cca6706fbb9c62f06bcb31f51bfc6f6071184fb Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 6 Dec 2017 07:31:33 +0100 Subject: [PATCH 49/52] Defer normalization of floating-point values until exported --- src/test/metadatatest.cpp | 4 +-- src/track/albuminfo.h | 6 ++++ src/track/bpm.cpp | 6 ++-- src/track/bpm.h | 10 +++++-- src/track/replaygain.h | 7 +++++ src/track/track.cpp | 60 ++++++++++++++++++++++++--------------- src/track/trackinfo.h | 7 +++++ src/track/trackmetadata.h | 7 +++++ 8 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index ea46f58139e4..a7c66025de2f 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -28,11 +28,9 @@ class MetadataTest : public testing::Test { } void normalizeBpm(double normalizedValue) { - // Re-normalize - auto normalizedBpm = mixxx::Bpm::fromValue(normalizedValue); // Expected: Re-normalization does not change the value // that should already be normalized. - EXPECT_EQ(normalizedBpm.getValue(), normalizedValue); + EXPECT_EQ(normalizedValue, mixxx::Bpm::normalizeValue(normalizedValue)); } void readBPMFromId3(const char* inputValue, double expectedValue) { diff --git a/src/track/albuminfo.h b/src/track/albuminfo.h index 4db1a5101e5a..d6b66cf3a98a 100644 --- a/src/track/albuminfo.h +++ b/src/track/albuminfo.h @@ -29,6 +29,12 @@ class AlbumInfo final { AlbumInfo& operator=(AlbumInfo&&) = default; AlbumInfo& operator=(const AlbumInfo&) = default; + + // Adjusts floating-point values to match their string representation + // in file tags to account for rounding errors. + void normalizeBeforeExport() { + //refReplayGain().normalizeBeforeExport(); + } }; bool operator==(const AlbumInfo& lhs, const AlbumInfo& rhs); diff --git a/src/track/bpm.cpp b/src/track/bpm.cpp index 951fb91c7355..fdd280590f01 100644 --- a/src/track/bpm.cpp +++ b/src/track/bpm.cpp @@ -45,15 +45,15 @@ QString Bpm::valueToString(double value) { } } -Bpm Bpm::fromValue(double value) { +double Bpm::normalizeValue(double value) { if (isValidValue(value)) { const double normalizedValue = valueFromString(valueToString(value)); // NOTE(uklotzde): Subsequently formatting and parsing the // normalized value should not alter it anymore! DEBUG_ASSERT(normalizedValue == valueFromString(valueToString(normalizedValue))); - return Bpm(normalizedValue); + return normalizedValue; } else { - return Bpm(value); + return value; } } diff --git a/src/track/bpm.h b/src/track/bpm.h index c1a2288e7dab..88677fa858fe 100644 --- a/src/track/bpm.h +++ b/src/track/bpm.h @@ -21,11 +21,17 @@ class Bpm final { : m_value(value) { } + static double normalizeValue(double value); + + // Adjusts floating-point values to match their string representation + // in file tags to account for rounding errors. + void normalizeBeforeExport() { + m_value = normalizeValue(m_value); + } + static bool isValidValue(double value) { return kValueMin < value; } - // Convert and normalize from a value - static Bpm fromValue(double value); bool hasValue() const { return isValidValue(m_value); diff --git a/src/track/replaygain.h b/src/track/replaygain.h index 541d823553c1..ae7c89e02135 100644 --- a/src/track/replaygain.h +++ b/src/track/replaygain.h @@ -92,6 +92,13 @@ class ReplayGain final { static CSAMPLE normalizePeak(CSAMPLE peak); + // Adjusts floating-point values to match their string representation + // in file tags to account for rounding errors. + void normalizeBeforeExport() { + m_ratio = normalizeRatio(m_ratio); + m_peak = normalizePeak(m_peak); + } + private: double m_ratio; CSAMPLE m_peak; diff --git a/src/track/track.cpp b/src/track/track.cpp index 398cafba65b5..b5c8c23de6e9 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -37,19 +37,18 @@ bool compareAndSet(T* pField, const T& value) { } } -mixxx::Bpm getActualFromImportedBpm( - mixxx::Bpm importedBpm, +inline +mixxx::Bpm getActualBpm( + mixxx::Bpm bpm, BeatsPointer pBeats = BeatsPointer()) { // Only use the imported BPM if the beat grid is not valid! // Reason: The BPM value in the metadata might be normalized // or rounded, e.g. ID3v2 only supports integer values. if (pBeats) { - const double beatsBpm = pBeats->getBpm(); - if (mixxx::Bpm::isValidValue(beatsBpm)) { - return mixxx::Bpm::fromValue(beatsBpm); - } + return mixxx::Bpm(pBeats->getBpm()); + } else { + return bpm; } - return importedBpm; } } // anonymous namespace @@ -126,7 +125,7 @@ void Track::setTrackMetadata( // Need to set BPM after sample rate since beat grid creation depends on // knowing the sample rate. Bug #1020438. - const auto actualBpm = getActualFromImportedBpm(newBpm, m_pBeats); + const auto actualBpm = getActualBpm(newBpm, m_pBeats); if (actualBpm.hasValue()) { setBpm(actualBpm.getValue()); } @@ -302,29 +301,29 @@ void Track::setBeatsAndUnlock(QMutexLocker* pLock, BeatsPointer pBeats) { return; } - QObject* pObject = nullptr; if (m_pBeats) { - pObject = dynamic_cast(m_pBeats.data()); + auto pObject = dynamic_cast(m_pBeats.data()); if (pObject) { disconnect(pObject, SIGNAL(updated()), this, SLOT(slotBeatsUpdated())); } } - mixxx::Bpm bpm; m_pBeats = pBeats; + + auto bpmValue = mixxx::Bpm::kValueUndefined; if (m_pBeats) { - bpm = mixxx::Bpm::fromValue(m_pBeats->getBpm()); - pObject = dynamic_cast(m_pBeats.data()); + bpmValue = m_pBeats->getBpm(); + auto pObject = dynamic_cast(m_pBeats.data()); if (pObject) { connect(pObject, SIGNAL(updated()), this, SLOT(slotBeatsUpdated())); } } - m_record.refMetadata().refTrackInfo().setBpm(bpm); + m_record.refMetadata().refTrackInfo().setBpm(mixxx::Bpm(bpmValue)); markDirtyAndUnlock(pLock); - emit(bpmUpdated(bpm.getValue())); + emit(bpmUpdated(bpmValue)); emit(beatsUpdated()); } @@ -335,10 +334,15 @@ BeatsPointer Track::getBeats() const { void Track::slotBeatsUpdated() { QMutexLocker lock(&m_qMutex); - const auto bpm = mixxx::Bpm::fromValue(m_pBeats->getBpm()); - m_record.refMetadata().refTrackInfo().setBpm(bpm); + + auto bpmValue = mixxx::Bpm::kValueUndefined; + if (m_pBeats) { + bpmValue = m_pBeats->getBpm(); + } + m_record.refMetadata().refTrackInfo().setBpm(mixxx::Bpm(bpmValue)); + markDirtyAndUnlock(&lock); - emit(bpmUpdated(bpm.getValue())); + emit(bpmUpdated(bpmValue)); emit(beatsUpdated()); } @@ -931,11 +935,13 @@ Track::ExportMetadataResult Track::exportMetadata( // be called after all references to the object have been dropped. // But it doesn't hurt much, so let's play it safe ;) QMutexLocker lock(&m_qMutex); + // Normalize metadata before export to adjust the precision of + // floating values, ... + m_record.refMetadata().normalizeBeforeExport(); if (!m_bExportMetadata) { // Perform some consistency checks if metadata is exported - // implicitly after a track has been modified and has NOT - // been explicitly requested by a user as indicated by this - // flag. + // implicitly after a track has been modified and NOT explicitly + // requested by a user as indicated by this flag. if (!m_record.getMetadataSynchronized()) { kLogger.debug() << "Skip exporting of unsynchronized track metadata:" @@ -956,14 +962,22 @@ Track::ExportMetadataResult Track::exportMetadata( mixxx::MetadataSource::ImportResult::Succeeded)) { // Before comparison: Adjust imported bpm values that might be imprecise, // e.g. integer values from ID3v2 - importedFromFile.refTrackInfo().setBpm( - getActualFromImportedBpm(importedFromFile.getTrackInfo().getBpm(), m_pBeats)); + auto actualBpm = + getActualBpm(importedFromFile.getTrackInfo().getBpm(), m_pBeats); + // Account for rounding errors during export + actualBpm.normalizeBeforeExport(); + // Replace the imported bpm value + importedFromFile.refTrackInfo().setBpm(actualBpm); if (!m_record.getMetadata().hasBeenModifiedAfterImport(importedFromFile)) { kLogger.debug() << "Skip exporting of unmodified track metadata:" << getLocation(); return ExportMetadataResult::Skipped; } + } else { + kLogger.warning() + << "Failed to import track metadata before exporting:" + << getLocation(); } } m_bExportMetadata = false; // reset flag diff --git a/src/track/trackinfo.h b/src/track/trackinfo.h index 5ff0a9b02965..9e140389d2cc 100644 --- a/src/track/trackinfo.h +++ b/src/track/trackinfo.h @@ -48,6 +48,13 @@ class TrackInfo final { TrackInfo& operator=(TrackInfo&&) = default; TrackInfo& operator=(const TrackInfo&) = default; + + // Adjusts floating-point values to match their string representation + // in file tags to account for rounding errors. + void normalizeBeforeExport() { + refBpm().normalizeBeforeExport(); + refReplayGain().normalizeBeforeExport(); + } }; bool operator==(const TrackInfo& lhs, const TrackInfo& rhs); diff --git a/src/track/trackmetadata.h b/src/track/trackmetadata.h index b250fbfefff7..f17743da3bfd 100644 --- a/src/track/trackmetadata.h +++ b/src/track/trackmetadata.h @@ -33,6 +33,13 @@ class TrackMetadata final { TrackMetadata& operator=(TrackMetadata&&) = default; TrackMetadata& operator=(const TrackMetadata&) = default; + // Adjusts floating-point values to match their string representation + // in file tags to account for rounding errors. + void normalizeBeforeExport() { + refAlbumInfo().normalizeBeforeExport(); + refTrackInfo().normalizeBeforeExport(); + } + // Compares the contents with metadata that has been freshly imported // from a file. bool hasBeenModifiedAfterImport(const TrackMetadata& importedFromFile) const { From db2f063a4c61535b412244237dff6c61e1bb3e04 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 6 Dec 2017 18:21:38 +0100 Subject: [PATCH 50/52] Re-enable new metadata properties ...with much less TODOs and excluded code lines than before. --- src/sources/soundsourceopus.cpp | 2 -- src/track/albuminfo.cpp | 25 ++++++++------ src/track/albuminfo.h | 15 +++++---- src/track/track.cpp | 12 +++++-- src/track/trackinfo.cpp | 55 ++++++++++++++++++------------- src/track/trackinfo.h | 24 +++++++------- src/track/trackmetadata.h | 6 ++++ src/track/trackmetadatataglib.cpp | 32 +++--------------- src/track/trackmetadatataglib.h | 6 ---- 9 files changed, 89 insertions(+), 88 deletions(-) diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 734dec2e2626..4e153c82065f 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -161,7 +161,6 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( trackGain.setRatio(gainRatio); pTrackMetadata->refTrackInfo().setReplayGain(trackGain); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { bool gainRatioValid = false; double gainRatio = ReplayGain::ratioFromString(l_SPayload, &gainRatioValid); @@ -170,7 +169,6 @@ SoundSourceOpus::importTrackMetadataAndCoverImage( albumGain.setRatio(gainRatio); pTrackMetadata->refAlbumInfo().setReplayGain(albumGain); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } } #endif // TAGLIB_HAS_OPUSFILE diff --git a/src/track/albuminfo.cpp b/src/track/albuminfo.cpp index e314a63fa333..43cf3fc4c53a 100644 --- a/src/track/albuminfo.cpp +++ b/src/track/albuminfo.cpp @@ -3,24 +3,29 @@ namespace mixxx { -// TODO(XXX): Add the commented out properties to the Mixxx library +void AlbumInfo::resetUnsupportedValues() { + setMusicBrainzArtistId(QString()); + setMusicBrainzReleaseId(QString()); + setMusicBrainzReleaseGroupId(QString()); + setReplayGain(ReplayGain()); +} + bool operator==(const AlbumInfo& lhs, const AlbumInfo& rhs) { return (lhs.getArtist() == rhs.getArtist()) && - //(lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && - //(lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && - //(lhs.getMusicBrainzReleaseGroupId() == rhs.getMusicBrainzReleaseGroupId()) && - //(lhs.getReplayGain() == rhs.getReplayGain()) && + (lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && + (lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && + (lhs.getMusicBrainzReleaseGroupId() == rhs.getMusicBrainzReleaseGroupId()) && + (lhs.getReplayGain() == rhs.getReplayGain()) && (lhs.getTitle() == rhs.getTitle()); } -// TODO(XXX): Add the commented out properties to the Mixxx library QDebug operator<<(QDebug dbg, const AlbumInfo& arg) { dbg << '{'; arg.dbgArtist(dbg); - //arg.dbgMusicBrainzArtistId(dbg); - //arg.dbgMusicBrainzReleaseId(dbg); - //arg.dbgMusicBrainzReleaseGroupId(dbg); - //arg.dbgReplayGain(dbg); + arg.dbgMusicBrainzArtistId(dbg); + arg.dbgMusicBrainzReleaseId(dbg); + arg.dbgMusicBrainzReleaseGroupId(dbg); + arg.dbgReplayGain(dbg); arg.dbgTitle(dbg); dbg << '}'; return dbg; diff --git a/src/track/albuminfo.h b/src/track/albuminfo.h index d6b66cf3a98a..f0e4ce65c176 100644 --- a/src/track/albuminfo.h +++ b/src/track/albuminfo.h @@ -12,15 +12,13 @@ namespace mixxx { class AlbumInfo final { // Album properties (in alphabetical order) - // TODO(XXX): Add the commented out properties to the Mixxx library PROPERTY_SET_BYVAL_GET_BYREF(QString, artist, Artist) - //PROPERTY_SET_BYVAL_GET_BYREF(ReplayGain, replayGain, ReplayGain) - //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) - //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) - //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseGroupId, MusicBrainzReleaseGroupId) + PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) + PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) + PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseGroupId, MusicBrainzReleaseGroupId) + PROPERTY_SET_BYVAL_GET_BYREF(ReplayGain, replayGain, ReplayGain) PROPERTY_SET_BYVAL_GET_BYREF(QString, title, Title) - public: AlbumInfo() = default; AlbumInfo(AlbumInfo&&) = default; @@ -30,10 +28,13 @@ class AlbumInfo final { AlbumInfo& operator=(AlbumInfo&&) = default; AlbumInfo& operator=(const AlbumInfo&) = default; + // TODO(XXX): Remove after all new fields have been added to the library + void resetUnsupportedValues(); + // Adjusts floating-point values to match their string representation // in file tags to account for rounding errors. void normalizeBeforeExport() { - //refReplayGain().normalizeBeforeExport(); + refReplayGain().normalizeBeforeExport(); } }; diff --git a/src/track/track.cpp b/src/track/track.cpp index b5c8c23de6e9..ca67185f3521 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -935,6 +935,9 @@ Track::ExportMetadataResult Track::exportMetadata( // be called after all references to the object have been dropped. // But it doesn't hurt much, so let's play it safe ;) QMutexLocker lock(&m_qMutex); + // Discard the values of all currently unsupported fields that are + // not stored in the library, yet + m_record.refMetadata().resetUnsupportedValues(); // Normalize metadata before export to adjust the precision of // floating values, ... m_record.refMetadata().normalizeBeforeExport(); @@ -960,14 +963,19 @@ Track::ExportMetadataResult Track::exportMetadata( mixxx::TrackMetadata importedFromFile; if ((pMetadataSource->importTrackMetadataAndCoverImage(&importedFromFile, nullptr).first == mixxx::MetadataSource::ImportResult::Succeeded)) { + // Discard the values of all currently unsupported fields that are + // not stored in the library, yet + importedFromFile.resetUnsupportedValues(); // Before comparison: Adjust imported bpm values that might be imprecise, // e.g. integer values from ID3v2 auto actualBpm = getActualBpm(importedFromFile.getTrackInfo().getBpm(), m_pBeats); - // Account for rounding errors during export + // ...account for bpm rounding errors during export... actualBpm.normalizeBeforeExport(); - // Replace the imported bpm value + // ...and replace the imported bpm value importedFromFile.refTrackInfo().setBpm(actualBpm); + // Finally the current and the just imported metadata can be checked for + // differences that will affect the file tags if (!m_record.getMetadata().hasBeenModifiedAfterImport(importedFromFile)) { kLogger.debug() << "Skip exporting of unmodified track metadata:" diff --git a/src/track/trackinfo.cpp b/src/track/trackinfo.cpp index 71d9b041efa1..c4bb7641ca87 100644 --- a/src/track/trackinfo.cpp +++ b/src/track/trackinfo.cpp @@ -3,53 +3,64 @@ namespace mixxx { -// TODO(XXX): Add the commented out properties to the Mixxx library +void TrackInfo::resetUnsupportedValues() { + setConductor(QString()); + setISRC(QString()); + setLanguage(QString()); + setLyricist(QString()); + setMood(QString()); + setMusicBrainzArtistId(QString()); + setMusicBrainzReleaseId(QString()); + setRecordLabel(QString()); + setRemixer(QString()); + setSubtitle(QString()); +} + bool operator==(const TrackInfo& lhs, const TrackInfo& rhs) { return (lhs.getArtist() == rhs.getArtist()) && (lhs.getBpm() == rhs.getBpm()) && (lhs.getComment() == rhs.getComment()) && (lhs.getComposer() == rhs.getComposer()) && - //(lhs.getConductor() == rhs.getConductor()) && + (lhs.getConductor() == rhs.getConductor()) && (lhs.getGrouping() == rhs.getGrouping()) && (lhs.getGenre() == rhs.getGenre()) && - //(lhs.getISRC() == rhs.getISRC()) && + (lhs.getISRC() == rhs.getISRC()) && (lhs.getKey() == rhs.getKey()) && - //(lhs.getLanguage() == rhs.getLanguage()) && - //(lhs.getLyricist() == rhs.getLyricist()) && - //(lhs.getMood() == rhs.getMood()) && - //(lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && - //(lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && - //(lhs.getRecordLabel() == rhs.getRecordLabel()) && - //(lhs.getRemixer() == rhs.getRemixer()) && + (lhs.getLanguage() == rhs.getLanguage()) && + (lhs.getLyricist() == rhs.getLyricist()) && + (lhs.getMood() == rhs.getMood()) && + (lhs.getMusicBrainzArtistId() == rhs.getMusicBrainzArtistId()) && + (lhs.getMusicBrainzReleaseId() == rhs.getMusicBrainzReleaseId()) && + (lhs.getRecordLabel() == rhs.getRecordLabel()) && + (lhs.getRemixer() == rhs.getRemixer()) && (lhs.getReplayGain() == rhs.getReplayGain()) && - //(lhs.getSubtitle() == rhs.getSubtitle()) && + (lhs.getSubtitle() == rhs.getSubtitle()) && (lhs.getTitle() == rhs.getTitle()) && (lhs.getTrackNumber() == rhs.getTrackNumber()) && (lhs.getTrackTotal() == rhs.getTrackTotal()) && (lhs.getYear() == rhs.getYear()); } -// TODO(XXX): Add the commented out properties to the Mixxx library QDebug operator<<(QDebug dbg, const TrackInfo& arg) { dbg << '{'; arg.dbgArtist(dbg); arg.dbgBpm(dbg); arg.dbgComment(dbg); arg.dbgComposer(dbg); - //arg.dbgConductor(dbg); + arg.dbgConductor(dbg); arg.dbgGrouping(dbg); arg.dbgGenre(dbg); - //arg.dbgISRC(dbg); + arg.dbgISRC(dbg); arg.dbgKey(dbg); - //arg.dbgLanguage(dbg); - //arg.dbgLyricist(dbg); - //arg.dbgMood(dbg); - //arg.dbgMusicBrainzArtistId(dbg); - //arg.dbgMusicBrainzReleaseId(dbg); - //arg.dbgRecordLabel(dbg); - //arg.dbgRemixer(dbg); + arg.dbgLanguage(dbg); + arg.dbgLyricist(dbg); + arg.dbgMood(dbg); + arg.dbgMusicBrainzArtistId(dbg); + arg.dbgMusicBrainzReleaseId(dbg); + arg.dbgRecordLabel(dbg); + arg.dbgRemixer(dbg); arg.dbgReplayGain(dbg); - //arg.dbgSubtitle(dbg); + arg.dbgSubtitle(dbg); arg.dbgTitle(dbg); arg.dbgTrackNumber(dbg); arg.dbgTrackTotal(dbg); diff --git a/src/track/trackinfo.h b/src/track/trackinfo.h index 9e140389d2cc..6304d78434c8 100644 --- a/src/track/trackinfo.h +++ b/src/track/trackinfo.h @@ -16,25 +16,24 @@ namespace mixxx { class TrackInfo final { // Track properties (in alphabetical order) - // TODO(XXX): Add the commented out properties to the Mixxx library PROPERTY_SET_BYVAL_GET_BYREF(QString, artist, Artist) PROPERTY_SET_BYVAL_GET_BYREF(Bpm, bpm, Bpm) PROPERTY_SET_BYVAL_GET_BYREF(QString, comment, Comment) PROPERTY_SET_BYVAL_GET_BYREF(QString, composer, Composer) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, conductor, Conductor) + PROPERTY_SET_BYVAL_GET_BYREF(QString, conductor, Conductor) PROPERTY_SET_BYVAL_GET_BYREF(QString, genre, Genre) PROPERTY_SET_BYVAL_GET_BYREF(QString, grouping, Grouping) PROPERTY_SET_BYVAL_GET_BYREF(QString, key, Key) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, isrc, ISRC) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, language, Language) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, lyricist, Lyricist) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, mood, Mood) - //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) - //PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, recordLabel, RecordLabel) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, remixer, Remixer) + PROPERTY_SET_BYVAL_GET_BYREF(QString, isrc, ISRC) + PROPERTY_SET_BYVAL_GET_BYREF(QString, language, Language) + PROPERTY_SET_BYVAL_GET_BYREF(QString, lyricist, Lyricist) + PROPERTY_SET_BYVAL_GET_BYREF(QString, mood, Mood) + PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzArtistId, MusicBrainzArtistId) + PROPERTY_SET_BYVAL_GET_BYREF(QUuid, musicBrainzReleaseId, MusicBrainzReleaseId) + PROPERTY_SET_BYVAL_GET_BYREF(QString, recordLabel, RecordLabel) + PROPERTY_SET_BYVAL_GET_BYREF(QString, remixer, Remixer) PROPERTY_SET_BYVAL_GET_BYREF(ReplayGain, replayGain, ReplayGain) - //PROPERTY_SET_BYVAL_GET_BYREF(QString, subtitle, Subtitle) + PROPERTY_SET_BYVAL_GET_BYREF(QString, subtitle, Subtitle) PROPERTY_SET_BYVAL_GET_BYREF(QString, title, Title) PROPERTY_SET_BYVAL_GET_BYREF(QString, trackNumber, TrackNumber) PROPERTY_SET_BYVAL_GET_BYREF(QString, trackTotal, TrackTotal) @@ -49,6 +48,9 @@ class TrackInfo final { TrackInfo& operator=(TrackInfo&&) = default; TrackInfo& operator=(const TrackInfo&) = default; + // TODO(XXX): Remove after all new fields have been added to the library + void resetUnsupportedValues(); + // Adjusts floating-point values to match their string representation // in file tags to account for rounding errors. void normalizeBeforeExport() { diff --git a/src/track/trackmetadata.h b/src/track/trackmetadata.h index f17743da3bfd..956b6c7107fa 100644 --- a/src/track/trackmetadata.h +++ b/src/track/trackmetadata.h @@ -33,6 +33,12 @@ class TrackMetadata final { TrackMetadata& operator=(TrackMetadata&&) = default; TrackMetadata& operator=(const TrackMetadata&) = default; + // TODO(XXX): Remove after all new fields have been added to the library + void resetUnsupportedValues() { + refAlbumInfo().resetUnsupportedValues(); + refTrackInfo().resetUnsupportedValues(); + } + // Adjusts floating-point values to match their string representation // in file tags to account for rounding errors. void normalizeBeforeExport() { diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp index 059ddb82be9b..e89a8966cc67 100644 --- a/src/track/trackmetadatataglib.cpp +++ b/src/track/trackmetadatataglib.cpp @@ -313,8 +313,6 @@ bool parseTrackPeak( return isPeakValid; } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES - inline bool hasAlbumGain(const TrackMetadata& trackMetadata) { return trackMetadata.getAlbumInfo().getReplayGain().hasRatio(); @@ -361,8 +359,6 @@ bool parseAlbumPeak( return isPeakValid; } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES - void readAudioProperties( TrackMetadata* pTrackMetadata, const TagLib::AudioProperties& audioProperties) { @@ -604,7 +600,6 @@ void writeID3v2TextIdentificationFrame( } } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES bool writeID3v2TextIdentificationFrameStringIfNotNull( TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, @@ -616,7 +611,6 @@ bool writeID3v2TextIdentificationFrameStringIfNotNull( return true; } } -#endif void writeID3v2CommentsFrame( TagLib::ID3v2::Tag* pTag, @@ -1201,7 +1195,6 @@ void importTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, parseTrackPeak(pTrackMetadata, trackPeak); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain = readFirstUserTextIdentificationFrame(tag, "REPLAYGAIN_ALBUM_GAIN"); if (!albumGain.isEmpty()) { @@ -1273,7 +1266,6 @@ void importTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, if (!subtitleFrame.isEmpty()) { pTrackMetadata->refTrackInfo().setSubtitle(toQStringFirstNotEmpty(subtitleFrame)); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { @@ -1332,7 +1324,6 @@ void importTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib:: parseTrackPeak(pTrackMetadata, trackPeak); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain; if (readAPEItem(tag, "REPLAYGAIN_ALBUM_GAIN", &albumGain)) { parseTrackGain(pTrackMetadata, albumGain); @@ -1395,7 +1386,6 @@ void importTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib:: if (readAPEItem(tag, "Subtitle", &subtitle)) { pTrackMetadata->refTrackInfo().setSubtitle(subtitle); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, @@ -1497,7 +1487,6 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, parseTrackPeak(pTrackMetadata, trackPeak); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain; if (readXiphCommentField(tag, "REPLAYGAIN_ALBUM_GAIN", &albumGain)) { parseAlbumGain(pTrackMetadata, albumGain); @@ -1560,7 +1549,6 @@ void importTrackMetadataFromVorbisCommentTag(TrackMetadata* pTrackMetadata, if (readXiphCommentField(tag, "SUBTITLE", &subtitle)) { pTrackMetadata->refTrackInfo().setSubtitle(subtitle); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP4::Tag& tag) { @@ -1634,7 +1622,6 @@ void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib:: parseTrackPeak(pTrackMetadata, trackPeak); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES QString albumGain; if (readMP4Atom(tag, "----:com.apple.iTunes:replaygain_album_gain", &albumGain)) { parseAlbumGain(pTrackMetadata, albumGain); @@ -1697,7 +1684,6 @@ void importTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib:: if (readMP4Atom(tag, "----:com.apple.iTunes:SUBTITLE", &subtitle)) { pTrackMetadata->refTrackInfo().setSubtitle(subtitle); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES } void importTrackMetadataFromRIFFTag(TrackMetadata* pTrackMetadata, const TagLib::RIFF::Info::Tag& tag) { @@ -1854,10 +1840,10 @@ bool exportTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, true); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES // TODO(XXX): The following tags are currently not stored in the // Mixxx library. Only write properties that have non-null values // to prevent deleting existing tags! + if (hasAlbumGain(trackMetadata)) { writeID3v2UserTextIdentificationFrame( pTag, @@ -1909,9 +1895,6 @@ bool exportTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, false); } - // TODO(XXX): The following tags are currently not stored in the - // Mixxx library. Only write properties that have non-null values - // to prevent deleting existing tags! writeID3v2TextIdentificationFrameStringIfNotNull( pTag, "TPE3", @@ -1946,7 +1929,6 @@ bool exportTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, pTag, "TIT3", trackMetadata.getTrackInfo().getSubtitle()); -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } @@ -1992,10 +1974,10 @@ bool exportTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& toTagLibString(formatTrackPeak(trackMetadata))); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES // TODO(XXX): The following tags are currently not stored in the // Mixxx library. Only write properties that have non-null values // to prevent deleting existing tags! + if (hasAlbumGain(trackMetadata)) { writeAPEItem(pTag, "REPLAYGAIN_ALBUM_GAIN", toTagLibString(formatAlbumGain(trackMetadata))); @@ -2026,9 +2008,6 @@ bool exportTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& toTagLibString(trackMetadata.getAlbumInfo().getMusicBrainzReleaseGroupId().toString())); } - // TODO(XXX): The following tags are currently not stored in the - // Mixxx library. Only write properties that have non-null values - // to prevent deleting existing tags! if (!trackMetadata.getTrackInfo().getConductor().isNull()) { writeAPEItem(pTag, "Conductor", toTagLibString(trackMetadata.getTrackInfo().getConductor())); @@ -2061,7 +2040,6 @@ bool exportTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& writeAPEItem(pTag, "Subtitle", toTagLibString(trackMetadata.getTrackInfo().getSubtitle())); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } @@ -2145,10 +2123,10 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, toTagLibString(formatTrackPeak(trackMetadata))); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES // TODO(XXX): The following tags are currently not stored in the // Mixxx library. Only write properties that have non-null values // to prevent deleting existing tags! + if (hasAlbumGain(trackMetadata)) { writeXiphCommentField(pTag, "REPLAYGAIN_ALBUM_GAIN", toTagLibString(formatAlbumGain(trackMetadata))); @@ -2209,7 +2187,6 @@ bool exportTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, writeXiphCommentField(pTag, "SUBTITLE", toTagLibString(trackMetadata.getTrackInfo().getSubtitle())); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } @@ -2279,10 +2256,10 @@ bool exportTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& toTagLibString(formatTrackPeak(trackMetadata))); } -#ifndef EXCLUDE_EXTRA_METADATA_PROPERTIES // TODO(XXX): The following tags are currently not stored in the // Mixxx library. Only write properties that have non-null values // to prevent deleting existing tags! + if (hasAlbumGain(trackMetadata)) { writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_album_gain", toTagLibString(formatAlbumGain(trackMetadata))); @@ -2343,7 +2320,6 @@ bool exportTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& writeMP4Atom(pTag, "----:com.apple.iTunes:SUBTITLE", toTagLibString(trackMetadata.getTrackInfo().getSubtitle())); } -#endif // EXCLUDE_EXTRA_METADATA_PROPERTIES return true; } diff --git a/src/track/trackmetadatataglib.h b/src/track/trackmetadatataglib.h index 1535eaa68767..f1ec48a4ee83 100644 --- a/src/track/trackmetadatataglib.h +++ b/src/track/trackmetadatataglib.h @@ -22,12 +22,6 @@ #define TAGLIB_HAS_AIFF_HAS_ID3V2TAG \ (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 10)) -// TODO(XXX): Add those fields to the Mixxx library and remove the #define -// If those properties are imported but not stored in the database -// Mixxx would detect metadata as modified and export it again although -// nothing has changed. -#define EXCLUDE_EXTRA_METADATA_PROPERTIES - #include #include "track/trackmetadata.h" From 32c15a1ba749ee162abaa45950570e57d0390b64 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 6 Dec 2017 20:08:23 +0100 Subject: [PATCH 51/52] Update the informational text about exporting track metadata --- build/depends.py | 2 +- src/library/dlgtrackmetadataexport.cpp | 23 +++++++++++++++++++++++ src/library/dlgtrackmetadataexport.h | 20 ++++++++++++++++++++ src/library/exporttrackmetadatainfo.cpp | 22 ---------------------- src/library/exporttrackmetadatainfo.h | 18 ------------------ src/preferences/dialog/dlgpreflibrary.cpp | 4 ++-- src/widget/wtracktableview.cpp | 18 +++++++++--------- 7 files changed, 55 insertions(+), 52 deletions(-) create mode 100644 src/library/dlgtrackmetadataexport.cpp create mode 100644 src/library/dlgtrackmetadataexport.h delete mode 100644 src/library/exporttrackmetadatainfo.cpp delete mode 100644 src/library/exporttrackmetadatainfo.h diff --git a/build/depends.py b/build/depends.py index 1c481bce0424..6154883096a4 100644 --- a/build/depends.py +++ b/build/depends.py @@ -942,7 +942,7 @@ def sources(self, build): "library/dlgmissing.cpp", "library/dlgtagfetcher.cpp", "library/dlgtrackinfo.cpp", - "library/exporttrackmetadatainfo.cpp", + "library/dlgtrackmetadataexport.cpp", "library/browse/browsetablemodel.cpp", "library/browse/browsethread.cpp", diff --git a/src/library/dlgtrackmetadataexport.cpp b/src/library/dlgtrackmetadataexport.cpp new file mode 100644 index 000000000000..7c580717f1cd --- /dev/null +++ b/src/library/dlgtrackmetadataexport.cpp @@ -0,0 +1,23 @@ +#include "library/dlgtrackmetadataexport.h" + +#include + + +namespace mixxx { + +//static +bool DlgTrackMetadataExport::s_bShownDuringThisSession = false; + +void DlgTrackMetadataExport::showMessageBoxOncePerSession() { + if (!s_bShownDuringThisSession) { + QMessageBox::information( + nullptr, + tr("Export Modified Track Metadata"), + tr("Mixxx may wait to modify files until they are not loaded to any decks or samplers. " + "If you do not see changed metadata in other programs immediately, " + "eject the track from all decks and samplers or shutdown Mixxx.")); + s_bShownDuringThisSession = true; + } +} + +} // namespace mixxx diff --git a/src/library/dlgtrackmetadataexport.h b/src/library/dlgtrackmetadataexport.h new file mode 100644 index 000000000000..b0d3df298f2d --- /dev/null +++ b/src/library/dlgtrackmetadataexport.h @@ -0,0 +1,20 @@ +#pragma once + +#include + + +namespace mixxx { + +// This is just an ugly hack to avoid duplicate code and to define +// translatable strings only once. Inheritance from QDialog is +// needed for i18n message strings. The class contains mutable +// static data and must only be used within the UI thread. +class DlgTrackMetadataExport: private QDialog { + public: + static void showMessageBoxOncePerSession(); + + private: + static bool s_bShownDuringThisSession; +}; + +} // namespace mixxx diff --git a/src/library/exporttrackmetadatainfo.cpp b/src/library/exporttrackmetadatainfo.cpp deleted file mode 100644 index 52fb17119a4f..000000000000 --- a/src/library/exporttrackmetadatainfo.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "library/exporttrackmetadatainfo.h" - -#include - - -namespace mixxx { - -//static -bool ExportTrackMetadataInfo::s_bShownOncePerSession = false; - -void ExportTrackMetadataInfo::showMessageBox() { - if (!s_bShownOncePerSession) { - QMessageBox::information( - nullptr, - tr("Export Modified Track Metadata"), - tr("Mixxx may wait to modify files until it is certain that writing into those files will not cause audible glitches. " - "If you do not see changed metadata in other programs, close Mixxx to modify the files immediately.")); - s_bShownOncePerSession = true; - } -} - -} // namespace mixxx diff --git a/src/library/exporttrackmetadatainfo.h b/src/library/exporttrackmetadatainfo.h deleted file mode 100644 index b787bdf98472..000000000000 --- a/src/library/exporttrackmetadatainfo.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - - -namespace mixxx { - -// This is just an ugly hack to avoid duplicate code and to define -// translatable strings only once. -class ExportTrackMetadataInfo: private QDialog { - public: - static void showMessageBox(); - - private: - static bool s_bShownOncePerSession; -}; - -} // namespace mixxx diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 95bfe6b01ad9..62a7eccebf56 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -9,7 +9,7 @@ #include #include "preferences/dialog/dlgpreflibrary.h" -#include "library/exporttrackmetadatainfo.h" +#include "library/dlgtrackmetadataexport.h" #include "sources/soundsourceproxy.h" #define MIXXX_ADDONS_URL "http://www.mixxx.org/wiki/doku.php/add-ons" @@ -355,6 +355,6 @@ void DlgPrefLibrary::slotSelectFont() { void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() { if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked()) { - mixxx::ExportTrackMetadataInfo::showMessageBox(); + mixxx::DlgTrackMetadataExport::showMessageBoxOncePerSession(); } } diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index a6c918530c4a..8a030daedefa 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -16,7 +16,7 @@ #include "library/librarytablemodel.h" #include "library/crate/cratefeaturehelper.h" #include "library/dao/trackschema.h" -#include "library/exporttrackmetadatainfo.h" +#include "library/dlgtrackmetadataexport.h" #include "control/controlobject.h" #include "control/controlproxy.h" #include "track/track.h" @@ -1421,20 +1421,20 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { return; } - QModelIndexList indices = selectionModel()->selectedRows(); - - TrackModel* trackModel = getTrackModel(); + TrackModel* pTrackModel = getTrackModel(); + if (!pTrackModel) { + return; + } - if (trackModel == NULL) { + QModelIndexList indices = selectionModel()->selectedRows(); + if (indices.isEmpty()) { return; } - // Inform the user once per session that the corresponding files - // will not be modified at once and changes may appear later. - mixxx::ExportTrackMetadataInfo::showMessageBox(); + mixxx::DlgTrackMetadataExport::showMessageBoxOncePerSession(); for (const QModelIndex& index : indices) { - TrackPointer pTrack = trackModel->getTrack(index); + TrackPointer pTrack = pTrackModel->getTrack(index); if (pTrack) { // Export of metadata is deferred until all references to the // corresponding track object have been dropped. Otherwise From 4f8d24c2cce0e03d28b705bd87594e2ce87f0922 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 10 Dec 2017 20:14:43 +0100 Subject: [PATCH 52/52] Improve comprehensibility of the code and many comments --- src/track/track.cpp | 80 +++++++++++++++++++++++++++++++++------------ src/track/track.h | 2 +- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/track/track.cpp b/src/track/track.cpp index ca67185f3521..8ab68fafe650 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -62,7 +62,7 @@ Track::Track( m_qMutex(QMutex::Recursive), m_record(trackId), m_bDirty(false), - m_bExportMetadata(false), + m_bMarkedForMetadataExport(false), m_analyzerProgress(-1) { } @@ -811,7 +811,7 @@ bool Track::isDirty() { void Track::markForMetadataExport() { QMutexLocker lock(&m_qMutex); - if (compareAndSet(&m_bExportMetadata, true)) { + if (compareAndSet(&m_bMarkedForMetadataExport, true)) { markDirtyAndUnlock(&lock); } } @@ -936,16 +936,26 @@ Track::ExportMetadataResult Track::exportMetadata( // But it doesn't hurt much, so let's play it safe ;) QMutexLocker lock(&m_qMutex); // Discard the values of all currently unsupported fields that are - // not stored in the library, yet + // not stored in the library, yet. Those fields are already imported + // from file tags, but the database schema needs to be extended for + // storing them. Currently those fields will be empty/null when read + // from the database and must be ignored until the schema has been + // updated. m_record.refMetadata().resetUnsupportedValues(); - // Normalize metadata before export to adjust the precision of - // floating values, ... + // Normalize metadata before exporting to adjust the precision of + // floating values, ... Otherwise the following comparisons may + // repeatedly indicate that values have changed only due to + // rounding errors. m_record.refMetadata().normalizeBeforeExport(); - if (!m_bExportMetadata) { + if (!m_bMarkedForMetadataExport) { // Perform some consistency checks if metadata is exported // implicitly after a track has been modified and NOT explicitly // requested by a user as indicated by this flag. if (!m_record.getMetadataSynchronized()) { + // If the metadata has never been imported from file tags it + // must be exported explicitly once. This ensures that we don't + // overwrite existing file tags with completely different + // information. kLogger.debug() << "Skip exporting of unsynchronized track metadata:" << getLocation(); @@ -954,44 +964,72 @@ Track::ExportMetadataResult Track::exportMetadata( // Check if the metadata has actually been modified. Otherwise // we don't need to write it back. Exporting unmodified metadata // would needlessly update the file's time stamp and should be - // avoided. - // TODO(XXX): How to we handle the case that importTrackMetadataAndCoverImage() - // returns a newer time stamp than m_record.getMetadataSynchronized(), i.e. - // if the file has been modified by another program since we have imported - // the metadata? But this is expected to happen if files have been copied - // or after upgrading the column 'header_parsed' from bool to QDateTime. + // avoided. Since we don't know in which state the file's metadata + // is we import it again into a temporary variable. + // TODO(XXX): m_record.getMetadataSynchronized() currently is a + // boolean flag, but it should become a time stamp in the future. + // We could take this time stamp and the file's last modification + // time stamp into // account and might decide to skip importing + // the metadata again. mixxx::TrackMetadata importedFromFile; if ((pMetadataSource->importTrackMetadataAndCoverImage(&importedFromFile, nullptr).first == mixxx::MetadataSource::ImportResult::Succeeded)) { // Discard the values of all currently unsupported fields that are - // not stored in the library, yet + // not stored in the library, yet. We have done the same with the track's + // current metadata to make the tags comparable (see above). importedFromFile.resetUnsupportedValues(); // Before comparison: Adjust imported bpm values that might be imprecise, - // e.g. integer values from ID3v2 + // e.g. integer values from ID3v2. The same strategy has is used when + // importing the track's metadata in order to preserve the more accurate + // bpm value stored by Mixxx. Again, this is necessary to make the tags + // comparable. auto actualBpm = getActualBpm(importedFromFile.getTrackInfo().getBpm(), m_pBeats); - // ...account for bpm rounding errors during export... + // All imported floating point values are already properly rounded, because + // they have just been imported. But the imported bpm value might have been + // replaced by a more accurate bpm value, that also needs to be rounded before + // comparison. actualBpm.normalizeBeforeExport(); - // ...and replace the imported bpm value + // Replace the imported with the actual bpm value managed by Mixxx. importedFromFile.refTrackInfo().setBpm(actualBpm); - // Finally the current and the just imported metadata can be checked for - // differences that will affect the file tags + // Finally the track's current metadata and the imported/adjusted metadata + // can be compared for differences to decide whether the tags in the file + // would change if we perform the write operation. We are using a special + // comparison function that excludes all read-only audio properties which + // are stored in file tags, but may not be accurate. They can't be written + // anyway, so we must not take them into account here. if (!m_record.getMetadata().hasBeenModifiedAfterImport(importedFromFile)) { + // The file tags are in-sync with the track's metadata and don't need + // to be updated. kLogger.debug() - << "Skip exporting of unmodified track metadata:" + << "Skip exporting of unmodified track metadata into file:" << getLocation(); return ExportMetadataResult::Skipped; } } else { + // Something must be wrong with the file or it doesn't + // contain any file tags. We don't want to risk a failure + // during export and abort the operation for safety here. + // The user may decide to explicitly export the metadata. kLogger.warning() - << "Failed to import track metadata before exporting:" + << "Skip exporting of track metadata after import failed." + << "Export of metadata must be triggered explicitly for this file:" << getLocation(); + return ExportMetadataResult::Skipped; } + // ...by continuing the file tags will be updated } - m_bExportMetadata = false; // reset flag + // The track's metadata will be exported instantly. The export should + // only be tried once so we reset the marker flag. + m_bMarkedForMetadataExport = false; const auto trackMetadataExported = pMetadataSource->exportTrackMetadata(m_record.getMetadata()); if (trackMetadataExported.first == mixxx::MetadataSource::ExportResult::Succeeded) { + // After successfully exporting the metadata we record the fact + // that now the file tags and the track's metadata are in sync. + // This information (flag or time stamp) is stored in the database. + // The database update will follow immediately after returning from + // this operation! // TODO(XXX): Replace bool with QDateTime DEBUG_ASSERT(!trackMetadataExported.second.isNull()); //pTrack->setMetadataSynchronized(trackMetadataExported.second); diff --git a/src/track/track.h b/src/track/track.h index a36e67268b4e..f388fd65333c 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -367,7 +367,7 @@ class Track : public QObject { // Flag indicating that the user has explicitly requested to save // the metadata. - bool m_bExportMetadata; + bool m_bMarkedForMetadataExport; // The list of cue points for the track QList m_cuePoints;