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 <QFileInfo>
#include <QList>
#include <QVector>
#include <QByteArray>
#include <QDateTime>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QVector>
#include <QtDebug>
#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<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *tag = file->xiphComment();
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(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<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 {
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<TagLib::FLAC::File*>(ref.file());
if (flac_file && flac_file->xiphComment()) {
TagLib::List<TagLib::FLAC::Picture*> 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<TagLib::FLAC::Picture*>::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<TagLib::FLAC::File*>(ref.file())) {
if (flac_file->xiphComment()) {
TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
if (!pics.isEmpty()) {
TagLib::FLAC::Picture *picture = nullptr;
for (std::list<TagLib::FLAC::Picture*>::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<TagLib::WavPack::File*>(ref.file());
if (wavpack_file) {
if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(ref.file())) {
return LoadEmbeddedAPEArt(wavpack_file->APETag()->itemListMap());
}
// APE
TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(ref.file());
if (ape_file) {
if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(ref.file())) {
return LoadEmbeddedAPEArt(ape_file->APETag()->itemListMap());
}
// MPC
TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(ref.file());
if (mpc_file) {
if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(ref.file())) {
return LoadEmbeddedAPEArt(mpc_file->APETag()->itemListMap());
}
// Ogg Vorbis / Speex
TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag());
if (xiph_comment) {
if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag())) {
TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
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());
}
// 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<TagLib::MPEG::File*>(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<TagLib::MPEG::File*>(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<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
TagLib::MP4::File *aac_file = dynamic_cast<TagLib::MP4::File*>(ref.file());
if (aac_file) {
if (TagLib::MP4::File *aac_file = dynamic_cast<TagLib::MP4::File*>(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<TagLib::ByteVector> 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<TagLib::FLAC::File*>(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<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 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);

View File

@ -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;
}

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()));
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<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) {
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;

View File

@ -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; }