diff --git a/autotests/samplefiles/no-meta/test.aax b/autotests/samplefiles/no-meta/test.aax new file mode 100644 index 0000000..a936d65 Binary files /dev/null and b/autotests/samplefiles/no-meta/test.aax differ diff --git a/autotests/samplefiles/nocoverage_test.aax b/autotests/samplefiles/nocoverage_test.aax new file mode 100644 index 0000000..81285f9 Binary files /dev/null and b/autotests/samplefiles/nocoverage_test.aax differ diff --git a/autotests/taglibextractortest.cpp b/autotests/taglibextractortest.cpp index e59574c..aaaf188 100644 --- a/autotests/taglibextractortest.cpp +++ b/autotests/taglibextractortest.cpp @@ -1,650 +1,684 @@ /* * TagLibExtractor tests. * * Copyright (C) 2015 Juan Palacios * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "taglibextractortest.h" #include "simpleextractionresult.h" #include "propertyinfo.h" //TODO: use QTESTFINDDATA and remove this #include "indexerextractortestsconfig.h" #include "extractors/taglibextractor.h" #include "mimeutils.h" #include #include using namespace KFileMetaData; QString TagLibExtractorTest::testFilePath(const QString& fileName) const { return QLatin1String(INDEXER_TESTS_SAMPLE_FILES_PATH) + QLatin1Char('/') + fileName; } const QStringList TagLibExtractorTest::propertyEnumNames(const QList& keys) const { QStringList result; for (auto key : keys) { result.append(PropertyInfo(key).name()); } return result; } void TagLibExtractorTest::testNoExtraction() { TagLibExtractor plugin{this}; SimpleExtractionResult result(testFilePath("test.opus"), QStringLiteral("audio/opus"), ExtractionResult::ExtractNothing); plugin.extract(&result); QCOMPARE(result.types().size(), 1); QCOMPARE(result.types().constFirst(), Type::Audio); QCOMPARE(result.properties().size(), 0); } void TagLibExtractorTest::testPropertyTypes() { TagLibExtractor plugin{this}; SimpleExtractionResult resultOpus(testFilePath("test.opus"), "audio/opus"); plugin.extract(&resultOpus); auto testForType = [](SimpleExtractionResult &result, Property::Property prop) { QCOMPARE(result.properties().value(prop).type(), PropertyInfo(prop).valueType()); }; QCOMPARE(resultOpus.types().size(), 1); QCOMPARE(resultOpus.types().constFirst(), Type::Audio); testForType(resultOpus, Property::Title); testForType(resultOpus, Property::Artist); testForType(resultOpus, Property::Album); testForType(resultOpus, Property::AlbumArtist); testForType(resultOpus, Property::Genre); testForType(resultOpus, Property::Comment); testForType(resultOpus, Property::Composer); testForType(resultOpus, Property::Lyricist); testForType(resultOpus, Property::Conductor); testForType(resultOpus, Property::Arranger); testForType(resultOpus, Property::Ensemble); testForType(resultOpus, Property::Location); testForType(resultOpus, Property::Performer); testForType(resultOpus, Property::Language); testForType(resultOpus, Property::Publisher); testForType(resultOpus, Property::Label); testForType(resultOpus, Property::Author); testForType(resultOpus, Property::Copyright); testForType(resultOpus, Property::Compilation); testForType(resultOpus, Property::License); testForType(resultOpus, Property::Opus); testForType(resultOpus, Property::TrackNumber); testForType(resultOpus, Property::ReleaseYear); testForType(resultOpus, Property::Channels); testForType(resultOpus, Property::DiscNumber); testForType(resultOpus, Property::Rating); testForType(resultOpus, Property::ReplayGainAlbumGain); testForType(resultOpus, Property::ReplayGainAlbumPeak); testForType(resultOpus, Property::ReplayGainTrackGain); testForType(resultOpus, Property::ReplayGainTrackPeak); } void TagLibExtractorTest::testCommonData() { QFETCH(QString, fileType); QString fileName = testFilePath(QStringLiteral("test.") + fileType); QString mimeType = MimeUtils::strictMimeType(fileName, mimeDb).name(); TagLibExtractor plugin{this}; QVERIFY(plugin.mimetypes().contains(mimeType)); SimpleExtractionResult result(fileName, mimeType); plugin.extract(&result); QCOMPARE(result.types().size(), 1); QCOMPARE(result.types().constFirst(), Type::Audio); QCOMPARE(result.properties().value(Property::Title), QVariant(QStringLiteral("Title"))); QCOMPARE(result.properties().value(Property::Artist), QVariant(QStringLiteral("Artist"))); QCOMPARE(result.properties().value(Property::Album), QVariant(QStringLiteral("Album"))); QCOMPARE(result.properties().value(Property::Genre), QVariant(QStringLiteral("Genre"))); QCOMPARE(result.properties().value(Property::Comment), QVariant(QStringLiteral("Comment"))); QCOMPARE(result.properties().value(Property::TrackNumber).toInt(), 1); QCOMPARE(result.properties().value(Property::ReleaseYear).toInt(), 2015); } void TagLibExtractorTest::testCommonData_data() { QTest::addColumn("fileType"); QTest::addRow("aiff") << QStringLiteral("aif") ; QTest::addRow("ape") << QStringLiteral("ape") ; QTest::addRow("flac") << QStringLiteral("flac") ; QTest::addRow("m4a") << QStringLiteral("m4a") ; QTest::addRow("mp3") << QStringLiteral("mp3") ; QTest::addRow("mpc") << QStringLiteral("mpc") ; QTest::addRow("ogg") << QStringLiteral("ogg") ; QTest::addRow("opus") << QStringLiteral("opus") ; QTest::addRow("speex") << QStringLiteral("spx") ; QTest::addRow("wav") << QStringLiteral("wav") ; QTest::addRow("wavpack") << QStringLiteral("wv") ; QTest::addRow("wma") << QStringLiteral("wma") ; } void TagLibExtractorTest::testVorbisComment() { QFETCH(QString, fileType); QString fileName = testFilePath(QStringLiteral("test.") + fileType); QString mimeType = MimeUtils::strictMimeType(fileName, mimeDb).name(); TagLibExtractor plugin{this}; SimpleExtractionResult result(fileName, mimeType); plugin.extract(&result); QCOMPARE(result.properties().value(Property::AlbumArtist), QVariant(QStringLiteral("Album Artist"))); QCOMPARE(result.properties().value(Property::Composer), QVariant(QStringLiteral("Composer"))); QCOMPARE(result.properties().value(Property::Lyricist), QVariant(QStringLiteral("Lyricist"))); QCOMPARE(result.properties().value(Property::Conductor), QVariant(QStringLiteral("Conductor"))); QCOMPARE(result.properties().value(Property::Arranger), QVariant(QStringLiteral("Arranger"))); QCOMPARE(result.properties().value(Property::Ensemble), QVariant(QStringLiteral("Ensemble"))); QCOMPARE(result.properties().value(Property::Location), QVariant(QStringLiteral("Location"))); QCOMPARE(result.properties().value(Property::Performer), QVariant(QStringLiteral("Performer"))); QCOMPARE(result.properties().value(Property::Language), QVariant(QStringLiteral("Language"))); QCOMPARE(result.properties().value(Property::Publisher), QVariant(QStringLiteral("Publisher"))); QCOMPARE(result.properties().value(Property::Label), QVariant(QStringLiteral("Label"))); QCOMPARE(result.properties().value(Property::Author), QVariant(QStringLiteral("Author"))); QCOMPARE(result.properties().value(Property::Copyright), QVariant(QStringLiteral("Copyright"))); QCOMPARE(result.properties().value(Property::Compilation), QVariant(QStringLiteral("Compilation"))); QCOMPARE(result.properties().value(Property::License), QVariant(QStringLiteral("License"))); QCOMPARE(result.properties().value(Property::Lyrics), QVariant(QStringLiteral("Lyrics"))); QCOMPARE(result.properties().value(Property::Opus).toInt(), 1); QCOMPARE(result.properties().value(Property::Channels).toInt(), 1); QCOMPARE(result.properties().value(Property::DiscNumber).toInt(), 1); QCOMPARE(result.properties().value(Property::Rating).toInt(), 5); QCOMPARE(result.properties().value(Property::ReplayGainAlbumGain), QVariant(-9.90)); QCOMPARE(result.properties().value(Property::ReplayGainAlbumPeak), QVariant(1.512)); QCOMPARE(result.properties().value(Property::ReplayGainTrackGain), QVariant(-10.44)); QCOMPARE(result.properties().value(Property::ReplayGainTrackPeak), QVariant(1.301)); } void TagLibExtractorTest::testVorbisComment_data() { QTest::addColumn("fileType"); QTest::addRow("flac") << QStringLiteral("flac") ; QTest::addRow("ogg") << QStringLiteral("ogg") ; QTest::addRow("opus") << QStringLiteral("opus") ; QTest::addRow("speex") << QStringLiteral("spx") ; } void TagLibExtractorTest::testVorbisCommentMultivalue() { QFETCH(QString, fileName); QFETCH(QString, mimeType); TagLibExtractor plugin{this}; SimpleExtractionResult result(testFilePath(fileName), mimeType); plugin.extract(&result); QCOMPARE(result.properties().values(Property::Artist), QVariantList({QStringLiteral("Artist1"), QStringLiteral("Artist2")})); QCOMPARE(result.properties().values(Property::Genre), QVariantList({QStringLiteral("Genre1"), QStringLiteral("Genre2")})); } void TagLibExtractorTest::testVorbisCommentMultivalue_data() { QTest::addColumn("fileName"); QTest::addColumn("mimeType"); QTest::addRow("ogg multivalue") << QStringLiteral("test_multivalue.ogg") << QStringLiteral("audio/ogg") ; } void TagLibExtractorTest::testId3() { QFETCH(QString, fileType); QString fileName = testFilePath(QStringLiteral("test.") + fileType); QString mimeType = MimeUtils::strictMimeType(fileName, mimeDb).name(); TagLibExtractor plugin{this}; SimpleExtractionResult result(fileName, mimeType); plugin.extract(&result); QCOMPARE(result.properties().value(Property::AlbumArtist), QVariant(QStringLiteral("Album Artist"))); QCOMPARE(result.properties().value(Property::Composer), QVariant(QStringLiteral("Composer"))); QCOMPARE(result.properties().value(Property::Lyricist), QVariant(QStringLiteral("Lyricist"))); QCOMPARE(result.properties().value(Property::Conductor), QVariant(QStringLiteral("Conductor"))); QCOMPARE(result.properties().value(Property::Publisher), QVariant(QStringLiteral("Publisher"))); QCOMPARE(result.properties().value(Property::Language), QVariant(QStringLiteral("Language"))); QCOMPARE(result.properties().value(Property::Compilation), QVariant(QStringLiteral("Compilation"))); QCOMPARE(result.properties().value(Property::Lyrics), QVariant(QStringLiteral("Lyrics"))); QCOMPARE(result.properties().value(Property::Channels).toInt(), 1); QCOMPARE(result.properties().value(Property::DiscNumber).toInt(), 1); QCOMPARE(result.properties().value(Property::Rating).toInt(), 10); QCOMPARE(result.properties().value(Property::ReplayGainAlbumGain), QVariant(-3.33)); QCOMPARE(result.properties().value(Property::ReplayGainAlbumPeak), QVariant(1.333)); QCOMPARE(result.properties().value(Property::ReplayGainTrackGain), QVariant(3.33)); QCOMPARE(result.properties().value(Property::ReplayGainTrackPeak), QVariant(1.369)); } void TagLibExtractorTest::testId3_data() { QTest::addColumn("fileType"); QTest::addRow("aiff") << QStringLiteral("aif") ; QTest::addRow("mp3") << QStringLiteral("mp3") ; QTest::addRow("wav") << QStringLiteral("wav") ; } void TagLibExtractorTest::testApe() { QFETCH(QString, fileType); QString fileName = testFilePath(QStringLiteral("test.") + fileType); QString mimeType = MimeUtils::strictMimeType(fileName, mimeDb).name(); TagLibExtractor plugin{this}; SimpleExtractionResult result(fileName, mimeType); plugin.extract(&result); QCOMPARE(result.properties().value(Property::AlbumArtist), QVariant(QStringLiteral("Album Artist"))); QCOMPARE(result.properties().value(Property::Composer), QVariant(QStringLiteral("Composer"))); QCOMPARE(result.properties().value(Property::Conductor), QVariant(QStringLiteral("Conductor"))); QCOMPARE(result.properties().value(Property::Arranger), QVariant(QStringLiteral("Arranger"))); QCOMPARE(result.properties().value(Property::Ensemble), QVariant(QStringLiteral("Ensemble"))); QCOMPARE(result.properties().value(Property::Location), QVariant(QStringLiteral("Location"))); QCOMPARE(result.properties().value(Property::Performer), QVariant(QStringLiteral("Performer"))); QCOMPARE(result.properties().value(Property::Language), QVariant(QStringLiteral("Language"))); QCOMPARE(result.properties().value(Property::Publisher), QVariant(QStringLiteral("Publisher"))); QCOMPARE(result.properties().value(Property::Label), QVariant(QStringLiteral("Label"))); QCOMPARE(result.properties().value(Property::Author), QVariant(QStringLiteral("Author"))); QCOMPARE(result.properties().value(Property::Copyright), QVariant(QStringLiteral("Copyright"))); QCOMPARE(result.properties().value(Property::Compilation), QVariant(QStringLiteral("Compilation"))); QCOMPARE(result.properties().value(Property::License), QVariant(QStringLiteral("License"))); QCOMPARE(result.properties().value(Property::Lyrics), QVariant(QStringLiteral("Lyrics"))); QCOMPARE(result.properties().value(Property::Channels).toInt(), 1); QCOMPARE(result.properties().value(Property::DiscNumber).toInt(), 1); QCOMPARE(result.properties().value(Property::Rating).toInt(), 4); QCOMPARE(result.properties().value(Property::ReplayGainAlbumGain), QVariant(-9.44)); QCOMPARE(result.properties().value(Property::ReplayGainAlbumPeak), QVariant(1.099)); QCOMPARE(result.properties().value(Property::ReplayGainTrackGain), QVariant(-5.23)); QCOMPARE(result.properties().value(Property::ReplayGainTrackPeak), QVariant(1.234)); } void TagLibExtractorTest::testApe_data() { QTest::addColumn("fileType"); QTest::addRow("ape") << QStringLiteral("ape") ; QTest::addRow("musepack") << QStringLiteral("mpc") ; QTest::addRow("wavpack") << QStringLiteral("wv") ; } void TagLibExtractorTest::testMp4() { QFETCH(QString, fileType); QString fileName = testFilePath(QStringLiteral("test.") + fileType); QString mimeType = MimeUtils::strictMimeType(fileName, mimeDb).name(); TagLibExtractor plugin{this}; SimpleExtractionResult resultMp4(fileName, mimeType); plugin.extract(&resultMp4); QCOMPARE(resultMp4.properties().value(Property::AlbumArtist), QVariant(QStringLiteral("Album Artist"))); QCOMPARE(resultMp4.properties().value(Property::Composer), QVariant(QStringLiteral("Composer"))); QCOMPARE(resultMp4.properties().value(Property::Copyright), QVariant(QStringLiteral("Copyright"))); QCOMPARE(resultMp4.properties().value(Property::Lyrics), QVariant(QStringLiteral("Lyrics"))); QCOMPARE(resultMp4.properties().value(Property::Channels).toInt(), 2); QCOMPARE(resultMp4.properties().value(Property::DiscNumber).toInt(), 1); QCOMPARE(resultMp4.properties().value(Property::Rating).toInt(), 8); } void TagLibExtractorTest::testMp4_data() { QTest::addColumn("fileType"); QTest::addRow("mp4") << QStringLiteral("m4a") ; } +void TagLibExtractorTest::testAax() +{ + QFETCH(QString, fileType); + + QString fileName = testFilePath(QStringLiteral("nocoverage_test.") + fileType); + QString mimeType = QStringLiteral("audio/vnd.audible.aax"); + + TagLibExtractor plugin{this}; + SimpleExtractionResult resultAax(fileName, mimeType); + plugin.extract(&resultAax); + + QCOMPARE(resultAax.properties().value(Property::AlbumArtist), QVariant(QStringLiteral("Album Artist"))); + QCOMPARE(resultAax.properties().value(Property::Artist), QVariant(QStringLiteral("Artist"))); + QCOMPARE(resultAax.properties().value(Property::Genre), QVariant(QStringLiteral("Hörbuch"))); + QCOMPARE(resultAax.properties().value(Property::Copyright), QVariant(QStringLiteral("CopyrightHolder"))); + QCOMPARE(resultAax.properties().value(Property::Title), QVariant(QStringLiteral("TrackTitle"))); + QCOMPARE(resultAax.properties().value(Property::Channels).toInt(), 2); +} + +void TagLibExtractorTest::testAax_data() +{ + QTest::addColumn("fileType"); + + QTest::addRow("aax") + << QStringLiteral("aax") + ; +} + void TagLibExtractorTest::testAsf() { QFETCH(QString, fileType); QString fileName = testFilePath(QStringLiteral("test.") + fileType); QString mimeType = MimeUtils::strictMimeType(fileName, mimeDb).name(); TagLibExtractor plugin{this}; SimpleExtractionResult result(fileName, mimeType); plugin.extract(&result); QCOMPARE(result.properties().value(Property::AlbumArtist), QVariant(QStringLiteral("Album Artist"))); QCOMPARE(result.properties().value(Property::Rating).toInt(), 6); QCOMPARE(result.properties().value(Property::DiscNumber).toInt(), 1); QCOMPARE(result.properties().value(Property::Conductor), QVariant(QStringLiteral("Conductor"))); QCOMPARE(result.properties().value(Property::Composer), QVariant(QStringLiteral("Composer"))); QCOMPARE(result.properties().value(Property::Author), QVariant(QStringLiteral("Author"))); QCOMPARE(result.properties().value(Property::Lyricist), QVariant(QStringLiteral("Lyricist"))); QCOMPARE(result.properties().value(Property::Copyright), QVariant(QStringLiteral("Copyright"))); QCOMPARE(result.properties().value(Property::Publisher), QVariant(QStringLiteral("Publisher"))); } void TagLibExtractorTest::testAsf_data() { QTest::addColumn("fileType"); QTest::addRow("asf") << QStringLiteral("wma") ; } void TagLibExtractorTest::testId3Rating_data() { QTest::addColumn("path"); QTest::addColumn("expectedRating"); QTest::addRow("WMP") << QFINDTESTDATA("samplefiles/mp3_rating/testWMP.mp3") << 0 ; QTest::addRow("WMP1") << QFINDTESTDATA("samplefiles/mp3_rating/testWMP1.mp3") << 2 ; QTest::addRow("WMP2") << QFINDTESTDATA("samplefiles/mp3_rating/testWMP2.mp3") << 4 ; QTest::addRow("WMP3") << QFINDTESTDATA("samplefiles/mp3_rating/testWMP3.mp3") << 6 ; QTest::addRow("WMP4") << QFINDTESTDATA("samplefiles/mp3_rating/testWMP4.mp3") << 8 ; QTest::addRow("WMP5") << QFINDTESTDATA("samplefiles/mp3_rating/testWMP5.mp3") << 10 ; QTest::addRow("MM") << QFINDTESTDATA("samplefiles/mp3_rating/testMM.mp3") << 0 ; QTest::addRow("MM1") << QFINDTESTDATA("samplefiles/mp3_rating/testMM1.mp3") << 1 ; QTest::addRow("MM2") << QFINDTESTDATA("samplefiles/mp3_rating/testMM2.mp3") << 2 ; QTest::addRow("MM3") << QFINDTESTDATA("samplefiles/mp3_rating/testMM3.mp3") << 3 ; QTest::addRow("MM4") << QFINDTESTDATA("samplefiles/mp3_rating/testMM4.mp3") << 4 ; QTest::addRow("MM5") << QFINDTESTDATA("samplefiles/mp3_rating/testMM5.mp3") << 5 ; QTest::addRow("MM6") << QFINDTESTDATA("samplefiles/mp3_rating/testMM6.mp3") << 6 ; QTest::addRow("MM7") << QFINDTESTDATA("samplefiles/mp3_rating/testMM7.mp3") << 7 ; QTest::addRow("MM8") << QFINDTESTDATA("samplefiles/mp3_rating/testMM8.mp3") << 8 ; QTest::addRow("MM9") << QFINDTESTDATA("samplefiles/mp3_rating/testMM9.mp3") << 9 ; QTest::addRow("MM10") << QFINDTESTDATA("samplefiles/mp3_rating/testMM10.mp3") << 10 ; } void TagLibExtractorTest::testId3Rating() { QFETCH(QString, path); QFETCH(int, expectedRating); TagLibExtractor plugin{this}; SimpleExtractionResult result(path, "audio/mpeg"); plugin.extract(&result); QCOMPARE(result.properties().value(Property::Rating).toInt(), expectedRating); } void TagLibExtractorTest::testWmaRating() { QFETCH(QString, path); QFETCH(int, expectedRating); TagLibExtractor plugin{this}; SimpleExtractionResult result(path, "audio/x-ms-wma"); plugin.extract(&result); QCOMPARE(result.properties().value(Property::Rating).toInt(), expectedRating); } void TagLibExtractorTest::testWmaRating_data() { QTest::addColumn("path"); QTest::addColumn("expectedRating"); QTest::addRow("WMP0") << QFINDTESTDATA("samplefiles/wma_rating/test0.wma") << 0 ; QTest::addRow("WMP1") << QFINDTESTDATA("samplefiles/wma_rating/test1.wma") << 2 ; QTest::addRow("WMP2") << QFINDTESTDATA("samplefiles/wma_rating/test2.wma") << 4 ; QTest::addRow("WMP3") << QFINDTESTDATA("samplefiles/wma_rating/test3.wma") << 6 ; QTest::addRow("WMP4") << QFINDTESTDATA("samplefiles/wma_rating/test4.wma") << 8 ; QTest::addRow("WMP5") << QFINDTESTDATA("samplefiles/wma_rating/test5.wma") << 10 ; } void TagLibExtractorTest::testNoMetadata_data() { const auto expectedKeys = QList{ Property::BitRate, Property::Channels, Property::Duration, Property::SampleRate, }; QTest::addColumn("path"); QTest::addColumn("mimeType"); QTest::addColumn>("expectedKeys"); QTest::addColumn("failMessage"); QTest::addRow("mp3") << QFINDTESTDATA("samplefiles/no-meta/test.mp3") << QStringLiteral("audio/mpeg") << expectedKeys << QString() ; QTest::addRow("m4a") << QFINDTESTDATA("samplefiles/no-meta/test.m4a") << QStringLiteral("audio/mp4") << expectedKeys << QString() ; QTest::addRow("flac") << QFINDTESTDATA("samplefiles/no-meta/test.flac") << QStringLiteral("audio/flac") << expectedKeys << QString() ; QTest::addRow("opus") << QFINDTESTDATA("samplefiles/no-meta/test.opus") << QStringLiteral("audio/opus") << expectedKeys << QString() ; QTest::addRow("ogg") << QFINDTESTDATA("samplefiles/no-meta/test.ogg") << QStringLiteral("audio/ogg") << expectedKeys << QString() ; QTest::addRow("mpc") << QFINDTESTDATA("samplefiles/no-meta/test.mpc") << QStringLiteral("audio/x-musepack") << expectedKeys << QString() ; + QTest::addRow("aax") + << QFINDTESTDATA("samplefiles/no-meta/test.aax") + << QStringLiteral("audio/vnd.audible.aax") + << expectedKeys << QString() + ; + } void TagLibExtractorTest::testNoMetadata() { QFETCH(QString, path); QFETCH(QString, mimeType); QFETCH(QList, expectedKeys); QFETCH(QString, failMessage); TagLibExtractor plugin{this}; SimpleExtractionResult extracted(path, mimeType); plugin.extract(&extracted); const auto resultList = extracted.properties(); const auto resultKeys = resultList.uniqueKeys(); #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) const QSet resultKeySet = resultKeys.toSet(); const QSet expectedKeySet = expectedKeys.toSet(); #else const QSet resultKeySet(resultKeys.begin(), resultKeys.end()); const QSet expectedKeySet(expectedKeys.begin(), expectedKeys.end()); #endif const auto excessKeys = resultKeySet - expectedKeySet; const auto missingKeys = expectedKeySet - resultKeySet; if (!excessKeys.isEmpty()) { const auto propNames = propertyEnumNames(excessKeys.values()).join(QLatin1String(", ")); if (failMessage.isEmpty()) { const auto message = QStringLiteral("Excess properties: %1").arg(propNames); QWARN(qPrintable(message)); } else { QEXPECT_FAIL("", qPrintable(QStringLiteral("%1: %2").arg(failMessage).arg(propNames)), Continue); } } else if (!missingKeys.isEmpty()) { const auto message = QStringLiteral("Missing properties: %1") .arg(propertyEnumNames(missingKeys.values()).join(QLatin1String(", "))); QWARN(qPrintable(message)); } QCOMPARE(resultKeys, expectedKeys); if (!failMessage.isEmpty()) { const auto message = QStringLiteral("%1: %2") .arg(failMessage) .arg(propertyEnumNames(excessKeys.values()).join(QLatin1String(", "))); QEXPECT_FAIL("", qPrintable(message), Continue); } QCOMPARE(resultKeys, expectedKeys); } void TagLibExtractorTest::testRobustness_data() { QTest::addColumn("path"); QTest::addColumn("mimeType"); QTest::addRow("ArcGIS GeoData spx") << QFINDTESTDATA("samplefiles/misdetected/test_arcgis_geodata.spx") << QStringLiteral("audio/speex"); } void TagLibExtractorTest::testRobustness() { QFETCH(QString, path); QFETCH(QString, mimeType); TagLibExtractor plugin{this}; SimpleExtractionResult extracted(path, mimeType); plugin.extract(&extracted); } QTEST_GUILESS_MAIN(TagLibExtractorTest) diff --git a/autotests/taglibextractortest.h b/autotests/taglibextractortest.h index 415bab0..c57caee 100644 --- a/autotests/taglibextractortest.h +++ b/autotests/taglibextractortest.h @@ -1,67 +1,69 @@ /* * TagLibExtractor tests. * * Copyright (C) 2015 Juan Palacios * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #ifndef TAGLIBEXTRACTORTEST_H #define TAGLIBEXTRACTORTEST_H #include #include #include "properties.h" class TagLibExtractorTest : public QObject { Q_OBJECT private: QString testFilePath(const QString& fileName) const; private Q_SLOTS: void testNoExtraction(); void testPropertyTypes(); void testCommonData(); void testCommonData_data(); void testVorbisComment(); void testVorbisComment_data(); void testVorbisCommentMultivalue(); void testVorbisCommentMultivalue_data(); void testId3(); void testId3_data(); void testApe(); void testApe_data(); void testMp4(); void testMp4_data(); + void testAax(); + void testAax_data(); void testAsf(); void testAsf_data(); void testId3Rating_data(); void testId3Rating(); void testWmaRating_data(); void testWmaRating(); void testNoMetadata(); void testNoMetadata_data(); void testRobustness(); void testRobustness_data(); private: // Convenience function const QStringList propertyEnumNames(const QList& key) const; QMimeDatabase mimeDb; }; #endif // TAGLIBEXTRACTORTEST_H diff --git a/src/extractors/taglibextractor.cpp b/src/extractors/taglibextractor.cpp index 8ecef34..c74cb4a 100644 --- a/src/extractors/taglibextractor.cpp +++ b/src/extractors/taglibextractor.cpp @@ -1,518 +1,520 @@ /* Copyright (C) 2012 Vishesh Handa This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "taglibextractor.h" #include "kfilemetadata_debug.h" // Taglib includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KFileMetaData; namespace { const QStringList supportedMimeTypes = { QStringLiteral("audio/flac"), QStringLiteral("audio/mp4"), QStringLiteral("audio/mpeg"), QStringLiteral("audio/mpeg3"), QStringLiteral("audio/ogg"), QStringLiteral("audio/opus"), QStringLiteral("audio/speex"), QStringLiteral("audio/wav"), + QStringLiteral("audio/vnd.audible.aax"), QStringLiteral("audio/x-aiff"), QStringLiteral("audio/x-aifc"), QStringLiteral("audio/x-ape"), QStringLiteral("audio/x-mpeg"), QStringLiteral("audio/x-ms-wma"), QStringLiteral("audio/x-musepack"), QStringLiteral("audio/x-opus+ogg"), QStringLiteral("audio/x-speex+ogg"), QStringLiteral("audio/x-vorbis+ogg"), QStringLiteral("audio/x-wav"), QStringLiteral("audio/x-wavpack"), }; void extractAudioProperties(TagLib::File* file, ExtractionResult* result) { TagLib::AudioProperties* audioProp = file->audioProperties(); if (audioProp && (result->inputFlags() & ExtractionResult::ExtractMetaData)) { if (audioProp->length()) { // What about the xml duration? result->add(Property::Duration, audioProp->length()); } if (audioProp->bitrate()) { result->add(Property::BitRate, audioProp->bitrate() * 1000); } if (audioProp->channels()) { result->add(Property::Channels, audioProp->channels()); } if (audioProp->sampleRate()) { result->add(Property::SampleRate, audioProp->sampleRate()); } } } void readGenericProperties(const TagLib::PropertyMap &savedProperties, ExtractionResult* result) { if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || savedProperties.isEmpty()) { return; } if (savedProperties.contains("TITLE")) { result->add(Property::Title, TStringToQString(savedProperties["TITLE"].toString()).trimmed()); } if (savedProperties.contains("ALBUM")) { result->add(Property::Album, TStringToQString(savedProperties["ALBUM"].toString()).trimmed()); } if (savedProperties.contains("COMMENT")) { result->add(Property::Comment, TStringToQString(savedProperties["COMMENT"].toString()).trimmed()); } if (savedProperties.contains("TRACKNUMBER")) { result->add(Property::TrackNumber, savedProperties["TRACKNUMBER"].toString().toInt()); } if (savedProperties.contains("DATE")) { result->add(Property::ReleaseYear, savedProperties["DATE"].toString().toInt()); } if (savedProperties.contains("OPUS")) { result->add(Property::Opus, savedProperties["OPUS"].toString().toInt()); } if (savedProperties.contains("DISCNUMBER")) { result->add(Property::DiscNumber, savedProperties["DISCNUMBER"].toString().toInt()); } if (savedProperties.contains("RATING")) { /* * There is no standard regarding ratings. Mimic MediaMonkey's behavior * with a range of 0 to 100 (stored in steps of 10) and make it compatible * with baloo rating with a range from 0 to 10 */ result->add(Property::Rating, savedProperties["RATING"].toString().toInt() / 10); } if (savedProperties.contains("LOCATION")) { result->add(Property::Location, TStringToQString(savedProperties["LOCATION"].toString()).trimmed()); } if (savedProperties.contains("LANGUAGE")) { result->add(Property::Language, TStringToQString(savedProperties["LANGUAGE"].toString()).trimmed()); } if (savedProperties.contains("LICENSE")) { result->add(Property::License, TStringToQString(savedProperties["LICENSE"].toString()).trimmed()); } if (savedProperties.contains("PUBLISHER")) { result->add(Property::Publisher, TStringToQString(savedProperties["PUBLISHER"].toString()).trimmed()); } if (savedProperties.contains("COPYRIGHT")) { result->add(Property::Copyright, TStringToQString(savedProperties["COPYRIGHT"].toString()).trimmed()); } if (savedProperties.contains("LABEL")) { result->add(Property::Label, TStringToQString(savedProperties["LABEL"].toString()).trimmed()); } if (savedProperties.contains("ENSEMBLE")) { result->add(Property::Ensemble, TStringToQString(savedProperties["ENSEMBLE"].toString()).trimmed()); } if (savedProperties.contains("COMPILATION")) { result->add(Property::Compilation, TStringToQString(savedProperties["COMPILATION"].toString()).trimmed()); } if (savedProperties.contains("LYRICS")) { result->add(Property::Lyrics, TStringToQString(savedProperties["LYRICS"].toString()).trimmed()); } if (savedProperties.contains("ARTIST")) { const auto artists = savedProperties["ARTIST"]; for (const auto& artist : artists) { result->add(Property::Artist, TStringToQString(artist).trimmed()); } } if (savedProperties.contains("GENRE")) { const auto genres = savedProperties["GENRE"]; for (const auto& genre : genres) { result->add(Property::Genre, TStringToQString(genre).trimmed()); } } if (savedProperties.contains("ALBUMARTIST")) { const auto albumArtists = savedProperties["ALBUMARTIST"]; for (const auto& albumArtist : albumArtists) { result->add(Property::AlbumArtist, TStringToQString(albumArtist).trimmed()); } } if (savedProperties.contains("COMPOSER")) { const auto composers = savedProperties["COMPOSER"]; for (const auto& composer : composers) { result->add(Property::Composer, TStringToQString(composer).trimmed()); } } if (savedProperties.contains("LYRICIST")) { const auto lyricists = savedProperties["LYRICIST"]; for (const auto& lyricist : lyricists) { result->add(Property::Lyricist, TStringToQString(lyricist).trimmed()); } } if (savedProperties.contains("CONDUCTOR")) { const auto conductors = savedProperties["CONDUCTOR"]; for (const auto& conductor : conductors) { result->add(Property::Conductor, TStringToQString(conductor).trimmed()); } } if (savedProperties.contains("ARRANGER")) { const auto arrangers = savedProperties["ARRANGER"]; for (const auto& arranger : arrangers) { result->add(Property::Arranger, TStringToQString(arranger).trimmed()); } } if (savedProperties.contains("PERFORMER")) { const auto performers = savedProperties["PERFORMER"]; for (const auto& performer : performers) { result->add(Property::Performer, TStringToQString(performer).trimmed()); } } if (savedProperties.contains("AUTHOR")) { const auto authors = savedProperties["AUTHOR"]; for (const auto& author: authors) { result->add(Property::Author, TStringToQString(author).trimmed()); } } if (savedProperties.contains("REPLAYGAIN_TRACK_GAIN")) { auto trackGainString = TStringToQString(savedProperties["REPLAYGAIN_TRACK_GAIN"].toString(";")).trimmed(); // remove " dB" suffix if (trackGainString.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) { trackGainString.chop(3); } bool success = false; double replayGainTrackGain = trackGainString.toDouble(&success); if (success) { result->add(Property::ReplayGainTrackGain, replayGainTrackGain); } } if (savedProperties.contains("REPLAYGAIN_ALBUM_GAIN")) { auto albumGainString = TStringToQString(savedProperties["REPLAYGAIN_ALBUM_GAIN"].toString(";")).trimmed(); // remove " dB" suffix if (albumGainString.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) { albumGainString.chop(3); } bool success = false; double replayGainAlbumGain = albumGainString.toDouble(&success); if (success) { result->add(Property::ReplayGainAlbumGain, replayGainAlbumGain); } } if (savedProperties.contains("REPLAYGAIN_TRACK_PEAK")) { auto trackPeakString = TStringToQString(savedProperties["REPLAYGAIN_TRACK_PEAK"].toString(";")).trimmed(); bool success = false; double replayGainTrackPeak = trackPeakString.toDouble(&success); if (success) { result->add(Property::ReplayGainTrackPeak, replayGainTrackPeak); } } if (savedProperties.contains("REPLAYGAIN_ALBUM_PEAK")) { auto albumPeakString = TStringToQString(savedProperties["REPLAYGAIN_ALBUM_PEAK"].toString(";")).trimmed(); bool success = false; double replayGainAlbumPeak = albumPeakString.toDouble(&success); if (success) { result->add(Property::ReplayGainAlbumPeak, replayGainAlbumPeak); } } } void extractId3Tags(TagLib::ID3v2::Tag* Id3Tags, ExtractionResult* result) { if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || Id3Tags->isEmpty()) { return; } TagLib::ID3v2::FrameList lstID3v2; /* * Publisher. * Special handling because TagLib::PropertyMap maps "TPUB" to "LABEL" * Insert manually for Publisher. */ lstID3v2 = Id3Tags->frameListMap()["TPUB"]; if (!lstID3v2.isEmpty()) { result->add(Property::Publisher, TStringToQString(lstID3v2.front()->toString())); } // Compilation. lstID3v2 = Id3Tags->frameListMap()["TCMP"]; if (!lstID3v2.isEmpty()) { result->add(Property::Compilation, TStringToQString(lstID3v2.front()->toString())); } /* * Rating. * There is no standard regarding ratings. Most of the implementations match * a 5 stars rating to a range of 0-255 for MP3. * Map it to baloo rating with a range of 0 - 10. */ lstID3v2 = Id3Tags->frameListMap()["POPM"]; if (!lstID3v2.isEmpty()) { TagLib::ID3v2::PopularimeterFrame *ratingFrame = static_cast(lstID3v2.front()); int rating = ratingFrame->rating(); if (rating == 0) { rating = 0; } else if (rating == 1) { TagLib::String ratingProvider = ratingFrame->email(); if (ratingProvider == "no@email" || ratingProvider == "org.kde.kfilemetadata") { rating = 1; } else { rating = 2; } } else if (rating >= 1 && rating <= 255) { rating = static_cast(0.032 * rating + 2); } result->add(Property::Rating, rating); } } void extractMp4Tags(TagLib::MP4::Tag* mp4Tags, ExtractionResult* result) { if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || mp4Tags->isEmpty()) { return; } TagLib::MP4::ItemListMap allTags = mp4Tags->itemListMap(); /* * There is no standard regarding ratings. Mimic MediaMonkey's behavior * with a range of 0 to 100 (stored in steps of 10) and make it compatible * with baloo rating with a range from 0 to 10. */ TagLib::MP4::ItemListMap::Iterator itRating = allTags.find("rate"); if (itRating != allTags.end()) { result->add(Property::Rating, itRating->second.toStringList().toString().toInt() / 10); } } void extractAsfTags(TagLib::ASF::Tag* asfTags, ExtractionResult* result) { if (!(result->inputFlags() & ExtractionResult::ExtractMetaData) || asfTags->isEmpty()) { return; } TagLib::ASF::AttributeList lstASF = asfTags->attribute("WM/SharedUserRating"); if (!lstASF.isEmpty()) { int rating = lstASF.front().toString().toInt(); /* * Map the rating values of WMP to Baloo rating. * 0->0, 1->2, 25->4, 50->6, 75->8, 99->10 */ if (rating == 0) { rating = 0; } else if (rating == 1) { rating = 2; } else { rating = static_cast(0.09 * rating + 2); } result->add(Property::Rating, rating); } lstASF = asfTags->attribute("Author"); if (!lstASF.isEmpty()) { const auto attribute = lstASF.front(); result->add(Property::Author, TStringToQString(attribute.toString()).trimmed()); } // Lyricist is called "WRITER" for wma/asf files lstASF = asfTags->attribute("WM/Writer"); if (!lstASF.isEmpty()) { const auto attribute = lstASF.front(); result->add(Property::Lyricist, TStringToQString(attribute.toString()).trimmed()); } /* * TagLib exports "WM/PUBLISHER" as "LABEL" in the PropertyMap, * add it manually to Publisher. */ lstASF = asfTags->attribute("WM/Publisher"); if (!lstASF.isEmpty()) { const auto attribute = lstASF.front(); result->add(Property::Publisher, TStringToQString(attribute.toString()).trimmed()); } } } // anonymous namespace TagLibExtractor::TagLibExtractor(QObject* parent) : ExtractorPlugin(parent) { } QStringList TagLibExtractor::mimetypes() const { return supportedMimeTypes; } void TagLibExtractor::extract(ExtractionResult* result) { const QString fileUrl = result->inputUrl(); const QString mimeType = getSupportedMimeType(result->inputMimetype()); // Open the file readonly. Important if we're sandboxed. #if defined Q_OS_WINDOWS TagLib::FileStream stream(fileUrl.toLocal8Bit().constData(), true); #else TagLib::FileStream stream(fileUrl.toUtf8().constData(), true); #endif if (!stream.isOpen()) { qCWarning(KFILEMETADATA_LOG) << "Unable to open file readonly: " << fileUrl; return; } if (mimeType == QLatin1String("audio/mpeg") || mimeType == QLatin1String("audio/mpeg3") || mimeType == QLatin1String("audio/x-mpeg")) { TagLib::MPEG::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); if (file.hasID3v2Tag()) { extractId3Tags(file.ID3v2Tag(), result); } } } else if (mimeType == QLatin1String("audio/x-aiff") || mimeType == QLatin1String("audio/x-aifc")) { TagLib::RIFF::AIFF::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); if (file.hasID3v2Tag()) { extractId3Tags(file.tag(), result); } } } else if (mimeType == QLatin1String("audio/wav") || mimeType == QLatin1String("audio/x-wav")) { TagLib::RIFF::WAV::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); if (file.hasID3v2Tag()) { extractId3Tags(file.tag(), result); } } } else if (mimeType == QLatin1String("audio/x-musepack")) { TagLib::MPC::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); } } else if (mimeType == QLatin1String("audio/x-ape")) { TagLib::APE::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); } } else if (mimeType == QLatin1String("audio/x-wavpack")) { TagLib::WavPack::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); } - } else if (mimeType == QLatin1String("audio/mp4")) { + } else if ((mimeType == QLatin1String("audio/mp4")) || + (mimeType == QLatin1String("audio/vnd.audible.aax"))) { TagLib::MP4::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); extractMp4Tags(file.tag(), result); } } else if (mimeType == QLatin1String("audio/flac")) { TagLib::FLAC::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); } } else if (mimeType == QLatin1String("audio/ogg") || mimeType == QLatin1String("audio/x-vorbis+ogg")) { TagLib::Ogg::Vorbis::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); } } else if (mimeType == QLatin1String("audio/opus") || mimeType == QLatin1String("audio/x-opus+ogg")) { TagLib::Ogg::Opus::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); } } else if (mimeType == QLatin1String("audio/speex") || mimeType == QLatin1String("audio/x-speex+ogg")) { TagLib::Ogg::Speex::File file(&stream, true); // Workaround for buggy taglib: // isValid() returns true for invalid files, but XiphComment* tag() returns a nullptr if (file.isValid() && file.tag()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); } } else if (mimeType == QLatin1String("audio/x-ms-wma")) { TagLib::ASF::File file(&stream, true); if (file.isValid()) { extractAudioProperties(&file, result); readGenericProperties(file.properties(), result); extractAsfTags(file.tag(), result); } } result->addType(Type::Audio); } // TAG information (incomplete). // https://xiph.org/vorbis/doc/v-comment.html // https://help.mp3tag.de/main_tags.html // http://id3.org/ // https://www.legroom.net/2009/05/09/ogg-vorbis-and-flac-comment-field-recommendations // https://kodi.wiki/view/Music_tagging#Tags_Kodi_reads // https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping // https://picard.musicbrainz.org/docs/mappings/ // -- FLAC/OGG -- // Artist: ARTIST, PERFORMER // Album artist: ALBUMARTIST // Composer: COMPOSER // Lyricist: LYRICIST // Conductor: CONDUCTOR // Disc number: DISCNUMBER // Total discs: TOTALDISCS, DISCTOTAL // Track number: TRACKNUMBER // Total tracks: TOTALTRACKS, TRACKTOTAL // Genre: GENRE // -- ID3v2 -- // Artist: TPE1 // Album artist: TPE2 // Composer: TCOM // Lyricist: TEXT // Conductor: TPE3 // Disc number[/total dics]: TPOS // Track number[/total tracks]: TRCK // Genre: TCON diff --git a/src/extractors/taglibextractor.json b/src/extractors/taglibextractor.json index f08ec79..159e56d 100644 --- a/src/extractors/taglibextractor.json +++ b/src/extractors/taglibextractor.json @@ -1,26 +1,27 @@ { "Name" : "TaglibExtractor", "Id" : "org.kde.taglibextractor", "MimeTypes" : { "audio/flac" : { "Version" : "0.0" }, "audio/mp4" : { "Version" : "0.0" }, "audio/mpeg" : { "Version" : "0.0" }, "audio/mpeg3" : { "Version" : "0.0" }, "audio/ogg" : { "Version" : "0.0" }, "audio/opus" : { "Version" : "0.0" }, "audio/speex" : { "Version" : "0.0" }, "audio/wav" : { "Version" : "0.0" }, + "audio/vnd.audible.aax" : { "Version" : "0.0" }, "audio/x-aiff" : { "Version" : "0.0" }, "audio/x-aifc" : { "Version" : "0.0" }, "audio/x-ape" : { "Version" : "0.0" }, "audio/x-mpeg" : { "Version" : "0.0" }, "audio/x-ms-wma" : { "Version" : "0.0" }, "audio/x-musepack" : { "Version" : "0.0" }, "audio/x-opus+ogg" : { "Version" : "0.0" }, "audio/x-speex+ogg" : { "Version" : "0.0" }, "audio/x-vorbis+ogg" : { "Version" : "0.0" }, "audio/x-wav" : { "Version" : "0.0" }, "audio/x-wavpack" : { "Version" : "0.0" } } }