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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2764,6 +2764,7 @@ if(BUILD_TESTING)
src/test/portmidienumeratortest.cpp
src/test/queryutiltest.cpp
src/test/rangelist_test.cpp
src/test/ratingexportimport_test.cpp
src/test/readaheadmanager_test.cpp
src/test/replaygaintest.cpp
src/test/rescalertest.cpp
Expand Down
11 changes: 11 additions & 0 deletions src/library/library_prefs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ const ConfigKey mixxx::library::prefs::kSyncSeratoMetadataConfigKey =
mixxx::library::prefs::kConfigGroup,
QStringLiteral("SeratoMetadataExport")};

// Rating import/export using FMPS_Rating standard
const ConfigKey mixxx::library::prefs::kExportRatingToFileTagsConfigKey =
ConfigKey{
mixxx::library::prefs::kConfigGroup,
QStringLiteral("export_rating_to_file_tags")};

const ConfigKey mixxx::library::prefs::kImportRatingFromFileTagsConfigKey =
ConfigKey{
mixxx::library::prefs::kConfigGroup,
QStringLiteral("import_rating_from_file_tags")};

const ConfigKey mixxx::library::prefs::kUseRelativePathOnExportConfigKey =
ConfigKey{
mixxx::library::prefs::kConfigGroup,
Expand Down
4 changes: 4 additions & 0 deletions src/library/library_prefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ extern const ConfigKey kResetMissingTagMetadataOnImportConfigKey;

extern const ConfigKey kSyncSeratoMetadataConfigKey;

extern const ConfigKey kExportRatingToFileTagsConfigKey;

extern const ConfigKey kImportRatingFromFileTagsConfigKey;

extern const ConfigKey kUseRelativePathOnExportConfigKey;

extern const ConfigKey kCoverArtFetcherQualityConfigKey;
Expand Down
14 changes: 9 additions & 5 deletions src/library/trackcollectionmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,15 @@ ExportTrackMetadataResult TrackCollectionManager::exportTrackMetadataBeforeSavin
// last synchronized. Exporting metadata will update this time
// stamp on the track object!
if (pTrack->isMarkedForMetadataExport() ||
(pTrack->isDirty() &&
m_pConfig &&
m_pConfig->getValueString(
mixxx::library::prefs::kSyncTrackMetadataConfigKey)
.toInt() == 1)) {
(pTrack->isDirty() && m_pConfig &&
(m_pConfig->getValueString(
mixxx::library::prefs::
kSyncTrackMetadataConfigKey)
.toInt() == 1 ||
m_pConfig->getValueString(
mixxx::library::prefs::
kExportRatingToFileTagsConfigKey)
.toInt() == 1))) {
switch (mode) {
case TrackMetadataExportMode::Immediate: {
// Export track metadata now by saving as file tags.
Expand Down
1 change: 1 addition & 0 deletions src/library/trackset/baseplaylistfeature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ BasePlaylistFeature::BasePlaylistFeature(

initActions();
connectPlaylistDAO();

connect(m_pLibrary,
&Library::trackSelected,
this,
Expand Down
12 changes: 12 additions & 0 deletions src/preferences/dialog/dlgpreflibrary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ void DlgPrefLibrary::slotResetToDefaults() {
spinbox_history_min_tracks_to_keep->setValue(1);
checkBox_sync_track_metadata->setChecked(false);
checkBox_serato_metadata_export->setChecked(false);
checkBox_rating_export->setChecked(false);
checkBox_rating_import->setChecked(false);
checkBox_use_relative_path->setChecked(false);
checkBox_edit_metadata_selected_clicked->setChecked(kEditMetadataSelectedClickDefault);
radioButton_dbclick_deck->setChecked(true);
Expand Down Expand Up @@ -293,6 +295,10 @@ void DlgPrefLibrary::slotUpdate() {
checkBox_serato_metadata_export->setChecked(
m_pConfig->getValue(kSyncSeratoMetadataConfigKey, false));
setSeratoMetadataEnabled(checkBox_sync_track_metadata->isChecked());
checkBox_rating_export->setChecked(
m_pConfig->getValue(kExportRatingToFileTagsConfigKey, false));
checkBox_rating_import->setChecked(
m_pConfig->getValue(kImportRatingFromFileTagsConfigKey, false));
checkBox_use_relative_path->setChecked(m_pConfig->getValue(
kUseRelativePathOnExportConfigKey, false));

Expand Down Expand Up @@ -531,6 +537,12 @@ void DlgPrefLibrary::slotApply() {
m_pConfig->set(
kSyncSeratoMetadataConfigKey,
ConfigValue{checkBox_serato_metadata_export->isChecked()});
m_pConfig->set(
kExportRatingToFileTagsConfigKey,
ConfigValue{checkBox_rating_export->isChecked()});
m_pConfig->set(
kImportRatingFromFileTagsConfigKey,
ConfigValue{checkBox_rating_import->isChecked()});

m_pConfig->set(kUseRelativePathOnExportConfigKey,
ConfigValue((int)checkBox_use_relative_path->isChecked()));
Expand Down
24 changes: 24 additions & 0 deletions src/preferences/dialog/dlgpreflibrarydlg.ui
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,28 @@
</item>

<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_rating_export">
<property name="toolTip">
<string>Export star ratings to file tags using the FMPS_Rating standard when saving track metadata.&lt;br/&gt;This allows ratings to be shared with other media players that support FMPS (e.g., Strawberry, Clementine).</string>
</property>
<property name="text">
<string>Export star ratings to file tags (FMPS_Rating)</string>
</property>
</widget>
</item>

<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_rating_import">
<property name="toolTip">
<string>Import star ratings from file tags during library scan.&lt;br/&gt;Reads FMPS_Rating (and POPM for MP3 files) from file tags.&lt;br/&gt;Only imports if the track doesn't already have a rating in Mixxx.</string>
</property>
<property name="text">
<string>Import star ratings from file tags on library scan</string>
</property>
</widget>
</item>

<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_use_relative_path">
<property name="text">
<string>Use relative paths for playlist export if possible</string>
Expand Down Expand Up @@ -685,6 +707,8 @@
<tabstop>checkBox_library_scan_summary</tabstop>
<tabstop>checkBox_sync_track_metadata</tabstop>
<tabstop>checkBox_serato_metadata_export</tabstop>
<tabstop>checkBox_rating_export</tabstop>
<tabstop>checkBox_rating_import</tabstop>
<tabstop>checkBox_use_relative_path</tabstop>
<tabstop>checkBox_edit_metadata_selected_clicked</tabstop>
<tabstop>radioButton_dbclick_deck</tabstop>
Expand Down
15 changes: 15 additions & 0 deletions src/sources/metadatasource.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <QFile>
#include <QImage>
#include <memory>
#include <optional>
#include <utility>

#include "track/trackmetadata.h"
Expand Down Expand Up @@ -61,6 +62,20 @@ class MetadataSource {
const TrackMetadata& /*trackMetadata*/) const {
return std::make_pair(ExportResult::Unsupported, QDateTime());
}

/// Import rating from file tags using FMPS_Rating standard.
/// Returns the rating (0-5) if found, nullopt otherwise.
/// Default implementation returns nullopt (no rating support).
virtual std::optional<int> importRating() const {
return std::nullopt;
}

/// Export rating to file tags using FMPS_Rating standard.
/// Returns true if export succeeded, false otherwise.
/// Default implementation returns false (no rating support).
virtual bool exportRating(int /*rating*/) const {
return false;
}
};

typedef std::shared_ptr<MetadataSource> MetadataSourcePointer;
Expand Down
201 changes: 201 additions & 0 deletions src/sources/metadatasourcetaglib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@

#include <QFile>
#include <memory>
#include <optional>

#include "track/taglib/trackmetadata.h"
#include "track/taglib/trackmetadata_ape.h"
#include "track/taglib/trackmetadata_common.h"
#include "track/taglib/trackmetadata_id3v2.h"
#include "track/taglib/trackmetadata_mp4.h"
#include "track/taglib/trackmetadata_xiph.h"
#include "util/logger.h"
#include "util/safelywritablefile.h"

Expand Down Expand Up @@ -697,4 +702,200 @@ MetadataSourceTagLib::exportTrackMetadata(
return afterExport(ExportResult::Failed);
}

std::optional<int> MetadataSourceTagLib::importRating() const {
switch (m_fileType) {
case taglib::FileType::MPEG: {
TagLib::MPEG::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen()) {
return std::nullopt;
}
// Try ID3v2 first, then APE
if (file.hasID3v2Tag()) {
auto rating = taglib::id3v2::importRatingFromTag(*file.ID3v2Tag());
if (rating.has_value()) {
return rating;
}
}
if (taglib::hasAPETag(file) && file.APETag()) {
return taglib::ape::importRatingFromTag(*file.APETag());
}
return std::nullopt;
}
case taglib::FileType::MP4: {
TagLib::MP4::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen() || !file.tag()) {
return std::nullopt;
}
return taglib::mp4::importRatingFromTag(*file.tag());
}
case taglib::FileType::FLAC: {
TagLib::FLAC::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen()) {
return std::nullopt;
}
// Try ID3v2 first, then Xiph comment
if (file.hasID3v2Tag()) {
auto rating = taglib::id3v2::importRatingFromTag(*file.ID3v2Tag());
if (rating.has_value()) {
return rating;
}
}
if (taglib::hasXiphComment(file) && file.xiphComment()) {
return taglib::xiph::importRatingFromTag(*file.xiphComment());
}
return std::nullopt;
}
case taglib::FileType::OggVorbis: {
TagLib::Ogg::Vorbis::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen() || !file.tag()) {
return std::nullopt;
}
return taglib::xiph::importRatingFromTag(*file.tag());
}
case taglib::FileType::Opus: {
TagLib::Ogg::Opus::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen() || !file.tag()) {
return std::nullopt;
}
return taglib::xiph::importRatingFromTag(*file.tag());
}
case taglib::FileType::WavPack: {
TagLib::WavPack::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen() || !file.APETag()) {
return std::nullopt;
}
return taglib::ape::importRatingFromTag(*file.APETag());
}
case taglib::FileType::WAV: {
TagLib::RIFF::WAV::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen()) {
return std::nullopt;
}
if (file.hasID3v2Tag()) {
return taglib::id3v2::importRatingFromTag(*file.ID3v2Tag());
}
return std::nullopt;
}
case taglib::FileType::AIFF: {
TagLib::RIFF::AIFF::File file(TAGLIB_FILENAME_FROM_QSTRING(m_fileName));
if (!file.isOpen() || !file.tag()) {
return std::nullopt;
}
return taglib::id3v2::importRatingFromTag(*file.tag());
}
default:
return std::nullopt;
}
}

bool MetadataSourceTagLib::exportRating(int rating) const {
SafelyWritableFile safelyWritableFile(m_fileName,
SafelyWritableFile::SafetyMode::Edit);
if (!safelyWritableFile.isReady()) {
kLogger.warning()
<< "Unable to export rating into file"
<< m_fileName
<< "- Please check file permissions and storage space";
return false;
}

bool success = false;
switch (m_fileType) {
case taglib::FileType::MPEG: {
TagLib::MPEG::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen()) {
// Export to ID3v2 (preferred)
TagLib::ID3v2::Tag* pTag = file.ID3v2Tag(true);
if (pTag && taglib::id3v2::exportRatingIntoTag(pTag, rating)) {
success = file.save(TagLib::MPEG::File::ID3v2);
}
}
break;
}
case taglib::FileType::MP4: {
TagLib::MP4::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen() && file.tag()) {
if (taglib::mp4::exportRatingIntoTag(file.tag(), rating)) {
success = file.save();
}
}
break;
}
case taglib::FileType::FLAC: {
TagLib::FLAC::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen()) {
TagLib::Ogg::XiphComment* pTag = file.xiphComment(true);
if (pTag && taglib::xiph::exportRatingIntoTag(pTag, rating)) {
success = file.save();
}
}
break;
}
case taglib::FileType::OggVorbis: {
TagLib::Ogg::Vorbis::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen() && file.tag()) {
if (taglib::xiph::exportRatingIntoTag(file.tag(), rating)) {
success = file.save();
}
}
break;
}
case taglib::FileType::Opus: {
TagLib::Ogg::Opus::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen() && file.tag()) {
if (taglib::xiph::exportRatingIntoTag(file.tag(), rating)) {
success = file.save();
}
}
break;
}
case taglib::FileType::WavPack: {
TagLib::WavPack::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen()) {
TagLib::APE::Tag* pTag = file.APETag(true);
if (pTag && taglib::ape::exportRatingIntoTag(pTag, rating)) {
success = file.save();
}
}
break;
}
case taglib::FileType::WAV: {
TagLib::RIFF::WAV::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen()) {
TagLib::ID3v2::Tag* pTag = file.ID3v2Tag();
if (pTag && taglib::id3v2::exportRatingIntoTag(pTag, rating)) {
success = file.save();
}
}
break;
}
case taglib::FileType::AIFF: {
TagLib::RIFF::AIFF::File file(TAGLIB_FILENAME_FROM_QSTRING(safelyWritableFile.fileName()));
if (file.isOpen() && file.tag()) {
if (taglib::id3v2::exportRatingIntoTag(file.tag(), rating)) {
success = file.save();
}
}
break;
}
default:
kLogger.warning()
<< "Rating export not supported for file type"
<< static_cast<int>(m_fileType);
return false;
}

if (success) {
if (!safelyWritableFile.commit()) {
kLogger.warning() << "Failed to commit rating changes to file" << m_fileName;
return false;
}
kLogger.debug() << "Exported rating" << rating << "to file" << m_fileName;
return true;
}

kLogger.warning() << "Failed to export rating to file" << m_fileName;
return false;
}

} // namespace mixxx
Loading