Add methods to tagreader for saving embedded art

This commit is contained in:
Jonas Kvinge
2021-02-16 22:50:35 +01:00
parent eb603e942f
commit 0ab214fd5d
6 changed files with 185 additions and 62 deletions

View File

@@ -79,12 +79,12 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QList> #include <QList>
#include <QVector>
#include <QByteArray> #include <QByteArray>
#include <QDateTime> #include <QDateTime>
#include <QVariant> #include <QVariant>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QVector>
#include <QtDebug> #include <QtDebug>
#include "core/logging.h" #include "core/logging.h"
@@ -540,8 +540,8 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad
bool result = false; bool result = false;
if (TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) { if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *tag = file->xiphComment(); TagLib::Ogg::XiphComment *tag = file_flac->xiphComment();
SetVorbisComments(tag, song); 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<TagLib::ByteVector> 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 { QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const {
if (filename.isEmpty()) return QByteArray(); if (filename.isEmpty()) return QByteArray();
@@ -668,43 +700,39 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const {
if (ref.isNull() || !ref.file()) return QByteArray(); if (ref.isNull() || !ref.file()) return QByteArray();
// FLAC // FLAC
TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file()); if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file())) {
if (flac_file && flac_file->xiphComment()) { if (flac_file->xiphComment()) {
TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList(); TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
if (!pics.isEmpty()) { if (!pics.isEmpty()) {
// Use the first picture in the file - this could be made cleverer and pick the front cover if it's present. TagLib::FLAC::Picture *picture = nullptr;
for (std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin() ; it != pics.end() ; ++it) {
std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin(); picture = *it;
TagLib::FLAC::Picture *picture = *it; if (picture->type() == TagLib::FLAC::Picture::FrontCover) {
break;
return QByteArray(picture->data().data(), picture->data().size()); }
}
if (picture) return QByteArray(picture->data().data(), picture->data().size());
}
} }
} }
// WavPack // WavPack
if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(ref.file())) {
TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(ref.file());
if (wavpack_file) {
return LoadEmbeddedAPEArt(wavpack_file->APETag()->itemListMap()); return LoadEmbeddedAPEArt(wavpack_file->APETag()->itemListMap());
} }
// APE // APE
if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(ref.file())) {
TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(ref.file());
if (ape_file) {
return LoadEmbeddedAPEArt(ape_file->APETag()->itemListMap()); return LoadEmbeddedAPEArt(ape_file->APETag()->itemListMap());
} }
// MPC // MPC
if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(ref.file())) {
TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(ref.file());
if (mpc_file) {
return LoadEmbeddedAPEArt(mpc_file->APETag()->itemListMap()); return LoadEmbeddedAPEArt(mpc_file->APETag()->itemListMap());
} }
// Ogg Vorbis / Speex // Ogg Vorbis / Speex
TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag()); if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag())) {
if (xiph_comment) {
TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap(); TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
TagLib::List<TagLib::FLAC::Picture*> pics = xiph_comment->pictureList(); TagLib::List<TagLib::FLAC::Picture*> pics = xiph_comment->pictureList();
@@ -720,8 +748,7 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const {
return QByteArray(picture->data().data(), picture->data().size()); return QByteArray(picture->data().data(), picture->data().size());
} }
// Ogg lacks a definitive standard for embedding cover art, but it seems // Ogg lacks a definitive standard for embedding cover art, but it seems b64 encoding a field called COVERART is the general convention
// b64 encoding a field called COVERART is the general convention
if (map.contains("COVERART")) if (map.contains("COVERART"))
return QByteArray::fromBase64(map["COVERART"].toString().toCString()); return QByteArray::fromBase64(map["COVERART"].toString().toCString());
@@ -729,20 +756,20 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const {
} }
// MP3 // MP3
TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File*>(ref.file()); if (TagLib::MPEG::File *file_mp3 = dynamic_cast<TagLib::MPEG::File*>(ref.file())) {
if (file && file->ID3v2Tag()) { if (file_mp3->ID3v2Tag()) {
TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"]; TagLib::ID3v2::FrameList apic_frames = file_mp3->ID3v2Tag()->frameListMap()["APIC"];
if (apic_frames.isEmpty()) if (apic_frames.isEmpty())
return QByteArray(); return QByteArray();
TagLib::ID3v2::AttachedPictureFrame *pic = static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front()); TagLib::ID3v2::AttachedPictureFrame *pic = static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
return QByteArray(reinterpret_cast<const char*>(pic->picture().data()), pic->picture().size()); return QByteArray(reinterpret_cast<const char*>(pic->picture().data()), pic->picture().size());
}
} }
// MP4/AAC // MP4/AAC
TagLib::MP4::File *aac_file = dynamic_cast<TagLib::MP4::File*>(ref.file()); if (TagLib::MP4::File *aac_file = dynamic_cast<TagLib::MP4::File*>(ref.file())) {
if (aac_file) {
TagLib::MP4::Tag *tag = aac_file->tag(); TagLib::MP4::Tag *tag = aac_file->tag();
if (tag->item("covr").isValid()) { if (tag->item("covr").isValid()) {
const TagLib::MP4::CoverArtList &art_list = tag->item("covr").toCoverArtList(); 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"); if (filename.isEmpty()) return false;
QVector<TagLib::ByteVector> frames_buffer;
// Store and clear existing frames qLog(Debug) << "Saving art to" << filename;
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; #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 (ref.isNull() || !ref.file()) return false;
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 // FLAC
for (int i = 0 ; i < frames_buffer.size() ; ++i) { if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file())) {
TagLib::ID3v2::UnsynchronizedLyricsFrame *frame = new TagLib::ID3v2::UnsynchronizedLyricsFrame(frames_buffer.at(i)); if (flac_file->xiphComment()) {
if (i == 0) { flac_file->removePictures();
frame->setText(StdStringToTaglibString(value)); 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<TagLib::Ogg::XiphComment*>(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<TagLib::MPEG::File*>(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<TagLib::ID3v2::AttachedPictureFrame*>(*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<TagLib::MP4::File*>(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();
} }

View File

@@ -58,6 +58,7 @@ class TagReader {
QByteArray LoadEmbeddedArt(const QString &filename) const; QByteArray LoadEmbeddedArt(const QString &filename) const;
QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) 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 TagLib::String &tag, std::string *output);
static void Decode(const QString &tag, std::string *output); static void Decode(const QString &tag, std::string *output);

View File

@@ -100,6 +100,15 @@ message LoadEmbeddedArtResponse {
optional bytes data = 1; optional bytes data = 1;
} }
message SaveEmbeddedArtRequest {
optional string filename = 1;
optional bytes data = 2;
}
message SaveEmbeddedArtResponse {
optional bool success = 1;
}
message Message { message Message {
optional int64 id = 1; optional int64 id = 1;
@@ -115,4 +124,7 @@ message Message {
optional LoadEmbeddedArtRequest load_embedded_art_request = 8; optional LoadEmbeddedArtRequest load_embedded_art_request = 8;
optional LoadEmbeddedArtResponse load_embedded_art_response = 9; optional LoadEmbeddedArtResponse load_embedded_art_response = 9;
optional SaveEmbeddedArtRequest save_embedded_art_request = 10;
optional SaveEmbeddedArtResponse save_embedded_art_response = 11;
} }

View File

@@ -47,12 +47,14 @@ void TagReaderWorker::MessageArrived(const pb::tagreader::Message &message) {
QByteArray data = tag_reader_.LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename())); QByteArray data = tag_reader_.LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename()));
reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size()); 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); SendReply(message, &reply);
} }
void TagReaderWorker::DeviceClosed() { void TagReaderWorker::DeviceClosed() {
AbstractMessageHandler<pb::tagreader::Message>::DeviceClosed(); AbstractMessageHandler<pb::tagreader::Message>::DeviceClosed();

View File

@@ -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) { void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) {
Q_ASSERT(QThread::currentThread() != thread()); Q_ASSERT(QThread::currentThread() != thread());
@@ -124,7 +136,7 @@ void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) {
if (reply->WaitForFinished()) { if (reply->WaitForFinished()) {
song->InitFromProtobuf(reply->message().read_file_response().metadata()); 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()) { if (reply->WaitForFinished()) {
ret = reply->message().save_file_response().success(); ret = reply->message().save_file_response().success();
} }
reply->deleteLater(); metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
return ret; return ret;
@@ -154,7 +166,7 @@ bool TagReaderClient::IsMediaFileBlocking(const QString &filename) {
if (reply->WaitForFinished()) { if (reply->WaitForFinished()) {
ret = reply->message().is_media_file_response().success(); ret = reply->message().is_media_file_response().success();
} }
reply->deleteLater(); metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
return ret; return ret;
@@ -171,7 +183,23 @@ QImage TagReaderClient::LoadEmbeddedArtBlocking(const QString &filename) {
const std::string &data_str = reply->message().load_embedded_art_response().data(); const std::string &data_str = reply->message().load_embedded_art_response().data();
ret.loadFromData(QByteArray(data_str.data(), data_str.size())); 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; return ret;

View File

@@ -56,6 +56,7 @@ class TagReaderClient : public QObject {
ReplyType *SaveFile(const QString &filename, const Song &metadata); ReplyType *SaveFile(const QString &filename, const Song &metadata);
ReplyType *IsMediaFile(const QString &filename); ReplyType *IsMediaFile(const QString &filename);
ReplyType *LoadEmbeddedArt(const QString &filename); ReplyType *LoadEmbeddedArt(const QString &filename);
ReplyType *SaveEmbeddedArt(const QString &filename, const QByteArray &data);
// Convenience functions that call the above functions and wait for a response. // Convenience functions that call the above functions and wait for a response.
// These block the calling thread with a semaphore, and must NOT be called from the TagReaderClient's thread. // These block the calling thread with a semaphore, and must NOT be called from the TagReaderClient's thread.
@@ -63,6 +64,7 @@ class TagReaderClient : public QObject {
bool SaveFileBlocking(const QString &filename, const Song &metadata); bool SaveFileBlocking(const QString &filename, const Song &metadata);
bool IsMediaFileBlocking(const QString &filename); bool IsMediaFileBlocking(const QString &filename);
QImage LoadEmbeddedArtBlocking(const QString &filename); QImage LoadEmbeddedArtBlocking(const QString &filename);
bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data);
// TODO: Make this not a singleton // TODO: Make this not a singleton
static TagReaderClient *Instance() { return sInstance; } static TagReaderClient *Instance() { return sInstance; }