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->lengthInSeconds()) {
79 // What about the xml duration?
80 result->add(Property::Duration, audioProp->lengthInSeconds());
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#if TAGLIB_MAJOR_VERSION >= 2
569 TagLib::MPEG::File file(&stream, true);
570#else
571 TagLib::MPEG::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true);
572#endif
573 if (file.isValid()) {
574 extractAudioProperties(&file, result);
575 readGenericProperties(file.properties(), result);
576 if (file.hasID3v2Tag()) {
577 result->addImageData(extractId3Cover(file.ID3v2Tag(), imageTypes));
578 extractId3Tags(file.ID3v2Tag(), result);
579 }
580 }
581 } else if (mimeType == QLatin1String("audio/x-aiff") || mimeType == QLatin1String("audio/x-aifc")) {
582 TagLib::RIFF::AIFF::File file(&stream, true);
583 if (file.isValid()) {
584 extractAudioProperties(&file, result);
585 readGenericProperties(file.properties(), result);
586 if (file.hasID3v2Tag()) {
587 result->addImageData(extractId3Cover(file.tag(), imageTypes));
588 extractId3Tags(file.tag(), result);
589 }
590 }
591 } else if (mimeType == QLatin1String("audio/wav") ||
592 mimeType == QLatin1String("audio/vnd.wave") ||
593 mimeType == QLatin1String("audio/x-wav")) {
594 TagLib::RIFF::WAV::File file(&stream, true);
595 if (file.isValid()) {
596 extractAudioProperties(&file, result);
597 readGenericProperties(file.properties(), result);
598 if (file.hasID3v2Tag()) {
599 result->addImageData(extractId3Cover(file.ID3v2Tag(), imageTypes));
600 extractId3Tags(file.ID3v2Tag(), result);
601 }
602 }
603 } else if (mimeType == QLatin1String("audio/x-musepack")) {
604 TagLib::MPC::File file(&stream, true);
605 if (file.isValid()) {
606 extractAudioProperties(&file, result);
607 readGenericProperties(file.properties(), result);
608 if (file.APETag()) {
609 result->addImageData(extractApeCover(file.APETag(), imageTypes));
610 }
611 }
612 } else if (mimeType == QLatin1String("audio/x-ape")) {
613 TagLib::APE::File file(&stream, true);
614 if (file.isValid()) {
615 extractAudioProperties(&file, result);
616 readGenericProperties(file.properties(), result);
617 if (file.APETag()) {
618 result->addImageData(extractApeCover(file.APETag(), imageTypes));
619 }
620 }
621 } else if (mimeType == QLatin1String("audio/x-wavpack")) {
622 TagLib::WavPack::File file(&stream, true);
623 if (file.isValid()) {
624 extractAudioProperties(&file, result);
625 readGenericProperties(file.properties(), result);
626 if (file.APETag()) {
627 result->addImageData(extractApeCover(file.APETag(), imageTypes));
628 }
629 }
630 } else if ((mimeType == QLatin1String("audio/mp4")) ||
631 (mimeType == QLatin1String("audio/vnd.audible.aax")) ||
632 (mimeType == QLatin1String("audio/vnd.audible.aaxc"))) {
633 TagLib::MP4::File file(&stream, true);
634 if (file.isValid()) {
635 extractAudioProperties(&file, result);
636 readGenericProperties(file.properties(), result);
637 extractMp4Tags(file.tag(), result);
638 result->addImageData(extractMp4Cover(file.tag(), imageTypes));
639 }
640 } else if (mimeType == QLatin1String("audio/flac")) {
641#if TAGLIB_MAJOR_VERSION >= 2
642 TagLib::FLAC::File file(&stream, true);
643#else
644 TagLib::FLAC::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true);
645#endif
646 if (file.isValid()) {
647 extractAudioProperties(&file, result);
648 readGenericProperties(file.properties(), result);
649 result->addImageData(extractFlacCover(file.pictureList(), imageTypes));
650 }
651 } else if (mimeType == QLatin1String("audio/x-flac+ogg")) {
652 TagLib::Ogg::FLAC::File file(&stream, true);
653 if (file.isValid()) {
654 extractAudioProperties(&file, result);
655 readGenericProperties(file.properties(), result);
656 if (file.tag()) {
657 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
658 }
659 }
660 } else if (mimeType == QLatin1String("audio/ogg") || mimeType == QLatin1String("audio/x-vorbis+ogg")) {
661 TagLib::Ogg::Vorbis::File file(&stream, true);
662 if (file.isValid()) {
663 extractAudioProperties(&file, result);
664 readGenericProperties(file.properties(), result);
665 if (file.tag()) {
666 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
667 }
668 }
669 } else if (mimeType == QLatin1String("audio/x-opus+ogg")) {
670 TagLib::Ogg::Opus::File file(&stream, true);
671 if (file.isValid()) {
672 extractAudioProperties(&file, result);
673 readGenericProperties(file.properties(), result);
674 if (file.tag()) {
675 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
676 }
677 }
678 } else if (mimeType == QLatin1String("audio/x-speex+ogg")) {
679 TagLib::Ogg::Speex::File file(&stream, true);
680 // Workaround for buggy taglib:
681 // isValid() returns true for invalid files, but XiphComment* tag() returns a nullptr
682 if (file.isValid() && file.tag()) {
683 extractAudioProperties(&file, result);
684 readGenericProperties(file.properties(), result);
685 result->addImageData(extractFlacCover(file.tag()->pictureList(), imageTypes));
686 }
687 } else if (mimeType == QLatin1String("audio/x-ms-wma")) {
688 TagLib::ASF::File file(&stream, true);
689 if (file.isValid()) {
690 extractAudioProperties(&file, result);
691 readGenericProperties(file.properties(), result);
692 extractAsfTags(file.tag(), result);
693 TagLib::ASF::Tag* asfTags = dynamic_cast<TagLib::ASF::Tag*>(file.tag());
694 if (asfTags) {
695 result->addImageData(extractAsfCover(asfTags, imageTypes));
696 }
697 }
698 } else if (mimeType == QLatin1String("audio/x-mod")) {
699 TagLib::Mod::File file(&stream);
700 if (file.isValid()) {
701 extractAudioProperties(&file, result);
702 readGenericProperties(file.properties(), result);
703 }
704 } else if (mimeType == QLatin1String("audio/x-s3m")) {
705 TagLib::S3M::File file(&stream);
706 if (file.isValid()) {
707 extractAudioProperties(&file, result);
708 readGenericProperties(file.properties(), result);
709 }
710 } else if (mimeType == QLatin1String("audio/x-xm")) {
711 TagLib::XM::File file(&stream);
712 if (file.isValid()) {
713 extractAudioProperties(&file, result);
714 readGenericProperties(file.properties(), result);
715 }
716 } else if (mimeType == QLatin1String("audio/x-it")) {
717 TagLib::IT::File file(&stream);
718 if (file.isValid()) {
719 extractAudioProperties(&file, result);
720 readGenericProperties(file.properties(), result);
721 }
722 }
723
724 result->addType(Type::Audio);
725}
726
727// TAG information (incomplete).
728// https://xiph.org/vorbis/doc/v-comment.html
729// https://help.mp3tag.de/main_tags.html
730// http://id3.org/
731// https://www.legroom.net/2009/05/09/ogg-vorbis-and-flac-comment-field-recommendations
732// https://kodi.wiki/view/Music_tagging#Tags_Kodi_reads
733// https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping
734// https://picard.musicbrainz.org/docs/mappings/
735// -- FLAC/OGG --
736// Artist: ARTIST, PERFORMER
737// Album artist: ALBUMARTIST
738// Composer: COMPOSER
739// Lyricist: LYRICIST
740// Conductor: CONDUCTOR
741// Disc number: DISCNUMBER
742// Total discs: TOTALDISCS, DISCTOTAL
743// Track number: TRACKNUMBER
744// Total tracks: TOTALTRACKS, TRACKTOTAL
745// Genre: GENRE
746// -- ID3v2 --
747// Artist: TPE1
748// Album artist: TPE2
749// Composer: TCOM
750// Lyricist: TEXT
751// Conductor: TPE3
752// Disc number[/total dics]: TPOS
753// Track number[/total tracks]: TRCK
754// Genre: TCON
755
756#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 Oct 11 2024 12:09:03 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.