diff --git a/res/schema.xml b/res/schema.xml index c4a70b26a00..6c086ee9799 100644 --- a/res/schema.xml +++ b/res/schema.xml @@ -557,4 +557,13 @@ reapplying those migrations. GROUP BY PlaylistTracks.track_id); + + + Add source_synchronized_ms column to library table + + + + ALTER TABLE library ADD COLUMN source_synchronized_ms INTEGER DEFAULT NULL; + + diff --git a/src/database/mixxxdb.cpp b/src/database/mixxxdb.cpp index 9e707bf260a..f2b38a425b8 100644 --- a/src/database/mixxxdb.cpp +++ b/src/database/mixxxdb.cpp @@ -12,7 +12,7 @@ const QString MixxxDb::kDefaultSchemaFile(":/schema.xml"); //static -const int MixxxDb::kRequiredSchemaVersion = 36; +const int MixxxDb::kRequiredSchemaVersion = 37; namespace { diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 747725ff068..43e89462ba9 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -396,6 +396,7 @@ void TrackDAO::addTracksPrepare() { "played," "mixxx_deleted," "header_parsed," + "source_synchronized_ms," "channels," "samplerate," "bitrate," @@ -443,6 +444,7 @@ void TrackDAO::addTracksPrepare() { ":played," ":mixxx_deleted," ":header_parsed," + ":source_synchronized_ms," ":channels," ":samplerate," ":bitrate," @@ -549,7 +551,17 @@ void bindTrackLibraryValues( trackMetadata.getStreamInfo().getDuration().toDoubleSeconds()); pTrackLibraryQuery->bindValue(":header_parsed", - track.getMetadataSynchronized() ? 1 : 0); + track.isSourceSynchronized() ? 1 : 0); + const QDateTime sourceSynchronizedAt = + track.getSourceSynchronizedAt(); + if (sourceSynchronizedAt.isValid()) { + DEBUG_ASSERT(sourceSynchronizedAt.timeSpec() == Qt::UTC); + pTrackLibraryQuery->bindValue(":source_synchronized_ms", + sourceSynchronizedAt.toMSecsSinceEpoch()); + } else { + pTrackLibraryQuery->bindValue(":source_synchronized_ms", + QVariant()); + } const PlayCounter& playCounter = track.getPlayCounter(); pTrackLibraryQuery->bindValue(":timesplayed", playCounter.getTimesPlayed()); @@ -828,7 +840,7 @@ TrackPointer TrackDAO::addTracksAddFile( // Initially (re-)import the metadata for the newly created track // from the file. SoundSourceProxy(pTrack).updateTrackFromSource(); - if (!pTrack->isMetadataSynchronized()) { + if (!pTrack->isSourceSynchronized()) { qWarning() << "TrackDAO::addTracksAddFile:" << "Failed to parse track metadata from file" << pTrack->getLocation(); @@ -1189,9 +1201,20 @@ bool setTrackFiletype(const QSqlRecord& record, const int column, return false; } -bool setTrackMetadataSynchronized(const QSqlRecord& record, const int column, - TrackPointer pTrack) { - pTrack->setMetadataSynchronized(record.value(column).toBool()); +bool setTrackHeaderParsed(const QSqlRecord& record, const int column, TrackPointer pTrack) { + pTrack->setHeaderParsedFromTrackDAO(record.value(column).toBool()); + return false; +} + +bool setTrackSourceSynchronizedAt(const QSqlRecord& record, const int column, TrackPointer pTrack) { + QDateTime sourceSynchronizedAt; + const QVariant value = record.value(column); + if (value.isValid() && value.canConvert()) { + const quint64 msecsSinceEpoch = qvariant_cast(value); + sourceSynchronizedAt.setTimeSpec(Qt::UTC); + sourceSynchronizedAt.setMSecsSinceEpoch(msecsSinceEpoch); + } + pTrack->setSourceSynchronizedAt(sourceSynchronizedAt); return false; } @@ -1332,7 +1355,8 @@ TrackPointer TrackDAO::getTrackById(TrackId trackId) const { {"last_played_at", setTrackLastPlayedAt}, {"played", setTrackPlayed}, {"datetime_added", setTrackDateAdded}, - {"header_parsed", setTrackMetadataSynchronized}, + {"header_parsed", setTrackHeaderParsed}, + {"source_synchronized_ms", setTrackSourceSynchronizedAt}, // Audio properties are set together at once. Do not change the // ordering of these columns or put other columns in between them! @@ -1600,6 +1624,7 @@ bool TrackDAO::updateTrack(Track* pTrack) const { "last_played_at=:last_played_at," "played=:played," "header_parsed=:header_parsed," + "source_synchronized_ms=:source_synchronized_ms," "channels=:channels," "bitrate=:bitrate," "samplerate=:samplerate," diff --git a/src/library/dlgtrackinfo.cpp b/src/library/dlgtrackinfo.cpp index a5810d1b172..a4f8ce93722 100644 --- a/src/library/dlgtrackinfo.cpp +++ b/src/library/dlgtrackinfo.cpp @@ -674,7 +674,7 @@ void DlgTrackInfo::slotImportMetadataFromFile() { mixxx::TrackRecord trackRecord = m_pLoadedTrack->getRecord(); mixxx::TrackMetadata trackMetadata = trackRecord.getMetadata(); QImage coverImage; - const auto [importResult, metadataSynchronized] = + const auto [importResult, sourceSynchronizedAt] = SoundSourceProxy(m_pLoadedTrack) .importTrackMetadataAndCoverImage( &trackMetadata, &coverImage); @@ -688,7 +688,7 @@ void DlgTrackInfo::slotImportMetadataFromFile() { coverImage); trackRecord.replaceMetadataFromSource( std::move(trackMetadata), - metadataSynchronized); + sourceSynchronizedAt); trackRecord.setCoverInfo( std::move(guessedCoverInfo)); replaceTrackRecord( diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index 607e9881cfd..85e09f49a15 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -75,20 +75,20 @@ class AiffFile : public TagLib::RIFF::AIFF::File { } }; -inline QDateTime getMetadataSynchronized(const QFileInfo& fileInfo) { - return fileInfo.lastModified(); +inline QDateTime getSourceSynchronizedAt(const QFileInfo& fileInfo) { + return fileInfo.lastModified().toUTC(); } } // anonymous namespace std::pair MetadataSourceTagLib::afterImport(ImportResult importResult) const { - return std::make_pair(importResult, getMetadataSynchronized(QFileInfo(m_fileName))); + return std::make_pair(importResult, getSourceSynchronizedAt(QFileInfo(m_fileName))); } std::pair MetadataSourceTagLib::afterExport(ExportResult exportResult) const { - return std::make_pair(exportResult, getMetadataSynchronized(QFileInfo(m_fileName))); + return std::make_pair(exportResult, getSourceSynchronizedAt(QFileInfo(m_fileName))); } std::pair diff --git a/src/sources/soundsourceproxy.cpp b/src/sources/soundsourceproxy.cpp index 69d94ea1982..cc773234016 100644 --- a/src/sources/soundsourceproxy.cpp +++ b/src/sources/soundsourceproxy.cpp @@ -539,9 +539,9 @@ bool SoundSourceProxy::updateTrackFromSource( // values if the corresponding file tags are missing. Depending // on the file type some kind of tags might even not be supported // at all and this information would get lost entirely otherwise! - bool metadataSynchronized = false; + bool headerParsed = false; mixxx::TrackMetadata trackMetadata = - m_pTrack->getMetadata(&metadataSynchronized); + m_pTrack->getMetadata(&headerParsed); // Save for later to replace the unreliable and imprecise audio // properties imported from file tags (see below). @@ -559,7 +559,7 @@ bool SoundSourceProxy::updateTrackFromSource( // if the user did not explicitly choose to (re-)import metadata // explicitly from this file. bool mergeExtraMetadataFromSource = false; - if (metadataSynchronized && mode == UpdateTrackFromSourceMode::Once) { + if (headerParsed && mode == UpdateTrackFromSourceMode::Once) { // No (re-)import needed or desired, only merge missing properties mergeExtraMetadataFromSource = true; } else { @@ -592,7 +592,7 @@ bool SoundSourceProxy::updateTrackFromSource( << "from file" << getUrl().toString(); // make sure that the trackMetadata was not messed up due to the failure - trackMetadata = m_pTrack->getMetadata(&metadataSynchronized); + trackMetadata = m_pTrack->getMetadata(&headerParsed); } // Partial import @@ -610,7 +610,7 @@ bool SoundSourceProxy::updateTrackFromSource( } // Full import - if (metadataSynchronized) { + if (headerParsed) { // Metadata has been synchronized successfully at least // once in the past. Only overwrite this information if // new data has actually been imported, otherwise abort diff --git a/src/test/trackupdate_test.cpp b/src/test/trackupdate_test.cpp index d413c922210..3b4b0f4709d 100644 --- a/src/test/trackupdate_test.cpp +++ b/src/test/trackupdate_test.cpp @@ -30,7 +30,7 @@ class TrackUpdateTest : public MixxxTest, SoundSourceProviderRegistration { static TrackPointer newTestTrackParsed() { auto pTrack = newTestTrack(); EXPECT_TRUE(SoundSourceProxy(pTrack).updateTrackFromSource()); - EXPECT_TRUE(pTrack->isMetadataSynchronized()); + EXPECT_TRUE(pTrack->isSourceSynchronized()); EXPECT_TRUE(hasTrackMetadata(pTrack)); EXPECT_TRUE(hasCoverArt(pTrack)); pTrack->markClean(); @@ -66,7 +66,7 @@ TEST_F(TrackUpdateTest, parseModifiedCleanOnce) { const auto coverInfoAfter = pTrack->getCoverInfo(); // Verify that the track has not been modified - ASSERT_TRUE(pTrack->isMetadataSynchronized()); + ASSERT_TRUE(pTrack->isSourceSynchronized()); ASSERT_FALSE(pTrack->isDirty()); ASSERT_EQ(trackMetadataBefore, trackMetadataAfter); ASSERT_EQ(coverInfoBefore, coverInfoAfter); @@ -86,7 +86,7 @@ TEST_F(TrackUpdateTest, parseModifiedCleanAgainSkipCover) { const auto coverInfoAfter = pTrack->getCoverInfo(); // Updated - EXPECT_TRUE(pTrack->isMetadataSynchronized()); + EXPECT_TRUE(pTrack->isSourceSynchronized()); EXPECT_TRUE(pTrack->isDirty()); EXPECT_NE(trackMetadataBefore, trackMetadataAfter); EXPECT_EQ(coverInfoBefore, coverInfoAfter); @@ -110,7 +110,7 @@ TEST_F(TrackUpdateTest, parseModifiedCleanAgainUpdateCover) { const auto coverInfoAfter = pTrack->getCoverInfo(); // Updated - EXPECT_TRUE(pTrack->isMetadataSynchronized()); + EXPECT_TRUE(pTrack->isSourceSynchronized()); EXPECT_TRUE(pTrack->isDirty()); EXPECT_NE(trackMetadataBefore, trackMetadataAfter); EXPECT_NE(coverInfoBefore, coverInfoAfter); @@ -129,7 +129,7 @@ TEST_F(TrackUpdateTest, parseModifiedDirtyAgain) { const auto coverInfoAfter = pTrack->getCoverInfo(); // Updated - EXPECT_TRUE(pTrack->isMetadataSynchronized()); + EXPECT_TRUE(pTrack->isSourceSynchronized()); EXPECT_TRUE(pTrack->isDirty()); EXPECT_NE(trackMetadataBefore, trackMetadataAfter); EXPECT_EQ(coverInfoBefore, coverInfoAfter); diff --git a/src/track/track.cpp b/src/track/track.cpp index 3c630bff404..753c98ae0af 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -135,7 +135,7 @@ void Track::relocate( void Track::replaceMetadataFromSource( mixxx::TrackMetadata importedMetadata, - const QDateTime& metadataSynchronized) { + const QDateTime& sourceSynchronizedAt) { // Information stored in Serato tags is imported separately after // importing the metadata (see below). The Serato tags BLOB itself // is updated together with the metadata. @@ -164,7 +164,7 @@ void Track::replaceMetadataFromSource( m_record.getMetadata().getTrackInfo().getReplayGain(); bool modified = m_record.replaceMetadataFromSource( std::move(importedMetadata), - metadataSynchronized); + sourceSynchronizedAt); const auto newReplayGain = m_record.getMetadata().getTrackInfo().getReplayGain(); @@ -245,10 +245,10 @@ bool Track::mergeExtraMetadataFromSource( } mixxx::TrackMetadata Track::getMetadata( - bool* pMetadataSynchronized) const { + bool* pSourceSynchronized) const { const QMutexLocker locked(&m_qMutex); - if (pMetadataSynchronized) { - *pMetadataSynchronized = m_record.getMetadataSynchronized(); + if (pSourceSynchronized) { + *pSourceSynchronized = m_record.isSourceSynchronized(); } return m_record.getMetadata(); } @@ -461,16 +461,23 @@ void Track::emitChangedSignalsForAllMetadata() { emit keyChanged(); } -void Track::setMetadataSynchronized(bool metadataSynchronized) { +bool Track::isSourceSynchronized() const { QMutexLocker lock(&m_qMutex); - if (compareAndSet(m_record.ptrMetadataSynchronized(), metadataSynchronized)) { + return m_record.isSourceSynchronized(); +} + +void Track::setSourceSynchronizedAt(const QDateTime& sourceSynchronizedAt) { + DEBUG_ASSERT(!sourceSynchronizedAt.isValid() || + sourceSynchronizedAt.timeSpec() == Qt::UTC); + QMutexLocker lock(&m_qMutex); + if (compareAndSet(m_record.ptrSourceSynchronizedAt(), sourceSynchronizedAt)) { markDirtyAndUnlock(&lock); } } -bool Track::isMetadataSynchronized() const { +QDateTime Track::getSourceSynchronizedAt() const { QMutexLocker lock(&m_qMutex); - return m_record.getMetadataSynchronized(); + return m_record.getSourceSynchronizedAt(); } QString Track::getInfo() const { @@ -1430,12 +1437,11 @@ ExportTrackMetadataResult 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); - // 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. - if (!m_bMarkedForMetadataExport && !m_record.getMetadataSynchronized()) { + // TODO(XXX): Use sourceSynchronizedAt to decide if metadata + // should be (re-)imported before exporting it. The file might + // have been updated by external applications. Overwriting + // this modified metadata might not be intended. + if (!m_bMarkedForMetadataExport && !m_record.isSourceSynchronized()) { // 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 @@ -1577,10 +1583,7 @@ ExportTrackMetadataResult Track::exportMetadata( // 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); - m_record.setMetadataSynchronized(!trackMetadataExported.second.isNull()); + m_record.updateSourceSynchronizedAt(trackMetadataExported.second); if (kLogger.debugEnabled()) { kLogger.debug() << "Exported track metadata:" diff --git a/src/track/track.h b/src/track/track.h index 10a66c5533a..7352d89045b 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -72,6 +72,7 @@ class Track : public QObject { STORED false NOTIFY durationChanged) Q_PROPERTY(QString info READ getInfo STORED false NOTIFY infoChanged) Q_PROPERTY(QString titleInfo READ getTitleInfo STORED false NOTIFY infoChanged) + Q_PROPERTY(QDateTime sourceSynchronizedAt READ getSourceSynchronizedAt STORED false) mixxx::FileAccess getFileAccess() const { // Copying QFileInfo is thread-safe due to implicit sharing, @@ -148,9 +149,18 @@ class Track : public QObject { mixxx::ReplayGain getReplayGain() const; // Indicates if the metadata has been parsed from file tags. - bool isMetadataSynchronized() const; - // Only used by a free function in TrackDAO! - void setMetadataSynchronized(bool metadataSynchronized); + bool isSourceSynchronized() const; + + void setHeaderParsedFromTrackDAO(bool headerParsed) { + // Always operating on a newly created, exclusive instance! No need + // to lock the mutex. + DEBUG_ASSERT(!m_record.m_headerParsed); + m_record.m_headerParsed = headerParsed; + } + + // The date/time of the last import or export of metadata + void setSourceSynchronizedAt(const QDateTime& sourceSynchronizedAt); + QDateTime getSourceSynchronizedAt() const; void setDateAdded(const QDateTime& dateAdded); QDateTime getDateAdded() const; @@ -336,10 +346,10 @@ class Track : public QObject { /// with file tags, either by importing or exporting the metadata. void replaceMetadataFromSource( mixxx::TrackMetadata importedMetadata, - const QDateTime& metadataSynchronized); + const QDateTime& sourceSynchronizedAt); mixxx::TrackMetadata getMetadata( - bool* pMetadataSynchronized = nullptr) const; + bool* pHeaderParsed = nullptr) const; mixxx::TrackRecord getRecord( bool* pDirty = nullptr) const; diff --git a/src/track/trackrecord.cpp b/src/track/trackrecord.cpp index 39a6d077245..6d09d3724a6 100644 --- a/src/track/trackrecord.cpp +++ b/src/track/trackrecord.cpp @@ -15,9 +15,9 @@ const Logger kLogger("TrackRecord"); TrackRecord::TrackRecord(TrackId id) : m_id(std::move(id)), - m_metadataSynchronized(false), m_rating(0), - m_bpmLocked(false) { + m_bpmLocked(false), + m_headerParsed(false) { } void TrackRecord::setKeys(const Keys& keys) { @@ -107,9 +107,47 @@ bool copyIfNotEmpty( } // anonymous namespace +bool TrackRecord::updateSourceSynchronizedAt( + const QDateTime& sourceSynchronizedAt) { + VERIFY_OR_DEBUG_ASSERT(sourceSynchronizedAt.isValid()) { + // Cannot be reset after it has been set at least once. + // This is required to prevent unintended and repeated + // reimporting of metadata from file tags. + return false; + } + if (getSourceSynchronizedAt() == sourceSynchronizedAt) { + return false; // unchanged + } + setSourceSynchronizedAt(sourceSynchronizedAt); + m_headerParsed = sourceSynchronizedAt.isValid(); + DEBUG_ASSERT(isSourceSynchronized()); + return true; +} + +bool TrackRecord::isSourceSynchronized() const { + // This method cannot be used to update m_headerParsed + // after modifying m_sourceSynchronizedAt during a short + // moment of inconsistency. Otherwise the debug assertion + // triggers! + DEBUG_ASSERT(m_headerParsed || + !getSourceSynchronizedAt().isValid()); + if (getSourceSynchronizedAt().isValid()) { + return true; + } + // Legacy fallback: The property sourceSynchronizedAt has been + // added later. Files that have been added before that time + // and that have never been re-imported will only have that + // legacy flag set while sourceSynchronizedAt is still invalid. + return m_headerParsed; +} + bool TrackRecord::replaceMetadataFromSource( TrackMetadata&& importedMetadata, - const QDateTime& metadataSynchronized) { + const QDateTime& sourceSynchronizedAt) { + VERIFY_OR_DEBUG_ASSERT(sourceSynchronizedAt.isValid()) { + return false; + } + DEBUG_ASSERT(sourceSynchronizedAt.timeSpec() == Qt::UTC); if (m_streamInfoFromSource) { // Preserve precise stream info if available, i.e. discard the // audio properties that are also stored as track metadata. @@ -120,16 +158,7 @@ bool TrackRecord::replaceMetadataFromSource( setMetadata(std::move(importedMetadata)); modified = true; } - // Only set the metadata synchronized flag (column `header_parsed` - // in the database) from false to true, but never reset it back to - // false. Otherwise file tags would be re-imported and overwrite - // the metadata stored in the database, e.g. after retrieving metadata - // from MusicBrainz! - // TODO: In the future this flag should become a time stamp - // to detect updates of files and then decide based on time - // stamps if file tags need to be re-imported. - if (!getMetadataSynchronized() && !metadataSynchronized.isNull()) { - setMetadataSynchronized(true); + if (updateSourceSynchronizedAt(sourceSynchronizedAt)) { modified = true; } return modified; @@ -278,7 +307,7 @@ bool operator==(const TrackRecord& lhs, const TrackRecord& rhs) { return lhs.getMetadata() == rhs.getMetadata() && lhs.getCoverInfo() == rhs.getCoverInfo() && lhs.getId() == rhs.getId() && - lhs.getMetadataSynchronized() == rhs.getMetadataSynchronized() && + lhs.getSourceSynchronizedAt() == rhs.getSourceSynchronizedAt() && lhs.getDateAdded() == rhs.getDateAdded() && lhs.getFileType() == rhs.getFileType() && lhs.getUrl() == rhs.getUrl() && @@ -287,7 +316,8 @@ bool operator==(const TrackRecord& lhs, const TrackRecord& rhs) { lhs.getCuePoint() == rhs.getCuePoint() && lhs.getBpmLocked() == rhs.getBpmLocked() && lhs.getKeys() == rhs.getKeys() && - lhs.getRating() == rhs.getRating(); + lhs.getRating() == rhs.getRating() && + lhs.m_headerParsed == rhs.m_headerParsed; } } // namespace mixxx diff --git a/src/track/trackrecord.h b/src/track/trackrecord.h index 354a41ef02b..9f4b6abdfa0 100644 --- a/src/track/trackrecord.h +++ b/src/track/trackrecord.h @@ -1,15 +1,13 @@ #pragma once +#include "library/coverart.h" #include "proto/keys.pb.h" - -#include "track/trackid.h" #include "track/cue.h" #include "track/keys.h" #include "track/keyutils.h" -#include "track/trackmetadata.h" #include "track/playcounter.h" - -#include "library/coverart.h" +#include "track/trackid.h" +#include "track/trackmetadata.h" #include "util/color/rgbcolor.h" @@ -39,19 +37,13 @@ class TrackRecord final { // has been inserted or is loaded from the library DB. MIXXX_DECL_PROPERTY(TrackId, id, Id) - // TODO(uklotz): Change data type from bool to QDateTime - // // Both import and export of metadata can be tracked by a single time // stamp, the direction doesn't matter. The value should be set to the // modification time stamp provided by the metadata source. This would // enable us to update the metadata of all tracks in the database after // the external metadata has been modified, i.e. if the corresponding // files have been modified. - // - // Requires a database update! We could reuse the 'header_parsed' column. - // During migration the boolean value will be substituted with either a - // default time stamp 1970-01-01 00:00:00.000 or NULL respectively. - MIXXX_DECL_PROPERTY(bool /*QDateTime*/, metadataSynchronized, MetadataSynchronized) + MIXXX_DECL_PROPERTY(QDateTime, sourceSynchronizedAt, SourceSynchronizedAt) MIXXX_DECL_PROPERTY(CoverInfoRelative, coverInfo, CoverInfo) @@ -117,9 +109,11 @@ class TrackRecord final { const QString& keyText, track::io::key::Source keySource); + bool isSourceSynchronized() const; bool replaceMetadataFromSource( TrackMetadata&& importedMetadata, - const QDateTime& metadataSynchronized); + const QDateTime& sourceSynchronizedAt); + // Merge the current metadata with new and additional properties // imported from the file. Since these properties are not (yet) // stored in the library or have been added later all existing @@ -148,7 +142,13 @@ class TrackRecord final { return m_streamInfoFromSource; } -private: + private: + // TODO: Remove this dependency + friend class ::Track; + + bool updateSourceSynchronizedAt( + const QDateTime& sourceSynchronizedAt); + Keys m_keys; // TODO: Use TrackMetadata as single source of truth and do not @@ -173,6 +173,8 @@ class TrackRecord final { // Stale metadata should be re-imported depending on the other flags. std::optional m_streamInfoFromSource; + bool m_headerParsed; // deprecated, replaced by sourceSynchronizedAt + /// Equality comparison /// /// Exception: The member m_streamInfoFromSource must not be considered