diff --git a/taglib/asf/asfattribute.cpp b/taglib/asf/asfattribute.cpp index 82bd319a..955724df 100644 --- a/taglib/asf/asfattribute.cpp +++ b/taglib/asf/asfattribute.cpp @@ -162,6 +162,13 @@ ASF::Picture ASF::Attribute::toPicture() const return d->pictureValue; } +ASF::Attribute ASF::Attribute::fromGuid(const ByteVector& guid) +{ + Attribute attr(guid); + attr.d->type = GuidType; + return attr; +} + String ASF::Attribute::parse(ASF::File &file, int kind) { unsigned int size, nameLength; diff --git a/taglib/asf/asfattribute.h b/taglib/asf/asfattribute.h index 4f686253..45dd6f49 100644 --- a/taglib/asf/asfattribute.h +++ b/taglib/asf/asfattribute.h @@ -165,6 +165,11 @@ namespace TagLib */ Picture toPicture() const; + /*! + * Create an attribute holding a Guid. + */ + static Attribute fromGuid(const ByteVector &guid); + /*! * Returns the language number, or 0 is no stream number was set. */ diff --git a/taglib/asf/asftag.cpp b/taglib/asf/asftag.cpp index 1cf8489f..9fd95964 100644 --- a/taglib/asf/asftag.cpp +++ b/taglib/asf/asftag.cpp @@ -26,6 +26,7 @@ #include "asftag.h" #include +#include #include #include "tpropertymap.h" @@ -228,71 +229,139 @@ bool ASF::Tag::isEmpty() const namespace { constexpr std::array keyTranslation { - std::pair("WM/AlbumTitle", "ALBUM"), - std::pair("WM/AlbumArtist", "ALBUMARTIST"), - std::pair("WM/AuthorURL", "ARTISTWEBPAGE"), - std::pair("WM/Composer", "COMPOSER"), - std::pair("WM/Writer", "LYRICIST"), - std::pair("WM/Conductor", "CONDUCTOR"), - std::pair("WM/ModifiedBy", "REMIXER"), - std::pair("WM/Year", "DATE"), - std::pair("WM/OriginalAlbumTitle", "ORIGINALALBUM"), - std::pair("WM/OriginalArtist", "ORIGINALARTIST"), - std::pair("WM/OriginalFilename", "ORIGINALFILENAME"), - std::pair("WM/OriginalLyricist", "ORIGINALLYRICIST"), - std::pair("WM/OriginalReleaseYear", "ORIGINALDATE"), - std::pair("WM/Producer", "PRODUCER"), - std::pair("WM/ContentGroupDescription", "WORK"), - std::pair("WM/SubTitle", "SUBTITLE"), - std::pair("WM/SetSubTitle", "DISCSUBTITLE"), - std::pair("WM/TrackNumber", "TRACKNUMBER"), - std::pair("WM/PartOfSet", "DISCNUMBER"), - std::pair("WM/Genre", "GENRE"), - std::pair("WM/BeatsPerMinute", "BPM"), - std::pair("WM/Mood", "MOOD"), - std::pair("WM/InitialKey", "INITIALKEY"), - std::pair("WM/ISRC", "ISRC"), - std::pair("WM/Lyrics", "LYRICS"), - std::pair("WM/Media", "MEDIA"), - std::pair("WM/Publisher", "LABEL"), - std::pair("WM/CatalogNo", "CATALOGNUMBER"), - std::pair("WM/Barcode", "BARCODE"), - std::pair("WM/EncodedBy", "ENCODEDBY"), - std::pair("WM/EncodingSettings", "ENCODING"), - std::pair("WM/EncodingTime", "ENCODINGTIME"), - std::pair("WM/AudioFileURL", "FILEWEBPAGE"), - std::pair("WM/AlbumSortOrder", "ALBUMSORT"), - std::pair("WM/AlbumArtistSortOrder", "ALBUMARTISTSORT"), - std::pair("WM/ArtistSortOrder", "ARTISTSORT"), - std::pair("WM/TitleSortOrder", "TITLESORT"), - std::pair("WM/Script", "SCRIPT"), - std::pair("WM/Language", "LANGUAGE"), - std::pair("WM/ARTISTS", "ARTISTS"), - std::pair("ASIN", "ASIN"), - std::pair("MusicBrainz/Track Id", "MUSICBRAINZ_TRACKID"), - std::pair("MusicBrainz/Artist Id", "MUSICBRAINZ_ARTISTID"), - std::pair("MusicBrainz/Album Id", "MUSICBRAINZ_ALBUMID"), - std::pair("MusicBrainz/Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID"), - std::pair("MusicBrainz/Album Release Country", "RELEASECOUNTRY"), - std::pair("MusicBrainz/Album Status", "RELEASESTATUS"), - std::pair("MusicBrainz/Album Type", "RELEASETYPE"), - std::pair("MusicBrainz/Release Group Id", "MUSICBRAINZ_RELEASEGROUPID"), - std::pair("MusicBrainz/Release Track Id", "MUSICBRAINZ_RELEASETRACKID"), - std::pair("MusicBrainz/Work Id", "MUSICBRAINZ_WORKID"), - std::pair("MusicIP/PUID", "MUSICIP_PUID"), - std::pair("Acoustid/Id", "ACOUSTID_ID"), - std::pair("Acoustid/Fingerprint", "ACOUSTID_FINGERPRINT"), + std::tuple("WM/AlbumTitle", "ALBUM", ASF::Attribute::UnicodeType), + std::tuple("WM/AlbumArtist", "ALBUMARTIST", ASF::Attribute::UnicodeType), + std::tuple("WM/AuthorURL", "ARTISTWEBPAGE", ASF::Attribute::UnicodeType), + std::tuple("WM/Composer", "COMPOSER", ASF::Attribute::UnicodeType), + std::tuple("WM/Writer", "LYRICIST", ASF::Attribute::UnicodeType), + std::tuple("WM/Conductor", "CONDUCTOR", ASF::Attribute::UnicodeType), + std::tuple("WM/ModifiedBy", "REMIXER", ASF::Attribute::UnicodeType), + std::tuple("WM/Year", "DATE", ASF::Attribute::UnicodeType), + std::tuple("WM/OriginalAlbumTitle", "ORIGINALALBUM", ASF::Attribute::UnicodeType), + std::tuple("WM/OriginalArtist", "ORIGINALARTIST", ASF::Attribute::UnicodeType), + std::tuple("WM/OriginalFilename", "ORIGINALFILENAME", ASF::Attribute::UnicodeType), + std::tuple("WM/OriginalLyricist", "ORIGINALLYRICIST", ASF::Attribute::UnicodeType), + std::tuple("WM/OriginalReleaseYear", "ORIGINALDATE", ASF::Attribute::UnicodeType), + std::tuple("WM/Producer", "PRODUCER", ASF::Attribute::UnicodeType), + std::tuple("WM/ContentGroupDescription", "WORK", ASF::Attribute::UnicodeType), + std::tuple("WM/SubTitle", "SUBTITLE", ASF::Attribute::UnicodeType), + std::tuple("WM/SetSubTitle", "DISCSUBTITLE", ASF::Attribute::UnicodeType), + std::tuple("WM/TrackNumber", "TRACKNUMBER", ASF::Attribute::UnicodeType), + std::tuple("WM/PartOfSet", "DISCNUMBER", ASF::Attribute::UnicodeType), + std::tuple("WM/Genre", "GENRE", ASF::Attribute::UnicodeType), + std::tuple("WM/BeatsPerMinute", "BPM", ASF::Attribute::UnicodeType), + std::tuple("WM/Mood", "MOOD", ASF::Attribute::UnicodeType), + std::tuple("WM/InitialKey", "INITIALKEY", ASF::Attribute::UnicodeType), + std::tuple("WM/ISRC", "ISRC", ASF::Attribute::UnicodeType), + std::tuple("WM/Lyrics", "LYRICS", ASF::Attribute::UnicodeType), + std::tuple("WM/Media", "MEDIA", ASF::Attribute::UnicodeType), + std::tuple("WM/Publisher", "LABEL", ASF::Attribute::UnicodeType), + std::tuple("WM/CatalogNo", "CATALOGNUMBER", ASF::Attribute::UnicodeType), + std::tuple("WM/Barcode", "BARCODE", ASF::Attribute::UnicodeType), + std::tuple("WM/EncodedBy", "ENCODEDBY", ASF::Attribute::UnicodeType), + std::tuple("WM/EncodingSettings", "ENCODING", ASF::Attribute::UnicodeType), + std::tuple("WM/EncodingTime", "ENCODINGTIME", ASF::Attribute::QWordType), + std::tuple("WM/AudioFileURL", "FILEWEBPAGE", ASF::Attribute::UnicodeType), + std::tuple("WM/AlbumSortOrder", "ALBUMSORT", ASF::Attribute::UnicodeType), + std::tuple("WM/AlbumArtistSortOrder", "ALBUMARTISTSORT", ASF::Attribute::UnicodeType), + std::tuple("WM/ArtistSortOrder", "ARTISTSORT", ASF::Attribute::UnicodeType), + std::tuple("WM/TitleSortOrder", "TITLESORT", ASF::Attribute::UnicodeType), + std::tuple("WM/Script", "SCRIPT", ASF::Attribute::UnicodeType), + std::tuple("WM/Language", "LANGUAGE", ASF::Attribute::UnicodeType), + std::tuple("WM/ARTISTS", "ARTISTS", ASF::Attribute::UnicodeType), + std::tuple("ASIN", "ASIN", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Track Id", "MUSICBRAINZ_TRACKID", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Artist Id", "MUSICBRAINZ_ARTISTID", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Album Id", "MUSICBRAINZ_ALBUMID", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Album Release Country", "RELEASECOUNTRY", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Album Status", "RELEASESTATUS", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Album Type", "RELEASETYPE", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Release Group Id", "MUSICBRAINZ_RELEASEGROUPID", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Release Track Id", "MUSICBRAINZ_RELEASETRACKID", ASF::Attribute::UnicodeType), + std::tuple("MusicBrainz/Work Id", "MUSICBRAINZ_WORKID", ASF::Attribute::UnicodeType), + std::tuple("MusicIP/PUID", "MUSICIP_PUID", ASF::Attribute::UnicodeType), + std::tuple("Acoustid/Id", "ACOUSTID_ID", ASF::Attribute::UnicodeType), + std::tuple("Acoustid/Fingerprint", "ACOUSTID_FINGERPRINT", ASF::Attribute::UnicodeType), + std::tuple("replaygain_track_gain", "REPLAYGAIN_TRACK_GAIN", ASF::Attribute::UnicodeType), + std::tuple("replaygain_track_peak", "REPLAYGAIN_TRACK_PEAK", ASF::Attribute::UnicodeType), + std::tuple("replaygain_album_gain", "REPLAYGAIN_ALBUM_GAIN", ASF::Attribute::UnicodeType), + std::tuple("replaygain_album_peak", "REPLAYGAIN_ALBUM_PEAK", ASF::Attribute::UnicodeType), + std::tuple("WM/MediaClassPrimaryID", "MEDIACLASSPRIMARYID", ASF::Attribute::GuidType), + std::tuple("WM/MediaClassSecondaryID", "MEDIACLASSSECONDARYID", ASF::Attribute::GuidType), + std::tuple("WM/WMCollectionGroupID", "COLLECTIONGROUPID", ASF::Attribute::GuidType), + std::tuple("WM/WMCollectionID", "COLLECTIONID", ASF::Attribute::GuidType), + std::tuple("WM/WMContentID", "CONTENTID", ASF::Attribute::GuidType), + std::tuple("WM/ContentDistributor", "CONTENTDISTRIBUTOR", ASF::Attribute::UnicodeType), + std::tuple("WM/ParentalRating", "PARENTALRATING", ASF::Attribute::UnicodeType), + std::tuple("WM/Period", "PERIOD", ASF::Attribute::UnicodeType), + std::tuple("WM/PromotionURL", "PROMOTIONURL", ASF::Attribute::UnicodeType), + std::tuple("WM/ToolName", "TOOLNAME", ASF::Attribute::UnicodeType), + std::tuple("WM/ToolVersion", "TOOLVERSION", ASF::Attribute::UnicodeType), + std::tuple("WM/Provider", "PROVIDER", ASF::Attribute::UnicodeType), + std::tuple("WM/UniqueFileIdentifier", "UNIQUEFILEIDENTIFIER", ASF::Attribute::UnicodeType), + std::tuple("WMFSDKVersion", "WMFSDKVERSION", ASF::Attribute::UnicodeType), + std::tuple("WMFSDKNeeded", "WMFSDKNEEDED", ASF::Attribute::UnicodeType), + std::tuple("DeviceConformanceTemplate", "DEVICECONFORMANCETEMPLATE", ASF::Attribute::UnicodeType), + std::tuple("MediaFoundationVersion", "MEDIAFOUNDATIONVERSION", ASF::Attribute::UnicodeType), + std::tuple("IsVBR", "ISVBR", ASF::Attribute::BoolType), + std::tuple("PeakValue", "PEAKVALUE", ASF::Attribute::DWordType), + std::tuple("AverageLevel", "AVERAGELEVEL", ASF::Attribute::DWordType), }; + // Attribute names are matched case-insensitively; taggers disagree on the + // casing of names not defined by Windows Media (e.g. replaygain_track_gain). String translateKey(const String &key) { - for(const auto &[k, t] : keyTranslation) { - if(key == k) - return t; + const String upperKey = key.upper(); + for(const auto &[k, v, t] : keyTranslation) { + if(upperKey == String(k).upper()) + return v; } return String(); } + + void eraseAttribute(ASF::AttributeListMap &attributeListMap, const String &name) + { + const String upperName = name.upper(); + StringList keys; + for(const auto &[k, attributes] : std::as_const(attributeListMap)) { + if(k.upper() == upperName) + keys.append(k); + } + for(const auto &k : keys) + attributeListMap.erase(k); + } + + String attributeToString(const ASF::Attribute &attr) + { + switch(attr.type()) { + case ASF::Attribute::WordType: + case ASF::Attribute::DWordType: + case ASF::Attribute::QWordType: + case ASF::Attribute::BoolType: + return String(std::to_string(attr.toULongLong())); + case ASF::Attribute::GuidType: + if(const ByteVector data = attr.toByteVector(); data.size() == 16) { + String str; + for(int i = 0; i < 16; ++i) { + if(i == 4 || i == 6 || i == 8 || i == 10) { + str += '-'; + } + const auto c = static_cast(data[i]); + unsigned char d = c >> 4; + str += static_cast(d >= 10 ? d - 10 + 'A' : d + '0'); + d = c & 0x0f; + str += static_cast(d >= 10 ? d - 10 + 'A' : d + '0'); + } + return str; + } + return {}; + default: + return attr.toString(); + } + } } // namespace PropertyMap ASF::Tag::properties() const @@ -315,14 +384,12 @@ PropertyMap ASF::Tag::properties() const for(const auto &[k, attributes] : std::as_const(d->attributeListMap)) { if(const String key = translateKey(k); !key.isEmpty()) { for(const auto &attr : attributes) { - if(key == "TRACKNUMBER") { - if(attr.type() == ASF::Attribute::DWordType) - props.insert(key, String::number(attr.toUInt())); - else - props.insert(key, attr.toString()); - } - else { - props.insert(key, attr.toString()); + // The same attribute can occur in both the extended content + // description object and the metadata (library) object, skip exact + // duplicates (e.g. a second identical IsVBR). + if(const String value = attributeToString(attr); + !props.value(key).contains(value)) { + props.insert(key, value); } } } @@ -341,10 +408,10 @@ void ASF::Tag::removeUnsupportedProperties(const StringList &props) PropertyMap ASF::Tag::setProperties(const PropertyMap &props) { - static const Map reverseKeyMap = [] { - Map map; - for(const auto &[k, t] : keyTranslation) { - map[t] = k; + static const Map> reverseKeyMap = [] { + Map> map; + for(const auto &[k, v, t] : keyTranslation) { + map[v] = {k, t}; } return map; }(); @@ -365,7 +432,7 @@ PropertyMap ASF::Tag::setProperties(const PropertyMap &props) d->copyright.clear(); } else { - d->attributeListMap.erase(reverseKeyMap[prop]); + eraseAttribute(d->attributeListMap, reverseKeyMap[prop].first); } } } @@ -373,10 +440,51 @@ PropertyMap ASF::Tag::setProperties(const PropertyMap &props) PropertyMap ignoredProps; for(const auto &[prop, attributes] : props) { if(reverseKeyMap.contains(prop)) { - String name = reverseKeyMap[prop]; - removeItem(name); - for(const auto &attr : attributes) { - addAttribute(name, attr); + const auto &[name, type] = reverseKeyMap[prop]; + eraseAttribute(d->attributeListMap, name); + for(const auto &str : attributes) { + switch(type) { + case Attribute::WordType: + addAttribute(name, static_cast(str.toULongLong())); + break; + case Attribute::DWordType: + addAttribute(name, static_cast(str.toULongLong())); + break; + case Attribute::QWordType: + addAttribute(name, str.toULongLong()); + break; + case Attribute::BoolType: + addAttribute(name, !str.isEmpty() && str != "0" && str.upper() != "TRUE"); + break; + case Attribute::GuidType: { + ByteVector data; + String hexStr; + for(wchar_t c : str) { + if(c >= 'a' && c <= 'f') { + hexStr += static_cast(c + 'A' - 'a'); + } else if((c >= 'A' && c <= 'F') || (c >= '0' && c <= '9')) { + hexStr += c; + } + } + if(hexStr.length() == 32) { + unsigned char buf[16]; + unsigned char* bufPtr = buf; + for(int i = 0; i < 32;) { + auto h = static_cast(hexStr[i++]); + auto l = static_cast(hexStr[i++]); + *bufPtr++ = static_cast( + ((h >= 'A' ? h + 10 - 'A' : h - '0') << 4) | + (l >= 'A' ? l + 10 - 'A' : l - '0')); + } + data = ByteVector(reinterpret_cast(buf), 16); + } + addAttribute(name, Attribute::fromGuid(data)); + break; + } + default: + addAttribute(name, str); + break; + } } } else if(prop == "TITLE") { diff --git a/tests/data/real_example.wma b/tests/data/real_example.wma new file mode 100644 index 00000000..9a08a0ea Binary files /dev/null and b/tests/data/real_example.wma differ diff --git a/tests/test_asf.cpp b/tests/test_asf.cpp index e48e1ced..94088d40 100644 --- a/tests/test_asf.cpp +++ b/tests/test_asf.cpp @@ -52,6 +52,8 @@ class TestASF : public CppUnit::TestFixture CPPUNIT_TEST(testSaveMultiplePictures); CPPUNIT_TEST(testProperties); CPPUNIT_TEST(testPropertiesAllSupported); + CPPUNIT_TEST(testPropertiesRealFile); + CPPUNIT_TEST(testCaseInsensitiveAttributeNames); CPPUNIT_TEST(testRepeatedSave); CPPUNIT_TEST_SUITE_END(); @@ -331,7 +333,7 @@ public: tags["DISCSUBTITLE"] = StringList("Disc Subtitle"); tags["ENCODEDBY"] = StringList("Encoded by"); tags["ENCODING"] = StringList("Encoding"); - tags["ENCODINGTIME"] = StringList("2021-01-03 11:52:19"); + tags["ENCODINGTIME"] = StringList("131452413620000000"); tags["FILEWEBPAGE"] = StringList("File Webpage"); tags["GENRE"] = StringList("Genre"); tags["WORK"] = StringList("Grouping"); @@ -366,6 +368,30 @@ public: tags["TITLE"] = StringList("Title"); tags["TITLESORT"] = StringList("Title Sort"); tags["TRACKNUMBER"] = StringList("2/4"); + tags["REPLAYGAIN_TRACK_GAIN"] = StringList("-4.25 dB"); + tags["REPLAYGAIN_TRACK_PEAK"] = StringList("0.985412"); + tags["REPLAYGAIN_ALBUM_GAIN"] = StringList("-3.80 dB"); + tags["REPLAYGAIN_ALBUM_PEAK"] = StringList("0.998201"); + tags["MEDIACLASSPRIMARYID"] = StringList("D1607DBC-E323-4BE2-86A1-48A42A28441E"); + tags["MEDIACLASSSECONDARYID"] = StringList("F24FF731-96FC-4D0F-A2F5-5A3483682B1A"); + tags["COLLECTIONGROUPID"] = StringList("A81F04B3-2B1B-4B52-8700-6F8091DF3B41"); + tags["COLLECTIONID"] = StringList("3F2504E0-4F89-11D3-9A0C-0305E82C3301"); + tags["CONTENTID"] = StringList("6B29FC40-CA47-1067-B31D-00DD010662DA"); + tags["CONTENTDISTRIBUTOR"] = StringList("Universal Music Group"); + tags["PARENTALRATING"] = StringList("Explicit"); + tags["PERIOD"] = StringList("Baroque"); + tags["PROMOTIONURL"] = StringList("https://www.artistwebsite.com/store/album-special-edition"); + tags["TOOLNAME"] = StringList("Windows Media Encoder"); + tags["TOOLVERSION"] = StringList("9.00.00.2980"); + tags["PROVIDER"] = StringList("AMG"); + tags["UNIQUEFILEIDENTIFIER"] = StringList("AMGI_0000000000012345"); + tags["WMFSDKVERSION"] = StringList("12.0.19041.1"); + tags["WMFSDKNEEDED"] = StringList("9.0.0.4503"); + tags["DEVICECONFORMANCETEMPLATE"] = StringList("L1"); + tags["MEDIAFOUNDATIONVERSION"] = StringList("2.0"); + tags["ISVBR"] = StringList("1"); + tags["PEAKVALUE"] = StringList("32104"); + tags["AVERAGELEVEL"] = StringList("2450"); ScopedFileCopy copy("silence-1", ".wma"); { @@ -385,10 +411,168 @@ public: { const ASF::File f(copy.fileName().c_str()); PropertyMap properties = f.properties(); - if (tags != properties) { + if(tags != properties) { CPPUNIT_ASSERT_EQUAL(tags.toString(), properties.toString()); } CPPUNIT_ASSERT(tags == properties); + + const ASF::Tag *asfTag = f.tag(); + CPPUNIT_ASSERT(asfTag); + std::map expected = { + {"ASIN", ASF::Attribute::UnicodeType}, + {"Acoustid/Fingerprint", ASF::Attribute::UnicodeType}, + {"Acoustid/Id", ASF::Attribute::UnicodeType}, + {"AverageLevel", ASF::Attribute::DWordType}, + {"DeviceConformanceTemplate", ASF::Attribute::UnicodeType}, + {"IsVBR", ASF::Attribute::BoolType}, + {"MediaFoundationVersion", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Album Artist Id", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Album Id", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Album Release Country", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Album Status", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Album Type", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Artist Id", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Release Group Id", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Release Track Id", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Track Id", ASF::Attribute::UnicodeType}, + {"MusicBrainz/Work Id", ASF::Attribute::UnicodeType}, + {"MusicIP/PUID", ASF::Attribute::UnicodeType}, + {"PeakValue", ASF::Attribute::DWordType}, + {"WM/ARTISTS", ASF::Attribute::UnicodeType}, + {"WM/AlbumArtist", ASF::Attribute::UnicodeType}, + {"WM/AlbumArtistSortOrder", ASF::Attribute::UnicodeType}, + {"WM/AlbumSortOrder", ASF::Attribute::UnicodeType}, + {"WM/AlbumTitle", ASF::Attribute::UnicodeType}, + {"WM/ArtistSortOrder", ASF::Attribute::UnicodeType}, + {"WM/AudioFileURL", ASF::Attribute::UnicodeType}, + {"WM/AuthorURL", ASF::Attribute::UnicodeType}, + {"WM/Barcode", ASF::Attribute::UnicodeType}, + {"WM/BeatsPerMinute", ASF::Attribute::UnicodeType}, + {"WM/CatalogNo", ASF::Attribute::UnicodeType}, + {"WM/Composer", ASF::Attribute::UnicodeType}, + {"WM/Conductor", ASF::Attribute::UnicodeType}, + {"WM/ContentDistributor", ASF::Attribute::UnicodeType}, + {"WM/ContentGroupDescription", ASF::Attribute::UnicodeType}, + {"WM/EncodedBy", ASF::Attribute::UnicodeType}, + {"WM/EncodingSettings", ASF::Attribute::UnicodeType}, + {"WM/EncodingTime", ASF::Attribute::QWordType}, + {"WM/Genre", ASF::Attribute::UnicodeType}, + {"WM/ISRC", ASF::Attribute::UnicodeType}, + {"WM/InitialKey", ASF::Attribute::UnicodeType}, + {"WM/Language", ASF::Attribute::UnicodeType}, + {"WM/Lyrics", ASF::Attribute::UnicodeType}, + {"WM/Media", ASF::Attribute::UnicodeType}, + {"WM/MediaClassPrimaryID", ASF::Attribute::GuidType}, + {"WM/MediaClassSecondaryID", ASF::Attribute::GuidType}, + {"WM/ModifiedBy", ASF::Attribute::UnicodeType}, + {"WM/Mood", ASF::Attribute::UnicodeType}, + {"WM/OriginalAlbumTitle", ASF::Attribute::UnicodeType}, + {"WM/OriginalArtist", ASF::Attribute::UnicodeType}, + {"WM/OriginalFilename", ASF::Attribute::UnicodeType}, + {"WM/OriginalLyricist", ASF::Attribute::UnicodeType}, + {"WM/OriginalReleaseYear", ASF::Attribute::UnicodeType}, + {"WM/ParentalRating", ASF::Attribute::UnicodeType}, + {"WM/PartOfSet", ASF::Attribute::UnicodeType}, + {"WM/Period", ASF::Attribute::UnicodeType}, + {"WM/Producer", ASF::Attribute::UnicodeType}, + {"WM/PromotionURL", ASF::Attribute::UnicodeType}, + {"WM/Provider", ASF::Attribute::UnicodeType}, + {"WM/Publisher", ASF::Attribute::UnicodeType}, + {"WM/Script", ASF::Attribute::UnicodeType}, + {"WM/SetSubTitle", ASF::Attribute::UnicodeType}, + {"WM/SubTitle", ASF::Attribute::UnicodeType}, + {"WM/TitleSortOrder", ASF::Attribute::UnicodeType}, + {"WM/ToolName", ASF::Attribute::UnicodeType}, + {"WM/ToolVersion", ASF::Attribute::UnicodeType}, + {"WM/TrackNumber", ASF::Attribute::UnicodeType}, + {"WM/UniqueFileIdentifier", ASF::Attribute::UnicodeType}, + {"WM/WMCollectionGroupID", ASF::Attribute::GuidType}, + {"WM/WMCollectionID", ASF::Attribute::GuidType}, + {"WM/WMContentID", ASF::Attribute::GuidType}, + {"WM/Writer", ASF::Attribute::UnicodeType}, + {"WM/Year", ASF::Attribute::UnicodeType}, + {"WMFSDKNeeded", ASF::Attribute::UnicodeType}, + {"WMFSDKVersion", ASF::Attribute::UnicodeType}, + {"replaygain_album_gain", ASF::Attribute::UnicodeType}, + {"replaygain_album_peak", ASF::Attribute::UnicodeType}, + {"replaygain_track_gain", ASF::Attribute::UnicodeType}, + {"replaygain_track_peak", ASF::Attribute::UnicodeType}, + }; + std::map actual; + for(const auto &[name, attr] : asfTag->attributeListMap()) { + if(!attr.isEmpty()) { + actual[name] = attr[0].type(); + } + } + if(expected != actual) { + auto mapToStrings = [](const std::map &map){ + StringList result; + for(const auto &[name, attr] : map) { + result.append(name + ":" + String::number(attr)); + } + return result; + }; + const StringList expectedStrs = mapToStrings(expected); + const StringList actualStrs = mapToStrings(actual); + CPPUNIT_ASSERT_EQUAL(expectedStrs, actualStrs); + } + CPPUNIT_ASSERT(expected == actual); + } + } + + void testPropertiesRealFile() + { + ASF::File f(TEST_FILE_PATH_C("real_example.wma")); + + PropertyMap tags = f.properties(); + + CPPUNIT_ASSERT_EQUAL(StringList("Wake Up, Get Up, Get Out There"), tags["TITLE"]); + CPPUNIT_ASSERT_EQUAL(StringList("Shoji Meguro"), tags["ARTIST"]); + CPPUNIT_ASSERT_EQUAL(StringList("Persona 5: Sounds of Rebellion"), tags["ALBUM"]); + CPPUNIT_ASSERT_EQUAL(StringList("-8.27 dB"), tags["REPLAYGAIN_TRACK_GAIN"]); + CPPUNIT_ASSERT_EQUAL(StringList("1.000000"), tags["REPLAYGAIN_TRACK_PEAK"]); + CPPUNIT_ASSERT_EQUAL(StringList("131452413620000000"), tags["ENCODINGTIME"]); + CPPUNIT_ASSERT_EQUAL(StringList("32673"), tags["PEAKVALUE"]); + CPPUNIT_ASSERT_EQUAL(StringList("8315"), tags["AVERAGELEVEL"]); + CPPUNIT_ASSERT_EQUAL(StringList("0"), tags["ISVBR"]); + CPPUNIT_ASSERT_EQUAL(StringList("12.0.15063.332"), tags["WMFSDKVERSION"]); + CPPUNIT_ASSERT_EQUAL(StringList("0.0.0.0000"), tags["WMFSDKNEEDED"]); + CPPUNIT_ASSERT_EQUAL(StringList("User Feedback"), tags["PROVIDER"]); + CPPUNIT_ASSERT_EQUAL(StringList("L1"), tags["DEVICECONFORMANCETEMPLATE"]); + CPPUNIT_ASSERT_EQUAL(StringList("2.112"), tags["MEDIAFOUNDATIONVERSION"]); + CPPUNIT_ASSERT_EQUAL(StringList("1"), tags["TRACKNUMBER"]); + CPPUNIT_ASSERT_EQUAL(StringList("Alternative"), tags["GENRE"]); + CPPUNIT_ASSERT_EQUAL(StringList("Shoji Meguro"), tags["ALBUMARTIST"]); + CPPUNIT_ASSERT_EQUAL(StringList("00000000-0000-0000-0000-000000000000"), tags["COLLECTIONGROUPID"]); + CPPUNIT_ASSERT_EQUAL(StringList("00000000-0000-0000-0000-000000000000"), tags["COLLECTIONID"]); + CPPUNIT_ASSERT_EQUAL(StringList("00000000-0000-0000-0000-000000000000"), tags["CONTENTID"]); + CPPUNIT_ASSERT_EQUAL(StringList("BC7D60D1-23E3-E24B-86A1-48A42A28441E"), tags["MEDIACLASSPRIMARYID"]); + CPPUNIT_ASSERT_EQUAL(StringList("00000000-0000-0000-0000-000000000000"), tags["MEDIACLASSSECONDARYID"]); + CPPUNIT_ASSERT_EQUAL(StringList(";"), tags["UNIQUEFILEIDENTIFIER"]); + } + + void testCaseInsensitiveAttributeNames() + { + ScopedFileCopy copy("silence-1", ".wma"); + { + ASF::File f(copy.fileName().c_str()); + f.tag()->setAttribute("REPLAYGAIN_TRACK_GAIN", String("-6.00 dB")); + f.save(); + } + { + ASF::File f(copy.fileName().c_str()); + PropertyMap tags = f.properties(); + CPPUNIT_ASSERT_EQUAL(StringList("-6.00 dB"), tags["REPLAYGAIN_TRACK_GAIN"]); + + // Replacing the value must not leave a second, differently cased + // attribute behind. + tags["REPLAYGAIN_TRACK_GAIN"] = StringList("-7.00 dB"); + f.setProperties(tags); + CPPUNIT_ASSERT(!f.tag()->contains("REPLAYGAIN_TRACK_GAIN")); + CPPUNIT_ASSERT_EQUAL(String("-7.00 dB"), + f.tag()->attribute("replaygain_track_gain").front().toString()); + tags = f.properties(); + CPPUNIT_ASSERT_EQUAL(StringList("-7.00 dB"), tags["REPLAYGAIN_TRACK_GAIN"]); } } diff --git a/tests/test_fileref_detect.cpp b/tests/test_fileref_detect.cpp index a5e74804..d84ff61a 100644 --- a/tests/test_fileref_detect.cpp +++ b/tests/test_fileref_detect.cpp @@ -224,6 +224,7 @@ class TestFileRefDetectByContent : public CppUnit::TestFixture #ifdef TAGLIB_WITH_ASF CPPUNIT_TEST(testLosslessWma); + CPPUNIT_TEST(testRealExampleWma); CPPUNIT_TEST(testSilence1Wma); #endif @@ -414,6 +415,7 @@ public: #ifdef TAGLIB_WITH_ASF // -- ASF::File -- void testLosslessWma() { detectByContent("lossless.wma"); } + void testRealExampleWma() { detectByContent("real_example.wma"); } void testSilence1Wma() { detectByContent("silence-1.wma"); } #endif