diff --git a/ext/libstrawberry-tagreader/tagreaderbase.h b/ext/libstrawberry-tagreader/tagreaderbase.h index 1d6104db7..085f6ba84 100644 --- a/ext/libstrawberry-tagreader/tagreaderbase.h +++ b/ext/libstrawberry-tagreader/tagreaderbase.h @@ -44,6 +44,9 @@ class TagReaderBase { virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 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: static const std::string kEmbeddedCover; diff --git a/ext/libstrawberry-tagreader/tagreadermessages.proto b/ext/libstrawberry-tagreader/tagreadermessages.proto index 0560dd9b3..4c58ad0b1 100644 --- a/ext/libstrawberry-tagreader/tagreadermessages.proto +++ b/ext/libstrawberry-tagreader/tagreadermessages.proto @@ -117,6 +117,24 @@ message SaveEmbeddedArtResponse { 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 { optional int32 id = 1; @@ -135,4 +153,10 @@ message Message { optional SaveEmbeddedArtRequest save_embedded_art_request = 10; 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; + } diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.cpp b/ext/libstrawberry-tagreader/tagreadertaglib.cpp index b244a6cb8..1c1677e29 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.cpp +++ b/ext/libstrawberry-tagreader/tagreadertaglib.cpp @@ -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; // apart, so we keep specific behavior for some formats by adding another "else if" block below. - if (TagLib::Ogg::XiphComment *tag_ogg = dynamic_cast(fileref->file()->tag())) { - ParseOggTag(tag_ogg->fieldListMap(), &disc, &compilation, song); - if (!tag_ogg->pictureList().isEmpty()) { + if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(fileref->file()->tag())) { + ParseOggTag(xiph_comment->fieldListMap(), &disc, &compilation, song); + if (!xiph_comment->pictureList().isEmpty()) { song->set_art_automatic(kEmbeddedCover); } } @@ -319,7 +319,7 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta } if (!map["POPM"].isEmpty()) { - const TagLib::ID3v2::PopularimeterFrame* frame = dynamic_cast(map["POPM"].front()); + const TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast(map["POPM"].front()); if (frame) { if (song->playcount() <= 0 && frame->counter() > 0) { 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["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["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; // apart, so we keep specific behavior for some formats by adding another "else if" block above. - if (TagLib::Ogg::XiphComment *tag = dynamic_cast(fileref->file()->tag())) { - SetVorbisComments(tag, song); + if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(fileref->file()->tag())) { + SetVorbisComments(xiph_comment, song); } 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 { TagLib::ByteVector id_vector("USLT"); @@ -1003,3 +1028,176 @@ int TagReaderTagLib::ConvertToPOPMRating(const float rating) { 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 fileref(factory_->GetFileRef(filename)); + if (!fileref || fileref->isNull()) return false; + + if (TagLib::FLAC::File *flac_file = dynamic_cast(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(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(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(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(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(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(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(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 fileref(factory_->GetFileRef(filename)); + + if (!fileref || fileref->isNull()) return false; + + if (TagLib::FLAC::File *flac_file = dynamic_cast(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(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(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(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(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(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(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(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; + +} diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.h b/ext/libstrawberry-tagreader/tagreadertaglib.h index 812e85438..3f9226838 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.h +++ b/ext/libstrawberry-tagreader/tagreadertaglib.h @@ -56,6 +56,9 @@ class TagReaderTagLib : public TagReaderBase { QByteArray LoadEmbeddedArt(const QString &filename) const 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: 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 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; QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const; diff --git a/ext/libstrawberry-tagreader/tagreadertagparser.cpp b/ext/libstrawberry-tagreader/tagreadertagparser.cpp index 1e251f7a8..b777f11eb 100644 --- a/ext/libstrawberry-tagreader/tagreadertagparser.cpp +++ b/ext/libstrawberry-tagreader/tagreadertagparser.cpp @@ -422,3 +422,62 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr 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; + +} diff --git a/ext/libstrawberry-tagreader/tagreadertagparser.h b/ext/libstrawberry-tagreader/tagreadertagparser.h index 3534dd86b..7880613dc 100644 --- a/ext/libstrawberry-tagreader/tagreadertagparser.h +++ b/ext/libstrawberry-tagreader/tagreadertagparser.h @@ -45,6 +45,9 @@ class TagReaderTagParser : public TagReaderBase { QByteArray LoadEmbeddedArt(const QString &filename) const 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) }; diff --git a/ext/strawberry-tagreader/tagreaderworker.cpp b/ext/strawberry-tagreader/tagreaderworker.cpp index becaa2d32..1ff4f30b2 100644 --- a/ext/strawberry-tagreader/tagreaderworker.cpp +++ b/ext/strawberry-tagreader/tagreaderworker.cpp @@ -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()))); } + 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); } diff --git a/src/collection/collection.cpp b/src/collection/collection.cpp index 8c4349f86..357f0a5c5 100644 --- a/src/collection/collection.cpp +++ b/src/collection/collection.cpp @@ -25,9 +25,12 @@ #include #include #include +#include +#include #include #include "core/application.h" +#include "core/taskmanager.h" #include "core/database.h" #include "core/player.h" #include "core/tagreaderclient.h" @@ -41,6 +44,7 @@ #include "collectionmodel.h" #include "playlist/playlistmanager.h" #include "scrobbler/lastfmimport.h" +#include "settings/collectionsettingspage.h" const char *SCollection::kSongsTable = "songs"; const char *SCollection::kFtsTable = "songs_fts"; @@ -54,7 +58,9 @@ SCollection::SCollection(Application *app, QObject *parent) model_(nullptr), watcher_(nullptr), watcher_thread_(nullptr), - original_thread_(nullptr) { + original_thread_(nullptr), + save_playcounts_to_files_(false), + save_ratings_to_files_(false) { original_thread_ = thread(); @@ -100,6 +106,9 @@ void SCollection::Init() { QObject::connect(backend_, &CollectionBackend::Error, this, &SCollection::Error); QObject::connect(backend_, &CollectionBackend::DirectoryDiscovered, watcher_, &CollectionWatcher::AddDirectory); 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::SongsMTimeUpdated, backend_, &CollectionBackend::UpdateMTimesOnly); QObject::connect(watcher_, &CollectionWatcher::SongsDeleted, backend_, &CollectionBackend::DeleteSongs); @@ -164,4 +173,53 @@ void SCollection::ReloadSettings() { watcher_->ReloadSettingsAsync(); 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); + } + } diff --git a/src/collection/collection.h b/src/collection/collection.h index 619abaf63..d4308f569 100644 --- a/src/collection/collection.h +++ b/src/collection/collection.h @@ -59,9 +59,10 @@ class SCollection : public QObject { QString full_rescan_reason(int schema_version) const { return full_rescan_revisions_.value(schema_version, QString()); } - int Total_Albums = 0; - int total_songs_ = 0; - int Total_Artists = 0; + void SyncPlaycountAndRatingToFilesAsync(); + + private: + void SyncPlaycountAndRatingToFiles(); public slots: void ReloadSettings(); @@ -77,6 +78,8 @@ class SCollection : public QObject { private slots: void ExitReceived(); + void SongsPlaycountChanged(const SongList &songs); + void SongsRatingChanged(const SongList &songs, const bool save_tags = false); signals: void Error(QString); @@ -95,6 +98,9 @@ class SCollection : public QObject { QHash full_rescan_revisions_; QList wait_for_exit_; + + bool save_playcounts_to_files_; + bool save_ratings_to_files_; }; #endif diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index 4fb914a41..ab65db5cb 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -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) { 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; - QList id_list; - id_list << id; - UpdateSongsRating(id_list, rating); + UpdateSongsRating(QList() << id, rating, save_tags); } -void CollectionBackend::UpdateSongsRating(const QList &id_list, const double rating) { +void CollectionBackend::UpdateSongsRating(const QList &id_list, const double rating, const bool save_tags) { if (id_list.isEmpty()) return; @@ -1957,16 +1977,16 @@ void CollectionBackend::UpdateSongsRating(const QList &id_list, const doubl 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) { - QMetaObject::invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(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), Q_ARG(bool, save_tags)); } -void CollectionBackend::UpdateSongsRatingAsync(const QList &ids, const double rating) { - QMetaObject::invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList, ids), Q_ARG(double, rating)); +void CollectionBackend::UpdateSongsRatingAsync(const QList &ids, const double rating, const bool save_tags) { + QMetaObject::invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList, ids), Q_ARG(double, rating), Q_ARG(bool, save_tags)); } 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 } + diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index 8a6d124ce..864d8d4dc 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -92,6 +92,8 @@ class CollectionBackendInterface : public QObject { virtual DirectoryList GetAllDirectories() = 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 GetAllArtistsWithAlbums(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; 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 GetAllArtists(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 UpdateSongsBySongIDAsync(const SongMap &new_songs); - void UpdateSongRatingAsync(const int id, const double rating); - void UpdateSongsRatingAsync(const QList &ids, const double rating); + void UpdateSongRatingAsync(const int id, const double rating, const bool save_tags = false); + void UpdateSongsRatingAsync(const QList &ids, const double rating, const bool save_tags = false); public slots: 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 UpdatePlayCount(const QString &artist, const QString &title, const int playcount); - void UpdateSongRating(const int id, const double rating); - void UpdateSongsRating(const QList &id_list, const double rating); + void UpdateSongRating(const int id, const double rating, const bool save_tags = false); + void UpdateSongsRating(const QList &id_list, const double rating, const bool save_tags = false); void UpdateLastSeen(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 TotalArtistCountUpdated(int); void TotalAlbumCountUpdated(int); - void SongsRatingChanged(SongList); + void SongsRatingChanged(SongList, bool); void ExitFinished(); diff --git a/src/core/tagreaderclient.cpp b/src/core/tagreaderclient.cpp index 36be9a57b..380f3f1a1 100644 --- a/src/core/tagreaderclient.cpp +++ b/src/core/tagreaderclient.cpp @@ -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) { Q_ASSERT(QThread::currentThread() != thread()); @@ -210,14 +252,46 @@ bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const QBy Q_ASSERT(QThread::currentThread() != thread()); - bool ret = false; + bool success = false; TagReaderReply *reply = SaveEmbeddedArt(filename, data); 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); - 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; } diff --git a/src/core/tagreaderclient.h b/src/core/tagreaderclient.h index 7045c6732..61cf8a5fe 100644 --- a/src/core/tagreaderclient.h +++ b/src/core/tagreaderclient.h @@ -58,6 +58,8 @@ class TagReaderClient : public QObject { ReplyType *IsMediaFile(const QString &filename); ReplyType *LoadEmbeddedArt(const QString &filename); 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. // 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); QImage LoadEmbeddedArtAsImageBlocking(const QString &filename); bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data); + bool UpdateSongPlaycountBlocking(const Song &metadata); + bool UpdateSongRatingBlocking(const Song &metadata); // TODO: Make this not a singleton static TagReaderClient *Instance() { return sInstance; } @@ -78,6 +82,10 @@ class TagReaderClient : public QObject { void Exit(); void WorkerFailedToStart(); + public slots: + void UpdateSongsPlaycount(const SongList &songs); + void UpdateSongsRating(const SongList &songs); + private: static TagReaderClient *sInstance; diff --git a/src/dialogs/edittagdialog.cpp b/src/dialogs/edittagdialog.cpp index b81f8d971..0f4481e56 100644 --- a/src/dialogs/edittagdialog.cpp +++ b/src/dialogs/edittagdialog.cpp @@ -166,6 +166,9 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) QObject::connect(checkbox, &QCheckBox::stateChanged, this, &EditTagDialog::FieldValueEdited); QObject::connect(checkbox, &CheckBox::Reset, this, &EditTagDialog::ResetField); } + else if (RatingBox *ratingbox = qobject_cast(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_->button_box, &QDialogButtonBox::clicked, this, &EditTagDialog::ButtonClicked); QObject::connect(ui_->playcount_reset, &QPushButton::clicked, this, &EditTagDialog::ResetPlayCounts); + QObject::connect(ui_->rating, &RatingWidget::RatingChanged, this, &EditTagDialog::SongRated); #ifdef HAVE_MUSICBRAINZ QObject::connect(ui_->fetch_tag, &QPushButton::clicked, this, &EditTagDialog::FetchTag); #endif @@ -478,6 +482,7 @@ QVariant EditTagDialog::Data::value(const Song &song, const QString &id) { if (id == "disc") return song.disc(); if (id == "year") return song.year(); if (id == "compilation") return song.compilation(); + if (id == "rating") { return song.rating(); } qLog(Warning) << "Unknown ID" << id; 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 == "year") current_.set_year(value.toInt()); else if (id == "compilation") current_.set_compilation(value.toBool()); + else if (id == "rating") { current_.set_rating(value.toDouble()); } 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_->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))); } @@ -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); } + 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; // 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()) { @@ -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() { #ifdef HAVE_MUSICBRAINZ diff --git a/src/dialogs/edittagdialog.h b/src/dialogs/edittagdialog.h index 9ab1b9988..b777d6bb0 100644 --- a/src/dialogs/edittagdialog.h +++ b/src/dialogs/edittagdialog.h @@ -116,6 +116,7 @@ class EditTagDialog : public QDialog { void ResetField(); void ButtonClicked(QAbstractButton *button); void ResetPlayCounts(); + void SongRated(const float rating); void FetchTag(); void FetchTagSongChosen(const Song &original_song, const Song &new_metadata); diff --git a/src/dialogs/edittagdialog.ui b/src/dialogs/edittagdialog.ui index 0bc0345cf..19744e69e 100644 --- a/src/dialogs/edittagdialog.ui +++ b/src/dialogs/edittagdialog.ui @@ -7,7 +7,7 @@ 0 0 800 - 843 + 889 @@ -125,10 +125,10 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Verdana'; font-size:10pt; font-weight:400; font-style:normal;"> -<p style="-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';"><br /></p></body></html> +</style></head><body style=" font-family:'Noto Sans'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-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;"><br /></p></body></html> Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse @@ -611,10 +611,10 @@ p, li { white-space: pre-wrap; } <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Verdana'; font-size:10pt; font-weight:400; font-style:normal;"> -<p style="-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';"><br /></p></body></html> +</style></head><body style=" font-family:'Noto Sans'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-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;"><br /></p></body></html> @@ -627,66 +627,44 @@ p, li { white-space: pre-wrap; } QLayout::SetMinimumSize - - - - true + + + + QAbstractSpinBox::CorrectToNearestValue + + + 9999 false - - - - - - - 80 - 0 - - - - Title - - - title + + true - - + + - false + true false - - - - - 80 - 0 - - + + - Genre + Disc - genre + disc - - - - - 0 - 0 - - + + true @@ -695,13 +673,19 @@ p, li { white-space: pre-wrap; } - - + + + + + 80 + 0 + + - Disc + Grouping - disc + grouping @@ -721,24 +705,28 @@ p, li { white-space: pre-wrap; } - - - - - 80 - 0 - + + + + true - - Compilation + + false - - compilation + + + + + + true + + + false - - + + true @@ -747,6 +735,32 @@ p, li { white-space: pre-wrap; } + + + + true + + + false + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + 9999 + + + false + + + true + + + @@ -763,8 +777,18 @@ p, li { white-space: pre-wrap; } - - + + + + Year + + + year + + + + + 80 @@ -772,15 +796,15 @@ p, li { white-space: pre-wrap; } - Artist + Title - artist + title - - + + 80 @@ -788,10 +812,10 @@ p, li { white-space: pre-wrap; } - Comment + Artist - comment + artist @@ -811,7 +835,7 @@ p, li { white-space: pre-wrap; } - + Complete tags automatically @@ -828,44 +852,34 @@ p, li { white-space: pre-wrap; } - - - - - 80 - 0 - + + + + QAbstractSpinBox::CorrectToNearestValue - - Performer + + 9999 - - performer + + false - - - - true - - false - - - + + - true + false false - - + + 80 @@ -873,31 +887,31 @@ p, li { white-space: pre-wrap; } - Grouping + Genre - grouping + genre - - - - QAbstractSpinBox::CorrectToNearestValue - - - 9999 + + + + + 80 + 0 + - - false + + Comment - - true + + comment - - + + true @@ -906,8 +920,8 @@ p, li { white-space: pre-wrap; } - - + + true @@ -916,59 +930,55 @@ p, li { white-space: pre-wrap; } - - - - QAbstractSpinBox::CorrectToNearestValue - - - 9999 - - - false + + + + + 0 + 0 + true - - - - - - true - false - - - - true + + + + + 80 + 0 + - - false + + Performer + + + performer - - - - QAbstractSpinBox::CorrectToNearestValue - - - 9999 + + + + + 80 + 0 + - - false + + Compilation - - true + + compilation - + Track @@ -978,16 +988,19 @@ p, li { white-space: pre-wrap; } - - + + - Year + Rating - year + rating + + + @@ -1088,6 +1101,12 @@ p, li { white-space: pre-wrap; } QCheckBox
widgets/lineedit.h
+ + RatingBox + QWidget +
widgets/lineedit.h
+ 1 +
song_list diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index 41514f92a..74188526d 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -40,10 +40,12 @@ #include #include #include +#include #include "core/application.h" #include "core/iconloader.h" #include "core/utilities.h" +#include "collection/collection.h" #include "collection/collectionmodel.h" #include "collection/collectiondirectorymodel.h" #include "collectionsettingspage.h" @@ -94,6 +96,8 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, QWidget * QObject::connect(ui_->combobox_cache_size, QOverload::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::CacheSizeUnitChanged); QObject::connect(ui_->combobox_disk_cache_size, QOverload::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::DiskCacheSizeUnitChanged); + QObject::connect(ui_->button_save_stats, &QPushButton::clicked, this, &CollectionSettingsPage::WriteAllSongsStatisticsToFiles); + #ifndef HAVE_SONGFINGERPRINTING ui_->song_tracking->hide(); #endif @@ -110,7 +114,7 @@ void CollectionSettingsPage::Add() { QString path(s.value("last_path", QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).toString()); path = QFileDialog::getExistingDirectory(this, tr("Add directory..."), path); - if (!path.isNull()) { + if (!path.isEmpty()) { dialog()->collection_directory_model()->AddDirectory(path); } @@ -206,6 +210,9 @@ void CollectionSettingsPage::Load() { ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, static_cast(CacheSizeUnit_MB)).toInt()); if (ui_->combobox_disk_cache_size->currentIndex() == -1) ui_->combobox_cache_size->setCurrentIndex(static_cast(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) ui_->checkbox_delete_files->setChecked(s.value("delete_files", false).toBool()); #else @@ -270,6 +277,9 @@ void CollectionSettingsPage::Save() { s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value()); 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.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(); + +} diff --git a/src/settings/collectionsettingspage.h b/src/settings/collectionsettingspage.h index c3c8623c8..8de2c3cae 100644 --- a/src/settings/collectionsettingspage.h +++ b/src/settings/collectionsettingspage.h @@ -82,6 +82,7 @@ class CollectionSettingsPage : public SettingsPage { void ClearPixmapDiskCache(); void CacheSizeUnitChanged(int index); void DiskCacheSizeUnitChanged(int index); + void WriteAllSongsStatisticsToFiles(); private: Ui_CollectionSettingsPage *ui_; diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui index 92e88601e..772513ae5 100644 --- a/src/settings/collectionsettingspage.ui +++ b/src/settings/collectionsettingspage.ui @@ -7,7 +7,7 @@ 0 0 516 - 1167 + 1339
@@ -540,6 +540,53 @@ If there are no matches then it will use the largest image in the directory.
+ + + + Song playcounts and ratings + + + + + + Save playcounts to song tags when possible + + + + + + + Save ratings to song tags when possible + + + + + + + + + Save playcounts and ratings to files now + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + diff --git a/src/widgets/lineedit.cpp b/src/widgets/lineedit.cpp index 1ee8f0bdd..05e0c76db 100644 --- a/src/widgets/lineedit.cpp +++ b/src/widgets/lineedit.cpp @@ -260,3 +260,12 @@ QString SpinBox::textFromValue(int val) const { return QSpinBox::textFromValue(val); } + +RatingBox::RatingBox(QWidget *parent) + : RatingWidget(parent), + ExtendedEditor(this) { + + clear_button_->hide(); + reset_button_->hide(); + +} diff --git a/src/widgets/lineedit.h b/src/widgets/lineedit.h index a5f79a79f..59a11ce72 100644 --- a/src/widgets/lineedit.h +++ b/src/widgets/lineedit.h @@ -33,6 +33,8 @@ #include #include +#include "ratingwidget.h" + class QToolButton; class QPaintDevice; class QPaintEvent; @@ -231,4 +233,25 @@ class CheckBox : public QCheckBox, public ExtendedEditor { 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 diff --git a/tests/src/tagreader_test.cpp b/tests/src/tagreader_test.cpp index ae8115b9c..646f1c6cb 100644 --- a/tests/src/tagreader_test.cpp +++ b/tests/src/tagreader_test.cpp @@ -94,6 +94,28 @@ class TagReaderTest : public ::testing::Test { 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) { @@ -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