Add BPM, mood and initial key support

This commit is contained in:
Jonas Kvinge
2025-08-10 01:34:44 +02:00
parent c4646531b0
commit da9e9840b8
8 changed files with 116 additions and 7 deletions

View File

@@ -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_);

View File

@@ -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;

View File

@@ -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<int>(Column::Mood)), idx.sibling(idx.row(), static_cast<int>(Column::Mood)));
Q_EMIT dataChanged(idx.sibling(idx.row(), static_cast<int>(Column::Moodbar)), idx.sibling(idx.row(), static_cast<int>(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;

View File

@@ -133,11 +133,14 @@ class Playlist : public QAbstractListModel {
Comment,
Grouping,
Source,
Mood,
Moodbar,
Rating,
HasCUE,
EBUR128IntegratedLoudness,
EBUR128LoudnessRange,
BPM,
Mood,
InitialKey,
ColumnCount
};
using Columns = QList<Column>;

View File

@@ -246,7 +246,7 @@ void PlaylistView::SetItemDelegates() {
setItemDelegateForColumn(static_cast<int>(Playlist::Column::Source), new SongSourceDelegate(this));
#ifdef HAVE_MOODBAR
setItemDelegateForColumn(static_cast<int>(Playlist::Column::Mood), new MoodbarItemDelegate(moodbar_loader_, this, this));
setItemDelegateForColumn(static_cast<int>(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<int>(Playlist::Column::EBUR128IntegratedLoudness), new Ebur128LoudnessLUFSItemDelegate(this));
setItemDelegateForColumn(static_cast<int>(Playlist::Column::EBUR128LoudnessRange), new Ebur128LoudnessRangeLUItemDelegate(this));
}
void PlaylistView::setModel(QAbstractItemModel *m) {
@@ -390,11 +391,14 @@ void PlaylistView::RestoreHeaderState() {
header_->HideSection(static_cast<int>(Playlist::Column::LastPlayed));
header_->HideSection(static_cast<int>(Playlist::Column::Comment));
header_->HideSection(static_cast<int>(Playlist::Column::Grouping));
header_->HideSection(static_cast<int>(Playlist::Column::Mood));
header_->HideSection(static_cast<int>(Playlist::Column::Moodbar));
header_->HideSection(static_cast<int>(Playlist::Column::Rating));
header_->HideSection(static_cast<int>(Playlist::Column::HasCUE));
header_->HideSection(static_cast<int>(Playlist::Column::EBUR128IntegratedLoudness));
header_->HideSection(static_cast<int>(Playlist::Column::EBUR128LoudnessRange));
header_->HideSection(static_cast<int>(Playlist::Column::BPM));
header_->HideSection(static_cast<int>(Playlist::Column::Mood));
header_->HideSection(static_cast<int>(Playlist::Column::InitialKey));
header_->ShowSection(static_cast<int>(Playlist::Column::Track));
header_->ShowSection(static_cast<int>(Playlist::Column::Title));
@@ -1390,6 +1394,7 @@ ColumnAlignmentMap PlaylistView::DefaultColumnAlignment() {
ret[static_cast<int>(Playlist::Column::Filesize)] =
ret[static_cast<int>(Playlist::Column::PlayCount)] =
ret[static_cast<int>(Playlist::Column::SkipCount)] =
ret[static_cast<int>(Playlist::Column::BPM)] =
(Qt::AlignRight | Qt::AlignVCenter);
return ret;

View File

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

View File

@@ -65,6 +65,9 @@ class SmartPlaylistSearchTerm {
ComposerSort,
PerformerSort,
TitleSort,
BPM,
Mood,
InitialKey,
FieldCount
};

View File

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