diff --git a/src/collection/collectionwatcher.cpp b/src/collection/collectionwatcher.cpp index fb0d9504ab..025f3eb12a 100644 --- a/src/collection/collectionwatcher.cpp +++ b/src/collection/collectionwatcher.cpp @@ -215,11 +215,13 @@ void CollectionWatcher::ReloadSettings() { const QStringList filters = s.value(CollectionSettings::kCoverArtPatterns, QStringList() << u"front"_s << u"cover"_s).toStringList(); if (source_ == Song::Source::Collection) { song_tracking_ = s.value(CollectionSettings::kSongTracking, false).toBool(); + write_fingerprint_to_file_tags_ = s.value(CollectionSettings::kWriteFingerprintToFileTags, false).toBool(); song_ebur128_loudness_analysis_ = s.value(CollectionSettings::kSongENUR128LoudnessAnalysis, false).toBool(); mark_songs_unavailable_ = song_tracking_ ? true : s.value(CollectionSettings::kMarkSongsUnavailable, true).toBool(); } else { song_tracking_ = false; + write_fingerprint_to_file_tags_ = false; song_ebur128_loudness_analysis_ = false; mark_songs_unavailable_ = false; } @@ -721,10 +723,16 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q QString fingerprint; #ifdef HAVE_SONGFINGERPRINTING if (song_tracking_) { - Chromaprinter chromaprinter(file); - fingerprint = chromaprinter.CreateFingerprint(); - if (fingerprint.isEmpty()) { - fingerprint = "NONE"_L1; + if (!changed && !matching_song.acoustid_fingerprint().isEmpty()) { + // File unchanged; cross-populate fingerprint from existing file-tag value to avoid recomputing Chromaprint. + fingerprint = matching_song.acoustid_fingerprint(); + } + else { + Chromaprinter chromaprinter(file); + fingerprint = chromaprinter.CreateFingerprint(); + if (fingerprint.isEmpty()) { + fingerprint = "NONE"_L1; + } } } #endif @@ -885,6 +893,9 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, new_cue_song.set_directory_id(t->dir_id()); PerformEBUR128Analysis(new_cue_song); new_cue_song.set_fingerprint(fingerprint); + if (!fingerprint.isEmpty() && fingerprint != "NONE"_L1) { + new_cue_song.set_acoustid_fingerprint(fingerprint); + } if (sections_map.contains(static_cast(new_cue_song.beginning_nanosec()))) { // Changed section const Song matching_cue_song = sections_map[static_cast(new_cue_song.beginning_nanosec())]; @@ -933,6 +944,22 @@ bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, song_on_disk.set_id(matching_song.id()); PerformEBUR128Analysis(song_on_disk); song_on_disk.set_fingerprint(fingerprint); +#ifdef HAVE_SONGFINGERPRINTING + if (!fingerprint.isEmpty() && fingerprint != "NONE"_L1) { + const bool fingerprint_was_missing_from_file = song_on_disk.acoustid_fingerprint().isEmpty(); + song_on_disk.set_acoustid_fingerprint(fingerprint); + if (write_fingerprint_to_file_tags_ && fingerprint_was_missing_from_file) { + const TagReaderResult write_result = tagreader_client_->WriteFileBlocking(file, song_on_disk, SaveTagsOption::Tags); + if (write_result.success()) { + // Refresh mtime so the next scan does not treat this file as changed due to our write. + song_on_disk.set_mtime(QFileInfo(file).lastModified().toSecsSinceEpoch()); + } + else { + qLog(Warning) << "Failed to write ACOUSTID_FINGERPRINT to" << file << ":" << write_result.error_string(); + } + } + } +#endif song_on_disk.set_art_automatic(art_automatic); song_on_disk.MergeUserSetData(matching_song, !overwrite_playcount_, !overwrite_rating_); AddChangedSong(file, matching_song, song_on_disk, t); @@ -986,6 +1013,22 @@ SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path song.set_source(source_); PerformEBUR128Analysis(song); song.set_fingerprint(fingerprint); +#ifdef HAVE_SONGFINGERPRINTING + if (!fingerprint.isEmpty() && fingerprint != "NONE"_L1) { + const bool fingerprint_was_missing_from_file = song.acoustid_fingerprint().isEmpty(); + song.set_acoustid_fingerprint(fingerprint); + if (write_fingerprint_to_file_tags_ && fingerprint_was_missing_from_file) { + const TagReaderResult write_result = tagreader_client_->WriteFileBlocking(file, song, SaveTagsOption::Tags); + if (write_result.success()) { + // Refresh mtime so the next scan does not treat this file as changed due to our write. + song.set_mtime(QFileInfo(file).lastModified().toSecsSinceEpoch()); + } + else { + qLog(Warning) << "Failed to write ACOUSTID_FINGERPRINT to" << file << ":" << write_result.error_string(); + } + } + } +#endif songs << song; } } diff --git a/src/collection/collectionwatcher.h b/src/collection/collectionwatcher.h index 5e7b0fd7fd..3e7f1c365f 100644 --- a/src/collection/collectionwatcher.h +++ b/src/collection/collectionwatcher.h @@ -237,6 +237,7 @@ class CollectionWatcher : public QObject { bool scan_on_startup_; bool monitor_; bool song_tracking_; + bool write_fingerprint_to_file_tags_ = false; bool song_ebur128_loudness_analysis_; bool mark_songs_unavailable_; int expire_unavailable_songs_days_; diff --git a/src/constants/collectionsettings.h b/src/constants/collectionsettings.h index 49ad2f6981..76f5ff2801 100644 --- a/src/constants/collectionsettings.h +++ b/src/constants/collectionsettings.h @@ -27,6 +27,7 @@ constexpr char kSettingsGroup[] = "Collection"; constexpr char kStartupScan[] = "startup_scan"; constexpr char kMonitor[] = "monitor"; constexpr char kSongTracking[] = "song_tracking"; +constexpr char kWriteFingerprintToFileTags[] = "write_fingerprint_to_file_tags"; constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable"; constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis"; constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs"; diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index b54e5f0028..126e65ea5d 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -3164,6 +3164,18 @@ void MainWindow::AutoCompleteTags() { if (songs.isEmpty()) return; + // Refresh fingerprint fields from the collection DB for any song where the + // in-memory playlist item was populated before the fingerprint was computed. + for (Song &song : songs) { + if (song.fingerprint().isEmpty() && song.acoustid_fingerprint().isEmpty() && song.url().isLocalFile()) { + const Song db_song = app_->collection_backend()->GetSongByUrl(song.url()); + if (db_song.is_valid()) { + if (!db_song.fingerprint().isEmpty()) song.set_fingerprint(db_song.fingerprint()); + if (!db_song.acoustid_fingerprint().isEmpty()) song.set_acoustid_fingerprint(db_song.acoustid_fingerprint()); + } + } + } + track_selection_dialog_->Init(songs); tag_fetcher_->StartFetch(songs); track_selection_dialog_->show(); diff --git a/src/dialogs/trackselectiondialog.cpp b/src/dialogs/trackselectiondialog.cpp index 13584d03b1..07f02f332c 100644 --- a/src/dialogs/trackselectiondialog.cpp +++ b/src/dialogs/trackselectiondialog.cpp @@ -164,6 +164,11 @@ void TrackSelectionDialog::FetchTagFinished(const Song &original_song, const Son data_[row].pending_ = false; data_[row].results_ = songs_guessed; + // Propagate any newly computed data (e.g. acoustid_fingerprint) so it is included in the file write on accept. + if (!original_song.acoustid_fingerprint().isEmpty() && data_[row].original_song_.acoustid_fingerprint().isEmpty()) { + data_[row].original_song_.set_acoustid_fingerprint(original_song.acoustid_fingerprint()); + } + // If it's the current item, update the display if (ui_->song_list->currentIndex().row() == row) { UpdateStack(); diff --git a/src/musicbrainz/tagfetcher.cpp b/src/musicbrainz/tagfetcher.cpp index 6f67ea9597..5401285300 100644 --- a/src/musicbrainz/tagfetcher.cpp +++ b/src/musicbrainz/tagfetcher.cpp @@ -50,6 +50,8 @@ TagFetcher::TagFetcher(SharedPtr network, QObject *parent) } QString TagFetcher::GetFingerprint(const Song &song) { + if (!song.fingerprint().isEmpty()) return song.fingerprint(); + if (!song.acoustid_fingerprint().isEmpty()) return song.acoustid_fingerprint(); return Chromaprinter(song.url().toLocalFile()).CreateFingerprint(); } @@ -60,15 +62,19 @@ void TagFetcher::StartFetch(const SongList &songs) { songs_ = songs; bool have_fingerprints = true; - if (std::any_of(songs.begin(), songs.end(), [](const Song &song) { return song.fingerprint().isEmpty(); })) { + if (std::any_of(songs.begin(), songs.end(), [](const Song &song) { return song.fingerprint().isEmpty() && song.acoustid_fingerprint().isEmpty(); })) { have_fingerprints = false; } if (have_fingerprints) { for (int i = 0; i < songs_.count(); ++i) { - const Song song = songs_.value(i); - Q_EMIT Progress(song, tr("Identifying song")); - acoustid_client_->Start(i, song.fingerprint(), static_cast(song.length_nanosec() / kNsecPerMsec)); + // Prefer internal fingerprint; fall back to file-tag fingerprint + const QString fp = songs_[i].fingerprint().isEmpty() ? songs_[i].acoustid_fingerprint() : songs_[i].fingerprint(); + if (songs_[i].acoustid_fingerprint().isEmpty()) { + songs_[i].set_acoustid_fingerprint(fp); + } + Q_EMIT Progress(songs_[i], tr("Identifying song")); + acoustid_client_->Start(i, fp, static_cast(songs_[i].length_nanosec() / kNsecPerMsec)); } } else { @@ -104,15 +110,18 @@ void TagFetcher::FingerprintFound(const int index) { if (!watcher || index >= songs_.count()) return; const QString fingerprint = watcher->resultAt(index); - const Song song = songs_.value(index); if (fingerprint.isEmpty()) { - Q_EMIT ResultAvailable(song, SongList()); + Q_EMIT ResultAvailable(songs_.value(index), SongList()); return; } - Q_EMIT Progress(song, tr("Identifying song")); - acoustid_client_->Start(index, fingerprint, static_cast(song.length_nanosec() / kNsecPerMsec)); + // Store the computed fingerprint so it is available when ResultAvailable is emitted. + // This allows the "Complete tags automatically" write path to include it in the file write. + songs_[index].set_acoustid_fingerprint(fingerprint); + + Q_EMIT Progress(songs_[index], tr("Identifying song")); + acoustid_client_->Start(index, fingerprint, static_cast(songs_[index].length_nanosec() / kNsecPerMsec)); } diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index 9be6daeaf3..9505f2ea1f 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -115,6 +115,7 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, #ifndef HAVE_SONGFINGERPRINTING ui_->song_tracking->hide(); + ui_->write_fingerprint_to_file_tags->hide(); #endif #ifndef HAVE_EBUR128 @@ -151,6 +152,8 @@ void CollectionSettingsPage::Load() { ui_->startup_scan->setChecked(s.value(kStartupScan, true).toBool()); ui_->monitor->setChecked(s.value(kMonitor, true).toBool()); ui_->song_tracking->setChecked(s.value(kSongTracking, false).toBool()); + ui_->write_fingerprint_to_file_tags->setChecked(s.value(kWriteFingerprintToFileTags, false).toBool()); + ui_->write_fingerprint_to_file_tags->setEnabled(ui_->song_tracking->isChecked()); ui_->mark_songs_unavailable->setChecked(ui_->song_tracking->isChecked() ? true : s.value(kMarkSongsUnavailable, true).toBool()); ui_->song_ebur128_loudness_analysis->setChecked(s.value(kSongENUR128LoudnessAnalysis, false).toBool()); ui_->expire_unavailable_songs_days->setValue(s.value(kExpireUnavailableSongs, 60).toInt()); @@ -199,6 +202,7 @@ void CollectionSettingsPage::Save() { s.setValue(kStartupScan, ui_->startup_scan->isChecked()); s.setValue(kMonitor, ui_->monitor->isChecked()); s.setValue(kSongTracking, ui_->song_tracking->isChecked()); + s.setValue(kWriteFingerprintToFileTags, ui_->write_fingerprint_to_file_tags->isChecked()); s.setValue(kMarkSongsUnavailable, ui_->song_tracking->isChecked() ? true : ui_->mark_songs_unavailable->isChecked()); s.setValue(kSongENUR128LoudnessAnalysis, ui_->song_ebur128_loudness_analysis->isChecked()); s.setValue(kExpireUnavailableSongs, ui_->expire_unavailable_songs_days->value()); @@ -283,6 +287,10 @@ void CollectionSettingsPage::CurrentRowChanged(const QModelIndex &idx) { void CollectionSettingsPage::SongTrackingToggled() { + ui_->write_fingerprint_to_file_tags->setEnabled(ui_->song_tracking->isChecked()); + if (!ui_->song_tracking->isChecked()) { + ui_->write_fingerprint_to_file_tags->setChecked(false); + } ui_->mark_songs_unavailable->setEnabled(!ui_->song_tracking->isChecked()); if (ui_->song_tracking->isChecked()) { ui_->mark_songs_unavailable->setChecked(true); diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui index 01c576263e..d1348c5f01 100644 --- a/src/settings/collectionsettingspage.ui +++ b/src/settings/collectionsettingspage.ui @@ -102,6 +102,16 @@ + + + + Write fingerprint to file tags (ACOUSTID_FINGERPRINT) + + + false + + + @@ -531,6 +541,7 @@ If there are no matches then it will use the largest image in the directory.startup_scan monitor song_tracking + write_fingerprint_to_file_tags mark_songs_unavailable song_ebur128_loudness_analysis expire_unavailable_songs_days diff --git a/src/tagreader/tagreadertaglib.cpp b/src/tagreader/tagreadertaglib.cpp index a0cc27f2cb..e67944336d 100644 --- a/src/tagreader/tagreadertaglib.cpp +++ b/src/tagreader/tagreadertaglib.cpp @@ -1199,6 +1199,9 @@ TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song & tag->setItem(kMP4_Lyrics, TagLib::StringList(QStringToTagLibString(song.lyrics()))); tag->setItem(kMP4_AlbumArtist, TagLib::StringList(QStringToTagLibString(song.albumartist()))); tag->setItem(kMP4_Compilation, TagLib::MP4::Item(song.compilation())); + if (!song.acoustid_fingerprint().isEmpty()) { + tag->setItem(kMP4_AcoustId_Fingerprint, TagLib::StringList(QStringToTagLibString(song.acoustid_fingerprint()))); + } } if (save_playcount) { SetPlaycount(tag, song.playcount()); @@ -1330,6 +1333,9 @@ void TagReaderTagLib::SetID3v2Tag(TagLib::ID3v2::Tag *tag, const Song &song) con SetTextFrame(kID3v2_TitleSort, song.titlesort().isEmpty() ? QString() : song.titlesort(), tag); SetTextFrame(kID3v2_Compilation, song.compilation() ? QString::number(1) : QString(), tag); SetUnsyncLyricsFrame(song.lyrics().isEmpty() ? QString() : song.lyrics(), tag); + if (!song.acoustid_fingerprint().isEmpty()) { + SetUserTextFrame(QLatin1String(kID3v2_AcoustId_Fingerprint), song.acoustid_fingerprint(), tag); + } } @@ -1433,6 +1439,9 @@ void TagReaderTagLib::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment vorbis_comment->addField(kVorbisComment_Lyrics, QStringToTagLibString(song.lyrics()), true); vorbis_comment->removeFields(kVorbisComment_UnsyncedLyrics); + if (!song.acoustid_fingerprint().isEmpty()) { + vorbis_comment->addField(kVorbisComment_AcoustId_Fingerprint, QStringToTagLibString(song.acoustid_fingerprint()), true); + } } @@ -1445,6 +1454,9 @@ void TagReaderTagLib::SetAPETag(TagLib::APE::Tag *tag, const Song &song) const { tag->setItem(kAPE_Performer, TagLib::APE::Item(kAPE_Performer, TagLib::StringList(QStringToTagLibString(song.performer())))); tag->setItem(kAPE_Lyrics, TagLib::APE::Item(kAPE_Lyrics, QStringToTagLibString(song.lyrics()))); tag->addValue(kAPE_Compilation, QStringToTagLibString(song.compilation() ? QString::number(1) : QString()), true); + if (!song.acoustid_fingerprint().isEmpty()) { + tag->setItem(kAPE_AcoustId_Fingerprint, TagLib::APE::Item(kAPE_AcoustId_Fingerprint, TagLib::StringList(QStringToTagLibString(song.acoustid_fingerprint())))); + } } @@ -1456,6 +1468,9 @@ void TagReaderTagLib::SetASFTag(TagLib::ASF::Tag *tag, const Song &song) const { SetAsfAttribute(tag, kASF_Disc, song.disc()); SetAsfAttribute(tag, kASF_OriginalDate, song.originalyear()); SetAsfAttribute(tag, kASF_OriginalYear, song.originalyear()); + if (!song.acoustid_fingerprint().isEmpty()) { + SetAsfAttribute(tag, kASF_AcoustId_Fingerprint, song.acoustid_fingerprint()); + } }