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>
32 #include <speexfile.h>
33 #include <wavpackfile.h>
35 #include <popularimeterframe.h>
40 QStringLiteral(
"audio/flac"),
41 QStringLiteral(
"audio/mp4"),
42 QStringLiteral(
"audio/mpeg"),
43 QStringLiteral(
"audio/mpeg3"),
44 QStringLiteral(
"audio/ogg"),
45 QStringLiteral(
"audio/opus"),
46 QStringLiteral(
"audio/wav"),
47 QStringLiteral(
"audio/x-aiff"),
48 QStringLiteral(
"audio/x-aifc"),
49 QStringLiteral(
"audio/x-ape"),
50 QStringLiteral(
"audio/x-mpeg"),
51 QStringLiteral(
"audio/x-ms-wma"),
52 QStringLiteral(
"audio/x-musepack"),
53 QStringLiteral(
"audio/x-opus+ogg"),
54 QStringLiteral(
"audio/x-speex+ogg"),
55 QStringLiteral(
"audio/x-vorbis+ogg"),
56 QStringLiteral(
"audio/x-wav"),
57 QStringLiteral(
"audio/x-wavpack"),
60 int id3v2RatingTranslation[11] = {
61 0, 1, 13, 54, 64, 118, 128, 186, 196, 242, 255
64 using namespace KFileMetaData;
66 template<
typename ImageType>
70 case ImageType::FrontCover:
71 return EmbeddedImageData::FrontCover;
72 case ImageType::Other:
73 return EmbeddedImageData::Other;
74 case ImageType::FileIcon:
75 return EmbeddedImageData::FileIcon;
76 case ImageType::OtherFileIcon:
77 return EmbeddedImageData::OtherFileIcon;
78 case ImageType::BackCover:
79 return EmbeddedImageData::BackCover;
80 case ImageType::LeafletPage:
81 return EmbeddedImageData::LeafletPage;
82 case ImageType::Media:
83 return EmbeddedImageData::Media;
84 case ImageType::LeadArtist:
85 return EmbeddedImageData::LeadArtist;
86 case ImageType::Artist:
87 return EmbeddedImageData::Artist;
88 case ImageType::Conductor:
89 return EmbeddedImageData::Conductor;
91 return EmbeddedImageData::Band;
92 case ImageType::Composer:
93 return EmbeddedImageData::Composer;
94 case ImageType::Lyricist:
95 return EmbeddedImageData::Lyricist;
96 case ImageType::RecordingLocation:
97 return EmbeddedImageData::RecordingLocation;
98 case ImageType::DuringRecording:
99 return EmbeddedImageData::DuringRecording;
100 case ImageType::DuringPerformance:
101 return EmbeddedImageData::DuringPerformance;
102 case ImageType::MovieScreenCapture:
103 return EmbeddedImageData::MovieScreenCapture;
104 case ImageType::ColouredFish:
105 return EmbeddedImageData::ColouredFish;
106 case ImageType::Illustration:
107 return EmbeddedImageData::Illustration;
108 case ImageType::BandLogo:
109 return EmbeddedImageData::BandLogo;
110 case ImageType::PublisherLogo:
111 return EmbeddedImageData::PublisherLogo;
113 return EmbeddedImageData::Unknown;
117 template<
typename ImageType>
118 static const std::array<typename ImageType::Type, 21> allImageTypes = {
119 ImageType::FrontCover,
122 ImageType::OtherFileIcon,
123 ImageType::BackCover,
124 ImageType::LeafletPage,
126 ImageType::LeadArtist,
128 ImageType::Conductor,
132 ImageType::RecordingLocation,
133 ImageType::DuringRecording,
134 ImageType::DuringPerformance,
135 ImageType::MovieScreenCapture,
136 ImageType::ColouredFish,
137 ImageType::Illustration,
139 ImageType::PublisherLogo,
142 TagLib::String determineMimeType(
const QByteArray &pictureData)
145 return TagLib::String(
"image/png");
150 return TagLib::String(
"image/jpeg");
152 return TagLib::String();
156 void writeID3v2Tags(TagLib::ID3v2::Tag *id3Tags,
const PropertyMultiMap &newProperties)
158 if (newProperties.
contains(Property::Rating)) {
159 int rating = newProperties.
value(Property::Rating).toInt();
160 if (rating >= 0 && rating <= 10) {
161 id3Tags->removeFrames(
"POPM");
163 auto ratingFrame =
new TagLib::ID3v2::PopularimeterFrame;
164 ratingFrame->setEmail(
"org.kde.kfilemetadata");
165 ratingFrame->setRating(id3v2RatingTranslation[rating]);
166 id3Tags->addFrame(ratingFrame);
171 void writeID3v2Cover(TagLib::ID3v2::Tag *id3Tags,
177 [&](
const std::pair<EmbeddedImageData::ImageType, QByteArray> it) {
178 if (it.second.isEmpty()) {
179 removeTypes |= it.first;
181 wantedTypes |= it.first;
185 using PictureFrame = TagLib::ID3v2::AttachedPictureFrame;
187 wantedTypes &= ~kfmType;
188 auto newCover = images[kfmType];
189 auto newMimeType = determineMimeType(newCover);
190 if (!newMimeType.isEmpty()) {
191 coverFrame->setPicture(TagLib::ByteVector(
static_cast<const char *
>(newCover.data()), newCover.size()));
192 coverFrame->setMimeType(newMimeType);
197 TagLib::ID3v2::FrameList lstID3v2 = id3Tags->frameListMap()[
"APIC"];
198 for (
auto& frame : std::as_const(lstID3v2)) {
199 auto* coverFrame =
static_cast<PictureFrame *
>(frame);
200 const auto kfmType = mapTaglibType<PictureFrame::Type>(coverFrame->type());
201 if (kfmType & wantedTypes) {
202 updateFrame(coverFrame, kfmType);
203 }
else if (kfmType & removeTypes) {
204 id3Tags->removeFrame(coverFrame);
208 for (
const auto type : allImageTypes<PictureFrame>) {
209 const auto kfmType = mapTaglibType<PictureFrame::Type>(type);
210 if (kfmType & wantedTypes) {
212 auto* coverFrame =
new PictureFrame;
213 coverFrame->setType(type);
214 updateFrame(coverFrame, kfmType);
215 id3Tags->addFrame(coverFrame);
222 template<
typename Container>
223 void writeFlacCover(Container* tags,
229 [&](
const std::pair<EmbeddedImageData::ImageType, QByteArray> it) {
230 if (it.second.isEmpty()) {
231 removeTypes |= it.first;
233 wantedTypes |= it.first;
237 using PictureFrame = TagLib::FLAC::Picture;
239 wantedTypes &= ~kfmType;
240 auto newCover = images[kfmType];
241 auto newMimeType = determineMimeType(newCover);
242 if (!newMimeType.isEmpty()) {
243 coverFrame->setData(TagLib::ByteVector(
static_cast<const char *
>(newCover.data()), newCover.size()));
244 coverFrame->setMimeType(newMimeType);
249 auto lstPic = tags->pictureList();
250 for (
auto it = lstPic.begin(); it != lstPic.end(); ++it) {
251 const auto kfmType = mapTaglibType<PictureFrame::Type>((*it)->type());
252 if (kfmType & wantedTypes) {
253 updateFrame((*it), kfmType);
254 }
else if (kfmType & removeTypes) {
255 tags->removePicture(*it);
259 for (
const auto type : allImageTypes<PictureFrame>) {
260 const auto kfmType = mapTaglibType<PictureFrame::Type>(type);
261 if (kfmType & wantedTypes) {
263 auto* coverFrame =
new PictureFrame;
264 coverFrame->setType(type);
265 updateFrame(coverFrame, kfmType);
266 tags->addPicture(coverFrame);
271 void writeApeTags(TagLib::PropertyMap &oldProperties,
const PropertyMultiMap &newProperties)
273 if (newProperties.
contains(Property::Rating)) {
274 oldProperties.replace(
"RATING", TagLib::String::number(newProperties.
value(Property::Rating).toInt() * 10));
278 void writeApeCover(TagLib::APE::Tag* apeTags,
281 if (images.
empty()) {
284 auto imageIt = images.
constFind(EmbeddedImageData::FrontCover);
285 if ((images.
size() > 1) || (imageIt == images.
constEnd())) {
292 const auto newCover = *imageIt;
293 if (newCover.isEmpty()) {
294 apeTags->removeItem(
"COVER ART (FRONT)");
298 TagLib::ByteVector imageData;
299 if (determineMimeType(newCover) == TagLib::String(
"image/png")) {
300 imageData.setData(
"frontCover.png\0", 15);
302 imageData.setData(
"frontCover.jpeg\0", 16);
304 imageData.append(TagLib::ByteVector(newCover.constData(), newCover.size()));
305 apeTags->setData(
"COVER ART (FRONT)", imageData);
308 void writeVorbisTags(TagLib::PropertyMap &oldProperties,
const PropertyMultiMap &newProperties)
310 if (newProperties.
contains(Property::Rating)) {
311 oldProperties.replace(
"RATING", TagLib::String::number(newProperties.
value(Property::Rating).toInt() * 10));
315 void writeAsfTags(TagLib::ASF::Tag *asfTags,
const PropertyMultiMap &properties)
320 int rating =
properties.value(Property::Rating).toInt();
323 }
else if (rating <= 2) {
325 }
else if (rating == 10){
328 rating =
static_cast<int>(12.5 * rating - 25);
330 asfTags->setAttribute(
"WM/SharedUserRating", TagLib::String::number(rating));
334 void writeAsfCover(TagLib::ASF::Tag* asfTags,
340 [&](
const std::pair<EmbeddedImageData::ImageType, QByteArray> it) {
341 if (it.second.isEmpty()) {
342 removeTypes |= it.first;
344 wantedTypes |= it.first;
348 using PictureFrame = TagLib::ASF::Picture;
350 wantedTypes &= ~kfmType;
351 auto newCover = images[kfmType];
352 auto newMimeType = determineMimeType(newCover);
353 if (!newMimeType.isEmpty()) {
354 coverFrame->setPicture(TagLib::ByteVector(
static_cast<const char *
>(newCover.data()), newCover.size()));
355 coverFrame->setMimeType(newMimeType);
360 TagLib::ASF::AttributeList lstPic = asfTags->attribute(
"WM/Picture");
361 for (
auto it = lstPic.begin(); it != lstPic.end(); ) {
362 PictureFrame picture = (*it).toPicture();
363 const auto kfmType = mapTaglibType<PictureFrame::Type>(picture.type());
364 if (kfmType & wantedTypes) {
365 updateFrame(&picture, kfmType);
367 }
else if (kfmType & removeTypes) {
368 it = lstPic.erase(it);
374 for (
const auto type : allImageTypes<PictureFrame>) {
375 const auto kfmType = mapTaglibType<PictureFrame::Type>(type);
376 if (kfmType & wantedTypes) {
377 PictureFrame coverFrame;
378 updateFrame(&coverFrame, kfmType);
379 coverFrame.setType(type);
380 lstPic.append(coverFrame);
383 asfTags->setAttribute(
"WM/Picture", lstPic);
385 void writeMp4Tags(TagLib::MP4::Tag *mp4Tags,
const PropertyMultiMap &newProperties)
387 if (newProperties.
contains(Property::Rating)) {
388 mp4Tags->setItem(
"rate", TagLib::StringList(TagLib::String::number(newProperties.
value(Property::Rating).toInt() * 10)));
392 void writeMp4Cover(TagLib::MP4::Tag *mp4Tags,
395 if (images.
empty()) {
398 auto imageIt = images.
constFind(EmbeddedImageData::FrontCover);
399 if ((images.
size() > 1) || (imageIt == images.
constEnd())) {
406 TagLib::MP4::CoverArtList coverArtList;
407 const auto newCover = *imageIt;
408 if (!newCover.isEmpty()) {
409 TagLib::ByteVector imageData(newCover.data(), newCover.size());
410 TagLib::MP4::CoverArt coverArt(TagLib::MP4::CoverArt::Format::Unknown, imageData);
411 coverArtList.append(coverArt);
413 mp4Tags->setItem(
"covr", coverArtList);
418 void writeGenericProperties(TagLib::PropertyMap &oldProperties,
const PropertyMultiMap &newProperties)
420 if (newProperties.
empty()) {
424 if (newProperties.
contains(Property::Title)) {
425 oldProperties.replace(
"TITLE", QStringToTString(newProperties.
value(Property::Title).toString()));
428 if (newProperties.
contains(Property::Artist)) {
429 oldProperties.replace(
"ARTIST", QStringToTString(newProperties.
value(Property::Artist).toString()));
432 if (newProperties.
contains(Property::AlbumArtist)) {
433 oldProperties.replace(
"ALBUMARTIST", QStringToTString(newProperties.
value(Property::AlbumArtist).toString()));
436 if (newProperties.
contains(Property::Album)) {
437 oldProperties.replace(
"ALBUM", QStringToTString(newProperties.
value(Property::Album).toString()));
440 if (newProperties.
contains(Property::TrackNumber)) {
441 int trackNumber = newProperties.
value(Property::TrackNumber).toInt();
443 if (trackNumber >= 0) {
444 oldProperties.replace(
"TRACKNUMBER", QStringToTString(newProperties.
value(Property::TrackNumber).toString()));
448 if (newProperties.
contains(Property::DiscNumber)) {
449 int discNumber = newProperties.
value(Property::DiscNumber).toInt();
451 if (discNumber >= 0) {
452 oldProperties.replace(
"DISCNUMBER", QStringToTString(newProperties.
value(Property::DiscNumber).toString()));
456 if (newProperties.
contains(Property::ReleaseYear)) {
457 int year = newProperties.
value(Property::ReleaseYear).toInt();
460 oldProperties.replace(
"DATE", QStringToTString(newProperties.
value(Property::ReleaseYear).toString()));
464 if (newProperties.
contains(Property::Genre)) {
465 oldProperties.replace(
"GENRE", QStringToTString(newProperties.
value(Property::Genre).toString()));
468 if (newProperties.
contains(Property::Comment)) {
469 oldProperties.replace(
"COMMENT", QStringToTString(newProperties.
value(Property::Comment).toString()));
472 if (newProperties.
contains(Property::Composer)) {
473 oldProperties.replace(
"COMPOSER", QStringToTString(newProperties.
value(Property::Composer).toString()));
476 if (newProperties.
contains(Property::Lyricist)) {
477 oldProperties.replace(
"LYRICIST", QStringToTString(newProperties.
value(Property::Lyricist).toString()));
480 if (newProperties.
contains(Property::Conductor)) {
481 oldProperties.replace(
"CONDUCTOR", QStringToTString(newProperties.
value(Property::Conductor).toString()));
484 if (newProperties.
contains(Property::Copyright)) {
485 oldProperties.replace(
"COPYRIGHT", QStringToTString(newProperties.
value(Property::Copyright).toString()));
488 if (newProperties.
contains(Property::Lyrics)) {
489 oldProperties.replace(
"LYRICS", QStringToTString(newProperties.
value(Property::Lyrics).toString()));
492 if (newProperties.
contains(Property::Language)) {
493 oldProperties.replace(
"LANGUAGE", QStringToTString(newProperties.
value(Property::Language).toString()));
497 TagLibWriter::TagLibWriter(
QObject* parent)
498 : WriterPlugin(parent)
504 return supportedMimeTypes;
507 void TagLibWriter::write(
const WriteData& data)
509 const QString fileUrl = data.inputUrl();
513 #if defined Q_OS_WINDOWS
518 if (!stream.isOpen() || stream.readOnly()) {
519 qCWarning(KFILEMETADATA_LOG) <<
"Unable to open file in write mode: " << fileUrl;
525 TagLib::MPEG::File file(&stream, TagLib::ID3v2::FrameFactory::instance(),
false);
526 if (file.isValid()) {
527 auto savedProperties = file.properties();
528 writeGenericProperties(savedProperties, properties);
529 file.setProperties(savedProperties);
530 if (file.hasID3v2Tag()) {
531 writeID3v2Tags(file.ID3v2Tag(), properties);
532 writeID3v2Cover(file.ID3v2Tag(), data.imageData());
537 TagLib::RIFF::AIFF::File file(&stream,
false);
538 if (file.isValid()) {
539 auto savedProperties = file.properties();
540 writeGenericProperties(savedProperties, properties);
541 file.setProperties(savedProperties);
542 auto id3Tags =
dynamic_cast<TagLib::ID3v2::Tag*
>(file.tag());
544 writeID3v2Tags(id3Tags, properties);
545 writeID3v2Cover(id3Tags, data.imageData());
550 TagLib::RIFF::WAV::File file(&stream,
false);
551 if (file.isValid()) {
552 auto savedProperties = file.properties();
553 writeGenericProperties(savedProperties, properties);
554 file.setProperties(savedProperties);
555 auto id3Tags =
dynamic_cast<TagLib::ID3v2::Tag*
>(file.tag());
557 writeID3v2Tags(id3Tags, properties);
558 writeID3v2Cover(id3Tags, data.imageData());
563 TagLib::MPC::File file(&stream,
false);
564 if (file.isValid()) {
565 auto savedProperties = file.properties();
566 writeGenericProperties(savedProperties, properties);
567 writeApeTags(savedProperties, properties);
568 file.setProperties(savedProperties);
569 if (file.hasAPETag()) {
570 writeApeCover(file.APETag(), data.imageData());
575 TagLib::APE::File file(&stream,
false);
576 if (file.isValid()) {
577 auto savedProperties = file.properties();
578 writeGenericProperties(savedProperties, properties);
579 writeApeTags(savedProperties, properties);
580 file.setProperties(savedProperties);
581 if (file.hasAPETag()) {
582 writeApeCover(file.APETag(), data.imageData());
587 TagLib::WavPack::File file(&stream,
false);
588 if (file.isValid()) {
589 auto savedProperties = file.properties();
590 writeGenericProperties(savedProperties, properties);
591 writeApeTags(savedProperties, properties);
592 file.setProperties(savedProperties);
593 if (file.hasAPETag()) {
594 writeApeCover(file.APETag(), data.imageData());
599 TagLib::MP4::File file(&stream,
false);
600 if (file.isValid()) {
601 auto savedProperties = file.properties();
602 writeGenericProperties(savedProperties, properties);
603 auto mp4Tags =
dynamic_cast<TagLib::MP4::Tag*
>(file.tag());
605 writeMp4Tags(mp4Tags, properties);
606 writeMp4Cover(mp4Tags, data.imageData());
608 file.setProperties(savedProperties);
612 TagLib::FLAC::File file(&stream, TagLib::ID3v2::FrameFactory::instance(),
false);
613 if (file.isValid()) {
614 auto savedProperties = file.properties();
615 writeGenericProperties(savedProperties, properties);
616 writeVorbisTags(savedProperties, properties);
617 file.setProperties(savedProperties);
618 writeFlacCover(&file, data.imageData());
622 TagLib::Ogg::Vorbis::File file(&stream,
false);
623 if (file.isValid()) {
624 auto savedProperties = file.properties();
625 writeGenericProperties(savedProperties, properties);
626 writeVorbisTags(savedProperties, properties);
627 file.setProperties(savedProperties);
628 writeFlacCover(file.tag(), data.imageData());
632 TagLib::Ogg::Opus::File file(&stream,
false);
633 if (file.isValid()) {
634 auto savedProperties = file.properties();
635 writeGenericProperties(savedProperties, properties);
636 writeVorbisTags(savedProperties, properties);
637 file.setProperties(savedProperties);
638 writeFlacCover(file.tag(), data.imageData());
642 TagLib::Ogg::Speex::File file(&stream,
false);
643 if (file.isValid()) {
644 auto savedProperties = file.properties();
645 writeGenericProperties(savedProperties, properties);
646 writeVorbisTags(savedProperties, properties);
647 file.setProperties(savedProperties);
648 writeFlacCover(file.tag(), data.imageData());
652 TagLib::ASF::File file(&stream,
false);
653 if (file.isValid()) {
654 auto savedProperties = file.properties();
655 writeGenericProperties(savedProperties, properties);
656 file.setProperties(savedProperties);
657 auto asfTags =
dynamic_cast<TagLib::ASF::Tag*
>(file.tag());
659 writeAsfTags(asfTags, properties);
660 writeAsfCover(asfTags, data.imageData());