From 0ab214fd5d43290262bb3d870a2203025cd2fecb Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Tue, 16 Feb 2021 22:50:35 +0100 Subject: [PATCH] Add methods to tagreader for saving embedded art --- ext/libstrawberry-tagreader/tagreader.cpp | 192 ++++++++++++------ ext/libstrawberry-tagreader/tagreader.h | 1 + .../tagreadermessages.proto | 12 ++ ext/strawberry-tagreader/tagreaderworker.cpp | 4 +- src/core/tagreaderclient.cpp | 36 +++- src/core/tagreaderclient.h | 2 + 6 files changed, 185 insertions(+), 62 deletions(-) diff --git a/ext/libstrawberry-tagreader/tagreader.cpp b/ext/libstrawberry-tagreader/tagreader.cpp index 2671f27b1..52cab04ba 100644 --- a/ext/libstrawberry-tagreader/tagreader.cpp +++ b/ext/libstrawberry-tagreader/tagreader.cpp @@ -79,12 +79,12 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include "core/logging.h" @@ -540,8 +540,8 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad bool result = false; - if (TagLib::FLAC::File *file = dynamic_cast(fileref->file())) { - TagLib::Ogg::XiphComment *tag = file->xiphComment(); + if (TagLib::FLAC::File *file_flac = dynamic_cast(fileref->file())) { + TagLib::Ogg::XiphComment *tag = file_flac->xiphComment(); SetVorbisComments(tag, song); } @@ -653,6 +653,38 @@ void TagReader::SetTextFrame(const char *id, const std::string &value, TagLib::I } +void TagReader::SetUnsyncLyricsFrame(const std::string &value, TagLib::ID3v2::Tag *tag) const { + + TagLib::ByteVector id_vector("USLT"); + QVector frames_buffer; + + // Store and clear existing frames + while (tag->frameListMap().contains(id_vector) && tag->frameListMap()[id_vector].size() != 0) { + frames_buffer.push_back(tag->frameListMap()[id_vector].front()->render()); + tag->removeFrame(tag->frameListMap()[id_vector].front()); + } + + if (value.empty()) return; + + // If no frames stored create empty frame + if (frames_buffer.isEmpty()) { + TagLib::ID3v2::UnsynchronizedLyricsFrame frame(TagLib::String::UTF8); + frame.setDescription("Clementine editor"); + frames_buffer.push_back(frame.render()); + } + + // Update and add the frames + for (int i = 0 ; i < frames_buffer.size() ; ++i) { + TagLib::ID3v2::UnsynchronizedLyricsFrame *frame = new TagLib::ID3v2::UnsynchronizedLyricsFrame(frames_buffer.at(i)); + if (i == 0) { + frame->setText(StdStringToTaglibString(value)); + } + // add frame takes ownership and clears the memory + tag->addFrame(frame); + } + +} + QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { if (filename.isEmpty()) return QByteArray(); @@ -668,43 +700,39 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { if (ref.isNull() || !ref.file()) return QByteArray(); // FLAC - TagLib::FLAC::File *flac_file = dynamic_cast(ref.file()); - if (flac_file && flac_file->xiphComment()) { - TagLib::List pics = flac_file->pictureList(); - if (!pics.isEmpty()) { - // Use the first picture in the file - this could be made cleverer and pick the front cover if it's present. - - std::list::iterator it = pics.begin(); - TagLib::FLAC::Picture *picture = *it; - - return QByteArray(picture->data().data(), picture->data().size()); + if (TagLib::FLAC::File *flac_file = dynamic_cast(ref.file())) { + if (flac_file->xiphComment()) { + TagLib::List pics = flac_file->pictureList(); + if (!pics.isEmpty()) { + TagLib::FLAC::Picture *picture = nullptr; + for (std::list::iterator it = pics.begin() ; it != pics.end() ; ++it) { + picture = *it; + if (picture->type() == TagLib::FLAC::Picture::FrontCover) { + break; + } + } + if (picture) return QByteArray(picture->data().data(), picture->data().size()); + } } } // WavPack - - TagLib::WavPack::File *wavpack_file = dynamic_cast(ref.file()); - if (wavpack_file) { + if (TagLib::WavPack::File *wavpack_file = dynamic_cast(ref.file())) { return LoadEmbeddedAPEArt(wavpack_file->APETag()->itemListMap()); } // APE - - TagLib::APE::File *ape_file = dynamic_cast(ref.file()); - if (ape_file) { + if (TagLib::APE::File *ape_file = dynamic_cast(ref.file())) { return LoadEmbeddedAPEArt(ape_file->APETag()->itemListMap()); } // MPC - - TagLib::MPC::File *mpc_file = dynamic_cast(ref.file()); - if (mpc_file) { + if (TagLib::MPC::File *mpc_file = dynamic_cast(ref.file())) { return LoadEmbeddedAPEArt(mpc_file->APETag()->itemListMap()); } // Ogg Vorbis / Speex - TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(ref.file()->tag()); - if (xiph_comment) { + if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(ref.file()->tag())) { TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap(); TagLib::List pics = xiph_comment->pictureList(); @@ -720,8 +748,7 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { return QByteArray(picture->data().data(), picture->data().size()); } - // Ogg lacks a definitive standard for embedding cover art, but it seems - // b64 encoding a field called COVERART is the general convention + // Ogg lacks a definitive standard for embedding cover art, but it seems b64 encoding a field called COVERART is the general convention if (map.contains("COVERART")) return QByteArray::fromBase64(map["COVERART"].toString().toCString()); @@ -729,20 +756,20 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { } // MP3 - TagLib::MPEG::File *file = dynamic_cast(ref.file()); - if (file && file->ID3v2Tag()) { - TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"]; - if (apic_frames.isEmpty()) - return QByteArray(); + if (TagLib::MPEG::File *file_mp3 = dynamic_cast(ref.file())) { + if (file_mp3->ID3v2Tag()) { + TagLib::ID3v2::FrameList apic_frames = file_mp3->ID3v2Tag()->frameListMap()["APIC"]; + if (apic_frames.isEmpty()) + return QByteArray(); - TagLib::ID3v2::AttachedPictureFrame *pic = static_cast(apic_frames.front()); + TagLib::ID3v2::AttachedPictureFrame *pic = static_cast(apic_frames.front()); - return QByteArray(reinterpret_cast(pic->picture().data()), pic->picture().size()); + return QByteArray(reinterpret_cast(pic->picture().data()), pic->picture().size()); + } } // MP4/AAC - TagLib::MP4::File *aac_file = dynamic_cast(ref.file()); - if (aac_file) { + if (TagLib::MP4::File *aac_file = dynamic_cast(ref.file())) { TagLib::MP4::Tag *tag = aac_file->tag(); if (tag->item("covr").isValid()) { const TagLib::MP4::CoverArtList &art_list = tag->item("covr").toCoverArtList(); @@ -777,34 +804,85 @@ QByteArray TagReader::LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) co } -void TagReader::SetUnsyncLyricsFrame(const std::string &value, TagLib::ID3v2::Tag *tag) const { +bool TagReader::SaveEmbeddedArt(const QString &filename, const QByteArray &data) { - TagLib::ByteVector id_vector("USLT"); - QVector frames_buffer; + if (filename.isEmpty()) return false; - // Store and clear existing frames - while (tag->frameListMap().contains(id_vector) && tag->frameListMap()[id_vector].size() != 0) { - frames_buffer.push_back(tag->frameListMap()[id_vector].front()->render()); - tag->removeFrame(tag->frameListMap()[id_vector].front()); - } + qLog(Debug) << "Saving art to" << filename; - if (value.empty()) return; +#ifdef Q_OS_WIN32 + TagLib::FileRef ref(filename.toStdWString().c_str()); +#else + TagLib::FileRef ref(QFile::encodeName(filename).constData()); +#endif - // If no frames stored create empty frame - if (frames_buffer.isEmpty()) { - TagLib::ID3v2::UnsynchronizedLyricsFrame frame(TagLib::String::UTF8); - frame.setDescription("Clementine editor"); - frames_buffer.push_back(frame.render()); - } + if (ref.isNull() || !ref.file()) return false; - // Update and add the frames - for (int i = 0 ; i < frames_buffer.size() ; ++i) { - TagLib::ID3v2::UnsynchronizedLyricsFrame *frame = new TagLib::ID3v2::UnsynchronizedLyricsFrame(frames_buffer.at(i)); - if (i == 0) { - frame->setText(StdStringToTaglibString(value)); + // FLAC + if (TagLib::FLAC::File *flac_file = dynamic_cast(ref.file())) { + if (flac_file->xiphComment()) { + 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); + } } - // add frame takes ownership and clears the memory - tag->addFrame(frame); } + // Ogg Vorbis / Speex + else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(ref.file()->tag())) { + 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); + } + + // MP3 + else if (TagLib::MPEG::File *file_mp3 = dynamic_cast(ref.file())) { + if (file_mp3->ID3v2Tag()) { + TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag(); + + // 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.count())); + tag->addFrame(frontcover); + } + } + } + + // MP4/AAC + else if (TagLib::MP4::File *aac_file = dynamic_cast(ref.file())) { + TagLib::MP4::Tag *tag = aac_file->tag(); + TagLib::MP4::CoverArtList covers; + if (tag) { + 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.count()))); + tag->setItem("covr", covers); + } + } + } + + // Not supported. + else return false; + + return ref.file()->save(); + } diff --git a/ext/libstrawberry-tagreader/tagreader.h b/ext/libstrawberry-tagreader/tagreader.h index aea8d2d3f..8fe998c90 100644 --- a/ext/libstrawberry-tagreader/tagreader.h +++ b/ext/libstrawberry-tagreader/tagreader.h @@ -58,6 +58,7 @@ class TagReader { QByteArray LoadEmbeddedArt(const QString &filename) const; QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const; + bool SaveEmbeddedArt(const QString &filename, const QByteArray &data); static void Decode(const TagLib::String &tag, std::string *output); static void Decode(const QString &tag, std::string *output); diff --git a/ext/libstrawberry-tagreader/tagreadermessages.proto b/ext/libstrawberry-tagreader/tagreadermessages.proto index 4b046254f..5ec17cdf5 100644 --- a/ext/libstrawberry-tagreader/tagreadermessages.proto +++ b/ext/libstrawberry-tagreader/tagreadermessages.proto @@ -100,6 +100,15 @@ message LoadEmbeddedArtResponse { optional bytes data = 1; } +message SaveEmbeddedArtRequest { + optional string filename = 1; + optional bytes data = 2; +} + +message SaveEmbeddedArtResponse { + optional bool success = 1; +} + message Message { optional int64 id = 1; @@ -115,4 +124,7 @@ message Message { optional LoadEmbeddedArtRequest load_embedded_art_request = 8; optional LoadEmbeddedArtResponse load_embedded_art_response = 9; + optional SaveEmbeddedArtRequest save_embedded_art_request = 10; + optional SaveEmbeddedArtResponse save_embedded_art_response = 11; + } diff --git a/ext/strawberry-tagreader/tagreaderworker.cpp b/ext/strawberry-tagreader/tagreaderworker.cpp index d348f7b6b..b27dbdb73 100644 --- a/ext/strawberry-tagreader/tagreaderworker.cpp +++ b/ext/strawberry-tagreader/tagreaderworker.cpp @@ -47,12 +47,14 @@ void TagReaderWorker::MessageArrived(const pb::tagreader::Message &message) { QByteArray data = tag_reader_.LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename())); reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size()); } + else if (message.has_save_embedded_art_request()) { + reply.mutable_save_embedded_art_response()->set_success(tag_reader_.SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), message.save_embedded_art_request().data().size()))); + } SendReply(message, &reply); } - void TagReaderWorker::DeviceClosed() { AbstractMessageHandler::DeviceClosed(); diff --git a/src/core/tagreaderclient.cpp b/src/core/tagreaderclient.cpp index d0429bb46..9d33661e4 100644 --- a/src/core/tagreaderclient.cpp +++ b/src/core/tagreaderclient.cpp @@ -116,6 +116,18 @@ TagReaderReply *TagReaderClient::LoadEmbeddedArt(const QString &filename) { } +TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const QByteArray &data) { + + pb::tagreader::Message message; + pb::tagreader::SaveEmbeddedArtRequest *req = message.mutable_save_embedded_art_request(); + + req->set_filename(DataCommaSizeFromQString(filename)); + req->set_data(data.constData(), data.size()); + + return worker_pool_->SendMessageWithReply(&message); + +} + void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) { Q_ASSERT(QThread::currentThread() != thread()); @@ -124,7 +136,7 @@ void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) { if (reply->WaitForFinished()) { song->InitFromProtobuf(reply->message().read_file_response().metadata()); } - reply->deleteLater(); + metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection); } @@ -138,7 +150,7 @@ bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &meta if (reply->WaitForFinished()) { ret = reply->message().save_file_response().success(); } - reply->deleteLater(); + metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection); return ret; @@ -154,7 +166,7 @@ bool TagReaderClient::IsMediaFileBlocking(const QString &filename) { if (reply->WaitForFinished()) { ret = reply->message().is_media_file_response().success(); } - reply->deleteLater(); + metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection); return ret; @@ -171,7 +183,23 @@ QImage TagReaderClient::LoadEmbeddedArtBlocking(const QString &filename) { const std::string &data_str = reply->message().load_embedded_art_response().data(); ret.loadFromData(QByteArray(data_str.data(), data_str.size())); } - reply->deleteLater(); + metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + + return ret; + +} + +bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data) { + + Q_ASSERT(QThread::currentThread() != thread()); + + bool ret = false; + + TagReaderReply *reply = SaveEmbeddedArt(filename, data); + if (reply->WaitForFinished()) { + ret = reply->message().save_embedded_art_response().success(); + } + metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection); return ret; diff --git a/src/core/tagreaderclient.h b/src/core/tagreaderclient.h index 42ad306d2..a0229a96a 100644 --- a/src/core/tagreaderclient.h +++ b/src/core/tagreaderclient.h @@ -56,6 +56,7 @@ class TagReaderClient : public QObject { ReplyType *SaveFile(const QString &filename, const Song &metadata); ReplyType *IsMediaFile(const QString &filename); ReplyType *LoadEmbeddedArt(const QString &filename); + ReplyType *SaveEmbeddedArt(const QString &filename, const QByteArray &data); // 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. @@ -63,6 +64,7 @@ class TagReaderClient : public QObject { bool SaveFileBlocking(const QString &filename, const Song &metadata); bool IsMediaFileBlocking(const QString &filename); QImage LoadEmbeddedArtBlocking(const QString &filename); + bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data); // TODO: Make this not a singleton static TagReaderClient *Instance() { return sInstance; }