8#include "taglibwriter.h"
9#include "embeddedimagedata.h"
10#include "kfilemetadata_debug.h"
15#include <tfilestream.h>
16#include <tpropertymap.h>
31#include <vorbisfile.h>
33#include <wavpackfile.h>
35#include <popularimeterframe.h>
36#include <attachedpictureframe.h>
41 QStringLiteral(
"audio/flac"),
42 QStringLiteral(
"audio/mp4"),
43 QStringLiteral(
"audio/mpeg"),
44 QStringLiteral(
"audio/mpeg3"),
45 QStringLiteral(
"audio/ogg"),
46 QStringLiteral(
"audio/opus"),
47 QStringLiteral(
"audio/wav"),
48 QStringLiteral(
"audio/vnd.wave"),
49 QStringLiteral(
"audio/x-aiff"),
50 QStringLiteral(
"audio/x-aifc"),
51 QStringLiteral(
"audio/x-ape"),
52 QStringLiteral(
"audio/x-mpeg"),
53 QStringLiteral(
"audio/x-ms-wma"),
54 QStringLiteral(
"audio/x-musepack"),
55 QStringLiteral(
"audio/x-opus+ogg"),
56 QStringLiteral(
"audio/x-speex+ogg"),
57 QStringLiteral(
"audio/x-vorbis+ogg"),
58 QStringLiteral(
"audio/x-wav"),
59 QStringLiteral(
"audio/x-wavpack"),
62int id3v2RatingTranslation[11] = {
63 0, 1, 13, 54, 64, 118, 128, 186, 196, 242, 255
66using namespace KFileMetaData;
68template<
typename ImageType>
72 case ImageType::FrontCover:
73 return EmbeddedImageData::FrontCover;
74 case ImageType::Other:
75 return EmbeddedImageData::Other;
76 case ImageType::FileIcon:
77 return EmbeddedImageData::FileIcon;
78 case ImageType::OtherFileIcon:
79 return EmbeddedImageData::OtherFileIcon;
80 case ImageType::BackCover:
81 return EmbeddedImageData::BackCover;
82 case ImageType::LeafletPage:
83 return EmbeddedImageData::LeafletPage;
84 case ImageType::Media:
85 return EmbeddedImageData::Media;
86 case ImageType::LeadArtist:
87 return EmbeddedImageData::LeadArtist;
88 case ImageType::Artist:
89 return EmbeddedImageData::Artist;
90 case ImageType::Conductor:
91 return EmbeddedImageData::Conductor;
93 return EmbeddedImageData::Band;
94 case ImageType::Composer:
95 return EmbeddedImageData::Composer;
96 case ImageType::Lyricist:
97 return EmbeddedImageData::Lyricist;
98 case ImageType::RecordingLocation:
99 return EmbeddedImageData::RecordingLocation;
100 case ImageType::DuringRecording:
101 return EmbeddedImageData::DuringRecording;
102 case ImageType::DuringPerformance:
103 return EmbeddedImageData::DuringPerformance;
104 case ImageType::MovieScreenCapture:
105 return EmbeddedImageData::MovieScreenCapture;
106 case ImageType::ColouredFish:
107 return EmbeddedImageData::ColouredFish;
108 case ImageType::Illustration:
109 return EmbeddedImageData::Illustration;
110 case ImageType::BandLogo:
111 return EmbeddedImageData::BandLogo;
112 case ImageType::PublisherLogo:
113 return EmbeddedImageData::PublisherLogo;
115 return EmbeddedImageData::Unknown;
119template<
typename ImageType>
120static const std::array<typename ImageType::Type, 21> allImageTypes = {
121 ImageType::FrontCover,
124 ImageType::OtherFileIcon,
125 ImageType::BackCover,
126 ImageType::LeafletPage,
128 ImageType::LeadArtist,
130 ImageType::Conductor,
134 ImageType::RecordingLocation,
135 ImageType::DuringRecording,
136 ImageType::DuringPerformance,
137 ImageType::MovieScreenCapture,
138 ImageType::ColouredFish,
139 ImageType::Illustration,
141 ImageType::PublisherLogo,
144TagLib::String determineMimeType(
const QByteArray &pictureData)
147 return TagLib::String(
"image/png");
152 return TagLib::String(
"image/jpeg");
154 return TagLib::String();
158void writeID3v2Tags(TagLib::ID3v2::Tag *id3Tags,
const PropertyMultiMap &newProperties)
160 if (newProperties.
contains(Property::Rating)) {
161 int rating = newProperties.
value(Property::Rating).toInt();
162 if (rating >= 0 && rating <= 10) {
163 id3Tags->removeFrames(
"POPM");
165 auto ratingFrame =
new TagLib::ID3v2::PopularimeterFrame;
166 ratingFrame->setEmail(
"org.kde.kfilemetadata");
167 ratingFrame->setRating(id3v2RatingTranslation[rating]);
168 id3Tags->addFrame(ratingFrame);
173void writeID3v2Cover(TagLib::ID3v2::Tag *id3Tags,
179 [&](
const std::pair<EmbeddedImageData::ImageType, QByteArray> it) {
180 if (it.second.isEmpty()) {
181 removeTypes |= it.first;
183 wantedTypes |= it.first;
187 using PictureFrame = TagLib::ID3v2::AttachedPictureFrame;
189 wantedTypes &= ~kfmType;
190 auto newCover = images[kfmType];
191 auto newMimeType = determineMimeType(newCover);
192 if (!newMimeType.isEmpty()) {
193 coverFrame->setPicture(TagLib::ByteVector(
static_cast<const char *
>(newCover.data()), newCover.size()));
194 coverFrame->setMimeType(newMimeType);
199 TagLib::ID3v2::FrameList lstID3v2 = id3Tags->frameListMap()[
"APIC"];
200 for (
auto& frame :
std::as_const(lstID3v2)) {
201 auto* coverFrame =
static_cast<PictureFrame *
>(frame);
202 const auto kfmType = mapTaglibType<PictureFrame::Type>(coverFrame->type());
203 if (kfmType & wantedTypes) {
204 updateFrame(coverFrame, kfmType);
205 }
else if (kfmType & removeTypes) {
206 id3Tags->removeFrame(coverFrame);
210 for (
const auto type : allImageTypes<PictureFrame>) {
211 const auto kfmType = mapTaglibType<PictureFrame::Type>(type);
212 if (kfmType & wantedTypes) {
214 auto* coverFrame =
new PictureFrame;
215 coverFrame->setType(type);
216 updateFrame(coverFrame, kfmType);
217 id3Tags->addFrame(coverFrame);
224template<
typename Container>
225void writeFlacCover(Container* tags,
231 [&](
const std::pair<EmbeddedImageData::ImageType, QByteArray> it) {
232 if (it.second.isEmpty()) {
233 removeTypes |= it.first;
235 wantedTypes |= it.first;
239 using PictureFrame = TagLib::FLAC::Picture;
241 wantedTypes &= ~kfmType;
242 auto newCover = images[kfmType];
243 auto newMimeType = determineMimeType(newCover);
244 if (!newMimeType.isEmpty()) {
245 coverFrame->setData(TagLib::ByteVector(
static_cast<const char *
>(newCover.data()), newCover.size()));
246 coverFrame->setMimeType(newMimeType);
251 auto lstPic = tags->pictureList();
252 for (
auto it = lstPic.begin(); it != lstPic.end(); ++it) {
253 const auto kfmType = mapTaglibType<PictureFrame::Type>((*it)->type());
254 if (kfmType & wantedTypes) {
255 updateFrame((*it), kfmType);
256 }
else if (kfmType & removeTypes) {
257 tags->removePicture(*it);
261 for (
const auto type : allImageTypes<PictureFrame>) {
262 const auto kfmType = mapTaglibType<PictureFrame::Type>(type);
263 if (kfmType & wantedTypes) {
265 auto* coverFrame =
new PictureFrame;
266 coverFrame->setType(type);
267 updateFrame(coverFrame, kfmType);
268 tags->addPicture(coverFrame);
273void writeApeTags(TagLib::PropertyMap &oldProperties,
const PropertyMultiMap &newProperties)
275 if (newProperties.
contains(Property::Rating)) {
276 oldProperties.replace(
"RATING", TagLib::String::number(newProperties.
value(Property::Rating).toInt() * 10));
280void writeApeCover(TagLib::APE::Tag* apeTags,
283 if (images.
empty()) {
286 auto imageIt = images.
constFind(EmbeddedImageData::FrontCover);
287 if ((images.
size() > 1) || (imageIt == images.
constEnd())) {
294 const auto newCover = *imageIt;
295 if (newCover.isEmpty()) {
296 apeTags->removeItem(
"COVER ART (FRONT)");
300 TagLib::ByteVector imageData;
301 if (determineMimeType(newCover) == TagLib::String(
"image/png")) {
302 imageData.setData(
"frontCover.png\0", 15);
304 imageData.setData(
"frontCover.jpeg\0", 16);
306 imageData.append(TagLib::ByteVector(newCover.constData(), newCover.size()));
307 apeTags->setData(
"COVER ART (FRONT)", imageData);
310void writeVorbisTags(TagLib::PropertyMap &oldProperties,
const PropertyMultiMap &newProperties)
312 if (newProperties.
contains(Property::Rating)) {
313 oldProperties.replace(
"RATING", TagLib::String::number(newProperties.
value(Property::Rating).toInt() * 10));
317void writeAsfTags(TagLib::ASF::Tag *asfTags,
const PropertyMultiMap &properties)
322 int rating =
properties.value(Property::Rating).toInt();
325 }
else if (rating <= 2) {
327 }
else if (rating == 10){
330 rating =
static_cast<int>(12.5 * rating - 25);
332 asfTags->setAttribute(
"WM/SharedUserRating", TagLib::String::number(rating));
336void writeAsfCover(TagLib::ASF::Tag* asfTags,
342 [&](
const std::pair<EmbeddedImageData::ImageType, QByteArray> it) {
343 if (it.second.isEmpty()) {
344 removeTypes |= it.first;
346 wantedTypes |= it.first;
350 using PictureFrame = TagLib::ASF::Picture;
352 wantedTypes &= ~kfmType;
353 auto newCover = images[kfmType];
354 auto newMimeType = determineMimeType(newCover);
355 if (!newMimeType.isEmpty()) {
356 coverFrame->setPicture(TagLib::ByteVector(
static_cast<const char *
>(newCover.data()), newCover.size()));
357 coverFrame->setMimeType(newMimeType);
362 TagLib::ASF::AttributeList lstPic = asfTags->attribute(
"WM/Picture");
363 for (
auto it = lstPic.begin(); it != lstPic.end(); ) {
364 PictureFrame picture = (*it).toPicture();
365 const auto kfmType = mapTaglibType<PictureFrame::Type>(picture.type());
366 if (kfmType & wantedTypes) {
367 updateFrame(&picture, kfmType);
369 }
else if (kfmType & removeTypes) {
370 it = lstPic.erase(it);
376 for (
const auto type : allImageTypes<PictureFrame>) {
377 const auto kfmType = mapTaglibType<PictureFrame::Type>(type);
378 if (kfmType & wantedTypes) {
379 PictureFrame coverFrame;
380 updateFrame(&coverFrame, kfmType);
381 coverFrame.setType(type);
382 lstPic.append(coverFrame);
385 asfTags->setAttribute(
"WM/Picture", lstPic);
387void writeMp4Tags(TagLib::MP4::Tag *mp4Tags,
const PropertyMultiMap &newProperties)
389 if (newProperties.
contains(Property::Rating)) {
390 mp4Tags->setItem(
"rate", TagLib::StringList(TagLib::String::number(newProperties.
value(Property::Rating).toInt() * 10)));
394void writeMp4Cover(TagLib::MP4::Tag *mp4Tags,
397 if (images.
empty()) {
400 auto imageIt = images.
constFind(EmbeddedImageData::FrontCover);
401 if ((images.
size() > 1) || (imageIt == images.
constEnd())) {
408 TagLib::MP4::CoverArtList coverArtList;
409 const auto newCover = *imageIt;
410 if (!newCover.isEmpty()) {
411 TagLib::ByteVector imageData(newCover.data(), newCover.size());
412 TagLib::MP4::CoverArt coverArt(TagLib::MP4::CoverArt::Format::Unknown, imageData);
413 coverArtList.append(coverArt);
415 mp4Tags->setItem(
"covr", coverArtList);
420void writeGenericProperties(TagLib::PropertyMap &oldProperties,
const PropertyMultiMap &newProperties)
422 if (newProperties.
empty()) {
426 if (newProperties.
contains(Property::Title)) {
427 oldProperties.replace(
"TITLE", QStringToTString(newProperties.
value(Property::Title).toString()));
430 if (newProperties.
contains(Property::Artist)) {
431 oldProperties.replace(
"ARTIST", QStringToTString(newProperties.
value(Property::Artist).toString()));
434 if (newProperties.
contains(Property::AlbumArtist)) {
435 oldProperties.replace(
"ALBUMARTIST", QStringToTString(newProperties.
value(Property::AlbumArtist).toString()));
438 if (newProperties.
contains(Property::Album)) {
439 oldProperties.replace(
"ALBUM", QStringToTString(newProperties.
value(Property::Album).toString()));
442 if (newProperties.
contains(Property::TrackNumber)) {
443 int trackNumber = newProperties.
value(Property::TrackNumber).toInt();
445 if (trackNumber >= 0) {
446 oldProperties.replace(
"TRACKNUMBER", QStringToTString(newProperties.
value(Property::TrackNumber).toString()));
450 if (newProperties.
contains(Property::DiscNumber)) {
451 int discNumber = newProperties.
value(Property::DiscNumber).toInt();
453 if (discNumber >= 0) {
454 oldProperties.replace(
"DISCNUMBER", QStringToTString(newProperties.
value(Property::DiscNumber).toString()));
458 if (newProperties.
contains(Property::ReleaseYear)) {
459 int year = newProperties.
value(Property::ReleaseYear).toInt();
462 oldProperties.replace(
"DATE", QStringToTString(newProperties.
value(Property::ReleaseYear).toString()));
466 if (newProperties.
contains(Property::Genre)) {
467 oldProperties.replace(
"GENRE", QStringToTString(newProperties.
value(Property::Genre).toString()));
470 if (newProperties.
contains(Property::Comment)) {
471 oldProperties.replace(
"COMMENT", QStringToTString(newProperties.
value(Property::Comment).toString()));
474 if (newProperties.
contains(Property::Composer)) {
475 oldProperties.replace(
"COMPOSER", QStringToTString(newProperties.
value(Property::Composer).toString()));
478 if (newProperties.
contains(Property::Lyricist)) {
479 oldProperties.replace(
"LYRICIST", QStringToTString(newProperties.
value(Property::Lyricist).toString()));
482 if (newProperties.
contains(Property::Conductor)) {
483 oldProperties.replace(
"CONDUCTOR", QStringToTString(newProperties.
value(Property::Conductor).toString()));
486 if (newProperties.
contains(Property::Copyright)) {
487 oldProperties.replace(
"COPYRIGHT", QStringToTString(newProperties.
value(Property::Copyright).toString()));
490 if (newProperties.
contains(Property::Lyrics)) {
491 oldProperties.replace(
"LYRICS", QStringToTString(newProperties.
value(Property::Lyrics).toString()));
494 if (newProperties.
contains(Property::Language)) {
495 oldProperties.replace(
"LANGUAGE", QStringToTString(newProperties.
value(Property::Language).toString()));
499TagLibWriter::TagLibWriter(
QObject* parent)
500 : WriterPlugin(parent)
506 return supportedMimeTypes;
509void TagLibWriter::write(
const WriteData& data)
511 const QString fileUrl = data.inputUrl();
515#if defined Q_OS_WINDOWS
520 if (!stream.isOpen() || stream.readOnly()) {
521 qCWarning(KFILEMETADATA_LOG) <<
"Unable to open file in write mode: " << fileUrl;
527 TagLib::MPEG::File file(&stream, TagLib::ID3v2::FrameFactory::instance(),
false);
528 if (file.isValid()) {
529 auto savedProperties = file.properties();
530 writeGenericProperties(savedProperties, properties);
531 file.setProperties(savedProperties);
532 if (file.hasID3v2Tag()) {
533 writeID3v2Tags(file.ID3v2Tag(), properties);
534 writeID3v2Cover(file.ID3v2Tag(), data.imageData());
539 TagLib::RIFF::AIFF::File file(&stream,
false);
540 if (file.isValid()) {
541 auto savedProperties = file.properties();
542 writeGenericProperties(savedProperties, properties);
543 file.setProperties(savedProperties);
544 auto id3Tags =
dynamic_cast<TagLib::ID3v2::Tag*
>(file.tag());
546 writeID3v2Tags(id3Tags, properties);
547 writeID3v2Cover(id3Tags, data.imageData());
554 TagLib::RIFF::WAV::File file(&stream,
false);
555 if (file.isValid()) {
556 auto savedProperties = file.properties();
557 writeGenericProperties(savedProperties, properties);
558 file.setProperties(savedProperties);
559 auto id3Tags = file.ID3v2Tag();
561 writeID3v2Tags(id3Tags, properties);
562 writeID3v2Cover(id3Tags, data.imageData());
567 TagLib::MPC::File file(&stream,
false);
568 if (file.isValid()) {
569 auto savedProperties = file.properties();
570 writeGenericProperties(savedProperties, properties);
571 writeApeTags(savedProperties, properties);
572 file.setProperties(savedProperties);
573 if (file.hasAPETag()) {
574 writeApeCover(file.APETag(), data.imageData());
579 TagLib::APE::File file(&stream,
false);
580 if (file.isValid()) {
581 auto savedProperties = file.properties();
582 writeGenericProperties(savedProperties, properties);
583 writeApeTags(savedProperties, properties);
584 file.setProperties(savedProperties);
585 if (file.hasAPETag()) {
586 writeApeCover(file.APETag(), data.imageData());
591 TagLib::WavPack::File file(&stream,
false);
592 if (file.isValid()) {
593 auto savedProperties = file.properties();
594 writeGenericProperties(savedProperties, properties);
595 writeApeTags(savedProperties, properties);
596 file.setProperties(savedProperties);
597 if (file.hasAPETag()) {
598 writeApeCover(file.APETag(), data.imageData());
603 TagLib::MP4::File file(&stream,
false);
604 if (file.isValid()) {
605 auto savedProperties = file.properties();
606 writeGenericProperties(savedProperties, properties);
607 auto mp4Tags =
dynamic_cast<TagLib::MP4::Tag*
>(file.tag());
609 writeMp4Tags(mp4Tags, properties);
610 writeMp4Cover(mp4Tags, data.imageData());
612 file.setProperties(savedProperties);
616 TagLib::FLAC::File file(&stream, TagLib::ID3v2::FrameFactory::instance(),
false);
617 if (file.isValid()) {
618 auto savedProperties = file.properties();
619 writeGenericProperties(savedProperties, properties);
620 writeVorbisTags(savedProperties, properties);
621 file.setProperties(savedProperties);
622 writeFlacCover(&file, data.imageData());
626 TagLib::Ogg::Vorbis::File file(&stream,
false);
627 if (file.isValid()) {
628 auto savedProperties = file.properties();
629 writeGenericProperties(savedProperties, properties);
630 writeVorbisTags(savedProperties, properties);
631 file.setProperties(savedProperties);
632 writeFlacCover(file.tag(), data.imageData());
636 TagLib::Ogg::Opus::File file(&stream,
false);
637 if (file.isValid()) {
638 auto savedProperties = file.properties();
639 writeGenericProperties(savedProperties, properties);
640 writeVorbisTags(savedProperties, properties);
641 file.setProperties(savedProperties);
642 writeFlacCover(file.tag(), data.imageData());
646 TagLib::Ogg::Speex::File file(&stream,
false);
647 if (file.isValid()) {
648 auto savedProperties = file.properties();
649 writeGenericProperties(savedProperties, properties);
650 writeVorbisTags(savedProperties, properties);
651 file.setProperties(savedProperties);
652 writeFlacCover(file.tag(), data.imageData());
656 TagLib::ASF::File file(&stream,
false);
657 if (file.isValid()) {
658 auto savedProperties = file.properties();
659 writeGenericProperties(savedProperties, properties);
660 file.setProperties(savedProperties);
661 auto asfTags =
dynamic_cast<TagLib::ASF::Tag*
>(file.tag());
663 writeAsfTags(asfTags, properties);
664 writeAsfCover(asfTags, data.imageData());
671#include "moc_taglibwriter.cpp"
KCALUTILS_EXPORT QString mimeType()
const char * constData() const const
QByteArray fromHex(const QByteArray &hexEncoded)
bool startsWith(QByteArrayView bv) const const
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
key_value_iterator keyValueBegin()
key_value_iterator keyValueEnd()
size_type size() const const
bool contains(const Key &key) const const
T value(const Key &key, const T &defaultValue) const const
QByteArray toLocal8Bit() const const
QByteArray toUtf8() const const