From da9e9840b8db99422f375fca7008b3ccbc60ef41 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 10 Aug 2025 01:34:44 +0200 Subject: [PATCH] Add BPM, mood and initial key support --- src/core/song.cpp | 25 ++++++++++++++++ src/core/song.h | 8 +++++ src/playlist/playlist.cpp | 29 +++++++++++++++--- src/playlist/playlist.h | 5 +++- src/playlist/playlistview.cpp | 9 ++++-- .../smartplaylistsearchterm.cpp | 14 +++++++++ src/smartplaylists/smartplaylistsearchterm.h | 3 ++ src/tagreader/tagreadertaglib.cpp | 30 +++++++++++++++++++ 8 files changed, 116 insertions(+), 7 deletions(-) diff --git a/src/core/song.cpp b/src/core/song.cpp index 59b509e2c..bb6b30da0 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -132,6 +132,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s << u"cue_path"_s << u"rating"_s + << u"bpm"_s + << u"mood"_s + << u"initial_key"_s << u"acoustid_id"_s << u"acoustid_fingerprint"_s @@ -328,6 +331,9 @@ struct Song::Private : public QSharedData { QString cue_path_; // If the song has a CUE, this contains it's path. float rating_; // Database rating, initial rating read from tag. + float bpm_; + QString mood_; + QString initial_key_; QString acoustid_id_; QString acoustid_fingerprint_; @@ -391,6 +397,7 @@ Song::Private::Private(const Source source) art_unset_(false), rating_(-1), + bpm_(-1), init_from_file_(false), suspicious_tags_(false) @@ -481,6 +488,9 @@ bool Song::art_unset() const { return d->art_unset_; } const QString &Song::cue_path() const { return d->cue_path_; } float Song::rating() const { return d->rating_; } +float Song::bpm() const { return d->bpm_; } +const QString &Song::mood() const { return d->mood_; } +const QString &Song::initial_key() const { return d->initial_key_; } const QString &Song::acoustid_id() const { return d->acoustid_id_; } const QString &Song::acoustid_fingerprint() const { return d->acoustid_fingerprint_; } @@ -592,6 +602,9 @@ void Song::set_art_unset(const bool v) { d->art_unset_ = v; } void Song::set_cue_path(const QString &v) { d->cue_path_ = v; } void Song::set_rating(const float v) { d->rating_ = v; } +void Song::set_bpm(const float v) { d->bpm_ = v; } +void Song::set_mood(const QString &v) { d->mood_ = v; } +void Song::set_initial_key(const QString &v) { d->initial_key_ = v; } void Song::set_acoustid_id(const QString &v) { d->acoustid_id_ = v; } void Song::set_acoustid_fingerprint(const QString &v) { d->acoustid_fingerprint_ = v; } @@ -645,6 +658,8 @@ void Song::set_musicbrainz_track_id(const TagLib::String &v) { d->musicbrainz_tr void Song::set_musicbrainz_disc_id(const TagLib::String &v) { d->musicbrainz_disc_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); } void Song::set_musicbrainz_release_group_id(const TagLib::String &v) { d->musicbrainz_release_group_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); } void Song::set_musicbrainz_work_id(const TagLib::String &v) { d->musicbrainz_work_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); } +void Song::set_mood(const TagLib::String &v) { d->mood_ = TagLibStringToQString(v); } +void Song::set_initial_key(const TagLib::String &v) { d->initial_key_ = TagLibStringToQString(v); } const QUrl &Song::effective_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; } const QString &Song::effective_titlesort() const { return d->titlesort_.isEmpty() ? d->title_ : d->titlesort_; } @@ -977,6 +992,9 @@ bool Song::IsMetadataEqual(const Song &other) const { d->bitrate_ == other.d->bitrate_ && d->samplerate_ == other.d->samplerate_ && d->bitdepth_ == other.d->bitdepth_ && + d->bpm_ == other.d->bpm_ && + d->mood_ == other.d->mood_ && + d->initial_key_ == other.d->initial_key_ && d->cue_path_ == other.d->cue_path_; } @@ -1570,7 +1588,11 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons d->art_unset_ = SqlHelper::ValueToBool(r, ColumnIndex(u"art_unset"_s) + col); d->cue_path_ = SqlHelper::ValueToString(r, ColumnIndex(u"cue_path"_s) + col); + d->rating_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"rating"_s) + col); + d->bpm_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"bpm"_s) + col); + d->mood_ = SqlHelper::ValueToString(r, ColumnIndex(u"mood"_s) + col); + d->initial_key_ = SqlHelper::ValueToString(r, ColumnIndex(u"initial_key"_s) + col); d->acoustid_id_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_id"_s) + col); d->acoustid_fingerprint_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_fingerprint"_s) + col); @@ -1900,6 +1922,9 @@ void Song::BindToQuery(SqlQuery *query) const { query->BindValue(u":cue_path"_s, d->cue_path_); query->BindFloatValue(u":rating"_s, d->rating_); + query->BindFloatValue(u":bpm"_s, d->bpm_); + query->BindStringValue(u":mood"_s, d->mood_); + query->BindStringValue(u":initial_key"_s, d->initial_key_); query->BindStringValue(u":acoustid_id"_s, d->acoustid_id_); query->BindStringValue(u":acoustid_fingerprint"_s, d->acoustid_fingerprint_); diff --git a/src/core/song.h b/src/core/song.h index 0e225a0c9..918d5c0cc 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -213,6 +213,9 @@ class Song { const QString &cue_path() const; float rating() const; + float bpm() const; + const QString &mood() const; + const QString &initial_key() const; const QString &acoustid_id() const; const QString &acoustid_fingerprint() const; @@ -325,6 +328,9 @@ class Song { void set_cue_path(const QString &v); void set_rating(const float v); + void set_bpm(const float v); + void set_mood(const QString &v); + void set_initial_key(const QString &v); void set_acoustid_id(const QString &v); void set_acoustid_fingerprint(const QString &v); @@ -378,6 +384,8 @@ class Song { void set_musicbrainz_disc_id(const TagLib::String &v); void set_musicbrainz_release_group_id(const TagLib::String &v); void set_musicbrainz_work_id(const TagLib::String &v); + void set_mood(const TagLib::String &v); + void set_initial_key(const TagLib::String &v); const QUrl &effective_url() const; const QString &effective_titlesort() const; diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index da0d513a9..58442c928 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -391,7 +391,11 @@ QVariant Playlist::data(const QModelIndex &idx, const int role) const { case Column::HasCUE: return song.has_cue(); - case Column::Mood: + case Column::BPM: return song.bpm(); + case Column::Mood: return song.mood(); + case Column::InitialKey: return song.initial_key(); + + case Column::Moodbar: case Column::ColumnCount: break; @@ -445,7 +449,7 @@ QVariant Playlist::data(const QModelIndex &idx, const int role) const { #ifdef HAVE_MOODBAR void Playlist::MoodbarUpdated(const QModelIndex &idx) { - Q_EMIT dataChanged(idx.sibling(idx.row(), static_cast(Column::Mood)), idx.sibling(idx.row(), static_cast(Column::Mood))); + Q_EMIT dataChanged(idx.sibling(idx.row(), static_cast(Column::Moodbar)), idx.sibling(idx.row(), static_cast(Column::Moodbar))); } #endif @@ -1412,7 +1416,11 @@ bool Playlist::CompareItems(const Column column, const Qt::SortOrder order, Play case Column::EBUR128IntegratedLoudness: return CompareVal(ma.ebur128_integrated_loudness_lufs(), mb.ebur128_integrated_loudness_lufs()); case Column::EBUR128LoudnessRange: return CompareVal(ma.ebur128_loudness_range_lu(), mb.ebur128_loudness_range_lu()); - case Column::Mood: + case Column::BPM: return CompareVal(ma.bpm(), mb.bpm()); + case Column::Mood: return CompareStr(ma.mood(), mb.mood()); + case Column::InitialKey: return CompareStr(ma.initial_key(), mb.initial_key()); + + case Column::Moodbar: case Column::ColumnCount: break; } @@ -1460,13 +1468,17 @@ QString Playlist::column_name(const Column column) { case Column::Comment: return tr("Comment"); case Column::Source: return tr("Source"); - case Column::Mood: return tr("Mood"); + case Column::Moodbar: return tr("Moodbar"); case Column::Rating: return tr("Rating"); case Column::HasCUE: return tr("CUE"); case Column::EBUR128IntegratedLoudness: return tr("Integrated Loudness"); case Column::EBUR128LoudnessRange: return tr("Loudness Range"); + case Column::BPM: return tr("BPM"); + case Column::Mood: return tr("Mood"); + case Column::InitialKey: return tr("Initial key"); + case Column::ColumnCount: break; } @@ -2266,6 +2278,15 @@ Playlist::Columns Playlist::ChangedColumns(const Song &metadata1, const Song &me if (metadata1.ebur128_loudness_range_lu() != metadata2.ebur128_loudness_range_lu()) { columns << Column::EBUR128LoudnessRange; } + if (metadata1.bpm() != metadata2.bpm()) { + columns << Column::BPM; + } + if (metadata1.mood() != metadata2.mood()) { + columns << Column::Mood; + } + if (metadata1.initial_key() != metadata2.initial_key()) { + columns << Column::InitialKey; + } return columns; diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 91619a77d..77dd50e98 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -133,11 +133,14 @@ class Playlist : public QAbstractListModel { Comment, Grouping, Source, - Mood, + Moodbar, Rating, HasCUE, EBUR128IntegratedLoudness, EBUR128LoudnessRange, + BPM, + Mood, + InitialKey, ColumnCount }; using Columns = QList; diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index 9557d57f3..da1d0dfaa 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -246,7 +246,7 @@ void PlaylistView::SetItemDelegates() { setItemDelegateForColumn(static_cast(Playlist::Column::Source), new SongSourceDelegate(this)); #ifdef HAVE_MOODBAR - setItemDelegateForColumn(static_cast(Playlist::Column::Mood), new MoodbarItemDelegate(moodbar_loader_, this, this)); + setItemDelegateForColumn(static_cast(Playlist::Column::Moodbar), new MoodbarItemDelegate(moodbar_loader_, this, this)); #endif rating_delegate_ = new RatingItemDelegate(this); @@ -254,6 +254,7 @@ void PlaylistView::SetItemDelegates() { setItemDelegateForColumn(static_cast(Playlist::Column::EBUR128IntegratedLoudness), new Ebur128LoudnessLUFSItemDelegate(this)); setItemDelegateForColumn(static_cast(Playlist::Column::EBUR128LoudnessRange), new Ebur128LoudnessRangeLUItemDelegate(this)); + } void PlaylistView::setModel(QAbstractItemModel *m) { @@ -390,11 +391,14 @@ void PlaylistView::RestoreHeaderState() { header_->HideSection(static_cast(Playlist::Column::LastPlayed)); header_->HideSection(static_cast(Playlist::Column::Comment)); header_->HideSection(static_cast(Playlist::Column::Grouping)); - header_->HideSection(static_cast(Playlist::Column::Mood)); + header_->HideSection(static_cast(Playlist::Column::Moodbar)); header_->HideSection(static_cast(Playlist::Column::Rating)); header_->HideSection(static_cast(Playlist::Column::HasCUE)); header_->HideSection(static_cast(Playlist::Column::EBUR128IntegratedLoudness)); header_->HideSection(static_cast(Playlist::Column::EBUR128LoudnessRange)); + header_->HideSection(static_cast(Playlist::Column::BPM)); + header_->HideSection(static_cast(Playlist::Column::Mood)); + header_->HideSection(static_cast(Playlist::Column::InitialKey)); header_->ShowSection(static_cast(Playlist::Column::Track)); header_->ShowSection(static_cast(Playlist::Column::Title)); @@ -1390,6 +1394,7 @@ ColumnAlignmentMap PlaylistView::DefaultColumnAlignment() { ret[static_cast(Playlist::Column::Filesize)] = ret[static_cast(Playlist::Column::PlayCount)] = ret[static_cast(Playlist::Column::SkipCount)] = + ret[static_cast(Playlist::Column::BPM)] = (Qt::AlignRight | Qt::AlignVCenter); return ret; diff --git a/src/smartplaylists/smartplaylistsearchterm.cpp b/src/smartplaylists/smartplaylistsearchterm.cpp index 17aa29361..822930e4a 100644 --- a/src/smartplaylists/smartplaylistsearchterm.cpp +++ b/src/smartplaylists/smartplaylistsearchterm.cpp @@ -214,6 +214,7 @@ SmartPlaylistSearchTerm::Type SmartPlaylistSearchTerm::TypeOf(const Field field) case Field::Samplerate: case Field::Bitdepth: case Field::Bitrate: + case Field::BPM: return Type::Number; case Field::LastPlayed: @@ -365,9 +366,16 @@ QString SmartPlaylistSearchTerm::FieldColumnName(const Field field) { return u"performersort"_s; case Field::TitleSort: return u"titlesort"_s; + case Field::BPM: + return u"bpm"_s; + case Field::Mood: + return u"mood"_s; + case Field::InitialKey: + return u"initial_key"_s; case Field::FieldCount: Q_ASSERT(0); } + return QString(); } @@ -439,6 +447,12 @@ QString SmartPlaylistSearchTerm::FieldName(const Field field) { return Playlist::column_name(Playlist::Column::PerformerSort); case Field::TitleSort: return Playlist::column_name(Playlist::Column::TitleSort); + case Field::BPM: + return Playlist::column_name(Playlist::Column::BPM); + case Field::Mood: + return Playlist::column_name(Playlist::Column::Mood); + case Field::InitialKey: + return Playlist::column_name(Playlist::Column::InitialKey); case Field::FieldCount: Q_ASSERT(0); } diff --git a/src/smartplaylists/smartplaylistsearchterm.h b/src/smartplaylists/smartplaylistsearchterm.h index 09cee2367..0669ffdaa 100644 --- a/src/smartplaylists/smartplaylistsearchterm.h +++ b/src/smartplaylists/smartplaylistsearchterm.h @@ -65,6 +65,9 @@ class SmartPlaylistSearchTerm { ComposerSort, PerformerSort, TitleSort, + BPM, + Mood, + InitialKey, FieldCount }; diff --git a/src/tagreader/tagreadertaglib.cpp b/src/tagreader/tagreadertaglib.cpp index 36a87f959..c0124f528 100644 --- a/src/tagreader/tagreadertaglib.cpp +++ b/src/tagreader/tagreadertaglib.cpp @@ -133,6 +133,9 @@ constexpr char kID3v2_FMPS_Rating[] = "FMPS_Rating"; constexpr char kID3v2_Unique_File_Identifier[] = "UFID"; constexpr char kID3v2_UserDefinedTextInformationFrame[] = "TXXX"; constexpr char kID3v2_Popularimeter[] = "POPM"; +constexpr char kID3v2_BPM[] = "TBPM"; +constexpr char kID3v2_Mood[] = "TMOO"; +constexpr char kID3v2_Initial_Key[] = "TKEY"; constexpr char kID3v2_AcoustId[] = "Acoustid Id"; constexpr char kID3v2_AcoustId_Fingerprint[] = "Acoustid Fingerprint"; constexpr char kID3v2_MusicBrainz_AlbumArtistId[] = "MusicBrainz Album Artist Id"; @@ -168,6 +171,9 @@ constexpr char kVorbisComment_FMPS_Playcount[] = "FMPS_PLAYCOUNT"; constexpr char kVorbisComment_FMPS_Rating[] = "FMPS_RATING"; constexpr char kVorbisComment_Lyrics[] = "LYRICS"; constexpr char kVorbisComment_UnsyncedLyrics[] = "UNSYNCEDLYRICS"; +constexpr char kVorbisComment_BPM[] = "BPM"; +constexpr char kVorbisComment_Mood[] = "MOOD"; +constexpr char kVorbisComment_Initial_Key[] = "INITIALKEY"; constexpr char kVorbisComment_AcoustId[] = "ACOUSTID_ID"; constexpr char kVorbisComment_AcoustId_Fingerprint[] = "ACOUSTID_FINGERPRINT"; constexpr char kVorbisComment_MusicBrainz_AlbumArtistId[] = "MUSICBRAINZ_ALBUMARTISTID"; @@ -191,6 +197,7 @@ constexpr char kMP4_CoverArt[] = "covr"; constexpr char kMP4_OriginalYear[] = "----:com.apple.iTunes:ORIGINAL YEAR"; constexpr char kMP4_FMPS_Playcount[] = "----:com.apple.iTunes:FMPS_Playcount"; constexpr char kMP4_FMPS_Rating[] = "----:com.apple.iTunes:FMPS_Rating"; +constexpr char kMP4_BPM[] = "tmpo"; constexpr char kMP4_AcoustId[] = "----:com.apple.iTunes:Acoustid Id"; constexpr char kMP4_AcoustId_Fingerprint[] = "----:com.apple.iTunes:Acoustid Fingerprint"; constexpr char kMP4_MusicBrainz_AlbumArtistId[] = "----:com.apple.iTunes:MusicBrainz Album Artist Id"; @@ -214,6 +221,7 @@ constexpr char kAPE_CoverArt[] = "COVER ART (FRONT)"; constexpr char kAPE_FMPS_Playcount[] = "FMPS_PLAYCOUNT"; constexpr char kAPE_FMPS_Rating[] = "FMPS_RATING"; constexpr char kAPE_Lyrics[] = "LYRICS"; +constexpr char kAPE_BPM[] = "BPM"; constexpr char kAPE_AcoustId[] = "ACOUSTID_ID"; constexpr char kAPE_AcoustId_Fingerprint[] = "ACOUSTID_FINGERPRINT"; constexpr char kAPE_MusicBrainz_AlbumArtistId[] = "MUSICBRAINZ_ALBUMARTISTID"; @@ -633,6 +641,10 @@ void TagReaderTagLib::ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QSt if (map.contains(kID3v2_CoverArt) && song->url().isLocalFile()) song->set_art_embedded(true); + if (map.contains(kID3v2_BPM)) song->set_bpm(TagLibStringToQString(map[kID3v2_BPM].front()->toString()).trimmed().toFloat()); + if (map.contains(kID3v2_Mood)) song->set_mood(map[kID3v2_Mood].front()->toString()); + if (map.contains(kID3v2_Initial_Key)) song->set_initial_key(map[kID3v2_Initial_Key].front()->toString()); + if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, kID3v2_FMPS_Playcount)) { TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList(); if (frame_field_list.size() > 1) { @@ -753,6 +765,10 @@ void TagReaderTagLib::ParseVorbisComments(const TagLib::Ogg::FieldListMap &map, if (map.contains(kVorbisComment_Lyrics)) song->set_lyrics(map[kVorbisComment_Lyrics].front()); else if (map.contains(kVorbisComment_UnsyncedLyrics)) song->set_lyrics(map[kVorbisComment_UnsyncedLyrics].front()); + if (map.contains(kVorbisComment_BPM)) song->set_bpm(TagLibStringToQString(map[kVorbisComment_BPM].front()).toFloat()); + if (map.contains(kVorbisComment_Mood)) song->set_mood(map[kVorbisComment_Mood].front()); + if (map.contains(kVorbisComment_Initial_Key)) song->set_initial_key(map[kVorbisComment_Initial_Key].front()); + if (map.contains(kVorbisComment_AcoustId)) song->set_acoustid_id(map[kVorbisComment_AcoustId].front()); if (map.contains(kVorbisComment_AcoustId_Fingerprint)) song->set_acoustid_fingerprint(map[kVorbisComment_AcoustId_Fingerprint].front()); @@ -818,6 +834,10 @@ void TagReaderTagLib::ParseAPETags(const TagLib::APE::ItemListMap &map, QString } } + if (map.contains(kAPE_BPM)) { + song->set_bpm(TagLibStringToQString(map[kAPE_BPM].toString()).toFloat()); + } + if (map.contains(kAPE_AcoustId)) song->set_acoustid_id(map[kAPE_AcoustId].toString()); if (map.contains(kAPE_AcoustId_Fingerprint)) song->set_acoustid_fingerprint(map[kAPE_AcoustId_Fingerprint].toString()); @@ -895,6 +915,16 @@ void TagReaderTagLib::ParseMP4Tags(TagLib::MP4::Tag *tag, QString *disc, QString song->set_comment(tag->comment()); + if (tag->item(kMP4_BPM).isValid()) { + const TagLib::MP4::Item item = tag->item(kMP4_BPM); + if (item.isValid()) { + const float bpm = TagLibStringToQString(item.toStringList().toString('\n')).toFloat(); + if (bpm > 0) { + song->set_bpm(bpm); + } + } + } + if (tag->contains(kMP4_AcoustId)) { song->set_acoustid_id(tag->item(kMP4_AcoustId).toStringList().toString()); }