diff --git a/ext/libstrawberry-tagreader/CMakeLists.txt b/ext/libstrawberry-tagreader/CMakeLists.txt index 37485b43..80a384ec 100644 --- a/ext/libstrawberry-tagreader/CMakeLists.txt +++ b/ext/libstrawberry-tagreader/CMakeLists.txt @@ -46,6 +46,7 @@ target_link_libraries(libstrawberry-tagreader PRIVATE ${Protobuf_LIBRARIES} ${QtCore_LIBRARIES} ${QtNetwork_LIBRARIES} + ${QtGui_LIBRARIES} libstrawberry-common ) diff --git a/ext/libstrawberry-tagreader/tagreaderbase.cpp b/ext/libstrawberry-tagreader/tagreaderbase.cpp index d382b903..8e807e5c 100644 --- a/ext/libstrawberry-tagreader/tagreaderbase.cpp +++ b/ext/libstrawberry-tagreader/tagreaderbase.cpp @@ -19,6 +19,15 @@ #include +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" #include "tagreaderbase.h" const std::string TagReaderBase::kEmbeddedCover = "(embedded)"; @@ -28,7 +37,8 @@ TagReaderBase::~TagReaderBase() = default; void TagReaderBase::Decode(const QString &tag, std::string *output) { - output->assign(DataCommaSizeFromQString(tag)); + const QByteArray data = tag.toUtf8(); + output->assign(data.constData(), data.size()); } @@ -55,3 +65,86 @@ int TagReaderBase::ConvertToPOPMRating(const float rating) { return 0xFF; } + +QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request) { + + if (!request.has_save_cover() || !request.save_cover()) { + return QByteArray(); + } + + const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size()); + QString cover_filename; + if (request.has_cover_filename()) { + cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size()); + } + QByteArray cover_data; + if (request.has_cover_data()) { + cover_data = QByteArray(request.cover_data().data(), request.cover_data().size()); + } + bool cover_is_jpeg = false; + if (request.has_cover_is_jpeg()) { + cover_is_jpeg = request.cover_is_jpeg(); + } + + return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg); + +} + +QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request) { + + const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size()); + QString cover_filename; + if (request.has_cover_filename()) { + cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size()); + } + QByteArray cover_data; + if (request.has_cover_data()) { + cover_data = QByteArray(request.cover_data().data(), request.cover_data().size()); + } + bool cover_is_jpeg = false; + if (request.has_cover_is_jpeg()) { + cover_is_jpeg = request.cover_is_jpeg(); + } + + return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg); + +} + +QByteArray TagReaderBase::LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg) { + + if (!cover_data.isEmpty() && cover_is_jpeg) { + qLog(Debug) << "Using cover from JPEG data for" << song_filename; + return cover_data; + } + + if (cover_data.isEmpty() && !cover_filename.isEmpty()) { + qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename; + QFile file(cover_filename); + if (!file.open(QIODevice::ReadOnly)) { + qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString(); + return QByteArray(); + } + cover_data = file.readAll(); + file.close(); + } + + if (!cover_data.isEmpty()) { + if (QMimeDatabase().mimeTypeForData(cover_data).name() == "image/jpeg") { + qLog(Debug) << "Using cover from JPEG data for" << song_filename; + return cover_data; + } + // Convert image to JPEG. + qLog(Debug) << "Converting cover to JPEG data for" << song_filename; + QImage cover_image(cover_data); + cover_data.clear(); + QBuffer buffer(&cover_data); + if (buffer.open(QIODevice::WriteOnly)) { + cover_image.save(&buffer, "JPEG"); + buffer.close(); + } + return cover_data; + } + + return QByteArray(); + +} diff --git a/ext/libstrawberry-tagreader/tagreaderbase.h b/ext/libstrawberry-tagreader/tagreaderbase.h index 6f43e8c2..f75d22e6 100644 --- a/ext/libstrawberry-tagreader/tagreaderbase.h +++ b/ext/libstrawberry-tagreader/tagreaderbase.h @@ -27,9 +27,6 @@ #include "tagreadermessages.pb.h" -#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size()) -#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length() - /* * This class holds all useful methods to read and write tags from/to files. * You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient) @@ -42,10 +39,10 @@ class TagReaderBase { virtual bool IsMediaFile(const QString &filename) const = 0; virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0; - virtual bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0; + virtual bool SaveFile(const spb::tagreader::SaveFileRequest &request) const = 0; virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0; - virtual bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) = 0; + virtual bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const = 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; @@ -55,6 +52,12 @@ class TagReaderBase { static float ConvertPOPMRating(const int POPM_rating); static int ConvertToPOPMRating(const float rating); + static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request); + static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request); + + private: + static QByteArray LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg); + protected: static const std::string kEmbeddedCover; diff --git a/ext/libstrawberry-tagreader/tagreadergme.cpp b/ext/libstrawberry-tagreader/tagreadergme.cpp index 2b185d2b..e2643c7d 100644 --- a/ext/libstrawberry-tagreader/tagreadergme.cpp +++ b/ext/libstrawberry-tagreader/tagreadergme.cpp @@ -278,7 +278,7 @@ bool TagReaderGME::ReadFile(const QString &filename, spb::tagreader::SongMetadat return GME::ReadFile(fileinfo, song); } -bool TagReaderGME::SaveFile(const QString&, const spb::tagreader::SongMetadata&) const { +bool TagReaderGME::SaveFile(const spb::tagreader::SaveFileRequest&) const { return false; } @@ -286,7 +286,7 @@ QByteArray TagReaderGME::LoadEmbeddedArt(const QString&) const { return QByteArray(); } -bool TagReaderGME::SaveEmbeddedArt(const QString&, const QByteArray&) { +bool TagReaderGME::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest&) const { return false; } diff --git a/ext/libstrawberry-tagreader/tagreadergme.h b/ext/libstrawberry-tagreader/tagreadergme.h index 035a74b9..77eade36 100644 --- a/ext/libstrawberry-tagreader/tagreadergme.h +++ b/ext/libstrawberry-tagreader/tagreadergme.h @@ -107,10 +107,10 @@ class TagReaderGME : public TagReaderBase { bool IsMediaFile(const QString &filename) const override; bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; - bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; + bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override; QByteArray LoadEmbeddedArt(const QString &filename) const override; - bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override; + bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const 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; diff --git a/ext/libstrawberry-tagreader/tagreadermessages.proto b/ext/libstrawberry-tagreader/tagreadermessages.proto index f058bd3d..42995a37 100644 --- a/ext/libstrawberry-tagreader/tagreadermessages.proto +++ b/ext/libstrawberry-tagreader/tagreadermessages.proto @@ -77,6 +77,14 @@ message SongMetadata { } +message IsMediaFileRequest { + optional string filename = 1; +} + +message IsMediaFileResponse { + optional bool success = 1; +} + message ReadFileRequest { optional string filename = 1; } @@ -87,21 +95,20 @@ message ReadFileResponse { message SaveFileRequest { optional string filename = 1; - optional SongMetadata metadata = 2; + optional bool save_tags = 2; + optional bool save_playcount = 3; + optional bool save_rating = 4; + optional bool save_cover = 5; + optional SongMetadata metadata = 6; + optional string cover_filename = 7; + optional bytes cover_data = 8; + optional bool cover_is_jpeg = 9; } message SaveFileResponse { optional bool success = 1; } -message IsMediaFileRequest { - optional string filename = 1; -} - -message IsMediaFileResponse { - optional bool success = 1; -} - message LoadEmbeddedArtRequest { optional string filename = 1; } @@ -112,7 +119,9 @@ message LoadEmbeddedArtResponse { message SaveEmbeddedArtRequest { optional string filename = 1; - optional bytes data = 2; + optional string cover_filename = 2; + optional bytes cover_data = 3; + optional bool cover_is_jpeg = 4; } message SaveEmbeddedArtResponse { diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.cpp b/ext/libstrawberry-tagreader/tagreadertaglib.cpp index ab3e625f..4d93efce 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.cpp +++ b/ext/libstrawberry-tagreader/tagreadertaglib.cpp @@ -81,11 +81,12 @@ #endif #include -#include -#include #include #include #include +#include +#include +#include #include #include #include @@ -193,7 +194,8 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta qLog(Debug) << "Reading tags from" << filename; - song->set_basefilename(DataCommaSizeFromQString(fileinfo.fileName())); + const QByteArray basefilename = fileinfo.fileName().toUtf8(); + song->set_basefilename(basefilename.constData(), basefilename.length()); song->set_url(url.constData(), url.size()); song->set_filesize(fileinfo.size()); song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL); @@ -494,7 +496,9 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta if (compilation.isEmpty()) { // well, it wasn't set, but if the artist is VA assume it's a compilation - if (QStringFromStdString(song->artist()).compare("various artists") == 0 || QStringFromStdString(song->albumartist()).compare("various artists") == 0) { + const QString albumartist = QString::fromUtf8(song->albumartist().data(), song->albumartist().size()); + const QString artist = QString::fromUtf8(song->artist().data(), song->artist().size()); + if (artist.compare("various artists") == 0 || albumartist.compare("various artists") == 0) { song->set_compilation(true); } } @@ -521,8 +525,9 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta void TagReaderTagLib::Decode(const TagLib::String &tag, std::string *output) { - QString tmp = TStringToQString(tag).trimmed(); - output->assign(DataCommaSizeFromQString(tmp)); + const QString tmp = TStringToQString(tag).trimmed(); + const QByteArray data = tmp.toUtf8(); + output->assign(data.constData(), data.size()); } @@ -624,79 +629,175 @@ void TagReaderTagLib::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment } -bool TagReaderTagLib::SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const { +bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) const { - if (filename.isEmpty()) return false; + if (request.filename().empty()) return false; + + const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size()); + const spb::tagreader::SongMetadata song = request.metadata(); + const bool save_tags = request.has_save_tags() && request.save_tags(); + const bool save_playcount = request.has_save_playcount() && request.save_playcount(); + const bool save_rating = request.has_save_rating() && request.save_rating(); + const bool save_cover = request.has_save_cover() && request.save_cover(); + + QStringList save_tags_options; + if (save_tags) { + save_tags_options << "tags"; + } + if (save_playcount) { + save_tags_options << "playcount"; + } + if (save_rating) { + save_tags_options << "rating"; + } + if (save_cover) { + save_tags_options << "embedded cover"; + } + + qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename; + + const QByteArray cover_data = LoadCoverDataFromRequest(request); - qLog(Debug) << "Saving tags to" << filename; std::unique_ptr fileref(factory_->GetFileRef(filename));; if (!fileref || fileref->isNull()) return false; - fileref->tag()->setTitle(song.title().empty() ? TagLib::String() : StdStringToTaglibString(song.title())); - fileref->tag()->setArtist(song.artist().empty() ? TagLib::String() : StdStringToTaglibString(song.artist())); - fileref->tag()->setAlbum(song.album().empty() ? TagLib::String() : StdStringToTaglibString(song.album())); - fileref->tag()->setGenre(song.genre().empty() ? TagLib::String() : StdStringToTaglibString(song.genre())); - fileref->tag()->setComment(song.comment().empty() ? TagLib::String() : StdStringToTaglibString(song.comment())); - fileref->tag()->setYear(song.year() <= 0 ? 0 : song.year()); - fileref->tag()->setTrack(song.track() <= 0 ? 0 : song.track()); - - bool result = false; + if (save_tags) { + fileref->tag()->setTitle(song.title().empty() ? TagLib::String() : StdStringToTaglibString(song.title())); + fileref->tag()->setArtist(song.artist().empty() ? TagLib::String() : StdStringToTaglibString(song.artist())); + fileref->tag()->setAlbum(song.album().empty() ? TagLib::String() : StdStringToTaglibString(song.album())); + fileref->tag()->setGenre(song.genre().empty() ? TagLib::String() : StdStringToTaglibString(song.genre())); + fileref->tag()->setComment(song.comment().empty() ? TagLib::String() : StdStringToTaglibString(song.comment())); + fileref->tag()->setYear(song.year() <= 0 ? 0 : song.year()); + fileref->tag()->setTrack(song.track() <= 0 ? 0 : song.track()); + } + bool is_flac = false; if (TagLib::FLAC::File *file_flac = dynamic_cast(fileref->file())) { - TagLib::Ogg::XiphComment *tag = file_flac->xiphComment(true); - if (!tag) return false; - SetVorbisComments(tag, song); + is_flac = true; + TagLib::Ogg::XiphComment *xiph_comment = file_flac->xiphComment(true); + if (!xiph_comment) return false; + if (save_tags) { + SetVorbisComments(xiph_comment, song); + } + if (save_playcount) { + SetPlaycount(xiph_comment, song); + } + if (save_rating) { + SetRating(xiph_comment, song); + } + if (save_cover) { + SetEmbeddedArt(file_flac, xiph_comment, cover_data); + } } else if (TagLib::WavPack::File *file_wavpack = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = file_wavpack->APETag(true); if (!tag) return false; - SaveAPETag(tag, song); + if (save_tags) { + SaveAPETag(tag, song); + } + if (save_playcount) { + SetPlaycount(tag, song); + } + if (save_rating) { + SetRating(tag, song); + } } else if (TagLib::APE::File *file_ape = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = file_ape->APETag(true); if (!tag) return false; - SaveAPETag(tag, song); + if (save_tags) { + SaveAPETag(tag, song); + } + if (save_playcount) { + SetPlaycount(tag, song); + } + if (save_rating) { + SetRating(tag, song); + } } else if (TagLib::MPC::File *file_mpc = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = file_mpc->APETag(true); if (!tag) return false; - SaveAPETag(tag, song); + if (save_tags) { + SaveAPETag(tag, song); + } + if (save_playcount) { + SetPlaycount(tag, song); + } + if (save_rating) { + SetRating(tag, song); + } } else if (TagLib::MPEG::File *file_mpeg = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag *tag = file_mpeg->ID3v2Tag(true); if (!tag) return false; - SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag); - SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag); - SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag); - SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag); - // Skip TPE1 (which is the artist) here because we already set it - SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag); - SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag); - SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag); + if (save_tags) { + SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag); + SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag); + SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag); + SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag); + // Skip TPE1 (which is the artist) here because we already set it + SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag); + SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag); + SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag); + } + if (save_playcount) { + SetPlaycount(tag, song); + } + if (save_rating) { + SetRating(tag, song); + } + if (save_cover) { + SetEmbeddedArt(file_mpeg, tag, cover_data); + } } else if (TagLib::MP4::File *file_mp4 = dynamic_cast(fileref->file())) { TagLib::MP4::Tag *tag = file_mp4->tag(); if (!tag) return false; - tag->setItem("disk", TagLib::MP4::Item(song.disc() <= 0 - 1 ? 0 : song.disc(), 0)); - tag->setItem("\251wrt", TagLib::StringList(TagLib::String(song.composer(), TagLib::String::UTF8))); - tag->setItem("\251grp", TagLib::StringList(TagLib::String(song.grouping(), TagLib::String::UTF8))); - tag->setItem("\251lyr", TagLib::StringList(TagLib::String(song.lyrics(), TagLib::String::UTF8))); - tag->setItem("aART", TagLib::StringList(TagLib::String(song.albumartist(), TagLib::String::UTF8))); - tag->setItem("cpil", TagLib::MP4::Item(song.compilation())); + if (save_tags) { + tag->setItem("disk", TagLib::MP4::Item(song.disc() <= 0 - 1 ? 0 : song.disc(), 0)); + tag->setItem("\251wrt", TagLib::StringList(TagLib::String(song.composer(), TagLib::String::UTF8))); + tag->setItem("\251grp", TagLib::StringList(TagLib::String(song.grouping(), TagLib::String::UTF8))); + tag->setItem("\251lyr", TagLib::StringList(TagLib::String(song.lyrics(), TagLib::String::UTF8))); + tag->setItem("aART", TagLib::StringList(TagLib::String(song.albumartist(), TagLib::String::UTF8))); + tag->setItem("cpil", TagLib::MP4::Item(song.compilation())); + } + if (save_playcount) { + SetPlaycount(tag, song); + } + if (save_rating) { + SetRating(tag, song); + } + if (save_cover) { + SetEmbeddedArt(file_mp4, tag, cover_data); + } } // 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 *xiph_comment = dynamic_cast(fileref->file()->tag())) { - SetVorbisComments(xiph_comment, song); + if (!is_flac) { + if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(fileref->file()->tag())) { + if (save_tags) { + SetVorbisComments(xiph_comment, song); + } + if (save_playcount) { + SetPlaycount(xiph_comment, song); + } + if (save_rating) { + SetRating(xiph_comment, song); + } + if (save_cover) { + SetEmbeddedArt(xiph_comment, cover_data); + } + } } - result = fileref->save(); + const bool result = fileref->save(); #ifdef Q_OS_LINUX if (result) { // Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does) @@ -705,6 +806,7 @@ bool TagReaderTagLib::SaveFile(const QString &filename, const spb::tagreader::So #endif // Q_OS_LINUX return result; + } void TagReaderTagLib::SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const { @@ -939,12 +1041,84 @@ QByteArray TagReaderTagLib::LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &m } -bool TagReaderTagLib::SaveEmbeddedArt(const QString &filename, const QByteArray &data) { +void TagReaderTagLib::SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const { - if (filename.isEmpty()) return false; + (void)xiph_comment; + + flac_file->removePictures(); + + if (!data.isEmpty()) { + TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture(); + picture->setType(TagLib::FLAC::Picture::FrontCover); + picture->setMimeType("image/jpeg"); + picture->setData(TagLib::ByteVector(data.constData(), data.size())); + flac_file->addPicture(picture); + } + +} + +void TagReaderTagLib::SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const { + + xiph_comment->removeAllPictures(); + + if (!data.isEmpty()) { + TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture(); + picture->setType(TagLib::FLAC::Picture::FrontCover); + picture->setMimeType("image/jpeg"); + picture->setData(TagLib::ByteVector(data.constData(), data.size())); + xiph_comment->addPicture(picture); + } + +} + +void TagReaderTagLib::SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data) const { + + (void)file_mp3; + + // Remove existing covers + TagLib::ID3v2::FrameList apiclist = tag->frameListMap()["APIC"]; + for (TagLib::ID3v2::FrameList::ConstIterator it = apiclist.begin(); it != apiclist.end(); ++it) { + TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast(*it); + tag->removeFrame(frame, false); + } + + if (!data.isEmpty()) { + // Add new cover + TagLib::ID3v2::AttachedPictureFrame *frontcover = nullptr; + frontcover = new TagLib::ID3v2::AttachedPictureFrame("APIC"); + frontcover->setType(TagLib::ID3v2::AttachedPictureFrame::FrontCover); + frontcover->setMimeType("image/jpeg"); + frontcover->setPicture(TagLib::ByteVector(data.constData(), data.size())); + tag->addFrame(frontcover); + } + +} + +void TagReaderTagLib::SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data) const { + + (void)aac_file; + + TagLib::MP4::CoverArtList covers; + if (data.isEmpty()) { + if (tag->contains("covr")) tag->removeItem("covr"); + } + else { + covers.append(TagLib::MP4::CoverArt(TagLib::MP4::CoverArt::JPEG, TagLib::ByteVector(data.constData(), data.size()))); + tag->setItem("covr", covers); + } + +} + +bool TagReaderTagLib::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const { + + if (request.filename().empty()) return false; + + const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size()); qLog(Debug) << "Saving art to" << filename; + const QByteArray cover_data = LoadCoverDataFromRequest(request); + #ifdef Q_OS_WIN32 TagLib::FileRef fileref(filename.toStdWString().c_str()); #else @@ -955,65 +1129,28 @@ bool TagReaderTagLib::SaveEmbeddedArt(const QString &filename, const QByteArray // FLAC if (TagLib::FLAC::File *flac_file = dynamic_cast(fileref.file())) { - TagLib::Ogg::XiphComment *vorbis_comments = flac_file->xiphComment(true); - if (!vorbis_comments) return false; - flac_file->removePictures(); - if (!data.isEmpty()) { - TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture(); - picture->setType(TagLib::FLAC::Picture::FrontCover); - picture->setMimeType("image/jpeg"); - picture->setData(TagLib::ByteVector(data.constData(), data.size())); - flac_file->addPicture(picture); - } + TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true); + if (!xiph_comment) return false; + SetEmbeddedArt(flac_file, xiph_comment, cover_data); } // Ogg Vorbis / Opus / Speex else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(fileref.file()->tag())) { - xiph_comment->removeAllPictures(); - if (!data.isEmpty()) { - TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture(); - picture->setType(TagLib::FLAC::Picture::FrontCover); - picture->setMimeType("image/jpeg"); - picture->setData(TagLib::ByteVector(data.constData(), data.size())); - xiph_comment->addPicture(picture); - } + SetEmbeddedArt(xiph_comment, cover_data); } // MP3 else if (TagLib::MPEG::File *file_mp3 = dynamic_cast(fileref.file())) { TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag(); if (!tag) return false; - - // Remove existing covers - TagLib::ID3v2::FrameList apiclist = tag->frameListMap()["APIC"]; - for (TagLib::ID3v2::FrameList::ConstIterator it = apiclist.begin(); it != apiclist.end(); ++it) { - TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast(*it); - tag->removeFrame(frame, false); - } - - if (!data.isEmpty()) { - // Add new cover - TagLib::ID3v2::AttachedPictureFrame *frontcover = nullptr; - frontcover = new TagLib::ID3v2::AttachedPictureFrame("APIC"); - frontcover->setType(TagLib::ID3v2::AttachedPictureFrame::FrontCover); - frontcover->setMimeType("image/jpeg"); - frontcover->setPicture(TagLib::ByteVector(data.constData(), data.size())); - tag->addFrame(frontcover); - } + SetEmbeddedArt(file_mp3, tag, cover_data); } // MP4/AAC else if (TagLib::MP4::File *aac_file = dynamic_cast(fileref.file())) { TagLib::MP4::Tag *tag = aac_file->tag(); if (!tag) return false; - TagLib::MP4::CoverArtList covers; - if (data.isEmpty()) { - if (tag->contains("covr")) tag->removeItem("covr"); - } - else { - covers.append(TagLib::MP4::CoverArt(TagLib::MP4::CoverArt::JPEG, TagLib::ByteVector(data.constData(), data.size()))); - tag->setItem("covr", covers); - } + SetEmbeddedArt(aac_file, tag, cover_data); } // Not supported. @@ -1041,6 +1178,60 @@ TagLib::ID3v2::PopularimeterFrame *TagReaderTagLib::GetPOPMFrameFromTag(TagLib:: } +void TagReaderTagLib::SetPlaycount(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const { + + if (song.playcount() > 0) { + xiph_comment->addField("FMPS_PLAYCOUNT", TagLib::String::number(static_cast(song.playcount())), true); + } + else { + xiph_comment->removeFields("FMPS_PLAYCOUNT"); + } + +} + +void TagReaderTagLib::SetPlaycount(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + if (song.playcount() > 0) { + tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast(song.playcount())))); + } + else { + tag->removeItem("FMPS_Playcount"); + } + +} + +void TagReaderTagLib::SetPlaycount(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + SetUserTextFrame("FMPS_Playcount", QString::number(song.playcount()), tag); + TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag); + if (frame) { + frame->setCounter(song.playcount()); + } + +} + +void TagReaderTagLib::SetPlaycount(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + if (song.playcount() > 0) { + tag->setItem(kMP4_FMPS_Playcount_ID, TagLib::MP4::Item(TagLib::String::number(static_cast(song.playcount())))); + } + else { + tag->removeItem(kMP4_FMPS_Playcount_ID); + } + +} + +void TagReaderTagLib::SetPlaycount(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + if (song.playcount() > 0) { + tag->setAttribute("FMPS/Playcount", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.playcount())))); + } + else { + tag->removeItem("FMPS/Playcount"); + } + +} + bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const { if (filename.isEmpty()) return false; @@ -1051,62 +1242,37 @@ bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb 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) return false; - if (song.playcount() > 0) { - vorbis_comments->addField("FMPS_PLAYCOUNT", TagLib::String::number(static_cast(song.playcount())), true); - } - else { - vorbis_comments->removeFields("FMPS_PLAYCOUNT"); - } + TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true); + if (!xiph_comment) return false; + SetPlaycount(xiph_comment, song); } else if (TagLib::WavPack::File *wavpack_file = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = wavpack_file->APETag(true); if (!tag) return false; - if (song.playcount() > 0) { - tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast(song.playcount())))); - } + SetPlaycount(tag, song); } else if (TagLib::APE::File *ape_file = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = ape_file->APETag(true); if (!tag) return false; - if (song.playcount() > 0) { - tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast(song.playcount())))); - } + SetPlaycount(tag, song); } else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(fileref->file()->tag())) { - if (song.playcount() > 0) { - xiph_comment->addField("FMPS_PLAYCOUNT", TagLib::String::number(static_cast(song.playcount())), true); - } - else { - xiph_comment->removeFields("FMPS_PLAYCOUNT"); - } + SetPlaycount(xiph_comment, song); } else if (TagLib::MPEG::File *mpeg_file = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true); if (!tag) return false; - if (song.playcount() > 0) { - SetUserTextFrame("FMPS_Playcount", QString::number(song.playcount()), tag); - TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag); - if (frame) { - frame->setCounter(song.playcount()); - } - } - + SetPlaycount(tag, song); } else if (TagLib::MP4::File *mp4_file = dynamic_cast(fileref->file())) { TagLib::MP4::Tag *tag = mp4_file->tag(); if (!tag) return false; - if (song.playcount() > 0) { - tag->setItem(kMP4_FMPS_Playcount_ID, TagLib::MP4::Item(TagLib::String::number(static_cast(song.playcount())))); - } + SetPlaycount(tag, song); } else if (TagLib::MPC::File *mpc_file = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = mpc_file->APETag(true); if (!tag) return false; - if (song.playcount() > 0) { - tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast(song.playcount())))); - } + SetPlaycount(tag, song); } else if (TagLib::ASF::File *asf_file = dynamic_cast(fileref->file())) { TagLib::ASF::Tag *tag = asf_file->tag(); @@ -1130,6 +1296,50 @@ bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb return ret; } +void TagReaderTagLib::SetRating(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const { + + if (song.rating() > 0.0F) { + xiph_comment->addField("FMPS_RATING", QStringToTaglibString(QString::number(song.rating())), true); + } + else { + xiph_comment->removeFields("FMPS_RATING"); + } + +} + +void TagReaderTagLib::SetRating(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + if (song.rating() > 0.0F) { + tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating()))))); + } + else { + tag->removeItem("FMPS_Rating"); + } + +} + +void TagReaderTagLib::SetRating(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + SetUserTextFrame("FMPS_Rating", QString::number(song.rating()), tag); + TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag); + if (frame) { + frame->setRating(ConvertToPOPMRating(song.rating())); + } + +} + +void TagReaderTagLib::SetRating(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + tag->setItem(kMP4_FMPS_Rating_ID, TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))); + +} + +void TagReaderTagLib::SetRating(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + tag->addAttribute("FMPS/Rating", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.rating())))); + +} + bool TagReaderTagLib::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const { if (filename.isNull()) return false; @@ -1145,56 +1355,42 @@ bool TagReaderTagLib::SaveSongRatingToFile(const QString &filename, const spb::t 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) return false; - if (song.rating() > 0) { - vorbis_comments->addField("FMPS_RATING", QStringToTaglibString(QString::number(song.rating())), true); - } - else { - vorbis_comments->removeFields("FMPS_RATING"); - } + TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true); + if (!xiph_comment) return false; + SetRating(xiph_comment, song); } else if (TagLib::WavPack::File *wavpack_file = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = wavpack_file->APETag(true); if (!tag) return false; - tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating()))))); + SetRating(tag, song); } else if (TagLib::APE::File *ape_file = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = ape_file->APETag(true); if (!tag) return false; - tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating()))))); + SetRating(tag, song); } 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"); - } + SetRating(xiph_comment, song); } else if (TagLib::MPEG::File *mpeg_file = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true); if (!tag) return false; - SetUserTextFrame("FMPS_Rating", QString::number(song.rating()), tag); - TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag); - if (frame) { - frame->setRating(ConvertToPOPMRating(song.rating())); - } + SetRating(tag, song); } else if (TagLib::MP4::File *mp4_file = dynamic_cast(fileref->file())) { TagLib::MP4::Tag *tag = mp4_file->tag(); if (!tag) return false; - tag->setItem(kMP4_FMPS_Rating_ID, TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))); + SetRating(tag, song); } else if (TagLib::ASF::File *asf_file = dynamic_cast(fileref->file())) { TagLib::ASF::Tag *tag = asf_file->tag(); if (!tag) return false; - tag->addAttribute("FMPS/Rating", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.rating())))); + SetRating(tag, song); } else if (TagLib::MPC::File *mpc_file = dynamic_cast(fileref->file())) { TagLib::APE::Tag *tag = mpc_file->APETag(true); if (!tag) return false; - tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating()))))); + SetRating(tag, song); } else { return true; diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.h b/ext/libstrawberry-tagreader/tagreadertaglib.h index 4174a4b4..269c426a 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.h +++ b/ext/libstrawberry-tagreader/tagreadertaglib.h @@ -29,8 +29,12 @@ #include #include #include +#include +#include +#include #include #include +#include #include #include @@ -51,10 +55,10 @@ class TagReaderTagLib : public TagReaderBase { bool IsMediaFile(const QString &filename) const override; bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; - bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; + bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override; QByteArray LoadEmbeddedArt(const QString &filename) const override; - bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override; + bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const 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; @@ -80,6 +84,23 @@ class TagReaderTagLib : public TagReaderBase { static TagLib::ID3v2::PopularimeterFrame *GetPOPMFrameFromTag(TagLib::ID3v2::Tag *tag); + void SetPlaycount(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const; + void SetPlaycount(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SetPlaycount(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SetPlaycount(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SetPlaycount(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const; + + void SetRating(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const; + void SetRating(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SetRating(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SetRating(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SetRating(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const; + + void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const; + void SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const; + void SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data) const; + void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data) const; + private: FileRefFactory *factory_; diff --git a/ext/libstrawberry-tagreader/tagreadertagparser.cpp b/ext/libstrawberry-tagreader/tagreadertagparser.cpp index 64422773..6bc939a2 100644 --- a/ext/libstrawberry-tagreader/tagreadertagparser.cpp +++ b/ext/libstrawberry-tagreader/tagreadertagparser.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -79,7 +80,7 @@ bool TagReaderTagParser::IsMediaFile(const QString &filename) const { } const auto tracks = taginfo.tracks(); - for (const auto track : tracks) { + for (TagParser::AbstractTrack *track : tracks) { if (track->mediaType() == TagParser::MediaType::Audio) { taginfo.close(); return true; @@ -102,8 +103,9 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return false; const QByteArray url(QUrl::fromLocalFile(filename).toEncoded()); + const QByteArray basefilename = fileinfo.fileName().toUtf8(); - song->set_basefilename(DataCommaSizeFromQString(fileinfo.fileName())); + song->set_basefilename(basefilename.constData(), basefilename.size()); song->set_url(url.constData(), url.size()); song->set_filesize(fileinfo.size()); song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL); @@ -154,8 +156,8 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM qLog(Debug) << QString::fromStdString(msg.message()); } - const auto tracks = taginfo.tracks(); - for (const auto track : tracks) { + std::vector tracks = taginfo.tracks(); + for (TagParser::AbstractTrack *track : tracks) { switch (track->format().general) { case TagParser::GeneralMediaFormat::Flac: song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_FLAC); @@ -209,7 +211,7 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM return false; } - for (const auto tag : taginfo.tags()) { + for (TagParser::Tag *tag : taginfo.tags()) { song->set_albumartist(tag->value(TagParser::KnownField::AlbumArtist).toString(TagParser::TagTextEncoding::Utf8)); song->set_artist(tag->value(TagParser::KnownField::Artist).toString(TagParser::TagTextEncoding::Utf8)); song->set_album(tag->value(TagParser::KnownField::Album).toString(TagParser::TagTextEncoding::Utf8)); @@ -256,11 +258,34 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM } -bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const { +bool TagReaderTagParser::SaveFile(const spb::tagreader::SaveFileRequest &request) const { - if (filename.isEmpty()) return false; + if (request.filename().empty()) return false; - qLog(Debug) << "Saving tags to" << filename; + const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size()); + const spb::tagreader::SongMetadata song = request.metadata(); + const bool save_tags = request.has_save_tags() && request.save_tags(); + const bool save_playcount = request.has_save_playcount() && request.save_playcount(); + const bool save_rating = request.has_save_rating() && request.save_rating(); + const bool save_cover = request.has_save_cover() && request.save_cover(); + + QStringList save_tags_options; + if (save_tags) { + save_tags_options << "tags"; + } + if (save_playcount) { + save_tags_options << "playcount"; + } + if (save_rating) { + save_tags_options << "rating"; + } + if (save_cover) { + save_tags_options << "embedded cover"; + } + + qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename; + + const QByteArray cover_data = LoadCoverDataFromRequest(request); try { TagParser::MediaFileInfo taginfo; @@ -295,22 +320,34 @@ bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader: taginfo.createAppropriateTags(); } - for (const auto tag : taginfo.tags()) { - tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); - tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track())); - tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc())); - tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year())); - tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear())); + for (TagParser::Tag *tag : taginfo.tags()) { + if (save_tags) { + tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding())); + tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track())); + tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc())); + tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year())); + tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear())); + } + if (save_playcount) { + SaveSongPlaycountToFile(tag, song); + } + if (save_rating) { + SaveSongRatingToFile(tag, song); + } + if (save_cover) { + SaveEmbeddedArt(tag, cover_data); + } } + taginfo.applyChanges(diag, progress); taginfo.close(); @@ -358,7 +395,7 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const { return QByteArray(); } - for (const auto tag : taginfo.tags()) { + for (TagParser::Tag *tag : taginfo.tags()) { if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) { QByteArray data(tag->value(TagParser::KnownField::Cover).dataPointer(), tag->value(TagParser::KnownField::Cover).dataSize()); taginfo.close(); @@ -379,12 +416,22 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const { } -bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArray &data) { +void TagReaderTagParser::SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const { - if (filename.isEmpty()) return false; + tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString())); + +} + +bool TagReaderTagParser::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const { + + if (request.filename().empty()) return false; + + const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size()); qLog(Debug) << "Saving art to" << filename; + const QByteArray cover_data = LoadCoverDataFromRequest(request); + try { TagParser::MediaFileInfo taginfo; @@ -415,8 +462,8 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr taginfo.createAppropriateTags(); } - for (const auto tag : taginfo.tags()) { - tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString())); + for (TagParser::Tag *tag : taginfo.tags()) { + SaveEmbeddedArt(tag, cover_data); } taginfo.applyChanges(diag, progress); @@ -435,8 +482,16 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr } +void TagReaderTagParser::SaveSongPlaycountToFile(TagParser::Tag*, const spb::tagreader::SongMetadata&) const {} + bool TagReaderTagParser::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const { return false; } +void TagReaderTagParser::SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const { + + tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating()))); + +} + bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const { if (filename.isEmpty()) return false; @@ -476,9 +531,10 @@ bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb taginfo.createAppropriateTags(); } - for (const auto tag : taginfo.tags()) { - tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating()))); + for (TagParser::Tag *tag : taginfo.tags()) { + SaveSongRatingToFile(tag, song); } + taginfo.applyChanges(diag, progress); taginfo.close(); diff --git a/ext/libstrawberry-tagreader/tagreadertagparser.h b/ext/libstrawberry-tagreader/tagreadertagparser.h index 95ff65ba..31a1c4fe 100644 --- a/ext/libstrawberry-tagreader/tagreadertagparser.h +++ b/ext/libstrawberry-tagreader/tagreadertagparser.h @@ -22,6 +22,8 @@ #include +#include + #include #include @@ -40,14 +42,20 @@ class TagReaderTagParser : public TagReaderBase { bool IsMediaFile(const QString &filename) const override; bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; - bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; + bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override; QByteArray LoadEmbeddedArt(const QString &filename) const override; - bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override; + bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const 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: + void SaveSongPlaycountToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const; + void SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const; + + public: Q_DISABLE_COPY(TagReaderTagParser) }; diff --git a/ext/strawberry-tagreader/tagreaderworker.cpp b/ext/strawberry-tagreader/tagreaderworker.cpp index 1ecc2db1..1af2722e 100644 --- a/ext/strawberry-tagreader/tagreaderworker.cpp +++ b/ext/strawberry-tagreader/tagreaderworker.cpp @@ -56,36 +56,41 @@ void TagReaderWorker::DeviceClosed() { bool TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase *reader) { if (message.has_is_media_file_request()) { - bool success = reader->IsMediaFile(QStringFromStdString(message.is_media_file_request().filename())); + const QString filename = QString::fromUtf8(message.is_media_file_request().filename().data(), message.is_media_file_request().filename().size()); + bool success = reader->IsMediaFile(filename); reply.mutable_is_media_file_response()->set_success(success); return success; } else if (message.has_read_file_request()) { - bool success = reader->ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata()); + const QString filename = QString::fromUtf8(message.read_file_request().filename().data(), message.read_file_request().filename().size()); + bool success = reader->ReadFile(filename, reply.mutable_read_file_response()->mutable_metadata()); return success; } else if (message.has_save_file_request()) { - bool success = reader->SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata()); + bool success = reader->SaveFile(message.save_file_request()); reply.mutable_save_file_response()->set_success(success); return success; } else if (message.has_load_embedded_art_request()) { - QByteArray data = reader->LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename())); + const QString filename = QString::fromUtf8(message.load_embedded_art_request().filename().data(), message.load_embedded_art_request().filename().size()); + QByteArray data = reader->LoadEmbeddedArt(filename); reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size()); return true; } else if (message.has_save_embedded_art_request()) { - bool success = reader->SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast(message.save_embedded_art_request().data().size()))); + bool success = reader->SaveEmbeddedArt(message.save_embedded_art_request()); reply.mutable_save_embedded_art_response()->set_success(success); return success; } else if (message.has_save_song_playcount_to_file_request()) { - bool success = reader->SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata()); + const QString filename = QString::fromUtf8(message.save_song_playcount_to_file_request().filename().data(), message.save_song_playcount_to_file_request().filename().size()); + bool success = reader->SaveSongPlaycountToFile(filename, message.save_song_playcount_to_file_request().metadata()); reply.mutable_save_song_playcount_to_file_response()->set_success(success); return success; } else if (message.has_save_song_rating_to_file_request()) { - bool success = reader->SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata()); + const QString filename = QString::fromUtf8(message.save_song_rating_to_file_request().filename().data(), message.save_song_rating_to_file_request().filename().size()); + bool success = reader->SaveSongRatingToFile(filename, message.save_song_rating_to_file_request().metadata()); reply.mutable_save_song_rating_to_file_response()->set_success(success); return success; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cd17988f..310c4291 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,6 +55,7 @@ set(SOURCES utilities/transliterate.cpp utilities/xmlutils.cpp utilities/filemanagerutils.cpp + utilities/coverutils.cpp engine/enginetype.cpp engine/enginebase.cpp diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index 0b899eb2..e4e7184d 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -140,8 +140,12 @@ void CollectionBackend::IncrementSkipCountAsync(const int id, const float progre QMetaObject::invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress)); } -void CollectionBackend::ResetStatisticsAsync(const int id, const bool save_tags) { - QMetaObject::invokeMethod(this, "ResetStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags)); +void CollectionBackend::ResetPlayStatisticsAsync(const int id, const bool save_tags) { + QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags)); +} + +void CollectionBackend::ResetPlayStatisticsAsync(const QList &id_list, const bool save_tags) { + QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(QList, id_list), Q_ARG(bool, save_tags)); } void CollectionBackend::LoadDirectories() { @@ -1776,23 +1780,48 @@ void CollectionBackend::IncrementSkipCount(const int id, const float progress) { } -void CollectionBackend::ResetStatistics(const int id, const bool save_tags) { +void CollectionBackend::ResetPlayStatistics(const int id, const bool save_tags) { if (id == -1) return; + ResetPlayStatistics(QList() << id, save_tags); + +} + +void CollectionBackend::ResetPlayStatistics(const QList &id_list, const bool save_tags) { + + if (id_list.isEmpty()) return; + + QStringList id_str_list; + id_str_list.reserve(id_list.count()); + for (const int id : id_list) { + id_str_list << QString::number(id); + } + + const bool success = ResetPlayStatistics(id_str_list); + if (success) { + const SongList songs = GetSongsById(id_list); + emit SongsStatisticsChanged(songs, save_tags); + } + +} + +bool CollectionBackend::ResetPlayStatistics(const QStringList &id_str_list) { + + if (id_str_list.isEmpty()) return false; + QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); - q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID = :id").arg(songs_table_)); - q.BindValue(":id", id); + q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID IN (:ids)").arg(songs_table_)); + q.BindValue(":ids", id_str_list.join(",")); if (!q.Exec()) { db_->ReportErrors(q); - return; + return false; } - Song new_song = GetSongById(id, db); - emit SongsStatisticsChanged(SongList() << new_song, save_tags); + return true; } diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index aeb595c8..be8d70be 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -200,7 +200,8 @@ class CollectionBackend : public CollectionBackendInterface { void IncrementPlayCountAsync(const int id); void IncrementSkipCountAsync(const int id, const float progress); - void ResetStatisticsAsync(const int id, const bool save_tags = false); + void ResetPlayStatisticsAsync(const int id, const bool save_tags = false); + void ResetPlayStatisticsAsync(const QList &id_list, const bool save_tags = false); void DeleteAllAsync(); @@ -236,7 +237,9 @@ class CollectionBackend : public CollectionBackendInterface { void ForceCompilation(const QString &album, const QList &artists, const bool on); void IncrementPlayCount(const int id); void IncrementSkipCount(const int id, const float progress); - void ResetStatistics(const int id, const bool save_tags = false); + void ResetPlayStatistics(const int id, const bool save_tags = false); + void ResetPlayStatistics(const QList &id_list, const bool save_tags = false); + bool ResetPlayStatistics(const QStringList &id_str_list); void DeleteAll(); void SongPathChanged(const Song &song, const QFileInfo &new_file, const std::optional new_collection_directory_id); diff --git a/src/collection/collectionwatcher.cpp b/src/collection/collectionwatcher.cpp index 952ccece..e04b93ca 100644 --- a/src/collection/collectionwatcher.cpp +++ b/src/collection/collectionwatcher.cpp @@ -855,8 +855,8 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching changes << "metadata"; notify_new = true; } - if (!matching_song.IsStatisticsEqual(new_song)) { - changes << "statistics"; + if (!matching_song.IsPlayStatisticsEqual(new_song)) { + changes << "play statistics"; notify_new = true; } if (!matching_song.IsRatingEqual(new_song)) { diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 4c38d519..75ed13bd 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -1169,7 +1169,6 @@ void MainWindow::ReloadAllSettings() { collection_view_->ReloadSettings(); ui_->playlist->view()->ReloadSettings(); app_->playlist_manager()->playlist_container()->ReloadSettings(); - app_->album_cover_loader()->ReloadSettings(); album_cover_choice_controller_->ReloadSettings(); context_view_->ReloadSettings(); file_view_->ReloadSettings(); @@ -2152,7 +2151,7 @@ void MainWindow::SongSaveComplete(TagReaderReply *reply, const QPersistentModelI if (reply->is_successful() && idx.isValid()) { app_->playlist_manager()->current()->ReloadItems(QList() << idx.row()); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); } diff --git a/src/core/song.cpp b/src/core/song.cpp index a9f4a44c..45bb19d4 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -52,7 +52,7 @@ #include "engine/enginebase.h" #include "utilities/strutils.h" #include "utilities/timeutils.h" -#include "utilities/cryptutils.h" +#include "utilities/coverutils.h" #include "utilities/timeconstants.h" #include "song.h" #include "application.h" @@ -61,9 +61,6 @@ #include "sqlrow.h" #include "tagreadermessages.pb.h" -#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size()) -#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length() - const QStringList Song::kColumns = QStringList() << "title" << "album" << "artist" @@ -903,27 +900,27 @@ void Song::InitFromProtobuf(const spb::tagreader::SongMetadata &pb) { d->init_from_file_ = true; d->valid_ = pb.valid(); - set_title(QStringFromStdString(pb.title())); - set_album(QStringFromStdString(pb.album())); - set_artist(QStringFromStdString(pb.artist())); - set_albumartist(QStringFromStdString(pb.albumartist())); + set_title(QString::fromUtf8(pb.title().data(), pb.title().size())); + set_album(QString::fromUtf8(pb.album().data(), pb.album().size())); + set_artist(QString::fromUtf8(pb.artist().data(), pb.artist().size())); + set_albumartist(QString::fromUtf8(pb.albumartist().data(), pb.albumartist().size())); d->track_ = pb.track(); d->disc_ = pb.disc(); d->year_ = pb.year(); d->originalyear_ = pb.originalyear(); - d->genre_ = QStringFromStdString(pb.genre()); + d->genre_ = QString::fromUtf8(pb.genre().data(), pb.genre().size()); d->compilation_ = pb.compilation(); - d->composer_ = QStringFromStdString(pb.composer()); - d->performer_ = QStringFromStdString(pb.performer()); - d->grouping_ = QStringFromStdString(pb.grouping()); - d->comment_ = QStringFromStdString(pb.comment()); - d->lyrics_ = QStringFromStdString(pb.lyrics()); + d->composer_ = QString::fromUtf8(pb.composer().data(), pb.composer().size()); + d->performer_ = QString::fromUtf8(pb.performer().data(), pb.performer().size()); + d->grouping_ = QString::fromUtf8(pb.grouping().data(), pb.grouping().size()); + d->comment_ = QString::fromUtf8(pb.comment().data(), pb.comment().size()); + d->lyrics_ = QString::fromUtf8(pb.lyrics().data(), pb.lyrics().size()); set_length_nanosec(static_cast(pb.length_nanosec())); d->bitrate_ = pb.bitrate(); d->samplerate_ = pb.samplerate(); d->bitdepth_ = pb.bitdepth(); set_url(QUrl::fromEncoded(QByteArray(pb.url().data(), static_cast(pb.url().size())))); - d->basefilename_ = QStringFromStdString(pb.basefilename()); + d->basefilename_ = QString::fromUtf8(pb.basefilename().data(), pb.basefilename().size()); d->filetype_ = static_cast(pb.filetype()); d->filesize_ = pb.filesize(); d->mtime_ = pb.mtime(); @@ -956,27 +953,27 @@ void Song::ToProtobuf(spb::tagreader::SongMetadata *pb) const { const QByteArray art_automatic(d->art_automatic_.toEncoded()); pb->set_valid(d->valid_); - pb->set_title(DataCommaSizeFromQString(d->title_)); - pb->set_album(DataCommaSizeFromQString(d->album_)); - pb->set_artist(DataCommaSizeFromQString(d->artist_)); - pb->set_albumartist(DataCommaSizeFromQString(d->albumartist_)); + pb->set_title(d->title_.toStdString()); + pb->set_album(d->album_.toStdString()); + pb->set_artist(d->artist_.toStdString()); + pb->set_albumartist(d->albumartist_.toStdString()); pb->set_track(d->track_); pb->set_disc(d->disc_); pb->set_year(d->year_); pb->set_originalyear(d->originalyear_); - pb->set_genre(DataCommaSizeFromQString(d->genre_)); + pb->set_genre(d->genre_.toStdString()); pb->set_compilation(d->compilation_); - pb->set_composer(DataCommaSizeFromQString(d->composer_)); - pb->set_performer(DataCommaSizeFromQString(d->performer_)); - pb->set_grouping(DataCommaSizeFromQString(d->grouping_)); - pb->set_comment(DataCommaSizeFromQString(d->comment_)); - pb->set_lyrics(DataCommaSizeFromQString(d->lyrics_)); + pb->set_composer(d->composer_.toStdString()); + pb->set_performer(d->performer_.toStdString()); + pb->set_grouping(d->grouping_.toStdString()); + pb->set_comment(d->comment_.toStdString()); + pb->set_lyrics(d->lyrics_.toStdString()); pb->set_length_nanosec(length_nanosec()); pb->set_bitrate(d->bitrate_); pb->set_samplerate(d->samplerate_); pb->set_bitdepth(d->bitdepth_); pb->set_url(url.constData(), url.size()); - pb->set_basefilename(DataCommaSizeFromQString(d->basefilename_)); + pb->set_basefilename(d->basefilename_.toStdString()); pb->set_filetype(static_cast(d->filetype_)); pb->set_filesize(d->filesize_); pb->set_mtime(d->mtime_); @@ -1080,7 +1077,7 @@ void Song::InitArtManual() { // If we don't have an art, check if we have one in the cache if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty() && !effective_albumartist().isEmpty() && !effective_album().isEmpty()) { - QString filename(Utilities::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg"); + QString filename(CoverUtils::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg"); QString path(ImageCacheDir(d->source_) + "/" + filename); if (QFile::exists(path)) { d->art_manual_ = QUrl::fromLocalFile(path); @@ -1153,7 +1150,7 @@ void Song::InitFromItdb(Itdb_Track *track, const QString &prefix) { QString cover_path = ImageCacheDir(Source::Device); QDir dir(cover_path); if (!dir.exists()) dir.mkpath(cover_path); - QString cover_file = cover_path + "/" + Utilities::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg"; + QString cover_file = cover_path + "/" + CoverUtils::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg"; GError *error = nullptr; if (dir.exists() && gdk_pixbuf_save(pixbuf, cover_file.toUtf8().constData(), "jpeg", &error, nullptr)) { d->art_manual_ = QUrl::fromLocalFile(cover_file); @@ -1526,7 +1523,7 @@ bool Song::IsMetadataEqual(const Song &other) const { d->cue_path_ == other.d->cue_path_; } -bool Song::IsStatisticsEqual(const Song &other) const { +bool Song::IsPlayStatisticsEqual(const Song &other) const { return d->playcount_ == other.d->playcount_ && d->skipcount_ == other.d->skipcount_ && @@ -1556,7 +1553,7 @@ bool Song::IsArtEqual(const Song &other) const { bool Song::IsAllMetadataEqual(const Song &other) const { return IsMetadataEqual(other) && - IsStatisticsEqual(other) && + IsPlayStatisticsEqual(other) && IsRatingEqual(other) && IsFingerprintEqual(other) && IsArtEqual(other); diff --git a/src/core/song.h b/src/core/song.h index b66f747c..2e22d325 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -390,7 +390,7 @@ class Song { // Comparison functions bool IsMetadataEqual(const Song &other) const; - bool IsStatisticsEqual(const Song &other) const; + bool IsPlayStatisticsEqual(const Song &other) const; bool IsRatingEqual(const Song &other) const; bool IsFingerprintEqual(const Song &other) const; bool IsArtEqual(const Song &other) const; diff --git a/src/core/tagreaderclient.cpp b/src/core/tagreaderclient.cpp index 676e8edb..14a3cf8a 100644 --- a/src/core/tagreaderclient.cpp +++ b/src/core/tagreaderclient.cpp @@ -38,8 +38,6 @@ #include "tagreaderclient.h" #include "settings/collectionsettingspage.h" -#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length() - const char *TagReaderClient::kWorkerExecutableName = "strawberry-tagreader"; TagReaderClient *TagReaderClient::sInstance = nullptr; @@ -82,9 +80,10 @@ void TagReaderClient::WorkerFailedToStart() { TagReaderReply *TagReaderClient::IsMediaFile(const QString &filename) { spb::tagreader::Message message; - spb::tagreader::IsMediaFileRequest *req = message.mutable_is_media_file_request(); + spb::tagreader::IsMediaFileRequest *request = message.mutable_is_media_file_request(); - req->set_filename(DataCommaSizeFromQString(filename)); + const QByteArray filename_data = filename.toUtf8(); + request->set_filename(filename_data.constData(), filename_data.length()); return worker_pool_->SendMessageWithReply(&message); @@ -93,21 +92,35 @@ TagReaderReply *TagReaderClient::IsMediaFile(const QString &filename) { TagReaderReply *TagReaderClient::ReadFile(const QString &filename) { spb::tagreader::Message message; - spb::tagreader::ReadFileRequest *req = message.mutable_read_file_request(); + spb::tagreader::ReadFileRequest *request = message.mutable_read_file_request(); - req->set_filename(DataCommaSizeFromQString(filename)); + const QByteArray filename_data = filename.toUtf8(); + request->set_filename(filename_data.constData(), filename_data.length()); return worker_pool_->SendMessageWithReply(&message); } -TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata) { +TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) { spb::tagreader::Message message; - spb::tagreader::SaveFileRequest *req = message.mutable_save_file_request(); + spb::tagreader::SaveFileRequest *request = message.mutable_save_file_request(); - req->set_filename(DataCommaSizeFromQString(filename)); - metadata.ToProtobuf(req->mutable_metadata()); + const QByteArray filename_data = filename.toUtf8(); + request->set_filename(filename_data.constData(), filename_data.length()); + request->set_save_tags(save_tags == SaveTags::On); + request->set_save_playcount(save_playcount == SavePlaycount::On); + request->set_save_rating(save_rating == SaveRating::On); + request->set_save_cover(save_cover_options.enabled); + request->set_cover_is_jpeg(save_cover_options.is_jpeg); + if (save_cover_options.cover_filename.length() > 0) { + const QByteArray cover_filename = filename.toUtf8(); + request->set_cover_filename(cover_filename.constData(), cover_filename.length()); + } + if (save_cover_options.cover_data.length() > 0) { + request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length()); + } + metadata.ToProtobuf(request->mutable_metadata()); ReplyType *reply = worker_pool_->SendMessageWithReply(&message); @@ -118,21 +131,31 @@ TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &m TagReaderReply *TagReaderClient::LoadEmbeddedArt(const QString &filename) { spb::tagreader::Message message; - spb::tagreader::LoadEmbeddedArtRequest *req = message.mutable_load_embedded_art_request(); + spb::tagreader::LoadEmbeddedArtRequest *request = message.mutable_load_embedded_art_request(); - req->set_filename(DataCommaSizeFromQString(filename)); + const QByteArray filename_data = filename.toUtf8(); + request->set_filename(filename_data.constData(), filename_data.length()); return worker_pool_->SendMessageWithReply(&message); } -TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const QByteArray &data) { +TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const SaveCoverOptions &save_cover_options) { spb::tagreader::Message message; - spb::tagreader::SaveEmbeddedArtRequest *req = message.mutable_save_embedded_art_request(); + spb::tagreader::SaveEmbeddedArtRequest *request = message.mutable_save_embedded_art_request(); + + const QByteArray filename_data = filename.toUtf8(); + request->set_filename(filename_data.constData(), filename_data.length()); + request->set_cover_is_jpeg(save_cover_options.is_jpeg); + if (save_cover_options.cover_filename.length() > 0) { + const QByteArray cover_filename = filename.toUtf8(); + request->set_cover_filename(cover_filename.constData(), cover_filename.length()); + } + if (save_cover_options.cover_data.length() > 0) { + request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length()); + } - req->set_filename(DataCommaSizeFromQString(filename)); - req->set_data(data.constData(), data.size()); return worker_pool_->SendMessageWithReply(&message); @@ -141,10 +164,11 @@ 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(); + spb::tagreader::SaveSongPlaycountToFileRequest *request = message.mutable_save_song_playcount_to_file_request(); - req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile())); - metadata.ToProtobuf(req->mutable_metadata()); + const QByteArray filename_data = metadata.url().toLocalFile().toUtf8(); + request->set_filename(filename_data.constData(), filename_data.length()); + metadata.ToProtobuf(request->mutable_metadata()); return worker_pool_->SendMessageWithReply(&message); @@ -162,10 +186,11 @@ void TagReaderClient::UpdateSongsPlaycount(const SongList &songs) { TagReaderReply *TagReaderClient::UpdateSongRating(const Song &metadata) { spb::tagreader::Message message; - spb::tagreader::SaveSongRatingToFileRequest *req = message.mutable_save_song_rating_to_file_request(); + spb::tagreader::SaveSongRatingToFileRequest *request = message.mutable_save_song_rating_to_file_request(); - req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile())); - metadata.ToProtobuf(req->mutable_metadata()); + const QByteArray filename_data = metadata.url().toLocalFile().toUtf8(); + request->set_filename(filename_data.constData(), filename_data.length()); + metadata.ToProtobuf(request->mutable_metadata()); return worker_pool_->SendMessageWithReply(&message); @@ -190,7 +215,7 @@ bool TagReaderClient::IsMediaFileBlocking(const QString &filename) { if (reply->WaitForFinished()) { ret = reply->message().is_media_file_response().success(); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); return ret; @@ -204,21 +229,21 @@ void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) { if (reply->WaitForFinished()) { song->InitFromProtobuf(reply->message().read_file_response().metadata()); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); } -bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata) { +bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) { Q_ASSERT(QThread::currentThread() != thread()); bool ret = false; - TagReaderReply *reply = SaveFile(filename, metadata); + TagReaderReply *reply = SaveFile(filename, metadata, save_tags, save_playcount, save_rating, save_cover_options); if (reply->WaitForFinished()) { ret = reply->message().save_file_response().success(); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); return ret; @@ -235,7 +260,7 @@ QByteArray TagReaderClient::LoadEmbeddedArtBlocking(const QString &filename) { const std::string &data_str = reply->message().load_embedded_art_response().data(); ret = QByteArray(data_str.data(), static_cast(data_str.size())); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); return ret; @@ -252,23 +277,23 @@ QImage TagReaderClient::LoadEmbeddedArtAsImageBlocking(const QString &filename) const std::string &data_str = reply->message().load_embedded_art_response().data(); ret.loadFromData(QByteArray(data_str.data(), static_cast(data_str.size()))); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); return ret; } -bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data) { +bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const SaveCoverOptions &save_cover_options) { Q_ASSERT(QThread::currentThread() != thread()); bool success = false; - TagReaderReply *reply = SaveEmbeddedArt(filename, data); + TagReaderReply *reply = SaveEmbeddedArt(filename, save_cover_options); if (reply->WaitForFinished()) { success = reply->message().save_embedded_art_response().success(); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); return success; diff --git a/src/core/tagreaderclient.h b/src/core/tagreaderclient.h index 60b85457..e4beee3e 100644 --- a/src/core/tagreaderclient.h +++ b/src/core/tagreaderclient.h @@ -53,22 +53,48 @@ class TagReaderClient : public QObject { void Start(); void ExitAsync(); - ReplyType *ReadFile(const QString &filename); - ReplyType *SaveFile(const QString &filename, const Song &metadata); + enum class SaveTags { + Off, + On + }; + + enum class SavePlaycount { + Off, + On + }; + + enum class SaveRating { + Off, + On + }; + + class SaveCoverOptions { + public: + explicit SaveCoverOptions(const bool _enabled = false, const bool _is_jpeg = false, const QString &_cover_filename = QString(), const QByteArray &_cover_data = QByteArray()) : enabled(_enabled), is_jpeg(_is_jpeg), cover_filename(_cover_filename), cover_data(_cover_data) {} + explicit SaveCoverOptions(const QString &_cover_filename) : enabled(true), is_jpeg(false), cover_filename(_cover_filename) {} + explicit SaveCoverOptions(const QByteArray &_cover_data) : enabled(true), is_jpeg(false), cover_data(_cover_data) {} + bool enabled; + bool is_jpeg; + QString cover_filename; + QByteArray cover_data; + }; + ReplyType *IsMediaFile(const QString &filename); + ReplyType *ReadFile(const QString &filename); + ReplyType *SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags(), const SavePlaycount save_playcount = SavePlaycount(), const SaveRating save_rating = SaveRating(), const SaveCoverOptions &save_cover_options = SaveCoverOptions()); ReplyType *LoadEmbeddedArt(const QString &filename); - ReplyType *SaveEmbeddedArt(const QString &filename, const QByteArray &data); + ReplyType *SaveEmbeddedArt(const QString &filename, const SaveCoverOptions &save_cover_options); 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. void ReadFileBlocking(const QString &filename, Song *song); - bool SaveFileBlocking(const QString &filename, const Song &metadata); + bool SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags(), const SavePlaycount save_playcount = SavePlaycount(), const SaveRating save_rating = SaveRating(), const SaveCoverOptions &save_cover_options = SaveCoverOptions()); bool IsMediaFileBlocking(const QString &filename); QByteArray LoadEmbeddedArtBlocking(const QString &filename); QImage LoadEmbeddedArtAsImageBlocking(const QString &filename); - bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data); + bool SaveEmbeddedArtBlocking(const QString &filename, const SaveCoverOptions &save_cover_options); bool UpdateSongPlaycountBlocking(const Song &metadata); bool UpdateSongRatingBlocking(const Song &metadata); diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp index e14220e3..dc815112 100644 --- a/src/covermanager/albumcoverchoicecontroller.cpp +++ b/src/covermanager/albumcoverchoicecontroller.cpp @@ -53,9 +53,12 @@ #include #include +#include "utilities/filenameconstants.h" #include "utilities/strutils.h" #include "utilities/mimeutils.h" #include "utilities/imageutils.h" +#include "utilities/coveroptions.h" +#include "utilities/coverutils.h" #include "core/application.h" #include "core/song.h" #include "core/iconloader.h" @@ -63,7 +66,6 @@ #include "collection/collectionfilteroptions.h" #include "collection/collectionbackend.h" #include "settings/collectionsettingspage.h" -#include "organize/organizeformat.h" #include "internet/internetservices.h" #include "internet/internetservice.h" #include "albumcoverchoicecontroller.h" @@ -98,11 +100,6 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent) separator2_(nullptr), show_cover_(nullptr), search_cover_auto_(nullptr), - save_cover_type_(CollectionSettingsPage::SaveCoverType::Cache), - save_cover_filename_(CollectionSettingsPage::SaveCoverFilename::Pattern), - cover_overwrite_(false), - cover_lowercase_(true), - cover_replace_spaces_(true), save_embedded_cover_override_(false) { cover_from_file_ = new QAction(IconLoader::Load("document-open"), tr("Load cover from disk..."), this); @@ -146,12 +143,12 @@ void AlbumCoverChoiceController::ReloadSettings() { QSettings s; s.beginGroup(CollectionSettingsPage::kSettingsGroup); - save_cover_type_ = static_cast(s.value("save_cover_type", static_cast(CollectionSettingsPage::SaveCoverType::Cache)).toInt()); - save_cover_filename_ = static_cast(s.value("save_cover_filename", static_cast(CollectionSettingsPage::SaveCoverFilename::Pattern)).toInt()); - cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString(); - cover_overwrite_ = s.value("cover_overwrite", false).toBool(); - cover_lowercase_ = s.value("cover_lowercase", false).toBool(); - cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool(); + cover_options_.cover_type = static_cast(s.value("save_cover_type", static_cast(CoverOptions::CoverType::Cache)).toInt()); + cover_options_.cover_filename = static_cast(s.value("save_cover_filename", static_cast(CoverOptions::CoverFilename::Pattern)).toInt()); + cover_options_.cover_pattern = s.value("cover_pattern", "%albumartist-%album").toString(); + cover_options_.cover_overwrite = s.value("cover_overwrite", false).toBool(); + cover_options_.cover_lowercase = s.value("cover_lowercase", false).toBool(); + cover_options_.cover_replace_spaces = s.value("cover_replace_spaces", false).toBool(); s.endGroup(); } @@ -214,14 +211,14 @@ QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) { if (QImage(cover_file).isNull()) return QUrl(); switch (get_save_album_cover_type()) { - case CollectionSettingsPage::SaveCoverType::Embedded: + case CoverOptions::CoverType::Embedded: if (song->save_embedded_cover_supported()) { SaveCoverEmbeddedAutomatic(*song, cover_file); return QUrl::fromLocalFile(Song::kEmbeddedCover); } [[fallthrough]]; - case CollectionSettingsPage::SaveCoverType::Cache: - case CollectionSettingsPage::SaveCoverType::Album:{ + case CoverOptions::CoverType::Cache: + case CoverOptions::CoverType::Album:{ QUrl cover_url = QUrl::fromLocalFile(cover_file); SaveArtManualToSong(song, cover_url); return cover_url; @@ -242,7 +239,7 @@ void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const A initial_file_name = initial_file_name + "-" + (song.effective_album().isEmpty() ? tr("unknown") : song.effective_album()) + ".jpg"; initial_file_name = initial_file_name.toLower(); initial_file_name.replace(QRegularExpression("\\s"), "-"); - initial_file_name.remove(OrganizeFormat::kInvalidFatCharacters); + initial_file_name.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption)); QString save_filename = QFileDialog::getSaveFileName(this, tr("Save album cover"), GetInitialPathForFileDialog(song, initial_file_name), tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter)); @@ -613,12 +610,12 @@ QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source sou const AlbumCoverImageResult &result, const bool force_overwrite) { - QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, result.cover_url, "jpg"); + QString filepath = CoverUtils::CoverFilePath(cover_options_, source, artist, album, album_id, album_dir, result.cover_url, "jpg"); if (filepath.isEmpty()) return QUrl(); QFile file(filepath); // Don't overwrite when saving in album dir if the filename is set to pattern unless "force_overwrite" is set. - if (source == Song::Source::Collection && !cover_overwrite_ && !force_overwrite && get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Album && save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename::Pattern && file.exists()) { + if (source == Song::Source::Collection && !cover_options_.cover_overwrite && !force_overwrite && get_save_album_cover_type() == CoverOptions::CoverType::Album && cover_options_.cover_filename == CoverOptions::CoverFilename::Pattern && file.exists()) { while (file.exists()) { QFileInfo fileinfo(file.fileName()); file.setFileName(fileinfo.path() + "/0" + fileinfo.fileName()); @@ -758,7 +755,7 @@ QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) { const QString suffix = QFileInfo(filename).suffix().toLower(); if (IsKnownImageExtension(suffix)) { - if (get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded && song->save_embedded_cover_supported()) { + if (get_save_album_cover_type() == CoverOptions::CoverType::Embedded && song->save_embedded_cover_supported()) { SaveCoverEmbeddedAutomatic(*song, filename); return QUrl::fromLocalFile(Song::kEmbeddedCover); } @@ -784,7 +781,7 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover QUrl cover_url; switch(get_save_album_cover_type()) { - case CollectionSettingsPage::SaveCoverType::Embedded:{ + case CoverOptions::CoverType::Embedded:{ if (song->save_embedded_cover_supported()) { SaveCoverEmbeddedAutomatic(*song, result); cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover); @@ -792,8 +789,8 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover } } [[fallthrough]]; - case CollectionSettingsPage::SaveCoverType::Cache: - case CollectionSettingsPage::SaveCoverType::Album:{ + case CoverOptions::CoverType::Cache: + case CoverOptions::CoverType::Album:{ cover_url = SaveCoverToFileAutomatic(song, result); if (!cover_url.isEmpty()) SaveArtManualToSong(song, cover_url); break; diff --git a/src/covermanager/albumcoverchoicecontroller.h b/src/covermanager/albumcoverchoicecontroller.h index df025c8d..0c1ee2a6 100644 --- a/src/covermanager/albumcoverchoicecontroller.h +++ b/src/covermanager/albumcoverchoicecontroller.h @@ -38,6 +38,7 @@ #include #include "core/song.h" +#include "utilities/coveroptions.h" #include "settings/collectionsettingspage.h" #include "albumcoverimageresult.h" @@ -69,8 +70,8 @@ class AlbumCoverChoiceController : public QWidget { void Init(Application *app); void ReloadSettings(); - CollectionSettingsPage::SaveCoverType get_save_album_cover_type() const { return (save_embedded_cover_override_ ? CollectionSettingsPage::SaveCoverType::Embedded : save_cover_type_); } - CollectionSettingsPage::SaveCoverType get_collection_save_album_cover_type() const { return save_cover_type_; } + CoverOptions::CoverType get_save_album_cover_type() const { return (save_embedded_cover_override_ ? CoverOptions::CoverType::Embedded : cover_options_.cover_type); } + CoverOptions::CoverType get_collection_save_album_cover_type() const { return cover_options_.cover_type; } // Getters for all QActions implemented by this controller. @@ -189,12 +190,7 @@ class AlbumCoverChoiceController : public QWidget { QMap cover_save_tasks_; QMutex mutex_cover_save_tasks_; - CollectionSettingsPage::SaveCoverType save_cover_type_; - CollectionSettingsPage::SaveCoverFilename save_cover_filename_; - QString cover_pattern_; - bool cover_overwrite_; - bool cover_lowercase_; - bool cover_replace_spaces_; + CoverOptions cover_options_; bool save_embedded_cover_override_; }; diff --git a/src/covermanager/albumcoverloader.cpp b/src/covermanager/albumcoverloader.cpp index cd970333..789249fe 100644 --- a/src/covermanager/albumcoverloader.cpp +++ b/src/covermanager/albumcoverloader.cpp @@ -34,24 +34,18 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include "core/networkaccessmanager.h" #include "core/song.h" #include "core/tagreaderclient.h" -#include "utilities/transliterate.h" #include "utilities/mimeutils.h" -#include "utilities/cryptutils.h" #include "utilities/imageutils.h" -#include "settings/collectionsettingspage.h" -#include "organize/organizeformat.h" #include "albumcoverloader.h" #include "albumcoverloaderoptions.h" #include "albumcoverloaderresult.h" @@ -63,15 +57,9 @@ AlbumCoverLoader::AlbumCoverLoader(QObject *parent) load_image_async_id_(1), save_image_async_id_(1), network_(new NetworkAccessManager(this)), - save_cover_type_(CollectionSettingsPage::SaveCoverType::Cache), - save_cover_filename_(CollectionSettingsPage::SaveCoverFilename::Pattern), - cover_overwrite_(false), - cover_lowercase_(true), - cover_replace_spaces_(true), original_thread_(nullptr) { original_thread_ = thread(); - ReloadSettings(); } @@ -90,145 +78,6 @@ void AlbumCoverLoader::Exit() { } -void AlbumCoverLoader::ReloadSettings() { - - QSettings s; - s.beginGroup(CollectionSettingsPage::kSettingsGroup); - save_cover_type_ = static_cast(s.value("save_cover_type", static_cast(CollectionSettingsPage::SaveCoverType::Cache)).toInt()); - save_cover_filename_ = static_cast(s.value("save_cover_filename", static_cast(CollectionSettingsPage::SaveCoverFilename::Pattern)).toInt()); - cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString(); - cover_overwrite_ = s.value("cover_overwrite", false).toBool(); - cover_lowercase_ = s.value("cover_lowercase", false).toBool(); - cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool(); - s.endGroup(); - -} - -QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album, const QString &extension) { - - artist.remove('/').remove('\\'); - album.remove('/').remove('\\'); - - QString filename = artist + "-" + album; - filename = Utilities::Transliterate(filename.toLower()); - filename = filename.replace(' ', '-') - .replace("--", "-") - .remove(OrganizeFormat::kInvalidFatCharacters) - .simplified(); - - if (!extension.isEmpty()) { - filename.append('.'); - filename.append(extension); - } - - return filename; - -} - -QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension) { - return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url, extension); -} - -QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension) { - - QString path; - if (source == Song::Source::Collection && save_cover_type_ == CollectionSettingsPage::SaveCoverType::Album && !album_dir.isEmpty()) { - path = album_dir; - } - else { - path = Song::ImageCacheDir(source); - } - - if (path.right(1) == QDir::separator() || path.right(1) == "/") { - path.chop(1); - } - - QDir dir; - if (!dir.mkpath(path)) { - qLog(Error) << "Unable to create directory" << path; - path = QStandardPaths::writableLocation(QStandardPaths::TempLocation); - } - - QString filename; - if (source == Song::Source::Collection && - save_cover_type_ == CollectionSettingsPage::SaveCoverType::Album && - save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename::Pattern && - !cover_pattern_.isEmpty()) { - filename = CoverFilenameFromVariable(artist, album); - filename.remove(OrganizeFormat::kInvalidFatCharacters).remove('/').remove('\\'); - if (cover_lowercase_) filename = filename.toLower(); - if (cover_replace_spaces_) filename.replace(QRegularExpression("\\s"), "-"); - if (!extension.isEmpty()) { - filename.append('.'); - filename.append(extension); - } - } - - if (filename.isEmpty()) { - filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id, extension); - } - - QString filepath(path + "/" + filename); - - return filepath; - -} - -QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension) { - - QString filename; - - switch (source) { - case Song::Source::Tidal: - if (!album_id.isEmpty()) { - filename = album_id + "-" + cover_url.fileName(); - break; - } - [[fallthrough]]; - case Song::Source::Subsonic: - case Song::Source::Qobuz: - if (!album_id.isEmpty()) { - filename = album_id; - break; - } - [[fallthrough]]; - case Song::Source::Collection: - case Song::Source::LocalFile: - case Song::Source::CDDA: - case Song::Source::Device: - case Song::Source::Stream: - case Song::Source::SomaFM: - case Song::Source::RadioParadise: - case Song::Source::Unknown: - filename = Utilities::Sha1CoverHash(artist, album).toHex(); - break; - } - - if (!extension.isEmpty()) { - filename.append('.'); - filename.append(extension); - } - - return filename; - -} - -QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, QString album, const QString &extension) { - - album = album.remove(Song::kAlbumRemoveDisc); - - QString filename(cover_pattern_); - filename.replace("%albumartist", artist); - filename.replace("%artist", artist); - filename.replace("%album", album); - if (!extension.isEmpty()) { - filename.append('.'); - filename.append(extension); - } - return filename; - -} - void AlbumCoverLoader::CancelTask(const quint64 id) { QMutexLocker l(&mutex_load_image_async_); @@ -260,7 +109,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, Task task; task.options = options; task.song = song; - task.state = State_Manual; + task.state = State::Manual; return EnqueueTask(task); @@ -276,7 +125,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, Task task; task.options = options; task.song = song; - task.state = State_Manual; + task.state = State::Manual; return EnqueueTask(task); @@ -360,9 +209,9 @@ void AlbumCoverLoader::ProcessTask(Task *task) { void AlbumCoverLoader::NextState(Task *task) { - if (task->state == State_Manual) { + if (task->state == State::Manual) { // Try the automatic one next - task->state = State_Automatic; + task->state = State::Automatic; ProcessTask(task); } else { @@ -382,13 +231,13 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) { // For local files and streams initialize art if found. if ((task->song.source() == Song::Source::LocalFile || task->song.is_radio()) && !task->song.art_manual_is_valid() && !task->song.art_automatic_is_valid()) { switch (task->state) { - case State_None: + case State::None: break; - case State_Manual: + case State::Manual: task->song.InitArtManual(); if (task->song.art_manual_is_valid()) task->art_updated = true; break; - case State_Automatic: + case State::Automatic: if (task->song.url().isLocalFile()) { task->song.InitArtAutomatic(); if (task->song.art_automatic_is_valid()) task->art_updated = true; @@ -400,12 +249,12 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) { AlbumCoverLoaderResult::Type type(AlbumCoverLoaderResult::Type_None); QUrl cover_url; switch (task->state) { - case State_None: - case State_Automatic: + case State::None: + case State::Automatic: type = AlbumCoverLoaderResult::Type_Automatic; cover_url = task->song.art_automatic(); break; - case State_Manual: + case State::Manual: type = AlbumCoverLoaderResult::Type_Manual; cover_url = task->song.art_manual(); break; @@ -593,7 +442,7 @@ quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList &urls, const void AlbumCoverLoader::SaveEmbeddedCover(const quint64 id, const QString &song_filename, const QByteArray &image_data) { - TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(song_filename, image_data); + TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(song_filename, TagReaderClient::SaveCoverOptions(image_data)); tagreader_save_embedded_art_requests_.insert(id, reply); const bool clear = image_data.isEmpty(); QObject::connect(reply, &TagReaderReply::Finished, this, [this, id, reply, clear]() { SaveEmbeddedArtFinished(id, reply, clear); }, Qt::QueuedConnection); @@ -702,6 +551,6 @@ void AlbumCoverLoader::SaveEmbeddedArtFinished(const quint64 id, TagReaderReply emit SaveEmbeddedCoverAsyncFinished(id, reply->is_successful(), cleared); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); } diff --git a/src/covermanager/albumcoverloader.h b/src/covermanager/albumcoverloader.h index d7e43e05..6c75b340 100644 --- a/src/covermanager/albumcoverloader.h +++ b/src/covermanager/albumcoverloader.h @@ -39,7 +39,6 @@ #include "core/song.h" #include "core/tagreaderclient.h" -#include "settings/collectionsettingspage.h" #include "albumcoverloaderoptions.h" #include "albumcoverloaderresult.h" #include "albumcoverimageresult.h" @@ -54,25 +53,15 @@ class AlbumCoverLoader : public QObject { public: explicit AlbumCoverLoader(QObject *parent = nullptr); - enum State { - State_None, - State_Manual, - State_Automatic, + enum class State { + None, + Manual, + Automatic }; - void ReloadSettings(); - void ExitAsync(); void Stop() { stop_requested_ = true; } - static QString AlbumCoverFilename(QString artist, QString album, const QString &extension); - - static QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension); - QString CoverFilenameFromVariable(const QString &artist, QString album, const QString &extension = QString()); - QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString()); - - QString CoverFilePath(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString()); - quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song); quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url = QUrl(), const Song::Source song_source = Song::Source::Unknown); quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const AlbumCoverImageResult &album_cover); @@ -110,7 +99,7 @@ class AlbumCoverLoader : public QObject { protected: struct Task { - explicit Task() : id(0), state(State_None), type(AlbumCoverLoaderResult::Type_None), art_updated(false), redirects(0) {} + explicit Task() : id(0), state(State::None), type(AlbumCoverLoaderResult::Type_None), art_updated(false), redirects(0) {} AlbumCoverLoaderOptions options; @@ -158,13 +147,6 @@ class AlbumCoverLoader : public QObject { static const int kMaxRedirects = 3; - CollectionSettingsPage::SaveCoverType save_cover_type_; - CollectionSettingsPage::SaveCoverFilename save_cover_filename_; - QString cover_pattern_; - bool cover_overwrite_; - bool cover_lowercase_; - bool cover_replace_spaces_; - QThread *original_thread_; QMultiMap tagreader_save_embedded_art_requests_; diff --git a/src/covermanager/albumcovermanager.cpp b/src/covermanager/albumcovermanager.cpp index d68b09d3..77f7c686 100644 --- a/src/covermanager/albumcovermanager.cpp +++ b/src/covermanager/albumcovermanager.cpp @@ -781,13 +781,13 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul QUrl cover_url = result.cover_url; switch (album_cover_choice_controller_->get_save_album_cover_type()) { - case CollectionSettingsPage::SaveCoverType::Cache: - case CollectionSettingsPage::SaveCoverType::Album: + case CoverOptions::CoverType::Cache: + case CoverOptions::CoverType::Album: if (cover_url.isEmpty() || !cover_url.isValid() || !cover_url.isLocalFile()) { cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(song, result); } break; - case CollectionSettingsPage::SaveCoverType::Embedded: + case CoverOptions::CoverType::Embedded: cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover); break; } @@ -798,14 +798,14 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul for (QListWidgetItem *item : context_menu_items_) { AlbumItem *album_item = static_cast(item); switch (album_cover_choice_controller_->get_save_album_cover_type()) { - case CollectionSettingsPage::SaveCoverType::Cache: - case CollectionSettingsPage::SaveCoverType::Album:{ + case CoverOptions::CoverType::Cache: + case CoverOptions::CoverType::Album:{ Song current_song = ItemAsSong(album_item); album_cover_choice_controller_->SaveArtManualToSong(¤t_song, cover_url); UpdateCoverInList(album_item, cover_url); break; } - case CollectionSettingsPage::SaveCoverType::Embedded:{ + case CoverOptions::CoverType::Embedded:{ urls << album_item->urls; album_items << album_item; break; @@ -813,7 +813,7 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul } } - if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded && !urls.isEmpty()) { + if (album_cover_choice_controller_->get_save_album_cover_type() == CoverOptions::CoverType::Embedded && !urls.isEmpty()) { quint64 id = -1; if (result.is_jpeg()) { id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data); @@ -971,7 +971,7 @@ void AlbumCoverManager::SaveAndSetCover(AlbumItem *item, const AlbumCoverImageRe const Song::FileType filetype = static_cast(item->data(Role_Filetype).toInt()); const bool has_cue = !item->data(Role_CuePath).toString().isEmpty(); - if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded && Song::save_embedded_cover_supported(filetype) && !has_cue) { + if (album_cover_choice_controller_->get_save_album_cover_type() == CoverOptions::CoverType::Embedded && Song::save_embedded_cover_supported(filetype) && !has_cue) { if (result.is_jpeg()) { quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data); cover_save_tasks_.insert(id, item); diff --git a/src/dialogs/edittagdialog.cpp b/src/dialogs/edittagdialog.cpp index 1ecf589a..41a58dc3 100644 --- a/src/dialogs/edittagdialog.cpp +++ b/src/dialogs/edittagdialog.cpp @@ -76,7 +76,8 @@ #include "utilities/strutils.h" #include "utilities/timeutils.h" #include "utilities/imageutils.h" -#include "utilities/cryptutils.h" +#include "utilities/coverutils.h" +#include "utilities/coveroptions.h" #include "widgets/busyindicator.h" #include "widgets/lineedit.h" #include "collection/collectionbackend.h" @@ -116,8 +117,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) summary_cover_art_id_(-1), tags_cover_art_id_(-1), cover_art_is_set_(false), - save_tag_pending_(0), - save_art_pending_(0) { + save_tag_pending_(0) { QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &EditTagDialog::AlbumCoverLoaded); @@ -187,7 +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::ResetStatistics); + QObject::connect(ui_->playcount_reset, &QPushButton::clicked, this, &EditTagDialog::ResetPlayStatistics); QObject::connect(ui_->rating, &RatingWidget::RatingChanged, this, &EditTagDialog::SongRated); #ifdef HAVE_MUSICBRAINZ QObject::connect(ui_->fetch_tag, &QPushButton::clicked, this, &EditTagDialog::FetchTag); @@ -728,7 +728,7 @@ void EditTagDialog::SelectionChanged() { ui_->tags_summary->setText(summary); - const bool embedded_cover = (first_song.save_embedded_cover_supported() && (first_song.has_embedded_cover() || album_cover_choice_controller_->get_collection_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded)); + const bool embedded_cover = (first_song.save_embedded_cover_supported() && (first_song.has_embedded_cover() || album_cover_choice_controller_->get_collection_save_album_cover_type() == CoverOptions::CoverType::Embedded)); ui_->checkbox_embedded_cover->setChecked(embedded_cover); album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover); @@ -1139,33 +1139,6 @@ void EditTagDialog::SaveData() { for (int i = 0; i < data_.count(); ++i) { Data &ref = data_[i]; - if (!ref.current_.IsMetadataEqual(ref.original_)) { - ++save_tag_pending_; - TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_); - QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_); }, Qt::QueuedConnection); - } - - if (ref.current_.playcount() == 0 && - ref.current_.skipcount() == 0 && - ref.current_.lastplayed() == -1 && - !ref.current_.IsStatisticsEqual(ref.original_)) { - if (ref.current_.is_collection_song()) { - app_->collection_backend()->ResetStatisticsAsync(ref.current_.id()); - } - else { - app_->tag_reader_client()->UpdateSongsPlaycount(SongList() << ref.current_); - } - } - - if (!ref.current_.IsRatingEqual(ref.original_)) { - if (ref.current_.is_collection_song()) { - app_->collection_backend()->UpdateSongRatingAsync(ref.current_.id(), ref.current_.rating()); - } - else { - app_->tag_reader_client()->UpdateSongsRating(SongList() << ref.current_); - } - } - 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()) { @@ -1179,6 +1152,11 @@ void EditTagDialog::SaveData() { } } + const bool save_tags = !ref.current_.IsMetadataEqual(ref.original_); + const bool save_rating = !ref.current_.IsRatingEqual(ref.original_); + const bool save_playcount = ref.current_.playcount() == 0 && ref.current_.skipcount() == 0 && ref.current_.lastplayed() == -1 && !ref.current_.IsPlayStatisticsEqual(ref.original_); + const bool save_embedded_cover = ref.cover_action_ != UpdateCoverAction::None && ui_->checkbox_embedded_cover->isChecked() && ref.original_.save_embedded_cover_supported(); + if (ref.cover_action_ != UpdateCoverAction::None) { switch (ref.cover_action_) { case UpdateCoverAction::None: @@ -1191,7 +1169,7 @@ void EditTagDialog::SaveData() { cover_url = ref.cover_result_.cover_url; } else { - QString cover_hash = Utilities::Sha1CoverHash(ref.current_.effective_albumartist(), ref.current_.album()).toHex(); + QString cover_hash = CoverUtils::Sha1CoverHash(ref.current_.effective_albumartist(), ref.current_.album()).toHex(); if (cover_urls.contains(cover_hash)) { cover_url = cover_urls[cover_hash]; } @@ -1232,66 +1210,41 @@ void EditTagDialog::SaveData() { break; } } - if (ui_->checkbox_embedded_cover->isChecked() && ref.original_.save_embedded_cover_supported()) { - if (ref.cover_action_ == UpdateCoverAction::New) { - if (ref.cover_result_.is_jpeg()) { // Save JPEG data directly. - ++save_art_pending_; - TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), ref.cover_result_.image_data); - QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { - SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); - }, Qt::QueuedConnection); - } - else if (!ref.cover_result_.image.isNull()) { // Convert image data to JPEG. - ++save_art_pending_; - QFuture future = QtConcurrent::run(&ImageUtils::SaveImageToJpegData, ref.cover_result_.image); - QFutureWatcher *watcher = new QFutureWatcher(); - QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, ref]() { - TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), watcher->result()); - QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { - SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); - }, Qt::QueuedConnection); - watcher->deleteLater(); - }); - watcher->setFuture(future); - } - else if (!embedded_cover_from_file.isEmpty()) { // Save existing file on disk as embedded cover. - ++save_art_pending_; - QFuture future = QtConcurrent::run(&ImageUtils::FileToJpegData, embedded_cover_from_file); - QFutureWatcher *watcher = new QFutureWatcher(); - QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, ref]() { - TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), watcher->result()); - QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { - SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); - }, Qt::QueuedConnection); - watcher->deleteLater(); - }); - watcher->setFuture(future); - } + } + + if (save_tags || save_playcount || save_rating || save_embedded_cover) { + ++save_tag_pending_; + TagReaderClient::SaveCoverOptions savecover_options; + savecover_options.enabled = save_embedded_cover; + if (save_embedded_cover && ref.cover_action_ == UpdateCoverAction::New) { + if (!ref.cover_result_.image.isNull()) { + savecover_options.is_jpeg = ref.cover_result_.is_jpeg(); + savecover_options.cover_data = ref.cover_result_.image_data; } - else if (ref.cover_action_ == UpdateCoverAction::Delete) { - ++save_art_pending_; - TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), QByteArray()); - QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { - SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); - }, Qt::QueuedConnection); + else if (!embedded_cover_from_file.isEmpty()) { + savecover_options.cover_filename = embedded_cover_from_file; } } - else if (!ref.current_.effective_albumartist().isEmpty() && !ref.current_.album().isEmpty()) { - if (ref.current_.is_collection_song()) { - collection_songs_.insert(ref.current_.id(), ref.current_); - } - if (ref.current_ == app_->current_albumcover_loader()->last_song()) { - app_->current_albumcover_loader()->LoadAlbumCover(ref.current_); - } + TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_, save_tags ? TagReaderClient::SaveTags::On : TagReaderClient::SaveTags::Off, save_playcount ? TagReaderClient::SavePlaycount::On : TagReaderClient::SavePlaycount::Off, save_rating ? TagReaderClient::SaveRating::On : TagReaderClient::SaveRating::Off, savecover_options); + QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); }, Qt::QueuedConnection); + } + // If the cover was changed, but no tags written, make sure to update the collection. + else if (ref.cover_action_ != UpdateCoverAction::None && !ref.current_.effective_albumartist().isEmpty() && !ref.current_.album().isEmpty()) { + if (ref.current_.is_collection_song()) { + collection_songs_.insert(ref.current_.id(), ref.current_); + } + if (ref.current_ == app_->current_albumcover_loader()->last_song()) { + app_->current_albumcover_loader()->LoadAlbumCover(ref.current_); } } + } - if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished(); + if (save_tag_pending_ <= 0) SaveDataFinished(); } -void EditTagDialog::AcceptFinished() { +void EditTagDialog::SaveDataFinished() { if (!collection_songs_.isEmpty()) { app_->collection_backend()->AddOrUpdateSongsAsync(collection_songs_.values()); @@ -1304,7 +1257,7 @@ void EditTagDialog::AcceptFinished() { } -void EditTagDialog::ResetStatistics() { +void EditTagDialog::ResetPlayStatistics() { const QModelIndexList idx_list = ui_->song_list->selectionModel()->selectedIndexes(); if (idx_list.isEmpty()) return; @@ -1312,7 +1265,7 @@ void EditTagDialog::ResetStatistics() { Song *song = &data_[idx_list.first().row()].current_; if (!song->is_valid()) return; - if (QMessageBox::question(this, tr("Reset song statistics"), tr("Are you sure you want to reset this song's statistics?"), QMessageBox::Reset, QMessageBox::Cancel) != QMessageBox::Reset) { + if (QMessageBox::question(this, tr("Reset song play statistics"), tr("Are you sure you want to reset this song's play statistics?"), QMessageBox::Reset, QMessageBox::Cancel) != QMessageBox::Reset) { return; } @@ -1399,67 +1352,46 @@ void EditTagDialog::FetchTagSongChosen(const Song &original_song, const Song &ne } -void EditTagDialog::SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song) { +void EditTagDialog::SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action) { --save_tag_pending_; + const bool success = reply->message().save_file_response().success(); + reply->deleteLater(); - if (!reply->message().save_file_response().success()) { - QString message = tr("An error occurred writing metadata to '%1'").arg(filename); - emit Error(message); - } - else if (song.is_collection_song()) { - if (collection_songs_.contains(song.id())) { - Song old_song = collection_songs_.take(song.id()); - song.set_art_automatic(old_song.art_automatic()); - song.set_art_manual(old_song.art_manual()); + if (success) { + if (song.is_collection_song()) { + if (collection_songs_.contains(song.id())) { + Song old_song = collection_songs_.take(song.id()); + song.set_art_automatic(old_song.art_automatic()); + song.set_art_manual(old_song.art_manual()); + } + switch (cover_action) { + case UpdateCoverAction::None: + break; + case UpdateCoverAction::New: + song.clear_art_manual(); + song.set_embedded_cover(); + break; + case UpdateCoverAction::Clear: + case UpdateCoverAction::Delete: + song.clear_art_automatic(); + song.clear_art_manual(); + break; + case UpdateCoverAction::Unset: + song.clear_art_automatic(); + song.set_manually_unset_cover(); + break; + } + collection_songs_.insert(song.id(), song); } - collection_songs_.insert(song.id(), song); + if (cover_action != UpdateCoverAction::None && song == app_->current_albumcover_loader()->last_song()) { + app_->current_albumcover_loader()->LoadAlbumCover(song); + } + } + else { + emit Error(tr("An error occurred writing metadata to '%1'").arg(filename)); } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); - - if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished(); - -} - -void EditTagDialog::SongSaveArtComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action) { - - --save_art_pending_; - - if (!reply->message().save_embedded_art_response().success()) { - QString message = tr("An error occurred writing cover art to '%1'").arg(filename); - emit Error(message); - } - else if (song.is_collection_song()) { - if (collection_songs_.contains(song.id())) { - song = collection_songs_.take(song.id()); - } - switch (cover_action) { - case UpdateCoverAction::None: - break; - case UpdateCoverAction::New: - song.clear_art_manual(); - song.set_embedded_cover(); - break; - case UpdateCoverAction::Clear: - case UpdateCoverAction::Delete: - song.clear_art_automatic(); - song.clear_art_manual(); - break; - case UpdateCoverAction::Unset: - song.clear_art_automatic(); - song.set_manually_unset_cover(); - break; - } - collection_songs_.insert(song.id(), song); - } - - if (song == app_->current_albumcover_loader()->last_song()) { - app_->current_albumcover_loader()->LoadAlbumCover(song); - } - - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); - - if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished(); + if (save_tag_pending_ <= 0) SaveDataFinished(); } diff --git a/src/dialogs/edittagdialog.h b/src/dialogs/edittagdialog.h index 735ce76b..4d8f67c0 100644 --- a/src/dialogs/edittagdialog.h +++ b/src/dialogs/edittagdialog.h @@ -109,13 +109,13 @@ class EditTagDialog : public QDialog { private slots: void SetSongsFinished(); - void AcceptFinished(); + void SaveDataFinished(); void SelectionChanged(); void FieldValueEdited(); void ResetField(); void ButtonClicked(QAbstractButton *button); - void ResetStatistics(); + void ResetPlayStatistics(); void SongRated(const float rating); void FetchTag(); void FetchTagSongChosen(const Song &original_song, const Song &new_metadata); @@ -134,8 +134,7 @@ class EditTagDialog : public QDialog { void PreviousSong(); void NextSong(); - void SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song); - void SongSaveArtComplete(TagReaderReply *reply, const QString &filename, Song song, const EditTagDialog::UpdateCoverAction cover_action); + void SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action); private: struct FieldData { @@ -206,7 +205,6 @@ class EditTagDialog : public QDialog { QPushButton *next_button_; int save_tag_pending_; - int save_art_pending_; QMap collection_songs_; }; diff --git a/src/organize/organizeformat.cpp b/src/organize/organizeformat.cpp index f71643ea..2c3a3b6c 100644 --- a/src/organize/organizeformat.cpp +++ b/src/organize/organizeformat.cpp @@ -36,20 +36,13 @@ #include #include -#include "core/arraysize.h" -#include "core/song.h" -#include "utilities/transliterate.h" +#include "utilities/filenameconstants.h" #include "utilities/timeconstants.h" +#include "utilities/transliterate.h" +#include "core/song.h" #include "organizeformat.h" -const QRegularExpression OrganizeFormat::kProblematicCharacters("[:?*\"<>|]"); -// From http://en.wikipedia.org/wiki/8.3_filename#Directory_table -const QRegularExpression OrganizeFormat::kInvalidFatCharacters("[^a-zA-Z0-9!#\\$%&'()\\-@\\^_`{}~/. ]", QRegularExpression::CaseInsensitiveOption); -const QRegularExpression OrganizeFormat::kInvalidDirCharacters("[/\\\\]"); -constexpr char OrganizeFormat::kInvalidPrefixCharacters[] = "."; -constexpr int OrganizeFormat::kInvalidPrefixCharactersCount = arraysize(OrganizeFormat::kInvalidPrefixCharacters) - 1; - constexpr char OrganizeFormat::kBlockPattern[] = "\\{([^{}]+)\\}"; constexpr char OrganizeFormat::kTagPattern[] = "\\%([a-zA-Z]*)"; @@ -138,9 +131,9 @@ OrganizeFormat::GetFilenameForSongResult OrganizeFormat::GetFilenameForSong(cons return GetFilenameForSongResult(); } - if (remove_problematic_) filepath = filepath.remove(kProblematicCharacters); + if (remove_problematic_) filepath = filepath.remove(QRegularExpression(QString(kProblematicCharactersRegex), QRegularExpression::PatternOption::CaseInsensitiveOption)); if (remove_non_fat_ || (remove_non_ascii_ && !allow_ascii_ext_)) filepath = Utilities::Transliterate(filepath); - if (remove_non_fat_) filepath = filepath.remove(kInvalidFatCharacters); + if (remove_non_fat_) filepath = filepath.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::PatternOption::CaseInsensitiveOption)); if (remove_non_ascii_) { int ascii = 128; @@ -327,7 +320,7 @@ QString OrganizeFormat::TagValue(const QString &tag, const Song &song) const { if (tag == "track" && value.length() == 1) value.prepend('0'); // Replace characters that really shouldn't be in paths - value = value.remove(kInvalidDirCharacters); + value = value.remove(QRegularExpression(QString(kInvalidDirCharactersRegex), QRegularExpression::PatternOption::CaseInsensitiveOption)); if (remove_problematic_) value = value.remove('.'); value = value.trimmed(); diff --git a/src/organize/organizeformat.h b/src/organize/organizeformat.h index f9eecccb..95a5a95e 100644 --- a/src/organize/organizeformat.h +++ b/src/organize/organizeformat.h @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -41,9 +40,6 @@ class OrganizeFormat { public: explicit OrganizeFormat(const QString &format = QString()); - static const QRegularExpression kProblematicCharacters; - static const QRegularExpression kInvalidFatCharacters; - QString format() const { return format_; } bool remove_problematic() const { return remove_problematic_; } bool remove_non_fat() const { return remove_non_fat_; } @@ -93,9 +89,6 @@ class OrganizeFormat { static const char kTagPattern[]; static const QStringList kKnownTags; static const QStringList kUniqueTags; - static const QRegularExpression kInvalidDirCharacters; - static const char kInvalidPrefixCharacters[]; - static const int kInvalidPrefixCharactersCount; QString ParseBlock(QString block, const Song &song, bool *have_tagdata = nullptr, bool *any_empty = nullptr) const; QString TagValue(const QString &tag, const Song &song) const; diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index 4db91bc7..7dd52976 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -420,7 +420,7 @@ void Playlist::SongSaveComplete(TagReaderReply *reply, const QPersistentModelInd } } - QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + reply->deleteLater(); } diff --git a/src/playlist/playlistmanager.cpp b/src/playlist/playlistmanager.cpp index 126d81bb..7d2c1daa 100644 --- a/src/playlist/playlistmanager.cpp +++ b/src/playlist/playlistmanager.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -44,10 +45,10 @@ #include "core/application.h" #include "core/player.h" +#include "utilities/filenameconstants.h" #include "utilities/timeutils.h" #include "collection/collectionbackend.h" #include "covermanager/currentalbumcoverloader.h" -#include "organize/organizeformat.h" #include "settings/playlistsettingspage.h" #include "playlist.h" #include "playlistbackend.h" @@ -248,7 +249,7 @@ void PlaylistManager::SaveWithUI(const int id, const QString &playlist_name) { s.endGroup(); QString suggested_filename = playlist_name; - QString filename = last_save_path + "/" + suggested_filename.remove(OrganizeFormat::kProblematicCharacters) + "." + last_save_extension; + QString filename = last_save_path + "/" + suggested_filename.remove(QRegularExpression(QString(kProblematicCharactersRegex), QRegularExpression::CaseInsensitiveOption)) + "." + last_save_extension; QFileInfo fileinfo; forever { diff --git a/src/qobuz/qobuzrequest.cpp b/src/qobuz/qobuzrequest.cpp index 2b25c68f..fac06cd0 100644 --- a/src/qobuz/qobuzrequest.cpp +++ b/src/qobuz/qobuzrequest.cpp @@ -38,8 +38,9 @@ #include "core/networkaccessmanager.h" #include "core/song.h" #include "core/application.h" -#include "utilities/imageutils.h" #include "utilities/timeconstants.h" +#include "utilities/imageutils.h" +#include "utilities/coverutils.h" #include "qobuzservice.h" #include "qobuzurlhandler.h" #include "qobuzbaserequest.h" @@ -1266,7 +1267,7 @@ void QobuzRequest::AddAlbumCoverRequest(const Song &song) { AlbumCoverRequest request; request.url = cover_url; - request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url); + request.filename = CoverUtils::CoverFilePath(CoverOptions(), song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url); if (request.filename.isEmpty()) return; album_covers_requests_sent_.insert(cover_url, song.song_id()); diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index 6d680cf3..603a7679 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -46,6 +46,7 @@ #include "core/iconloader.h" #include "utilities/strutils.h" #include "utilities/timeutils.h" +#include "utilities/coveroptions.h" #include "collection/collection.h" #include "collection/collectionmodel.h" #include "collection/collectiondirectorymodel.h" @@ -204,25 +205,25 @@ void CollectionSettingsPage::Load() { QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList(); ui_->cover_art_patterns->setText(filters.join(",")); - const SaveCoverType save_cover_type = static_cast(s.value("save_cover_type", static_cast(SaveCoverType::Cache)).toInt()); + const CoverOptions::CoverType save_cover_type = static_cast(s.value("save_cover_type", static_cast(CoverOptions::CoverType::Cache)).toInt()); switch (save_cover_type) { - case SaveCoverType::Cache: + case CoverOptions::CoverType::Cache: ui_->radiobutton_save_albumcover_cache->setChecked(true); break; - case SaveCoverType::Album: + case CoverOptions::CoverType::Album: ui_->radiobutton_save_albumcover_albumdir->setChecked(true); break; - case SaveCoverType::Embedded: + case CoverOptions::CoverType::Embedded: ui_->radiobutton_save_albumcover_embedded->setChecked(true); break; } - const SaveCoverFilename save_cover_filename = static_cast(s.value("save_cover_filename", static_cast(SaveCoverFilename::Pattern)).toInt()); + const CoverOptions::CoverFilename save_cover_filename = static_cast(s.value("save_cover_filename", static_cast(CoverOptions::CoverFilename::Pattern)).toInt()); switch (save_cover_filename) { - case SaveCoverFilename::Hash: + case CoverOptions::CoverFilename::Hash: ui_->radiobutton_cover_hash->setChecked(true); break; - case SaveCoverFilename::Pattern: + case CoverOptions::CoverFilename::Pattern: ui_->radiobutton_cover_pattern->setChecked(true); break; } @@ -297,15 +298,15 @@ void CollectionSettingsPage::Save() { s.setValue("cover_art_patterns", filters); - SaveCoverType save_cover_type = SaveCoverType::Cache; - if (ui_->radiobutton_save_albumcover_cache->isChecked()) save_cover_type = SaveCoverType::Cache; - else if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) save_cover_type = SaveCoverType::Album; - else if (ui_->radiobutton_save_albumcover_embedded->isChecked()) save_cover_type = SaveCoverType::Embedded; + CoverOptions::CoverType save_cover_type = CoverOptions::CoverType::Cache; + if (ui_->radiobutton_save_albumcover_cache->isChecked()) save_cover_type = CoverOptions::CoverType::Cache; + else if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) save_cover_type = CoverOptions::CoverType::Album; + else if (ui_->radiobutton_save_albumcover_embedded->isChecked()) save_cover_type = CoverOptions::CoverType::Embedded; s.setValue("save_cover_type", static_cast(save_cover_type)); - SaveCoverFilename save_cover_filename = SaveCoverFilename::Hash; - if (ui_->radiobutton_cover_hash->isChecked()) save_cover_filename = SaveCoverFilename::Hash; - else if (ui_->radiobutton_cover_pattern->isChecked()) save_cover_filename = SaveCoverFilename::Pattern; + CoverOptions::CoverFilename save_cover_filename = CoverOptions::CoverFilename::Hash; + if (ui_->radiobutton_cover_hash->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Hash; + else if (ui_->radiobutton_cover_pattern->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Pattern; s.setValue("save_cover_filename", static_cast(save_cover_filename)); s.setValue("cover_pattern", ui_->lineedit_cover_pattern->text()); diff --git a/src/settings/collectionsettingspage.h b/src/settings/collectionsettingspage.h index c0235a60..9960a638 100644 --- a/src/settings/collectionsettingspage.h +++ b/src/settings/collectionsettingspage.h @@ -50,17 +50,6 @@ class CollectionSettingsPage : public SettingsPage { static const int kSettingsCacheSizeDefault; static const int kSettingsDiskCacheSizeDefault; - enum class SaveCoverType { - Cache = 1, - Album = 2, - Embedded = 3 - }; - - enum class SaveCoverFilename { - Hash = 1, - Pattern = 2 - }; - enum class CacheSizeUnit { KB, MB, diff --git a/src/tidal/tidalrequest.cpp b/src/tidal/tidalrequest.cpp index 98dc043d..185c1034 100644 --- a/src/tidal/tidalrequest.cpp +++ b/src/tidal/tidalrequest.cpp @@ -40,6 +40,7 @@ #include "core/application.h" #include "utilities/timeconstants.h" #include "utilities/imageutils.h" +#include "utilities/coverutils.h" #include "tidalservice.h" #include "tidalurlhandler.h" #include "tidalbaserequest.h" @@ -1208,7 +1209,7 @@ void TidalRequest::AddAlbumCoverRequest(const Song &song) { AlbumCoverRequest request; request.album_id = song.album_id(); request.url = song.art_automatic(); - request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url); + request.filename = CoverUtils::CoverFilePath(CoverOptions(), song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url); if (request.filename.isEmpty()) return; album_covers_requests_sent_.insert(song.album_id(), song.song_id()); diff --git a/src/utilities/coveroptions.h b/src/utilities/coveroptions.h new file mode 100644 index 00000000..bbb57134 --- /dev/null +++ b/src/utilities/coveroptions.h @@ -0,0 +1,46 @@ +/* +* Strawberry Music Player +* Copyright 2023, Jonas Kvinge +* +* Strawberry is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Strawberry is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Strawberry. If not, see . +* +*/ + +#ifndef COVEROPTIONS_H +#define COVEROPTIONS_H + +class CoverOptions { + public: + + enum class CoverType { + Cache = 1, + Album = 2, + Embedded = 3 + }; + + enum class CoverFilename { + Hash = 1, + Pattern = 2 + }; + + explicit CoverOptions() : cover_type(CoverType::Cache), cover_filename(CoverFilename::Hash), cover_overwrite(false), cover_lowercase(true), cover_replace_spaces(true) {} + CoverType cover_type; + CoverFilename cover_filename; + QString cover_pattern; + bool cover_overwrite; + bool cover_lowercase; + bool cover_replace_spaces; +}; + +#endif // COVEROPTIONS_H diff --git a/src/utilities/coverutils.cpp b/src/utilities/coverutils.cpp new file mode 100644 index 00000000..bd3cb2a3 --- /dev/null +++ b/src/utilities/coverutils.cpp @@ -0,0 +1,166 @@ +/* + * Strawberry Music Player + * Copyright 2019-2023, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "filenameconstants.h" +#include "transliterate.h" +#include "coverutils.h" +#include "core/logging.h" + +QByteArray CoverUtils::Sha1CoverHash(const QString &artist, const QString &album) { + + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(artist.toLower().toUtf8()); + hash.addData(album.toLower().toUtf8()); + + return hash.result(); + +} + +QString CoverUtils::AlbumCoverFilename(QString artist, QString album, const QString &extension) { + + artist.remove('/').remove('\\'); + album.remove('/').remove('\\'); + + QString filename = artist + "-" + album; + filename = Utilities::Transliterate(filename.toLower()); + filename = filename.replace(' ', '-') + .replace("--", "-") + .remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption)) + .simplified(); + + if (!extension.isEmpty()) { + filename.append('.'); + filename.append(extension); + } + + return filename; + +} + +QString CoverUtils::CoverFilePath(const CoverOptions &options, const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension) { + return CoverFilePath(options, song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url, extension); +} + +QString CoverUtils::CoverFilePath(const CoverOptions &options, const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension) { + + QString path; + if (source == Song::Source::Collection && options.cover_type == CoverOptions::CoverType::Album && !album_dir.isEmpty()) { + path = album_dir; + } + else { + path = Song::ImageCacheDir(source); + } + + if (path.right(1) == QDir::separator() || path.right(1) == "/") { + path.chop(1); + } + + QDir dir; + if (!dir.mkpath(path)) { + qLog(Error) << "Unable to create directory" << path; + path = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + } + + QString filename; + if (source == Song::Source::Collection && + options.cover_type == CoverOptions::CoverType::Album && + options.cover_filename == CoverOptions::CoverFilename::Pattern && + !options.cover_pattern.isEmpty()) { + filename = CoverFilenameFromVariable(options, artist, album); + filename.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption)).remove('/').remove('\\'); + if (options.cover_lowercase) filename = filename.toLower(); + if (options.cover_replace_spaces) filename.replace(QRegularExpression("\\s"), "-"); + if (!extension.isEmpty()) { + filename.append('.'); + filename.append(extension); + } + } + + if (filename.isEmpty()) { + filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id, extension); + } + + QString filepath(path + "/" + filename); + + return filepath; + +} + +QString CoverUtils::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension) { + + QString filename; + + switch (source) { + case Song::Source::Tidal: + if (!album_id.isEmpty()) { + filename = album_id + "-" + cover_url.fileName(); + break; + } + [[fallthrough]]; + case Song::Source::Subsonic: + case Song::Source::Qobuz: + if (!album_id.isEmpty()) { + filename = album_id; + break; + } + [[fallthrough]]; + case Song::Source::Collection: + case Song::Source::LocalFile: + case Song::Source::CDDA: + case Song::Source::Device: + case Song::Source::Stream: + case Song::Source::SomaFM: + case Song::Source::RadioParadise: + case Song::Source::Unknown: + filename = Sha1CoverHash(artist, album).toHex(); + break; + } + + if (!extension.isEmpty()) { + filename.append('.'); + filename.append(extension); + } + + return filename; + +} + +QString CoverUtils::CoverFilenameFromVariable(const CoverOptions &options, const QString &artist, QString album, const QString &extension) { + + album = album.remove(Song::kAlbumRemoveDisc); + + QString filename(options.cover_pattern); + filename.replace("%albumartist", artist); + filename.replace("%artist", artist); + filename.replace("%album", album); + if (!extension.isEmpty()) { + filename.append('.'); + filename.append(extension); + } + return filename; + +} diff --git a/src/utilities/coverutils.h b/src/utilities/coverutils.h new file mode 100644 index 00000000..1ad47dc7 --- /dev/null +++ b/src/utilities/coverutils.h @@ -0,0 +1,42 @@ +/* + * Strawberry Music Player + * Copyright 2019-2023, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef COVERUTILS_H +#define COVERUTILS_H + +#include +#include +#include + +#include "core/song.h" +#include "coveroptions.h" + +class CoverUtils { + + public: + static QByteArray Sha1CoverHash(const QString &artist, const QString &album); + static QString AlbumCoverFilename(QString artist, QString album, const QString &extension); + static QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension); + static QString CoverFilenameFromVariable(const CoverOptions &options, const QString &artist, QString album, const QString &extension = QString()); + static QString CoverFilePath(const CoverOptions &options, const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString()); + static QString CoverFilePath(const CoverOptions &options, const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString()); + +}; + +#endif // COVERUTILS_H diff --git a/src/utilities/cryptutils.cpp b/src/utilities/cryptutils.cpp index fea526c4..98b1b9e2 100644 --- a/src/utilities/cryptutils.cpp +++ b/src/utilities/cryptutils.cpp @@ -62,14 +62,4 @@ QByteArray HmacSha1(const QByteArray &key, const QByteArray &data) { return Hmac(key, data, QCryptographicHash::Sha1); } -QByteArray Sha1CoverHash(const QString &artist, const QString &album) { - - QCryptographicHash hash(QCryptographicHash::Sha1); - hash.addData(artist.toLower().toUtf8()); - hash.addData(album.toLower().toUtf8()); - - return hash.result(); - -} - } // namespace Utilities diff --git a/src/utilities/cryptutils.h b/src/utilities/cryptutils.h index bfb2ae13..9f77ed0d 100644 --- a/src/utilities/cryptutils.h +++ b/src/utilities/cryptutils.h @@ -30,7 +30,6 @@ QByteArray Hmac(const QByteArray &key, const QByteArray &data, const QCryptograp QByteArray HmacMd5(const QByteArray &key, const QByteArray &data); QByteArray HmacSha256(const QByteArray &key, const QByteArray &data); QByteArray HmacSha1(const QByteArray &key, const QByteArray &data); -QByteArray Sha1CoverHash(const QString &artist, const QString &album); } // namespace Utilities diff --git a/src/utilities/filenameconstants.h b/src/utilities/filenameconstants.h new file mode 100644 index 00000000..e442bcad --- /dev/null +++ b/src/utilities/filenameconstants.h @@ -0,0 +1,31 @@ +/* +* Strawberry Music Player +* Copyright 2023, Jonas Kvinge +* +* Strawberry is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Strawberry is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Strawberry. If not, see . +* +*/ + +#ifndef FILENAMECONSTANTS_H +#define FILENAMECONSTANTS_H + +#include "core/arraysize.h" + +constexpr char kProblematicCharactersRegex[] = "[:?*\"<>|]"; +constexpr char kInvalidFatCharactersRegex[] = "[^a-zA-Z0-9!#\\$%&'()\\-@\\^_`{}~/. ]"; +constexpr char kInvalidDirCharactersRegex[] = "[/\\\\]"; +constexpr char kInvalidPrefixCharacters[] = "."; +constexpr int kInvalidPrefixCharactersCount = arraysize(kInvalidPrefixCharacters) - 1; + +#endif // FILENAMECONSTANTS_H diff --git a/tests/src/tagreader_test.cpp b/tests/src/tagreader_test.cpp index 9b410478..a6c68c69 100644 --- a/tests/src/tagreader_test.cpp +++ b/tests/src/tagreader_test.cpp @@ -66,15 +66,18 @@ class TagReaderTest : public ::testing::Test { return song; } - static void WriteSongToFile(const Song& song, const QString& filename) { + static void WriteSongToFile(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.SaveFile(filename, pb_song); + ::spb::tagreader::SaveFileRequest request; + const QByteArray filename_data = filename.toUtf8(); + request.set_filename(filename_data.constData(), filename_data.length()); + request.set_save_tags(true); + song.ToProtobuf(request.mutable_metadata()); + tag_reader.SaveFile(request); } static QString SHA256SUM(const QString &filename) {