Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions src/collection/collectionwatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<quint64>(new_cue_song.beginning_nanosec()))) { // Changed section
const Song matching_cue_song = sections_map[static_cast<quint64>(new_cue_song.beginning_nanosec())];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/collection/collectionwatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down
1 change: 1 addition & 0 deletions src/constants/collectionsettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
12 changes: 12 additions & 0 deletions src/core/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/dialogs/trackselectiondialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
25 changes: 17 additions & 8 deletions src/musicbrainz/tagfetcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ TagFetcher::TagFetcher(SharedPtr<NetworkAccessManager> 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();
}

Expand All @@ -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<int>(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<int>(songs_[i].length_nanosec() / kNsecPerMsec));
}
}
else {
Expand Down Expand Up @@ -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<int>(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<int>(songs_[index].length_nanosec() / kNsecPerMsec));

}

Expand Down
8 changes: 8 additions & 0 deletions src/settings/collectionsettingspage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/settings/collectionsettingspage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="write_fingerprint_to_file_tags">
<property name="text">
<string>Write fingerprint to file tags (ACOUSTID_FINGERPRINT)</string>
</property>
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="mark_songs_unavailable">
<property name="text">
Expand Down Expand Up @@ -531,6 +541,7 @@ If there are no matches then it will use the largest image in the directory.</st
<tabstop>startup_scan</tabstop>
<tabstop>monitor</tabstop>
<tabstop>song_tracking</tabstop>
<tabstop>write_fingerprint_to_file_tags</tabstop>
<tabstop>mark_songs_unavailable</tabstop>
<tabstop>song_ebur128_loudness_analysis</tabstop>
<tabstop>expire_unavailable_songs_days</tabstop>
Expand Down
15 changes: 15 additions & 0 deletions src/tagreader/tagreadertaglib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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);
}

}

Expand Down Expand Up @@ -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);
}

}

Expand All @@ -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()))));
}

}

Expand All @@ -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());
}

}

Expand Down