1
0
mirror of https://github.com/strawberrymusicplayer/strawberry synced 2025-02-03 10:57:33 +01:00

Add support for saving playcounts and ratings to tags

This commit is contained in:
Jonas Kvinge 2021-10-24 16:08:17 +02:00
parent ce7926cfa4
commit 3ab86543ad
22 changed files with 1230 additions and 286 deletions

View File

@ -44,6 +44,9 @@ class TagReaderBase {
virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0; virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0;
virtual bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) = 0; virtual bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) = 0;
virtual bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
virtual bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
protected: protected:
static const std::string kEmbeddedCover; static const std::string kEmbeddedCover;

View File

@ -117,6 +117,24 @@ message SaveEmbeddedArtResponse {
optional bool success = 1; optional bool success = 1;
} }
message SaveSongPlaycountToFileRequest {
optional string filename = 1;
optional SongMetadata metadata = 2;
}
message SaveSongPlaycountToFileResponse {
optional bool success = 1;
}
message SaveSongRatingToFileRequest {
optional string filename = 1;
optional SongMetadata metadata = 2;
}
message SaveSongRatingToFileResponse {
optional bool success = 1;
}
message Message { message Message {
optional int32 id = 1; optional int32 id = 1;
@ -135,4 +153,10 @@ message Message {
optional SaveEmbeddedArtRequest save_embedded_art_request = 10; optional SaveEmbeddedArtRequest save_embedded_art_request = 10;
optional SaveEmbeddedArtResponse save_embedded_art_response = 11; optional SaveEmbeddedArtResponse save_embedded_art_response = 11;
optional SaveSongPlaycountToFileRequest save_song_playcount_to_file_request = 12;
optional SaveSongPlaycountToFileResponse save_song_playcount_to_file_response = 13;
optional SaveSongRatingToFileRequest save_song_rating_to_file_request = 14;
optional SaveSongRatingToFileResponse save_song_rating_to_file_response = 15;
} }

View File

@ -233,9 +233,9 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way; // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way;
// apart, so we keep specific behavior for some formats by adding another "else if" block below. // apart, so we keep specific behavior for some formats by adding another "else if" block below.
if (TagLib::Ogg::XiphComment *tag_ogg = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) { if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
ParseOggTag(tag_ogg->fieldListMap(), &disc, &compilation, song); ParseOggTag(xiph_comment->fieldListMap(), &disc, &compilation, song);
if (!tag_ogg->pictureList().isEmpty()) { if (!xiph_comment->pictureList().isEmpty()) {
song->set_art_automatic(kEmbeddedCover); song->set_art_automatic(kEmbeddedCover);
} }
} }
@ -319,7 +319,7 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
} }
if (!map["POPM"].isEmpty()) { if (!map["POPM"].isEmpty()) {
const TagLib::ID3v2::PopularimeterFrame* frame = dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front()); const TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
if (frame) { if (frame) {
if (song->playcount() <= 0 && frame->counter() > 0) { if (song->playcount() <= 0 && frame->counter() > 0) {
song->set_playcount(frame->counter()); song->set_playcount(frame->counter());
@ -520,7 +520,7 @@ void TagReaderTagLib::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString
if (!map["COVERART"].isEmpty()) song->set_art_automatic(kEmbeddedCover); if (!map["COVERART"].isEmpty()) song->set_art_automatic(kEmbeddedCover);
if (!map["METADATA_BLOCK_PICTURE"].isEmpty()) song->set_art_automatic(kEmbeddedCover); if (!map["METADATA_BLOCK_PICTURE"].isEmpty()) song->set_art_automatic(kEmbeddedCover);
if (!map["FMPS_PLAYCOUNT"].isEmpty() && song->playcount() <= 0) song->set_playcount(TStringToQString( map["FMPS_PLAYCOUNT"].front() ).trimmed().toFloat()); if (!map["FMPS_PLAYCOUNT"].isEmpty() && song->playcount() <= 0) song->set_playcount(TStringToQString(map["FMPS_PLAYCOUNT"].front()).trimmed().toFloat());
if (!map["FMPS_RATING"].isEmpty() && song->rating() <= 0) song->set_rating(TStringToQString(map["FMPS_RATING"].front()).trimmed().toFloat()); if (!map["FMPS_RATING"].isEmpty() && song->rating() <= 0) song->set_rating(TStringToQString(map["FMPS_RATING"].front()).trimmed().toFloat());
if (!map["LYRICS"].isEmpty()) Decode(map["LYRICS"].front(), song->mutable_lyrics()); if (!map["LYRICS"].isEmpty()) Decode(map["LYRICS"].front(), song->mutable_lyrics());
@ -663,8 +663,8 @@ bool TagReaderTagLib::SaveFile(const QString &filename, const spb::tagreader::So
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way; // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way;
// apart, so we keep specific behavior for some formats by adding another "else if" block above. // apart, so we keep specific behavior for some formats by adding another "else if" block above.
if (TagLib::Ogg::XiphComment *tag = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) { if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
SetVorbisComments(tag, song); SetVorbisComments(xiph_comment, song);
} }
result = fileref->save(); result = fileref->save();
@ -728,6 +728,31 @@ void TagReaderTagLib::SetTextFrame(const char *id, const std::string &value, Tag
} }
void TagReaderTagLib::SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const {
const QByteArray descr_utf8(description.toUtf8());
const QByteArray value_utf8(value.toUtf8());
qLog(Debug) << "Setting FMPSFrame:" << description << ", " << value;
SetUserTextFrame(std::string(descr_utf8.constData(), descr_utf8.length()), std::string(value_utf8.constData(), value_utf8.length()), tag);
}
void TagReaderTagLib::SetUserTextFrame(const std::string &description, const std::string &value, TagLib::ID3v2::Tag *tag) const {
const TagLib::String t_description = StdStringToTaglibString(description);
TagLib::ID3v2::UserTextIdentificationFrame *frame = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, t_description);
if (frame) {
tag->removeFrame(frame);
}
// Create and add a new frame
frame = new TagLib::ID3v2::UserTextIdentificationFrame(TagLib::String::UTF8);
frame->setDescription(t_description);
frame->setText(StdStringToTaglibString(value));
tag->addFrame(frame);
}
void TagReaderTagLib::SetUnsyncLyricsFrame(const std::string &value, TagLib::ID3v2::Tag *tag) const { void TagReaderTagLib::SetUnsyncLyricsFrame(const std::string &value, TagLib::ID3v2::Tag *tag) const {
TagLib::ByteVector id_vector("USLT"); TagLib::ByteVector id_vector("USLT");
@ -1003,3 +1028,176 @@ int TagReaderTagLib::ConvertToPOPMRating(const float rating) {
return 0xFF; return 0xFF;
} }
bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
if (filename.isEmpty()) return false;
qLog(Debug) << "Saving song playcount to" << filename;
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) return false;
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comments = flac_file->xiphComment(true);
if (vorbis_comments) {
if (song.playcount() > 0) {
vorbis_comments->addField("FMPS_PLAYCOUNT", TagLib::String::number(song.playcount()), true);
}
else {
vorbis_comments->removeFields("FMPS_PLAYCOUNT");
}
}
}
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
if (tag && song.playcount() > 0) {
tag->setItem("FMPS_PlayCount", TagLib::APE::Item("FMPS_PlayCount", TagLib::String::number(song.playcount())));
}
}
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = ape_file->APETag(true);
if (tag && song.playcount() > 0) {
tag->setItem("FMPS_PlayCount", TagLib::APE::Item("FMPS_PlayCount", TagLib::String::number(song.playcount())));
}
}
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
if (song.playcount() > 0) {
xiph_comment->addField("FMPS_PLAYCOUNT", TagLib::String::number(song.playcount()), true);
}
else {
xiph_comment->removeFields("FMPS_PLAYCOUNT");
}
}
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
if (tag && song.playcount() > 0) {
SetUserTextFrame("FMPS_PlayCount", QString::number(song.playcount()), tag);
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
if (frame) {
frame->setCounter(song.playcount());
}
}
}
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = mp4_file->tag();
if (tag && song.playcount() > 0) {
tag->setItem(kMP4_FMPS_Playcount_ID, TagLib::MP4::Item(TagLib::String::number(song.playcount())));
}
}
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = mpc_file->APETag(true);
if (tag && song.playcount() > 0) {
tag->setItem("FMPS_PlayCount", TagLib::APE::Item("FMPS_PlayCount", TagLib::String::number(song.playcount())));
}
}
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = asf_file->tag();
if (tag && song.playcount() > 0) {
tag->addAttribute("FMPS/Playcount", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.playcount()))));
}
}
else {
return true;
}
bool ret = fileref->save();
#ifdef Q_OS_LINUX
if (ret) {
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
}
#endif // Q_OS_LINUX
return ret;
}
bool TagReaderTagLib::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
if (filename.isNull()) return false;
qLog(Debug) << "Saving song rating to" << filename;
if (song.rating() < 0) {
return true;
}
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) return false;
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comments = flac_file->xiphComment(true);
if (vorbis_comments) {
if (song.rating() > 0) {
vorbis_comments->addField("FMPS_RATING", QStringToTaglibString(QString::number(song.rating())), true);
}
else {
vorbis_comments->removeFields("FMPS_RATING");
}
}
}
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
if (tag) {
tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))));
}
}
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = ape_file->APETag(true);
if (tag) {
tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))));
}
}
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
if (song.rating() > 0) {
xiph_comment->addField("FMPS_RATING", QStringToTaglibString(QString::number(song.rating())), true);
}
else {
xiph_comment->removeFields("FMPS_RATING");
}
}
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
if (tag) {
SetUserTextFrame("FMPS_Rating", QString::number(song.rating()), tag);
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
if (frame) {
frame->setRating(ConvertToPOPMRating(song.rating()));
}
}
}
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = mp4_file->tag();
if (tag) {
tag->setItem(kMP4_FMPS_Rating_ID, TagLib::StringList(QStringToTaglibString(QString::number(song.rating()))));
}
}
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = asf_file->tag();
if (tag) {
tag->addAttribute("FMPS/Rating", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.rating()))));
}
}
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = mpc_file->APETag(true);
if (tag) {
tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))));
}
}
else {
return true;
}
bool ret = fileref->save();
#ifdef Q_OS_LINUX
if (ret) {
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
}
#endif // Q_OS_LINUX
return ret;
}

View File

@ -56,6 +56,9 @@ class TagReaderTagLib : public TagReaderBase {
QByteArray LoadEmbeddedArt(const QString &filename) const override; QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override; bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
private: private:
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const; spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
@ -70,6 +73,8 @@ class TagReaderTagLib : public TagReaderBase {
void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const; void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const;
void SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const; void SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const;
void SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const;
void SetUserTextFrame(const std::string &description, const std::string &value, TagLib::ID3v2::Tag *tag) const;
void SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const; void SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const;
QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const; QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const;

View File

@ -422,3 +422,62 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr
return false; return false;
} }
bool TagReaderTagParser::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const {}
bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
if (filename.isEmpty()) return false;
qLog(Debug) << "Saving song rating to" << filename;
try {
TagParser::MediaFileInfo taginfo;
TagParser::Diagnostics diag;
TagParser::AbortableProgressFeedback progress;
#ifdef Q_OS_WIN32
taginfo.setPath(filename.toStdWString().toStdString());
#else
taginfo.setPath(QFile::encodeName(filename).toStdString());
#endif
taginfo.open(false);
taginfo.parseContainerFormat(diag, progress);
if (progress.isAborted()) {
taginfo.close();
return false;
}
taginfo.parseTracks(diag, progress);
if (progress.isAborted()) {
taginfo.close();
return false;
}
taginfo.parseTags(diag, progress);
if (progress.isAborted()) {
taginfo.close();
return false;
}
if (taginfo.tags().size() <= 0) {
taginfo.createAppropriateTags();
}
for (const auto tag : taginfo.tags()) {
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(song.rating()));
}
taginfo.applyChanges(diag, progress);
taginfo.close();
for (const TagParser::DiagMessage &msg : diag) {
qLog(Debug) << QString::fromStdString(msg.message());
}
return true;
}
catch(...) {}
return false;
}

View File

@ -45,6 +45,9 @@ class TagReaderTagParser : public TagReaderBase {
QByteArray LoadEmbeddedArt(const QString &filename) const override; QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override; bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
Q_DISABLE_COPY(TagReaderTagParser) Q_DISABLE_COPY(TagReaderTagParser)
}; };

View File

@ -51,6 +51,13 @@ void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) {
reply.mutable_save_embedded_art_response()->set_success(tag_reader_.SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), message.save_embedded_art_request().data().size()))); reply.mutable_save_embedded_art_response()->set_success(tag_reader_.SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), message.save_embedded_art_request().data().size())));
} }
else if (message.has_save_song_playcount_to_file_request()) {
reply.mutable_save_song_playcount_to_file_response()->set_success(tag_reader_.SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata()));
}
else if (message.has_save_song_rating_to_file_request()) {
reply.mutable_save_song_rating_to_file_response()->set_success(tag_reader_.SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata()));
}
SendReply(message, &reply); SendReply(message, &reply);
} }

View File

@ -25,9 +25,12 @@
#include <QObject> #include <QObject>
#include <QThread> #include <QThread>
#include <QList> #include <QList>
#include <QSettings>
#include <QtConcurrentRun>
#include <QtDebug> #include <QtDebug>
#include "core/application.h" #include "core/application.h"
#include "core/taskmanager.h"
#include "core/database.h" #include "core/database.h"
#include "core/player.h" #include "core/player.h"
#include "core/tagreaderclient.h" #include "core/tagreaderclient.h"
@ -41,6 +44,7 @@
#include "collectionmodel.h" #include "collectionmodel.h"
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "scrobbler/lastfmimport.h" #include "scrobbler/lastfmimport.h"
#include "settings/collectionsettingspage.h"
const char *SCollection::kSongsTable = "songs"; const char *SCollection::kSongsTable = "songs";
const char *SCollection::kFtsTable = "songs_fts"; const char *SCollection::kFtsTable = "songs_fts";
@ -54,7 +58,9 @@ SCollection::SCollection(Application *app, QObject *parent)
model_(nullptr), model_(nullptr),
watcher_(nullptr), watcher_(nullptr),
watcher_thread_(nullptr), watcher_thread_(nullptr),
original_thread_(nullptr) { original_thread_(nullptr),
save_playcounts_to_files_(false),
save_ratings_to_files_(false) {
original_thread_ = thread(); original_thread_ = thread();
@ -100,6 +106,9 @@ void SCollection::Init() {
QObject::connect(backend_, &CollectionBackend::Error, this, &SCollection::Error); QObject::connect(backend_, &CollectionBackend::Error, this, &SCollection::Error);
QObject::connect(backend_, &CollectionBackend::DirectoryDiscovered, watcher_, &CollectionWatcher::AddDirectory); QObject::connect(backend_, &CollectionBackend::DirectoryDiscovered, watcher_, &CollectionWatcher::AddDirectory);
QObject::connect(backend_, &CollectionBackend::DirectoryDeleted, watcher_, &CollectionWatcher::RemoveDirectory); QObject::connect(backend_, &CollectionBackend::DirectoryDeleted, watcher_, &CollectionWatcher::RemoveDirectory);
QObject::connect(backend_, &CollectionBackend::SongsRatingChanged, this, &SCollection::SongsRatingChanged);
QObject::connect(backend_, &CollectionBackend::SongsStatisticsChanged, this, &SCollection::SongsPlaycountChanged);
QObject::connect(watcher_, &CollectionWatcher::NewOrUpdatedSongs, backend_, &CollectionBackend::AddOrUpdateSongs); QObject::connect(watcher_, &CollectionWatcher::NewOrUpdatedSongs, backend_, &CollectionBackend::AddOrUpdateSongs);
QObject::connect(watcher_, &CollectionWatcher::SongsMTimeUpdated, backend_, &CollectionBackend::UpdateMTimesOnly); QObject::connect(watcher_, &CollectionWatcher::SongsMTimeUpdated, backend_, &CollectionBackend::UpdateMTimesOnly);
QObject::connect(watcher_, &CollectionWatcher::SongsDeleted, backend_, &CollectionBackend::DeleteSongs); QObject::connect(watcher_, &CollectionWatcher::SongsDeleted, backend_, &CollectionBackend::DeleteSongs);
@ -164,4 +173,53 @@ void SCollection::ReloadSettings() {
watcher_->ReloadSettingsAsync(); watcher_->ReloadSettingsAsync();
model_->ReloadSettings(); model_->ReloadSettings();
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
save_playcounts_to_files_ = s.value("save_playcounts", false).toBool();
save_ratings_to_files_ = s.value("save_ratings", false).toBool();
s.endGroup();
}
void SCollection::SyncPlaycountAndRatingToFilesAsync() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
(void)QtConcurrent::run(&SCollection::SyncPlaycountAndRatingToFiles, this);
#else
(void)QtConcurrent::run(this, &SCollection::SyncPlaycountAndRatingToFiles);
#endif
}
void SCollection::SyncPlaycountAndRatingToFiles() {
const int task_id = app_->task_manager()->StartTask(tr("Saving playcounts and ratings"));
app_->task_manager()->SetTaskBlocksCollectionScans(task_id);
const SongList songs = backend_->GetAllSongs();
const int nb_songs = songs.size();
int i = 0;
for (const Song &song : songs) {
TagReaderClient::Instance()->UpdateSongPlaycountBlocking(song);
TagReaderClient::Instance()->UpdateSongRatingBlocking(song);
app_->task_manager()->SetTaskProgress(task_id, ++i, nb_songs);
}
app_->task_manager()->SetTaskFinished(task_id);
}
void SCollection::SongsPlaycountChanged(const SongList &songs) {
if (save_playcounts_to_files_) {
app_->tag_reader_client()->UpdateSongsPlaycount(songs);
}
}
void SCollection::SongsRatingChanged(const SongList &songs, const bool save_tags) {
if (save_tags || save_ratings_to_files_) {
app_->tag_reader_client()->UpdateSongsRating(songs);
}
} }

View File

@ -59,9 +59,10 @@ class SCollection : public QObject {
QString full_rescan_reason(int schema_version) const { return full_rescan_revisions_.value(schema_version, QString()); } QString full_rescan_reason(int schema_version) const { return full_rescan_revisions_.value(schema_version, QString()); }
int Total_Albums = 0; void SyncPlaycountAndRatingToFilesAsync();
int total_songs_ = 0;
int Total_Artists = 0; private:
void SyncPlaycountAndRatingToFiles();
public slots: public slots:
void ReloadSettings(); void ReloadSettings();
@ -77,6 +78,8 @@ class SCollection : public QObject {
private slots: private slots:
void ExitReceived(); void ExitReceived();
void SongsPlaycountChanged(const SongList &songs);
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
signals: signals:
void Error(QString); void Error(QString);
@ -95,6 +98,9 @@ class SCollection : public QObject {
QHash<int, QString> full_rescan_revisions_; QHash<int, QString> full_rescan_revisions_;
QList<QObject*> wait_for_exit_; QList<QObject*> wait_for_exit_;
bool save_playcounts_to_files_;
bool save_ratings_to_files_;
}; };
#endif #endif

View File

@ -509,6 +509,28 @@ void CollectionBackend::AddOrUpdateSubdirs(const SubdirectoryList &subdirs) {
} }
SongList CollectionBackend::GetAllSongs() {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SqlQuery q(db);
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1").arg(songs_table_));
if (!q.Exec()) {
db_->ReportErrors(q);
return SongList();
}
SongList songs;
while (q.next()) {
Song song;
song.InitFromQuery(q, true);
songs << song;
}
return songs;
}
void CollectionBackend::AddOrUpdateSongsAsync(const SongList &songs) { void CollectionBackend::AddOrUpdateSongsAsync(const SongList &songs) {
QMetaObject::invokeMethod(this, "AddOrUpdateSongs", Qt::QueuedConnection, Q_ARG(SongList, songs)); QMetaObject::invokeMethod(this, "AddOrUpdateSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
} }
@ -1924,17 +1946,15 @@ void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &ti
} }
void CollectionBackend::UpdateSongRating(const int id, const double rating) { void CollectionBackend::UpdateSongRating(const int id, const double rating, const bool save_tags) {
if (id == -1) return; if (id == -1) return;
QList<int> id_list; UpdateSongsRating(QList<int>() << id, rating, save_tags);
id_list << id;
UpdateSongsRating(id_list, rating);
} }
void CollectionBackend::UpdateSongsRating(const QList<int> &id_list, const double rating) { void CollectionBackend::UpdateSongsRating(const QList<int> &id_list, const double rating, const bool save_tags) {
if (id_list.isEmpty()) return; if (id_list.isEmpty()) return;
@ -1957,16 +1977,16 @@ void CollectionBackend::UpdateSongsRating(const QList<int> &id_list, const doubl
SongList new_song_list = GetSongsById(id_str_list, db); SongList new_song_list = GetSongsById(id_str_list, db);
emit SongsRatingChanged(new_song_list); emit SongsRatingChanged(new_song_list, save_tags);
} }
void CollectionBackend::UpdateSongRatingAsync(const int id, const double rating) { void CollectionBackend::UpdateSongRatingAsync(const int id, const double rating, const bool save_tags) {
QMetaObject::invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(double, rating)); QMetaObject::invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(double, rating), Q_ARG(bool, save_tags));
} }
void CollectionBackend::UpdateSongsRatingAsync(const QList<int> &ids, const double rating) { void CollectionBackend::UpdateSongsRatingAsync(const QList<int> &ids, const double rating, const bool save_tags) {
QMetaObject::invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList<int>, ids), Q_ARG(double, rating)); QMetaObject::invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList<int>, ids), Q_ARG(double, rating), Q_ARG(bool, save_tags));
} }
void CollectionBackend::UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days) { void CollectionBackend::UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days) {
@ -2014,3 +2034,4 @@ void CollectionBackend::ExpireSongs(const int directory_id, const int expire_una
} }

View File

@ -92,6 +92,8 @@ class CollectionBackendInterface : public QObject {
virtual DirectoryList GetAllDirectories() = 0; virtual DirectoryList GetAllDirectories() = 0;
virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0; virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0;
virtual SongList GetAllSongs() = 0;
virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0; virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0;
virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0; virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0;
virtual SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) = 0; virtual SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) = 0;
@ -157,6 +159,8 @@ class CollectionBackend : public CollectionBackendInterface {
DirectoryList GetAllDirectories() override; DirectoryList GetAllDirectories() override;
void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) override; void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) override;
SongList GetAllSongs() override;
QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions()); QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions());
QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) override; QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) override;
QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) override; QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) override;
@ -208,8 +212,8 @@ class CollectionBackend : public CollectionBackendInterface {
void AddOrUpdateSongsAsync(const SongList &songs); void AddOrUpdateSongsAsync(const SongList &songs);
void UpdateSongsBySongIDAsync(const SongMap &new_songs); void UpdateSongsBySongIDAsync(const SongMap &new_songs);
void UpdateSongRatingAsync(const int id, const double rating); void UpdateSongRatingAsync(const int id, const double rating, const bool save_tags = false);
void UpdateSongsRatingAsync(const QList<int> &ids, const double rating); void UpdateSongsRatingAsync(const QList<int> &ids, const double rating, const bool save_tags = false);
public slots: public slots:
void Exit(); void Exit();
@ -236,8 +240,8 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const qint64 lastplayed); void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const qint64 lastplayed);
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount); void UpdatePlayCount(const QString &artist, const QString &title, const int playcount);
void UpdateSongRating(const int id, const double rating); void UpdateSongRating(const int id, const double rating, const bool save_tags = false);
void UpdateSongsRating(const QList<int> &id_list, const double rating); void UpdateSongsRating(const QList<int> &id_list, const double rating, const bool save_tags = false);
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days); void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days); void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days);
@ -255,7 +259,7 @@ class CollectionBackend : public CollectionBackendInterface {
void TotalSongCountUpdated(int); void TotalSongCountUpdated(int);
void TotalArtistCountUpdated(int); void TotalArtistCountUpdated(int);
void TotalAlbumCountUpdated(int); void TotalAlbumCountUpdated(int);
void SongsRatingChanged(SongList); void SongsRatingChanged(SongList, bool);
void ExitFinished(); void ExitFinished();

View File

@ -128,6 +128,48 @@ TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const
} }
TagReaderReply *TagReaderClient::UpdateSongPlaycount(const Song &metadata) {
spb::tagreader::Message message;
spb::tagreader::SaveSongPlaycountToFileRequest *req = message.mutable_save_song_playcount_to_file_request();
req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile()));
metadata.ToProtobuf(req->mutable_metadata());
return worker_pool_->SendMessageWithReply(&message);
}
void TagReaderClient::UpdateSongsPlaycount(const SongList &songs) {
for (const Song &song : songs) {
TagReaderReply *reply = UpdateSongPlaycount(song);
QObject::connect(reply, &TagReaderReply::Finished, reply, &TagReaderReply::deleteLater);
}
}
TagReaderReply *TagReaderClient::UpdateSongRating(const Song &metadata) {
spb::tagreader::Message message;
spb::tagreader::SaveSongRatingToFileRequest *req = message.mutable_save_song_rating_to_file_request();
req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile()));
metadata.ToProtobuf(req->mutable_metadata());
return worker_pool_->SendMessageWithReply(&message);
}
void TagReaderClient::UpdateSongsRating(const SongList &songs) {
for (const Song &song : songs) {
TagReaderReply *reply = UpdateSongRating(song);
QObject::connect(reply, &TagReaderReply::Finished, reply, &TagReaderReply::deleteLater);
}
}
bool TagReaderClient::IsMediaFileBlocking(const QString &filename) { bool TagReaderClient::IsMediaFileBlocking(const QString &filename) {
Q_ASSERT(QThread::currentThread() != thread()); Q_ASSERT(QThread::currentThread() != thread());
@ -210,14 +252,46 @@ bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const QBy
Q_ASSERT(QThread::currentThread() != thread()); Q_ASSERT(QThread::currentThread() != thread());
bool ret = false; bool success = false;
TagReaderReply *reply = SaveEmbeddedArt(filename, data); TagReaderReply *reply = SaveEmbeddedArt(filename, data);
if (reply->WaitForFinished()) { if (reply->WaitForFinished()) {
ret = reply->message().save_embedded_art_response().success(); success = reply->message().save_embedded_art_response().success();
} }
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
return ret; return success;
}
bool TagReaderClient::UpdateSongPlaycountBlocking(const Song &metadata) {
Q_ASSERT(QThread::currentThread() != thread());
bool success = false;
TagReaderReply *reply = UpdateSongPlaycount(metadata);
if (reply->WaitForFinished()) {
success = reply->message().save_song_playcount_to_file_response().success();
}
reply->deleteLater();
return success;
}
bool TagReaderClient::UpdateSongRatingBlocking(const Song &metadata) {
Q_ASSERT(QThread::currentThread() != thread());
bool success = false;
TagReaderReply *reply = UpdateSongRating(metadata);
if (reply->WaitForFinished()) {
success = reply->message().save_song_rating_to_file_response().success();
}
reply->deleteLater();
return success;
} }

View File

@ -58,6 +58,8 @@ class TagReaderClient : public QObject {
ReplyType *IsMediaFile(const QString &filename); ReplyType *IsMediaFile(const QString &filename);
ReplyType *LoadEmbeddedArt(const QString &filename); ReplyType *LoadEmbeddedArt(const QString &filename);
ReplyType *SaveEmbeddedArt(const QString &filename, const QByteArray &data); ReplyType *SaveEmbeddedArt(const QString &filename, const QByteArray &data);
ReplyType* UpdateSongPlaycount(const Song &metadata);
ReplyType* UpdateSongRating(const Song &metadata);
// Convenience functions that call the above functions and wait for a response. // Convenience functions that call the above functions and wait for a response.
// These block the calling thread with a semaphore, and must NOT be called from the TagReaderClient's thread. // These block the calling thread with a semaphore, and must NOT be called from the TagReaderClient's thread.
@ -67,6 +69,8 @@ class TagReaderClient : public QObject {
QByteArray LoadEmbeddedArtBlocking(const QString &filename); QByteArray LoadEmbeddedArtBlocking(const QString &filename);
QImage LoadEmbeddedArtAsImageBlocking(const QString &filename); QImage LoadEmbeddedArtAsImageBlocking(const QString &filename);
bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data); bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data);
bool UpdateSongPlaycountBlocking(const Song &metadata);
bool UpdateSongRatingBlocking(const Song &metadata);
// TODO: Make this not a singleton // TODO: Make this not a singleton
static TagReaderClient *Instance() { return sInstance; } static TagReaderClient *Instance() { return sInstance; }
@ -78,6 +82,10 @@ class TagReaderClient : public QObject {
void Exit(); void Exit();
void WorkerFailedToStart(); void WorkerFailedToStart();
public slots:
void UpdateSongsPlaycount(const SongList &songs);
void UpdateSongsRating(const SongList &songs);
private: private:
static TagReaderClient *sInstance; static TagReaderClient *sInstance;

View File

@ -166,6 +166,9 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent)
QObject::connect(checkbox, &QCheckBox::stateChanged, this, &EditTagDialog::FieldValueEdited); QObject::connect(checkbox, &QCheckBox::stateChanged, this, &EditTagDialog::FieldValueEdited);
QObject::connect(checkbox, &CheckBox::Reset, this, &EditTagDialog::ResetField); QObject::connect(checkbox, &CheckBox::Reset, this, &EditTagDialog::ResetField);
} }
else if (RatingBox *ratingbox = qobject_cast<RatingBox*>(widget)) {
QObject::connect(ratingbox, &RatingWidget::RatingChanged, this, &EditTagDialog::FieldValueEdited);
}
} }
} }
@ -184,6 +187,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent)
QObject::connect(ui_->song_list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EditTagDialog::SelectionChanged); QObject::connect(ui_->song_list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EditTagDialog::SelectionChanged);
QObject::connect(ui_->button_box, &QDialogButtonBox::clicked, this, &EditTagDialog::ButtonClicked); QObject::connect(ui_->button_box, &QDialogButtonBox::clicked, this, &EditTagDialog::ButtonClicked);
QObject::connect(ui_->playcount_reset, &QPushButton::clicked, this, &EditTagDialog::ResetPlayCounts); QObject::connect(ui_->playcount_reset, &QPushButton::clicked, this, &EditTagDialog::ResetPlayCounts);
QObject::connect(ui_->rating, &RatingWidget::RatingChanged, this, &EditTagDialog::SongRated);
#ifdef HAVE_MUSICBRAINZ #ifdef HAVE_MUSICBRAINZ
QObject::connect(ui_->fetch_tag, &QPushButton::clicked, this, &EditTagDialog::FetchTag); QObject::connect(ui_->fetch_tag, &QPushButton::clicked, this, &EditTagDialog::FetchTag);
#endif #endif
@ -478,6 +482,7 @@ QVariant EditTagDialog::Data::value(const Song &song, const QString &id) {
if (id == "disc") return song.disc(); if (id == "disc") return song.disc();
if (id == "year") return song.year(); if (id == "year") return song.year();
if (id == "compilation") return song.compilation(); if (id == "compilation") return song.compilation();
if (id == "rating") { return song.rating(); }
qLog(Warning) << "Unknown ID" << id; qLog(Warning) << "Unknown ID" << id;
return QVariant(); return QVariant();
@ -499,6 +504,7 @@ void EditTagDialog::Data::set_value(const QString &id, const QVariant &value) {
else if (id == "disc") current_.set_disc(value.toInt()); else if (id == "disc") current_.set_disc(value.toInt());
else if (id == "year") current_.set_year(value.toInt()); else if (id == "year") current_.set_year(value.toInt());
else if (id == "compilation") current_.set_compilation(value.toBool()); else if (id == "compilation") current_.set_compilation(value.toBool());
else if (id == "rating") { current_.set_rating(value.toDouble()); }
else qLog(Warning) << "Unknown ID" << id; else qLog(Warning) << "Unknown ID" << id;
} }
@ -832,7 +838,6 @@ void EditTagDialog::UpdateStatisticsTab(const Song &song) {
ui_->playcount->setText(QString::number(qMax(0, song.playcount()))); ui_->playcount->setText(QString::number(qMax(0, song.playcount())));
ui_->skipcount->setText(QString::number(qMax(0, song.skipcount()))); ui_->skipcount->setText(QString::number(qMax(0, song.skipcount())));
ui_->lastplayed->setText(song.lastplayed() <= 0 ? tr("Never") : QDateTime::fromSecsSinceEpoch(song.lastplayed()).toString(QLocale::system().dateTimeFormat(QLocale::LongFormat))); ui_->lastplayed->setText(song.lastplayed() <= 0 ? tr("Never") : QDateTime::fromSecsSinceEpoch(song.lastplayed()).toString(QLocale::system().dateTimeFormat(QLocale::LongFormat)));
} }
@ -1094,6 +1099,10 @@ void EditTagDialog::SaveData() {
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_); }, Qt::QueuedConnection); QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_); }, Qt::QueuedConnection);
} }
if (ref.current_.rating() != ref.original_.rating() && ref.current_.is_collection_song()) {
app_->collection_backend()->UpdateSongRatingAsync(ref.current_.id(), ref.current_.rating(), true);
}
QString embedded_cover_from_file; QString embedded_cover_from_file;
// If embedded album cover is selected and it isn't saved to the tags, then save it even if no action was done. // If embedded album cover is selected and it isn't saved to the tags, then save it even if no action was done.
if (ui_->checkbox_embedded_cover->isChecked() && ref.cover_action_ == UpdateCoverAction_None && !ref.original_.has_embedded_cover() && ref.original_.save_embedded_cover_supported()) { if (ui_->checkbox_embedded_cover->isChecked() && ref.cover_action_ == UpdateCoverAction_None && !ref.original_.has_embedded_cover() && ref.original_.save_embedded_cover_supported()) {
@ -1256,6 +1265,18 @@ void EditTagDialog::ResetPlayCounts() {
} }
void EditTagDialog::SongRated(const float rating) {
const QModelIndexList indexes = ui_->song_list->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) return;
for (const QModelIndex &idx : indexes) {
if (!data_[idx.row()].current_.is_valid() || data_[idx.row()].current_.id() == -1) return;
data_[idx.row()].current_.set_rating(rating);
}
}
void EditTagDialog::FetchTag() { void EditTagDialog::FetchTag() {
#ifdef HAVE_MUSICBRAINZ #ifdef HAVE_MUSICBRAINZ

View File

@ -116,6 +116,7 @@ class EditTagDialog : public QDialog {
void ResetField(); void ResetField();
void ButtonClicked(QAbstractButton *button); void ButtonClicked(QAbstractButton *button);
void ResetPlayCounts(); void ResetPlayCounts();
void SongRated(const float rating);
void FetchTag(); void FetchTag();
void FetchTagSongChosen(const Song &original_song, const Song &new_metadata); void FetchTagSongChosen(const Song &original_song, const Song &new_metadata);

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>800</width>
<height>843</height> <height>889</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -125,10 +125,10 @@
</property> </property>
<property name="html"> <property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt; <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt; &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Verdana'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Noto Sans';&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="textInteractionFlags"> <property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
@ -611,10 +611,10 @@ p, li { white-space: pre-wrap; }
</property> </property>
<property name="html"> <property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt; <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt; &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Verdana'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Noto Sans';&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -627,8 +627,24 @@ p, li { white-space: pre-wrap; }
<property name="sizeConstraint"> <property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum> <enum>QLayout::SetMinimumSize</enum>
</property> </property>
<item row="13" column="2"> <item row="4" column="3">
<widget class="LineEdit" name="grouping"> <widget class="SpinBox" name="year">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="maximum">
<number>9999</number>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="LineEdit" name="genre">
<property name="has_reset_button" stdset="0"> <property name="has_reset_button" stdset="0">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -637,66 +653,8 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="3" column="2">
<widget class="QLabel" name="label_title"> <widget class="QLabel" name="label_disc">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Title</string>
</property>
<property name="buddy">
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="15" column="2">
<widget class="CheckBox" name="compilation">
<property name="has_reset_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="genre_label">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Genre</string>
</property>
<property name="buddy">
<cstring>genre</cstring>
</property>
</widget>
</item>
<item row="21" column="2">
<widget class="TextEdit" name="comment">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLabel" name="disc_label">
<property name="text"> <property name="text">
<string>Disc</string> <string>Disc</string>
</property> </property>
@ -705,157 +663,8 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="8" column="1">
<widget class="QLabel" name="label_albumartist"> <widget class="LineEdit" name="composer">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Album artist</string>
</property>
<property name="buddy">
<cstring>albumartist</cstring>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="label_compilation">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Compilation</string>
</property>
<property name="buddy">
<cstring>compilation</cstring>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="LineEdit" name="album">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_album">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Album</string>
</property>
<property name="buddy">
<cstring>album</cstring>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_artist">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Artist</string>
</property>
<property name="buddy">
<cstring>artist</cstring>
</property>
</widget>
</item>
<item row="21" column="0">
<widget class="QLabel" name="label_comment">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Comment</string>
</property>
<property name="buddy">
<cstring>comment</cstring>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_composer">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Composer</string>
</property>
<property name="buddy">
<cstring>composer</cstring>
</property>
</widget>
</item>
<item row="20" column="2">
<widget class="QPushButton" name="fetch_tag">
<property name="text">
<string>Complete tags automatically</string>
</property>
<property name="icon">
<iconset resource="../../data/data.qrc">
<normaloff>:/pictures/musicbrainz.png</normaloff>:/pictures/musicbrainz.png</iconset>
</property>
<property name="iconSize">
<size>
<width>38</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_performer">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Performer</string>
</property>
<property name="buddy">
<cstring>performer</cstring>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="LineEdit" name="artist">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="14" column="2">
<widget class="LineEdit" name="genre">
<property name="has_reset_button" stdset="0"> <property name="has_reset_button" stdset="0">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -880,24 +689,24 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="4"> <item row="5" column="0">
<widget class="SpinBox" name="disc"> <widget class="QLabel" name="label_albumartist">
<property name="correctionMode"> <property name="minimumSize">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum> <size>
<width>80</width>
<height>0</height>
</size>
</property> </property>
<property name="maximum"> <property name="text">
<number>9999</number> <string>Album artist</string>
</property> </property>
<property name="has_clear_button" stdset="0"> <property name="buddy">
<bool>false</bool> <cstring>albumartist</cstring>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="2"> <item row="13" column="1">
<widget class="LineEdit" name="composer"> <widget class="LineEdit" name="grouping">
<property name="has_reset_button" stdset="0"> <property name="has_reset_button" stdset="0">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -906,7 +715,27 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="2"> <item row="9" column="1">
<widget class="LineEdit" name="performer">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="LineEdit" name="artist">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEdit" name="title"> <widget class="LineEdit" name="title">
<property name="has_reset_button" stdset="0"> <property name="has_reset_button" stdset="0">
<bool>true</bool> <bool>true</bool>
@ -916,7 +745,7 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="4"> <item row="0" column="3">
<widget class="SpinBox" name="track"> <widget class="SpinBox" name="track">
<property name="correctionMode"> <property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum> <enum>QAbstractSpinBox::CorrectToNearestValue</enum>
@ -932,28 +761,99 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="2"> <item row="4" column="0">
<widget class="LineEdit" name="albumartist"> <widget class="QLabel" name="label_album">
<property name="has_reset_button" stdset="0"> <property name="minimumSize">
<bool>true</bool> <size>
<width>80</width>
<height>0</height>
</size>
</property> </property>
<property name="has_clear_button" stdset="0"> <property name="text">
<bool>false</bool> <string>Album</string>
</property>
<property name="buddy">
<cstring>album</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="2"> <item row="4" column="2">
<widget class="LineEdit" name="performer"> <widget class="QLabel" name="label_year">
<property name="has_reset_button" stdset="0"> <property name="text">
<bool>true</bool> <string>Year</string>
</property> </property>
<property name="has_clear_button" stdset="0"> <property name="buddy">
<bool>false</bool> <cstring>year</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="4"> <item row="0" column="0">
<widget class="SpinBox" name="year"> <widget class="QLabel" name="label_title">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Title</string>
</property>
<property name="buddy">
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_artist">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Artist</string>
</property>
<property name="buddy">
<cstring>artist</cstring>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_composer">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Composer</string>
</property>
<property name="buddy">
<cstring>composer</cstring>
</property>
</widget>
</item>
<item row="21" column="1">
<widget class="QPushButton" name="fetch_tag">
<property name="text">
<string>Complete tags automatically</string>
</property>
<property name="icon">
<iconset resource="../../data/data.qrc">
<normaloff>:/pictures/musicbrainz.png</normaloff>:/pictures/musicbrainz.png</iconset>
</property>
<property name="iconSize">
<size>
<width>38</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="SpinBox" name="disc">
<property name="correctionMode"> <property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum> <enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property> </property>
@ -968,7 +868,117 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="3"> <item row="15" column="1">
<widget class="CheckBox" name="compilation">
<property name="has_reset_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="label_genre">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Genre</string>
</property>
<property name="buddy">
<cstring>genre</cstring>
</property>
</widget>
</item>
<item row="22" column="0">
<widget class="QLabel" name="label_comment">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Comment</string>
</property>
<property name="buddy">
<cstring>comment</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="LineEdit" name="album">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="LineEdit" name="albumartist">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="22" column="1">
<widget class="TextEdit" name="comment">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_performer">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Performer</string>
</property>
<property name="buddy">
<cstring>performer</cstring>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="label_compilation">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Compilation</string>
</property>
<property name="buddy">
<cstring>compilation</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_track"> <widget class="QLabel" name="label_track">
<property name="text"> <property name="text">
<string>Track</string> <string>Track</string>
@ -978,16 +988,19 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="3"> <item row="16" column="0">
<widget class="QLabel" name="label_year"> <widget class="QLabel" name="label_rating">
<property name="text"> <property name="text">
<string>Year</string> <string>Rating</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>year</cstring> <cstring>rating</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="16" column="1">
<widget class="RatingBox" name="rating" native="true"/>
</item>
</layout> </layout>
</item> </item>
<item> <item>
@ -1088,6 +1101,12 @@ p, li { white-space: pre-wrap; }
<extends>QCheckBox</extends> <extends>QCheckBox</extends>
<header>widgets/lineedit.h</header> <header>widgets/lineedit.h</header>
</customwidget> </customwidget>
<customwidget>
<class>RatingBox</class>
<extends>QWidget</extends>
<header>widgets/lineedit.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>song_list</tabstop> <tabstop>song_list</tabstop>

View File

@ -40,10 +40,12 @@
#include <QRadioButton> #include <QRadioButton>
#include <QSpinBox> #include <QSpinBox>
#include <QSettings> #include <QSettings>
#include <QMessageBox>
#include "core/application.h" #include "core/application.h"
#include "core/iconloader.h" #include "core/iconloader.h"
#include "core/utilities.h" #include "core/utilities.h"
#include "collection/collection.h"
#include "collection/collectionmodel.h" #include "collection/collectionmodel.h"
#include "collection/collectiondirectorymodel.h" #include "collection/collectiondirectorymodel.h"
#include "collectionsettingspage.h" #include "collectionsettingspage.h"
@ -94,6 +96,8 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, QWidget *
QObject::connect(ui_->combobox_cache_size, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::CacheSizeUnitChanged); QObject::connect(ui_->combobox_cache_size, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::CacheSizeUnitChanged);
QObject::connect(ui_->combobox_disk_cache_size, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::DiskCacheSizeUnitChanged); QObject::connect(ui_->combobox_disk_cache_size, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::DiskCacheSizeUnitChanged);
QObject::connect(ui_->button_save_stats, &QPushButton::clicked, this, &CollectionSettingsPage::WriteAllSongsStatisticsToFiles);
#ifndef HAVE_SONGFINGERPRINTING #ifndef HAVE_SONGFINGERPRINTING
ui_->song_tracking->hide(); ui_->song_tracking->hide();
#endif #endif
@ -110,7 +114,7 @@ void CollectionSettingsPage::Add() {
QString path(s.value("last_path", QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).toString()); QString path(s.value("last_path", QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).toString());
path = QFileDialog::getExistingDirectory(this, tr("Add directory..."), path); path = QFileDialog::getExistingDirectory(this, tr("Add directory..."), path);
if (!path.isNull()) { if (!path.isEmpty()) {
dialog()->collection_directory_model()->AddDirectory(path); dialog()->collection_directory_model()->AddDirectory(path);
} }
@ -206,6 +210,9 @@ void CollectionSettingsPage::Load() {
ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, static_cast<int>(CacheSizeUnit_MB)).toInt()); ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, static_cast<int>(CacheSizeUnit_MB)).toInt());
if (ui_->combobox_disk_cache_size->currentIndex() == -1) ui_->combobox_cache_size->setCurrentIndex(static_cast<int>(CacheSizeUnit_MB)); if (ui_->combobox_disk_cache_size->currentIndex() == -1) ui_->combobox_cache_size->setCurrentIndex(static_cast<int>(CacheSizeUnit_MB));
ui_->checkbox_save_playcounts->setChecked(s.value("save_playcounts", false).toBool());
ui_->checkbox_save_ratings->setChecked(s.value("save_ratings", false).toBool());
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
ui_->checkbox_delete_files->setChecked(s.value("delete_files", false).toBool()); ui_->checkbox_delete_files->setChecked(s.value("delete_files", false).toBool());
#else #else
@ -270,6 +277,9 @@ void CollectionSettingsPage::Save() {
s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value()); s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value());
s.setValue(kSettingsDiskCacheSizeUnit, ui_->combobox_disk_cache_size->currentIndex()); s.setValue(kSettingsDiskCacheSizeUnit, ui_->combobox_disk_cache_size->currentIndex());
s.setValue("save_playcounts", ui_->checkbox_save_playcounts->isChecked());
s.setValue("save_ratings", ui_->checkbox_save_ratings->isChecked());
s.setValue("delete_files", ui_->checkbox_delete_files->isChecked()); s.setValue("delete_files", ui_->checkbox_delete_files->isChecked());
s.endGroup(); s.endGroup();
@ -334,3 +344,14 @@ void CollectionSettingsPage::DiskCacheSizeUnitChanged(int index) {
} }
} }
void CollectionSettingsPage::WriteAllSongsStatisticsToFiles() {
QMessageBox confirmation_dialog(QMessageBox::Question, tr("Write all playcounts and ratings to files"), tr("Are you sure you want to write song playcounts and ratings to file for all songs in your collection?"), QMessageBox::Yes | QMessageBox::Cancel);
if (confirmation_dialog.exec() != QMessageBox::Yes) {
return;
}
dialog()->app()->collection()->SyncPlaycountAndRatingToFilesAsync();
}

View File

@ -82,6 +82,7 @@ class CollectionSettingsPage : public SettingsPage {
void ClearPixmapDiskCache(); void ClearPixmapDiskCache();
void CacheSizeUnitChanged(int index); void CacheSizeUnitChanged(int index);
void DiskCacheSizeUnitChanged(int index); void DiskCacheSizeUnitChanged(int index);
void WriteAllSongsStatisticsToFiles();
private: private:
Ui_CollectionSettingsPage *ui_; Ui_CollectionSettingsPage *ui_;

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>516</width> <width>516</width>
<height>1167</height> <height>1339</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -540,6 +540,53 @@ If there are no matches then it will use the largest image in the directory.</st
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="groupbox_song_playcounts_and_ratings">
<property name="title">
<string>Song playcounts and ratings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="checkbox_save_playcounts">
<property name="text">
<string>Save playcounts to song tags when possible</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_save_ratings">
<property name="text">
<string>Save ratings to song tags when possible</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_statistics_button">
<item>
<widget class="QPushButton" name="button_save_stats">
<property name="text">
<string>Save playcounts and ratings to files now</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_save_stats">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QCheckBox" name="checkbox_delete_files"> <widget class="QCheckBox" name="checkbox_delete_files">
<property name="text"> <property name="text">

View File

@ -260,3 +260,12 @@ QString SpinBox::textFromValue(int val) const {
return QSpinBox::textFromValue(val); return QSpinBox::textFromValue(val);
} }
RatingBox::RatingBox(QWidget *parent)
: RatingWidget(parent),
ExtendedEditor(this) {
clear_button_->hide();
reset_button_->hide();
}

View File

@ -33,6 +33,8 @@
#include <QSpinBox> #include <QSpinBox>
#include <QCheckBox> #include <QCheckBox>
#include "ratingwidget.h"
class QToolButton; class QToolButton;
class QPaintDevice; class QPaintDevice;
class QPaintEvent; class QPaintEvent;
@ -231,4 +233,25 @@ class CheckBox : public QCheckBox, public ExtendedEditor {
void Reset(); void Reset();
}; };
class RatingBox : public RatingWidget, public ExtendedEditor {
Q_OBJECT
Q_PROPERTY(QString hint READ hint WRITE set_hint)
public:
explicit RatingBox(QWidget *parent = nullptr);
void set_enabled(bool enabled) override { RatingWidget::setEnabled(enabled); }
QVariant value() const override { return RatingWidget::rating(); }
void set_value(const QVariant &value) override { RatingWidget::set_rating(value.toDouble()); }
void set_partially() override { RatingWidget::set_rating(0.0); }
public slots:
void set_focus() override { RatingWidget::setFocus(); }
void clear() override {}
};
#endif // LINEEDIT_H #endif // LINEEDIT_H

View File

@ -94,6 +94,28 @@ class TagReaderTest : public ::testing::Test {
return QString(); return QString();
} }
static void WriteSongPlaycountToFile(const Song &song, const QString &filename) {
#if defined(USE_TAGLIB)
TagReaderTagLib tag_reader;
#elif defined(USE_TAGPARSER)
TagReaderTagParser tag_reader;
#endif
spb::tagreader::SongMetadata pb_song;
song.ToProtobuf(&pb_song);
tag_reader.SaveSongPlaycountToFile(filename, pb_song);
}
static void WriteSongRatingToFile(const Song &song, const QString &filename) {
#if defined(USE_TAGLIB)
TagReaderTagLib tag_reader;
#elif defined(USE_TAGPARSER)
TagReaderTagParser tag_reader;
#endif
spb::tagreader::SongMetadata pb_song;
song.ToProtobuf(&pb_song);
tag_reader.SaveSongRatingToFile(filename, pb_song);
}
}; };
TEST_F(TagReaderTest, TestFLACAudioFileTagging) { TEST_F(TagReaderTest, TestFLACAudioFileTagging) {
@ -1691,4 +1713,314 @@ TEST_F(TagReaderTest, TestM4AAudioFileTagging) {
} }
#ifndef USE_TAGPARSER
TEST_F(TagReaderTest, TestFLACAudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.flac");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestWavPackAudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.wv");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestOggFLACAudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.oga");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestOggVorbisAudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.ogg");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestOggOpusAudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.opus");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestOggSpeexAudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.spx");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestOggASFAudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.asf");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestOggMP3AudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.mp3");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
TEST_F(TagReaderTest, TestOggMP4AudioFilePlaycount) {
TemporaryResource r(":/audio/strawberry.m4a");
{
Song song;
song.set_playcount(4);
WriteSongPlaycountToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_EQ(4, song.playcount());
}
}
#endif // USE_TAGPARSER
TEST_F(TagReaderTest, TestFLACAudioFileRating) {
TemporaryResource r(":/audio/strawberry.flac");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestWavPackAudioFileRating) {
TemporaryResource r(":/audio/strawberry.wv");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestOggFLACAudioFileRating) {
TemporaryResource r(":/audio/strawberry.oga");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestOggVorbisAudioFileRating) {
TemporaryResource r(":/audio/strawberry.ogg");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestOggOpusAudioFileRating) {
TemporaryResource r(":/audio/strawberry.opus");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestOggSpeexAudioFileRating) {
TemporaryResource r(":/audio/strawberry.spx");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestASFAudioFileRating) {
TemporaryResource r(":/audio/strawberry.asf");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestMP3AudioFileRating) {
TemporaryResource r(":/audio/strawberry.mp3");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
TEST_F(TagReaderTest, TestMP4AudioFileRating) {
TemporaryResource r(":/audio/strawberry.m4a");
{
Song song;
song.set_rating(0.4);
WriteSongRatingToFile(song, r.fileName());
}
{
Song song = ReadSongFromFile(r.fileName());
EXPECT_NE(0.4, song.rating());
}
}
} // namespace } // namespace