KFileMetaData

taglibextractor.cpp
1/*
2 SPDX-FileCopyrightText: 2012 Vishesh Handa <me@vhanda.in>
3
4 SPDX-License-Identifier: LGPL-2.1-or-later
5*/
6
7
8#include "taglibextractor.h"
9#include "embeddedimagedata.h"
10#include "kfilemetadata_debug.h"
11
12// Taglib includes
13#include <taglib.h>
14#include <modfile.h>
15#include <s3mfile.h>
16#include <xmfile.h>
17#include <itfile.h>
18#include <tag.h>
19#include <tfilestream.h>
20#include <tpropertymap.h>
21#include <aifffile.h>
22#include <apefile.h>
23#include <apetag.h>
24#include <asffile.h>
25#include <flacfile.h>
26#include <mp4file.h>
27#include <mpcfile.h>
28#include <mpegfile.h>
29#include <oggfile.h>
30#include <oggflacfile.h>
31#include <opusfile.h>
32#include <speexfile.h>
33#include <vorbisfile.h>
34#include <wavfile.h>
35#include <wavpackfile.h>
36#include <asftag.h>
37#include <asfattribute.h>
38#include <asfpicture.h>
39#include <id3v2tag.h>
40#include <mp4tag.h>
41#include <popularimeterframe.h>
42#include <attachedpictureframe.h>
43
44using namespace KFileMetaData;
45
46namespace {
47
48const QStringList supportedMimeTypes = {
49 QStringLiteral("audio/flac"),
50 QStringLiteral("audio/mp4"),
51 QStringLiteral("audio/mpeg"),
52 QStringLiteral("audio/ogg"),
53 QStringLiteral("audio/wav"),
54 QStringLiteral("audio/vnd.audible.aax"),
55 QStringLiteral("audio/vnd.audible.aaxc"),
56 QStringLiteral("audio/vnd.wave"),
57 QStringLiteral("audio/x-aiff"),
58 QStringLiteral("audio/x-aifc"),
59 QStringLiteral("audio/x-ape"),
60 QStringLiteral("audio/x-flac+ogg"),
61 QStringLiteral("audio/x-ms-wma"),
62 QStringLiteral("audio/x-musepack"),
63 QStringLiteral("audio/x-opus+ogg"),
64 QStringLiteral("audio/x-speex+ogg"),
65 QStringLiteral("audio/x-vorbis+ogg"),
66 QStringLiteral("audio/x-wav"),
67 QStringLiteral("audio/x-wavpack"),
68 QStringLiteral("audio/x-mod"),
69 QStringLiteral("audio/x-s3m"),
70 QStringLiteral("audio/x-xm"),
71 QStringLiteral("audio/x-it"),
72};
73
74void extractAudioProperties(TagLib::File* file, ExtractionResult* result)
75{
76 TagLib::AudioProperties* audioProp = file->audioProperties();
77 if (audioProp && (result->inputFlags() & ExtractionResult::ExtractMetaData)) {
78 if (audioProp->length()) {
79 // What about the xml duration?
80 result->add(Property::Duration, audioProp->length());
81 }
82
83 if (audioProp->bitrate()) {
84 result->add(Property::BitRate, audioProp->bitrate() * 1000);
85 }
86
87 if (audioProp->channels()) {
88 result->add(Property::Channels, audioProp->channels());
89 }
90
91 if (audioProp->sampleRate()) {
92 result->add(Property::SampleRate, audioProp->sampleRate());
93 }
94 }
95}
96
97void readGenericProperties(const TagLib::PropertyMap &savedProperties, ExtractionResult* result)
98{
99 if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || savedProperties.isEmpty()) {
100 return;
101 }
102
103 if (savedProperties.contains("TITLE")) {
104 result->add(Property::Title, TStringToQString(savedProperties["TITLE"].toString()).trimmed());
105 }
106 if (savedProperties.contains("ALBUM")) {
107 result->add(Property::Album, TStringToQString(savedProperties["ALBUM"].toString()).trimmed());
108 }
109 if (savedProperties.contains("COMMENT")) {
110 result->add(Property::Comment, TStringToQString(savedProperties["COMMENT"].toString()).trimmed());
111 }
112 if (savedProperties.contains("TRACKNUMBER")) {
113 result->add(Property::TrackNumber, savedProperties["TRACKNUMBER"].toString().toInt());
114 }
115 if (savedProperties.contains("DATE")) {
116 result->add(Property::ReleaseYear, savedProperties["DATE"].toString().toInt());
117 }
118 if (savedProperties.contains("OPUS")) {
119 result->add(Property::Opus, savedProperties["OPUS"].toString().toInt());
120 }
121 if (savedProperties.contains("DISCNUMBER")) {
122 result->add(Property::DiscNumber, savedProperties["DISCNUMBER"].toString().toInt());
123 }
124 if (savedProperties.contains("RATING")) {
125 /*
126 * There is no standard regarding ratings. Mimic MediaMonkey's behavior
127 * with a range of 0 to 100 (stored in steps of 10) and make it compatible
128 * with baloo rating with a range from 0 to 10
129 */
130 result->add(Property::Rating, savedProperties["RATING"].toString().toInt() / 10);
131 }
132 if (savedProperties.contains("LOCATION")) {
133 result->add(Property::Location, TStringToQString(savedProperties["LOCATION"].toString()).trimmed());
134 }
135 if (savedProperties.contains("LANGUAGE")) {
136 result->add(Property::Language, TStringToQString(savedProperties["LANGUAGE"].toString()).trimmed());
137 }
138 if (savedProperties.contains("LICENSE")) {
139 result->add(Property::License, TStringToQString(savedProperties["LICENSE"].toString()).trimmed());
140 }
141 if (savedProperties.contains("PUBLISHER")) {
142 result->add(Property::Publisher, TStringToQString(savedProperties["PUBLISHER"].toString()).trimmed());
143 }
144 if (savedProperties.contains("COPYRIGHT")) {
145 result->add(Property::Copyright, TStringToQString(savedProperties["COPYRIGHT"].toString()).trimmed());
146 }
147 if (savedProperties.contains("LABEL")) {
148 result->add(Property::Label, TStringToQString(savedProperties["LABEL"].toString()).trimmed());
149 }
150 if (savedProperties.contains("ENSEMBLE")) {
151 result->add(Property::Ensemble, TStringToQString(savedProperties["ENSEMBLE"].toString()).trimmed());
152 }
153 if (savedProperties.contains("COMPILATION")) {
154 result->add(Property::Compilation, TStringToQString(savedProperties["COMPILATION"].toString()).trimmed());
155 }
156 if (savedProperties.contains("LYRICS")) {
157 result->add(Property::Lyrics, TStringToQString(savedProperties["LYRICS"].toString()).trimmed().replace(QLatin1Char('\r'), QLatin1Char('\n'))); // Convert old Mac line endings
158 }
159 if (savedProperties.contains("ARTIST")) {
160 const auto artists = savedProperties["ARTIST"];
161 for (const auto& artist : artists) {
162 result->add(Property::Artist, TStringToQString(artist).trimmed());
163 }
164 }
165 if (savedProperties.contains("GENRE")) {
166 const auto genres = savedProperties["GENRE"];
167 for (const auto& genre : genres) {
168 result->add(Property::Genre, TStringToQString(genre).trimmed());
169 }
170 }
171 if (savedProperties.contains("ALBUMARTIST")) {
172 const auto albumArtists = savedProperties["ALBUMARTIST"];
173 for (const auto& albumArtist : albumArtists) {
174 result->add(Property::AlbumArtist, TStringToQString(albumArtist).trimmed());
175 }
176 }
177 if (savedProperties.contains("COMPOSER")) {
178 const auto composers = savedProperties["COMPOSER"];
179 for (const auto& composer : composers) {
180 result->add(Property::Composer, TStringToQString(composer).trimmed());
181 }
182 }
183 if (savedProperties.contains("LYRICIST")) {
184 const auto lyricists = savedProperties["LYRICIST"];
185 for (const auto& lyricist : lyricists) {
186 result->add(Property::Lyricist, TStringToQString(lyricist).trimmed());
187 }
188 }
189 if (savedProperties.contains("CONDUCTOR")) {
190 const auto conductors = savedProperties["CONDUCTOR"];
191 for (const auto& conductor : conductors) {
192 result->add(Property::Conductor, TStringToQString(conductor).trimmed());
193 }
194 }
195 if (savedProperties.contains("ARRANGER")) {
196 const auto arrangers = savedProperties["ARRANGER"];
197 for (const auto& arranger : arrangers) {
198 result->add(Property::Arranger, TStringToQString(arranger).trimmed());
199 }
200 }
201 if (savedProperties.contains("PERFORMER")) {
202 const auto performers = savedProperties["PERFORMER"];
203 for (const auto& performer : performers) {
204 result->add(Property::Performer, TStringToQString(performer).trimmed());
205 }
206 }
207 if (savedProperties.contains("AUTHOR")) {
208 const auto authors = savedProperties["AUTHOR"];
209 for (const auto& author: authors) {
210 result->add(Property::Author, TStringToQString(author).trimmed());
211 }
212 }
213
214 if (savedProperties.contains("REPLAYGAIN_TRACK_GAIN")) {
215 auto trackGainString = TStringToQString(savedProperties["REPLAYGAIN_TRACK_GAIN"].toString(";")).trimmed();
216 // remove " dB" suffix
217 if (trackGainString.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) {
218 trackGainString.chop(3);
219 }
220 bool success = false;
221 double replayGainTrackGain = trackGainString.toDouble(&success);
222 if (success) {
223 result->add(Property::ReplayGainTrackGain, replayGainTrackGain);
224 }
225 }
226 if (savedProperties.contains("REPLAYGAIN_ALBUM_GAIN")) {
227 auto albumGainString = TStringToQString(savedProperties["REPLAYGAIN_ALBUM_GAIN"].toString(";")).trimmed();
228 // remove " dB" suffix
229 if (albumGainString.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) {
230 albumGainString.chop(3);
231 }
232 bool success = false;
233 double replayGainAlbumGain = albumGainString.toDouble(&success);
234 if (success) {
235 result->add(Property::ReplayGainAlbumGain, replayGainAlbumGain);
236 }
237 }
238 if (savedProperties.contains("REPLAYGAIN_TRACK_PEAK")) {
239 auto trackPeakString = TStringToQString(savedProperties["REPLAYGAIN_TRACK_PEAK"].toString(";")).trimmed();
240 bool success = false;
241 double replayGainTrackPeak = trackPeakString.toDouble(&success);
242 if (success) {
243 result->add(Property::ReplayGainTrackPeak, replayGainTrackPeak);
244 }
245 }
246 if (savedProperties.contains("REPLAYGAIN_ALBUM_PEAK")) {
247 auto albumPeakString = TStringToQString(savedProperties["REPLAYGAIN_ALBUM_PEAK"].toString(";")).trimmed();
248 bool success = false;
249 double replayGainAlbumPeak = albumPeakString.toDouble(&success);
250 if (success) {
251 result->add(Property::ReplayGainAlbumPeak, replayGainAlbumPeak);
252 }
253 }
254 if (savedProperties.contains("TRACKERNAME")) {
255 result->add(Property::Generator, TStringToQString(savedProperties["TRACKERNAME"].toString()).trimmed());
256 }
257}
258
259void extractId3Tags(TagLib::ID3v2::Tag* Id3Tags, ExtractionResult* result)
260{
261 if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || Id3Tags->isEmpty()) {
262 return;
263 }
264
265 TagLib::ID3v2::FrameList lstID3v2;
266
267 /*
268 * Publisher.
269 * Special handling because TagLib::PropertyMap maps "TPUB" to "LABEL"
270 * Insert manually for Publisher.
271 */
272 lstID3v2 = Id3Tags->frameListMap()["TPUB"];
273 if (!lstID3v2.isEmpty()) {
274 result->add(Property::Publisher, TStringToQString(lstID3v2.front()->toString()));
275 }
276
277 // Compilation.
278 lstID3v2 = Id3Tags->frameListMap()["TCMP"];
279 if (!lstID3v2.isEmpty()) {
280 result->add(Property::Compilation, TStringToQString(lstID3v2.front()->toString()));
281 }
282
283 /*
284 * Rating.
285 * There is no standard regarding ratings. Most of the implementations match
286 * a 5 stars rating to a range of 0-255 for MP3.
287 * Map it to baloo rating with a range of 0 - 10.
288 */
289 lstID3v2 = Id3Tags->frameListMap()["POPM"];
290 if (!lstID3v2.isEmpty()) {
291 TagLib::ID3v2::PopularimeterFrame *ratingFrame = static_cast<TagLib::ID3v2::PopularimeterFrame *>(lstID3v2.front());
292 int rating = ratingFrame->rating();
293 if (rating == 0) {
294 rating = 0;
295 } else if (rating == 1) {
296 TagLib::String ratingProvider = ratingFrame->email();
297 if (ratingProvider == "no@email" || ratingProvider == "org.kde.kfilemetadata") {
298 rating = 1;
299 } else {
300 rating = 2;
301 }
302 } else if (rating >= 1 && rating <= 255) {
303 rating = static_cast<int>(0.032 * rating + 2);
304 }
305 result->add(Property::Rating, rating);
306 }
307}
308
309template<typename ImageType>
310EmbeddedImageData::ImageType mapTaglibType(const ImageType type)
311{
312 switch (type) {
313 case ImageType::FrontCover:
314 return EmbeddedImageData::FrontCover;
315 case ImageType::Other:
316 return EmbeddedImageData::Other;
317 case ImageType::FileIcon:
318 return EmbeddedImageData::FileIcon;
319 case ImageType::OtherFileIcon:
320 return EmbeddedImageData::OtherFileIcon;
321 case ImageType::BackCover:
322 return EmbeddedImageData::BackCover;
323 case ImageType::LeafletPage:
324 return EmbeddedImageData::LeafletPage;
325 case ImageType::Media:
326 return EmbeddedImageData::Media;
327 case ImageType::LeadArtist:
328 return EmbeddedImageData::LeadArtist;
329 case ImageType::Artist:
330 return EmbeddedImageData::Artist;
331 case ImageType::Conductor:
332 return EmbeddedImageData::Conductor;
333 case ImageType::Band:
334 return EmbeddedImageData::Band;
335 case ImageType::Composer:
336 return EmbeddedImageData::Composer;
337 case ImageType::Lyricist:
338 return EmbeddedImageData::Lyricist;
339 case ImageType::RecordingLocation:
340 return EmbeddedImageData::RecordingLocation;
341 case ImageType::DuringRecording:
342 return EmbeddedImageData::DuringRecording;
343 case ImageType::DuringPerformance:
344 return EmbeddedImageData::DuringPerformance;
345 case ImageType::MovieScreenCapture:
346 return EmbeddedImageData::MovieScreenCapture;
347 case ImageType::ColouredFish:
348 return EmbeddedImageData::ColouredFish;
349 case ImageType::Illustration:
350 return EmbeddedImageData::Illustration;
351 case ImageType::BandLogo:
352 return EmbeddedImageData::BandLogo;
353 case ImageType::PublisherLogo:
354 return EmbeddedImageData::PublisherLogo;
355 default:
356 return EmbeddedImageData::Unknown;
357 }
358}
359
361extractId3Cover(const TagLib::ID3v2::Tag* Id3Tags,
363{
365 if (!types || Id3Tags->isEmpty()) {
366 return images;
367 }
368
369 // Attached Picture.
370 TagLib::ID3v2::FrameList lstID3v2 = Id3Tags->frameListMap()["APIC"];
371
372 using PictureFrame = TagLib::ID3v2::AttachedPictureFrame;
373 for (const auto& frame : std::as_const(lstID3v2)) {
374 const auto *coverFrame = static_cast<PictureFrame *>(frame);
375 const auto imageType = mapTaglibType<PictureFrame::Type>(coverFrame->type());
376 if (types & imageType) {
377 const auto& picture = coverFrame->picture();
378 images.insert(imageType, QByteArray(picture.data(), picture.size()));
379 }
380 }
381 return images;
382}
383
385extractFlacCover(const TagLib::List<TagLib::FLAC::Picture *> picList,
387{
389 if (!types || picList.isEmpty()) {
390 return images;
391 }
392
393 for (const auto& picture : std::as_const(picList)) {
394 const auto imageType = mapTaglibType<TagLib::FLAC::Picture::Type>(picture->type());
395 if (types & imageType) {
396 images.insert(imageType, QByteArray(picture->data().data(), picture->data().size()));
397 }
398 }
399 return images;
400}
401
402void extractMp4Tags(TagLib::MP4::Tag* mp4Tags, ExtractionResult* result)
403{
404 if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || mp4Tags->isEmpty()) {
405 return;
406 }
407
408 auto ratingItem = mp4Tags->item("rate");
409
410 /*
411 * There is no standard regarding ratings. Mimic MediaMonkey's behavior
412 * with a range of 0 to 100 (stored in steps of 10) and make it compatible
413 * with baloo rating with a range from 0 to 10.
414 */
415 if (ratingItem.isValid()) {
416 result->add(Property::Rating, ratingItem.toStringList().toString().toInt() / 10);
417 }
418}
419
421extractMp4Cover(const TagLib::MP4::Tag* mp4Tags,
423{
425 TagLib::MP4::Item coverArtItem = mp4Tags->item("covr");
426 if (!(types & EmbeddedImageData::FrontCover) || !coverArtItem.isValid()) {
427 return images;
428 }
429
430 const TagLib::MP4::CoverArtList coverArtList = coverArtItem.toCoverArtList();
431 if (!coverArtList.isEmpty()) {
432 const TagLib::MP4::CoverArt& cover = coverArtList.front();
433 images.insert(EmbeddedImageData::FrontCover, QByteArray(cover.data().data(), cover.data().size()));
434 }
435 return images;
436}
437
439extractApeCover(const TagLib::APE::Tag* apeTags,
441{
443 if (!(types & EmbeddedImageData::FrontCover) || apeTags->isEmpty()) {
444 return images;
445 }
446
447 TagLib::APE::ItemListMap lstApe = apeTags->itemListMap();
448 TagLib::APE::ItemListMap::ConstIterator itApe;
449
450 /* The cover art tag for APEv2 tags starts with the filename as string
451 * with zero termination followed by the actual picture data */
452 itApe = lstApe.find("COVER ART (FRONT)");
453 if (itApe != lstApe.end()) {
454 const auto& picture = (*itApe).second.binaryData();
455 int position = picture.find('\0');
456 if (position >= 0) {
457 position += 1;
458 images.insert(EmbeddedImageData::FrontCover, QByteArray(picture.data() + position, picture.size() - position));
459 }
460 }
461 return images;
462}
463
464void extractAsfTags(TagLib::ASF::Tag* asfTags, ExtractionResult* result)
465{
466 if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || asfTags->isEmpty()) {
467 return;
468 }
469
470 TagLib::ASF::AttributeList lstASF = asfTags->attribute("WM/SharedUserRating");
471 if (!lstASF.isEmpty()) {
472 int rating = lstASF.front().toString().toInt();
473 /*
474 * Map the rating values of WMP to Baloo rating.
475 * 0->0, 1->2, 25->4, 50->6, 75->8, 99->10
476 */
477 if (rating == 0) {
478 rating = 0;
479 } else if (rating == 1) {
480 rating = 2;
481 } else {
482 rating = static_cast<int>(0.09 * rating + 2);
483 }
484 result->add(Property::Rating, rating);
485 }
486
487 lstASF = asfTags->attribute("Author");
488 if (!lstASF.isEmpty()) {
489 const auto attribute = lstASF.front();
490 result->add(Property::Author, TStringToQString(attribute.toString()).trimmed());
491 }
492
493 // Lyricist is called "WRITER" for wma/asf files
494 lstASF = asfTags->attribute("WM/Writer");
495 if (!lstASF.isEmpty()) {
496 const auto attribute = lstASF.front();
497 result->add(Property::Lyricist, TStringToQString(attribute.toString()).trimmed());
498 }
499
500 /*
501 * TagLib exports "WM/PUBLISHER" as "LABEL" in the PropertyMap,
502 * add it manually to Publisher.
503 */
504 lstASF = asfTags->attribute("WM/Publisher");
505 if (!lstASF.isEmpty()) {
506 const auto attribute = lstASF.front();
507 result->add(Property::Publisher, TStringToQString(attribute.toString()).trimmed());
508 }
509}
510
512extractAsfCover(const TagLib::ASF::Tag* asfTags,
514{
516 if (!types || asfTags->isEmpty()) {
517 return images;
518 }
519
520 // Attached Picture.
521 TagLib::ASF::AttributeList lstASF = asfTags->attribute("WM/Picture");
522
523 using Picture = TagLib::ASF::Picture;
524 for (const auto& attribute: std::as_const(lstASF)) {
525 Picture picture = attribute.toPicture();
526 const auto imageType = mapTaglibType<Picture::Type>(picture.type());
527 if (types & imageType) {
528 const auto& pictureData = picture.picture();
529 images.insert(imageType, QByteArray(pictureData.data(), pictureData.size()));
530 }
531 }
532 return images;
533}
534
535} // anonymous namespace
536
537TagLibExtractor::TagLibExtractor(QObject* parent)
538 : ExtractorPlugin(parent)
539{
540}
541
542QStringList TagLibExtractor::mimetypes() const
543{
544 return supportedMimeTypes;
545}
546
547void TagLibExtractor::extract(ExtractionResult* result)
548{
549 const QString fileUrl = result->inputUrl();
551
552 // Open the file readonly. Important if we're sandboxed.
553#if defined Q_OS_WINDOWS
554 TagLib::FileStream stream(fileUrl.toLocal8Bit().constData(), true);
555#else
556 TagLib::FileStream stream(fileUrl.toUtf8().constData(), true);
557#endif
558 if (!stream.isOpen()) {
559 qCWarning(KFILEMETADATA_LOG) << "Unable to open file readonly: " << fileUrl;
560 return;
561 }
562
563 const EmbeddedImageData::ImageTypes imageTypes{
564 result->inputFlags() & ExtractionResult::ExtractImageData ? EmbeddedImageData::AllImages : 0
565 };
566
567 if (mimeType == QLatin1String("audio/mpeg")) {
568 TagLib::MPEG::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true);
569 if (file.isValid()) {
570 extractAudioProperties(&file, result);
571 readGenericProperties(file.properties(), result);
572 if (file.hasID3v2Tag()) {
573 result->addImageData(extractId3Cover(file.ID3v2Tag(), imageTypes));
574 extractId3Tags(file.ID3v2Tag(), result);
575 }
576 }
577 } else if (mimeType == QLatin1String("audio/x-aiff") || mimeType == QLatin1String("audio/x-aifc")) {
578 TagLib::RIFF::AIFF::File file(&stream, true);
579 if (file.isValid()) {
580 extractAudioProperties(&file, result);
581 readGenericProperties(file.properties(), result);
582 if (file.hasID3v2Tag()) {
583 result->addImageData(extractId3Cover(file.tag(), imageTypes));
584 extractId3Tags(file.tag(), result);
585 }
586 }
587 } else if (mimeType == QLatin1String("audio/wav") ||
588 mimeType == QLatin1String("audio/vnd.wave") ||
589 mimeType == QLatin1String("audio/x-wav")) {
590 TagLib::RIFF::WAV::File file(&stream, true);
591 if (file.isValid()) {
592 extractAudioProperties(&file, result);
593 readGenericProperties(file.properties(), result);
594 if (file.hasID3v2Tag()) {
595 result->addImageData(extractId3Cover(file.ID3v2Tag(), imageTypes));
596 extractId3Tags(file.ID3v2Tag(), result);
597 }
598 }
599 } else if (mimeType == QLatin1String("audio/x-musepack")) {
600 TagLib::MPC::File file(&stream, true);
601 if (file.isValid()) {
602 extractAudioProperties(&file, result);
603 readGenericProperties(file.properties(), result);
604 if (file.APETag()) {
605 result->addImageData(extractApeCover(file.APETag(), imageTypes));
606 }
607 }
608 } else if (mimeType == QLatin1String("audio/x-ape")) {
609 TagLib::APE::File file(&stream, true);
610 if (file.isValid()) {
611 extractAudioProperties(&file, result);
612 readGenericProperties(file.properties(), result);
613 if (file.APETag()) {
614 result->addImageData(extractApeCover(file.APETag(), imageTypes));
615 }
616 }
617 } else if (mimeType == QLatin1String("audio/x-wavpack")) {
618 TagLib::WavPack::File file(&stream, true);
619 if (file.isValid()) {
620 extractAudioProperties(&file, result);
621 readGenericProperties(file.properties(), result);
622 if (file.APETag()) {
623 result->addImageData(extractApeCover(file.APETag(), imageTypes));
624 }
625 }
626 } else if ((mimeType == QLatin1String("audio/mp4")) ||
627 (mimeType == QLatin1String("audio/vnd.audible.aax")) ||
628 (mimeType == QLatin1String("audio/vnd.audible.aaxc"))) {
629 TagLib::MP4::File file(&stream, true);
630 if (file.isValid()) {
631 extractAudioProperties(&file, result);
632 readGenericProperties(file.properties(), result);
633 extractMp4Tags(file.tag(), result);
634 result->addImageData(extractMp4Cover(file.tag(), imageTypes));
635 }
636 } else if (mimeType == QLatin1String("audio/flac")) {
637 TagLib::FLAC::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true);
638 if (file.isValid()) {
639 extractAudioProperties(&file, result);
640 readGenericProperties(file.properties(), result);
641 result->addImageData(extractFlacCover(file.pictureList(), imageTypes));
642 }
643 } else if (mimeType == QLatin1String("audio/x-flac+ogg")) {
644 TagLib::Ogg::FLAC::File file(&stream, true);
645 if (file.isValid()) {
646 extractAudioProperties(&file, result);
647 readGenericProperties(file.properties(), result);
648 if (file.tag()) {
649 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
650 }
651 }
652 } else if (mimeType == QLatin1String("audio/ogg") || mimeType == QLatin1String("audio/x-vorbis+ogg")) {
653 TagLib::Ogg::Vorbis::File file(&stream, true);
654 if (file.isValid()) {
655 extractAudioProperties(&file, result);
656 readGenericProperties(file.properties(), result);
657 if (file.tag()) {
658 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
659 }
660 }
661 } else if (mimeType == QLatin1String("audio/x-opus+ogg")) {
662 TagLib::Ogg::Opus::File file(&stream, true);
663 if (file.isValid()) {
664 extractAudioProperties(&file, result);
665 readGenericProperties(file.properties(), result);
666 if (file.tag()) {
667 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
668 }
669 }
670 } else if (mimeType == QLatin1String("audio/x-speex+ogg")) {
671 TagLib::Ogg::Speex::File file(&stream, true);
672 // Workaround for buggy taglib:
673 // isValid() returns true for invalid files, but XiphComment* tag() returns a nullptr
674 if (file.isValid() && file.tag()) {
675 extractAudioProperties(&file, result);
676 readGenericProperties(file.properties(), result);
677 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
678 }
679 } else if (mimeType == QLatin1String("audio/x-ms-wma")) {
680 TagLib::ASF::File file(&stream, true);
681 if (file.isValid()) {
682 extractAudioProperties(&file, result);
683 readGenericProperties(file.properties(), result);
684 extractAsfTags(file.tag(), result);
685 TagLib::ASF::Tag* asfTags = dynamic_cast<TagLib::ASF::Tag*>(file.tag());
686 if (asfTags) {
687 result->addImageData(extractAsfCover(asfTags, imageTypes));
688 }
689 }
690 } else if (mimeType == QLatin1String("audio/x-mod")) {
691 TagLib::Mod::File file(&stream);
692 if (file.isValid()) {
693 extractAudioProperties(&file, result);
694 readGenericProperties(file.properties(), result);
695 }
696 } else if (mimeType == QLatin1String("audio/x-s3m")) {
697 TagLib::S3M::File file(&stream);
698 if (file.isValid()) {
699 extractAudioProperties(&file, result);
700 readGenericProperties(file.properties(), result);
701 }
702 } else if (mimeType == QLatin1String("audio/x-xm")) {
703 TagLib::XM::File file(&stream);
704 if (file.isValid()) {
705 extractAudioProperties(&file, result);
706 readGenericProperties(file.properties(), result);
707 }
708 } else if (mimeType == QLatin1String("audio/x-it")) {
709 TagLib::IT::File file(&stream);
710 if (file.isValid()) {
711 extractAudioProperties(&file, result);
712 readGenericProperties(file.properties(), result);
713 }
714 }
715
716 result->addType(Type::Audio);
717}
718
719// TAG information (incomplete).
720// https://xiph.org/vorbis/doc/v-comment.html
721// https://help.mp3tag.de/main_tags.html
722// http://id3.org/
723// https://www.legroom.net/2009/05/09/ogg-vorbis-and-flac-comment-field-recommendations
724// https://kodi.wiki/view/Music_tagging#Tags_Kodi_reads
725// https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping
726// https://picard.musicbrainz.org/docs/mappings/
727// -- FLAC/OGG --
728// Artist: ARTIST, PERFORMER
729// Album artist: ALBUMARTIST
730// Composer: COMPOSER
731// Lyricist: LYRICIST
732// Conductor: CONDUCTOR
733// Disc number: DISCNUMBER
734// Total discs: TOTALDISCS, DISCTOTAL
735// Track number: TRACKNUMBER
736// Total tracks: TOTALTRACKS, TRACKTOTAL
737// Genre: GENRE
738// -- ID3v2 --
739// Artist: TPE1
740// Album artist: TPE2
741// Composer: TCOM
742// Lyricist: TEXT
743// Conductor: TPE3
744// Disc number[/total dics]: TPOS
745// Track number[/total tracks]: TRCK
746// Genre: TCON
747
748#include "moc_taglibextractor.cpp"
The ExtractionResult class is where all the data extracted by the indexer is saved.
QString inputUrl() const
The input URL which the plugins will use to locate the file.
virtual void addType(Type::Type type)=0
This function is called by the plugins.
QString inputMimetype() const
The input MIME type.
virtual void add(Property::Property property, const QVariant &value)=0
This function is called by the plugins when they wish to add a key value pair which should be indexed...
Flags inputFlags() const
The flags which the extraction plugin should considering following when extracting metadata from the ...
void addImageData(QMap< EmbeddedImageData::ImageType, QByteArray > &&images)
This function is called by the plugins.
The ExtractorPlugin is the base class for all file metadata extractors.
QString getSupportedMimeType(const QString &mimetype) const
Return the inherited MIME type which the extractor directly supports.
char * toString(const EngineQuery &query)
KCALUTILS_EXPORT QString mimeType()
@ Audio
Used to mark any file which just contains audio.
Definition types.h:46
The KFileMetaData namespace.
QAction * replace(const QObject *recvr, const char *slot, QObject *parent)
const char * constData() const const
iterator find(const Key &key)
iterator insert(const Key &key, const T &value)
QByteArray toLocal8Bit() const const
QByteArray toUtf8() const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:49:15 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.