diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index 911ce058..59f6527e 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -126,6 +126,7 @@ set(tag_HDRS mp4/mp4item.h mp4/mp4properties.h mp4/mp4coverart.h + mp4/mp4itemfactory.h mod/modfilebase.h mod/modfile.h mod/modtag.h @@ -221,6 +222,7 @@ set(mp4_SRCS mp4/mp4item.cpp mp4/mp4properties.cpp mp4/mp4coverart.cpp + mp4/mp4itemfactory.cpp ) set(ape_SRCS diff --git a/taglib/mp4/mp4file.cpp b/taglib/mp4/mp4file.cpp index 02353718..f8e87fdb 100644 --- a/taglib/mp4/mp4file.cpp +++ b/taglib/mp4/mp4file.cpp @@ -29,6 +29,8 @@ #include "tpropertymap.h" #include "tagutils.h" +#include "mp4itemfactory.h" + using namespace TagLib; namespace @@ -43,6 +45,15 @@ namespace class MP4::File::FilePrivate { public: + FilePrivate(MP4::ItemFactory *mp4ItemFactory) + : itemFactory(mp4ItemFactory ? mp4ItemFactory + : MP4::ItemFactory::instance()) + { + } + + ~FilePrivate() = default; + + const ItemFactory *itemFactory; std::unique_ptr tag; std::unique_ptr atoms; std::unique_ptr properties; @@ -64,17 +75,19 @@ bool MP4::File::isSupported(IOStream *stream) // public members //////////////////////////////////////////////////////////////////////////////// -MP4::File::File(FileName file, bool readProperties, AudioProperties::ReadStyle) : +MP4::File::File(FileName file, bool readProperties, AudioProperties::ReadStyle, + ItemFactory *itemFactory) : TagLib::File(file), - d(std::make_unique()) + d(std::make_unique(itemFactory)) { if(isOpen()) read(readProperties); } -MP4::File::File(IOStream *stream, bool readProperties, AudioProperties::ReadStyle) : +MP4::File::File(IOStream *stream, bool readProperties, AudioProperties::ReadStyle, + ItemFactory *itemFactory) : TagLib::File(stream), - d(std::make_unique()) + d(std::make_unique(itemFactory)) { if(isOpen()) read(readProperties); @@ -125,7 +138,7 @@ MP4::File::read(bool readProperties) return; } - d->tag = std::make_unique(this, d->atoms.get()); + d->tag = std::make_unique(this, d->atoms.get(), d->itemFactory); if(readProperties) { d->properties = std::make_unique(this, d->atoms.get()); } diff --git a/taglib/mp4/mp4file.h b/taglib/mp4/mp4file.h index 76cd95cf..ad708909 100644 --- a/taglib/mp4/mp4file.h +++ b/taglib/mp4/mp4file.h @@ -36,6 +36,8 @@ namespace TagLib { //! An implementation of MP4 (AAC, ALAC, ...) metadata namespace MP4 { class Atoms; + class ItemFactory; + /*! * This implements and provides an interface for MP4 files to the @@ -64,9 +66,12 @@ namespace TagLib { * file's audio properties will also be read. * * \note In the current implementation, \a propertiesStyle is ignored. + * + * The items will be created using \a itemFactory (default if null). */ File(FileName file, bool readProperties = true, - Properties::ReadStyle audioPropertiesStyle = Properties::Average); + Properties::ReadStyle audioPropertiesStyle = Properties::Average, + ItemFactory *itemFactory = nullptr); /*! * Constructs an MP4 file from \a stream. If \a readProperties is true the @@ -76,9 +81,12 @@ namespace TagLib { * responsible for deleting it after the File object. * * \note In the current implementation, \a propertiesStyle is ignored. + * + * The items will be created using \a itemFactory (default if null). */ File(IOStream *stream, bool readProperties = true, - Properties::ReadStyle audioPropertiesStyle = Properties::Average); + Properties::ReadStyle audioPropertiesStyle = Properties::Average, + ItemFactory *itemFactory = nullptr); /*! * Destroys this instance of the File. diff --git a/taglib/mp4/mp4item.h b/taglib/mp4/mp4item.h index b54985be..1c6de582 100644 --- a/taglib/mp4/mp4item.h +++ b/taglib/mp4/mp4item.h @@ -83,6 +83,8 @@ namespace TagLib { class ItemPrivate; std::shared_ptr d; }; + + using ItemMap = TagLib::Map; } // namespace MP4 } // namespace TagLib #endif diff --git a/taglib/mp4/mp4itemfactory.cpp b/taglib/mp4/mp4itemfactory.cpp new file mode 100644 index 00000000..06127c8b --- /dev/null +++ b/taglib/mp4/mp4itemfactory.cpp @@ -0,0 +1,776 @@ +/*************************************************************************** + copyright : (C) 2023 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * 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 * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "mp4itemfactory.h" + +#include + +#include "tbytevector.h" +#include "tdebug.h" + +#include "id3v1genres.h" + +using namespace TagLib; +using namespace MP4; + +class ItemFactory::ItemFactoryPrivate +{ +public: + NameHandlerMap handlerTypeForName; + Map propertyKeyForName; + Map nameForPropertyKey; +}; + +ItemFactory ItemFactory::factory; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ItemFactory *ItemFactory::instance() +{ + return &factory; +} + +std::pair ItemFactory::parseItem( + const Atom *atom, const ByteVector &data) const +{ + auto handlerType = handlerTypeForName(atom->name); + switch(handlerType) { + case ItemHandlerType::Unknown: + break; + case ItemHandlerType::FreeForm: + return parseFreeForm(atom, data); + case ItemHandlerType::IntPair: + case ItemHandlerType::IntPairNoTrailing: + return parseIntPair(atom, data); + case ItemHandlerType::Bool: + return parseBool(atom, data); + case ItemHandlerType::Int: + return parseInt(atom, data); + case ItemHandlerType::TextOrInt: + return parseTextOrInt(atom, data); + case ItemHandlerType::UInt: + return parseUInt(atom, data); + case ItemHandlerType::LongLong: + return parseLongLong(atom, data); + case ItemHandlerType::Byte: + return parseByte(atom, data); + case ItemHandlerType::Gnre: + return parseGnre(atom, data); + case ItemHandlerType::Covr: + return parseCovr(atom, data); + case ItemHandlerType::TextImplicit: + return parseText(atom, data, -1); + case ItemHandlerType::Text: + return parseText(atom, data); + } + return {atom->name, Item()}; +} + +ByteVector ItemFactory::renderItem( + const String &itemName, const Item &item) const +{ + if(itemName.startsWith("----")) { + return renderFreeForm(itemName, item); + } + else { + const ByteVector name = itemName.data(String::Latin1); + auto handlerType = handlerTypeForName(name); + switch(handlerType) { + case ItemHandlerType::Unknown: + debug("MP4: Unknown item name \"" + name + "\""); + break; + case ItemHandlerType::FreeForm: + return renderFreeForm(name, item); + case ItemHandlerType::IntPair: + return renderIntPair(name, item); + case ItemHandlerType::IntPairNoTrailing: + return renderIntPairNoTrailing(name, item); + case ItemHandlerType::Bool: + return renderBool(name, item); + case ItemHandlerType::Int: + return renderInt(name, item); + case ItemHandlerType::TextOrInt: + return renderTextOrInt(name, item); + case ItemHandlerType::UInt: + return renderUInt(name, item); + case ItemHandlerType::LongLong: + return renderLongLong(name, item); + case ItemHandlerType::Byte: + return renderByte(name, item); + case ItemHandlerType::Gnre: + return renderInt(name, item); + case ItemHandlerType::Covr: + return renderCovr(name, item); + case ItemHandlerType::TextImplicit: + return renderText(name, item, TypeImplicit); + case ItemHandlerType::Text: + return renderText(name, item); + } + } + return ByteVector(); +} + +std::pair ItemFactory::itemFromProperty( + const String &key, const StringList &values) const +{ + ByteVector name = nameForPropertyKey(key); + if(!name.isEmpty()) { + if(values.isEmpty()) { + return {name, values}; + } + auto handlerType = name.startsWith("----") + ? ItemHandlerType::FreeForm + : handlerTypeForName(name); + switch(handlerType) { + case ItemHandlerType::IntPair: + case ItemHandlerType::IntPairNoTrailing: + if(StringList parts = StringList::split(values.front(), "/"); + !parts.isEmpty()) { + int first = parts[0].toInt(); + int second = 0; + if(parts.size() > 1) { + second = parts[1].toInt(); + } + return {name, Item(first, second)}; + } + break; + case ItemHandlerType::Int: + case ItemHandlerType::Gnre: + return {name, Item(values.front().toInt())}; + case ItemHandlerType::UInt: + return {name, Item(static_cast(values.front().toInt()))}; + case ItemHandlerType::LongLong: + return {name, Item(static_cast(values.front().toInt()))}; + case ItemHandlerType::Byte: + return {name, Item(static_cast(values.front().toInt()))}; + case ItemHandlerType::Bool: + return {name, Item(values.front().toInt() != 0)}; + case ItemHandlerType::FreeForm: + case ItemHandlerType::TextOrInt: + case ItemHandlerType::TextImplicit: + case ItemHandlerType::Text: + return {name, values}; + + case ItemHandlerType::Covr: + debug("MP4: Invalid item \"" + name + "\" for property"); + break; + case ItemHandlerType::Unknown: + debug("MP4: Unknown item name \"" + name + "\" for property"); + break; + } + } + return {name, Item()}; +} + +std::pair ItemFactory::itemToProperty( + const ByteVector &itemName, const Item &item) const +{ + const String key = propertyKeyForName(itemName); + if(!key.isEmpty()) { + auto handlerType = itemName.startsWith("----") + ? ItemHandlerType::FreeForm + : handlerTypeForName(itemName); + switch(handlerType) { + case ItemHandlerType::IntPair: + case ItemHandlerType::IntPairNoTrailing: + { + auto [vn, tn] = item.toIntPair(); + String value = String::number(vn); + if(tn) { + value += "/" + String::number(tn); + } + return {key, value}; + } + case ItemHandlerType::Int: + case ItemHandlerType::Gnre: + return {key, String::number(item.toInt())}; + case ItemHandlerType::UInt: + return {key, String::number(item.toUInt())}; + case ItemHandlerType::LongLong: + return {key, String::number(item.toLongLong())}; + case ItemHandlerType::Byte: + return {key, String::number(item.toByte())}; + case ItemHandlerType::Bool: + return {key, String::number(item.toBool())}; + case ItemHandlerType::FreeForm: + case ItemHandlerType::TextOrInt: + case ItemHandlerType::TextImplicit: + case ItemHandlerType::Text: + return {key, item.toStringList()}; + + case ItemHandlerType::Covr: + debug("MP4: Invalid item \"" + itemName + "\" for property"); + break; + case ItemHandlerType::Unknown: + debug("MP4: Unknown item name \"" + itemName + "\" for property"); + break; + } + } + return {String(), StringList()}; +} + +String ItemFactory::propertyKeyForName(const ByteVector &name) const +{ + if(d->propertyKeyForName.isEmpty()) { + d->propertyKeyForName = namePropertyMap(); + } + return d->propertyKeyForName.value(name); +} + +ByteVector ItemFactory::nameForPropertyKey(const String &key) const +{ + if(d->nameForPropertyKey.isEmpty()) { + if(d->propertyKeyForName.isEmpty()) { + d->propertyKeyForName = namePropertyMap(); + } + for(const auto &[k, t] : std::as_const(d->propertyKeyForName)) { + d->nameForPropertyKey[t] = k; + } + } + return d->nameForPropertyKey.value(key); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +ItemFactory::ItemFactory() : + d(std::make_unique()) +{ +} + +ItemFactory::~ItemFactory() = default; + +ItemFactory::NameHandlerMap ItemFactory::nameHandlerMap() const +{ + return { + {"----", ItemHandlerType::FreeForm}, + {"trkn", ItemHandlerType::IntPair}, + {"disk", ItemHandlerType::IntPairNoTrailing}, + {"cpil", ItemHandlerType::Bool}, + {"pgap", ItemHandlerType::Bool}, + {"pcst", ItemHandlerType::Bool}, + {"shwm", ItemHandlerType::Bool}, + {"tmpo", ItemHandlerType::Int}, + {"\251mvi", ItemHandlerType::Int}, + {"\251mvc", ItemHandlerType::Int}, + {"hdvd", ItemHandlerType::Int}, + {"rate", ItemHandlerType::TextOrInt}, + {"tvsn", ItemHandlerType::UInt}, + {"tves", ItemHandlerType::UInt}, + {"cnID", ItemHandlerType::UInt}, + {"sfID", ItemHandlerType::UInt}, + {"atID", ItemHandlerType::UInt}, + {"geID", ItemHandlerType::UInt}, + {"cmID", ItemHandlerType::UInt}, + {"plID", ItemHandlerType::LongLong}, + {"stik", ItemHandlerType::Byte}, + {"rtng", ItemHandlerType::Byte}, + {"akID", ItemHandlerType::Byte}, + {"gnre", ItemHandlerType::Gnre}, + {"covr", ItemHandlerType::Covr}, + {"purl", ItemHandlerType::TextImplicit}, + {"egid", ItemHandlerType::TextImplicit}, + }; +} + +ItemFactory::ItemHandlerType ItemFactory::handlerTypeForName( + const ByteVector &name) const +{ + if(d->handlerTypeForName.isEmpty()) { + d->handlerTypeForName = nameHandlerMap(); + } + auto type = d->handlerTypeForName.value(name, ItemHandlerType::Unknown); + if (type == ItemHandlerType::Unknown && name.size() == 4) { + type = ItemHandlerType::Text; + } + return type; +} + +Map ItemFactory::namePropertyMap() const +{ + return { + {"\251nam", "TITLE"}, + {"\251ART", "ARTIST"}, + {"\251alb", "ALBUM"}, + {"\251cmt", "COMMENT"}, + {"\251gen", "GENRE"}, + {"\251day", "DATE"}, + {"\251wrt", "COMPOSER"}, + {"\251grp", "GROUPING"}, + {"aART", "ALBUMARTIST"}, + {"trkn", "TRACKNUMBER"}, + {"disk", "DISCNUMBER"}, + {"cpil", "COMPILATION"}, + {"tmpo", "BPM"}, + {"cprt", "COPYRIGHT"}, + {"\251lyr", "LYRICS"}, + {"\251too", "ENCODEDBY"}, + {"soal", "ALBUMSORT"}, + {"soaa", "ALBUMARTISTSORT"}, + {"soar", "ARTISTSORT"}, + {"sonm", "TITLESORT"}, + {"soco", "COMPOSERSORT"}, + {"sosn", "SHOWSORT"}, + {"shwm", "SHOWWORKMOVEMENT"}, + {"pgap", "GAPLESSPLAYBACK"}, + {"pcst", "PODCAST"}, + {"catg", "PODCASTCATEGORY"}, + {"desc", "PODCASTDESC"}, + {"egid", "PODCASTID"}, + {"purl", "PODCASTURL"}, + {"tves", "TVEPISODE"}, + {"tven", "TVEPISODEID"}, + {"tvnn", "TVNETWORK"}, + {"tvsn", "TVSEASON"}, + {"tvsh", "TVSHOW"}, + {"\251wrk", "WORK"}, + {"\251mvn", "MOVEMENTNAME"}, + {"\251mvi", "MOVEMENTNUMBER"}, + {"\251mvc", "MOVEMENTCOUNT"}, + {"----:com.apple.iTunes:MusicBrainz Track Id", "MUSICBRAINZ_TRACKID"}, + {"----:com.apple.iTunes:MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID"}, + {"----:com.apple.iTunes:MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID"}, + {"----:com.apple.iTunes:MusicBrainz Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID"}, + {"----:com.apple.iTunes:MusicBrainz Release Group Id", "MUSICBRAINZ_RELEASEGROUPID"}, + {"----:com.apple.iTunes:MusicBrainz Release Track Id", "MUSICBRAINZ_RELEASETRACKID"}, + {"----:com.apple.iTunes:MusicBrainz Work Id", "MUSICBRAINZ_WORKID"}, + {"----:com.apple.iTunes:MusicBrainz Album Release Country", "RELEASECOUNTRY"}, + {"----:com.apple.iTunes:MusicBrainz Album Status", "RELEASESTATUS"}, + {"----:com.apple.iTunes:MusicBrainz Album Type", "RELEASETYPE"}, + {"----:com.apple.iTunes:ARTISTS", "ARTISTS"}, + {"----:com.apple.iTunes:originaldate", "ORIGINALDATE"}, + {"----:com.apple.iTunes:ASIN", "ASIN"}, + {"----:com.apple.iTunes:LABEL", "LABEL"}, + {"----:com.apple.iTunes:LYRICIST", "LYRICIST"}, + {"----:com.apple.iTunes:CONDUCTOR", "CONDUCTOR"}, + {"----:com.apple.iTunes:REMIXER", "REMIXER"}, + {"----:com.apple.iTunes:ENGINEER", "ENGINEER"}, + {"----:com.apple.iTunes:PRODUCER", "PRODUCER"}, + {"----:com.apple.iTunes:DJMIXER", "DJMIXER"}, + {"----:com.apple.iTunes:MIXER", "MIXER"}, + {"----:com.apple.iTunes:SUBTITLE", "SUBTITLE"}, + {"----:com.apple.iTunes:DISCSUBTITLE", "DISCSUBTITLE"}, + {"----:com.apple.iTunes:MOOD", "MOOD"}, + {"----:com.apple.iTunes:ISRC", "ISRC"}, + {"----:com.apple.iTunes:CATALOGNUMBER", "CATALOGNUMBER"}, + {"----:com.apple.iTunes:BARCODE", "BARCODE"}, + {"----:com.apple.iTunes:SCRIPT", "SCRIPT"}, + {"----:com.apple.iTunes:LANGUAGE", "LANGUAGE"}, + {"----:com.apple.iTunes:LICENSE", "LICENSE"}, + {"----:com.apple.iTunes:MEDIA", "MEDIA"} + }; +} + +MP4::AtomDataList ItemFactory::parseData2( + const MP4::Atom *atom, const ByteVector &data, int expectedFlags, + bool freeForm) +{ + AtomDataList result; + int i = 0; + unsigned int pos = 0; + while(pos < data.size()) { + const auto length = static_cast(data.toUInt(pos)); + if(length < 12) { + debug("MP4: Too short atom"); + return result; + } + + const ByteVector name = data.mid(pos + 4, 4); + const auto flags = static_cast(data.toUInt(pos + 8)); + if(freeForm && i < 2) { + if(i == 0 && name != "mean") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\""); + return result; + } + if(i == 1 && name != "name") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\""); + return result; + } + result.append(AtomData(static_cast(flags), + data.mid(pos + 12, length - 12))); + } + else { + if(name != "data") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); + return result; + } + if(expectedFlags == -1 || flags == expectedFlags) { + result.append(AtomData(static_cast(flags), + data.mid(pos + 16, length - 16))); + } + } + pos += length; + i++; + } + return result; +} + +ByteVectorList ItemFactory::parseData( + const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags, + bool freeForm) +{ + const AtomDataList data = parseData2(atom, bytes, expectedFlags, freeForm); + ByteVectorList result; + for(const auto &atom : data) { + result.append(atom.data); + } + return result; +} + +std::pair ItemFactory::parseInt( + const MP4::Atom *atom, const ByteVector &bytes) +{ + ByteVectorList data = parseData(atom, bytes); + return { + atom->name, + !data.isEmpty() ? Item(static_cast(data[0].toShort())) : Item() + }; +} + +std::pair ItemFactory::parseTextOrInt( + const MP4::Atom *atom, const ByteVector &bytes) +{ + AtomDataList data = parseData2(atom, bytes); + if(!data.isEmpty()) { + AtomData val = data[0]; + return { + atom->name, + val.type == TypeUTF8 ? Item(StringList(String(val.data, String::UTF8))) + : Item(static_cast(val.data.toShort())) + }; + } + return {atom->name, Item()}; +} + +std::pair ItemFactory::parseUInt( + const MP4::Atom *atom, const ByteVector &bytes) +{ + ByteVectorList data = parseData(atom, bytes); + return { + atom->name, + !data.isEmpty() ? Item(data[0].toUInt()) : Item() + }; +} + +std::pair ItemFactory::parseLongLong( + const MP4::Atom *atom, const ByteVector &bytes) +{ + ByteVectorList data = parseData(atom, bytes); + return { + atom->name, + !data.isEmpty() ?Item (data[0].toLongLong()) : Item() + }; +} + +std::pair ItemFactory::parseByte( + const MP4::Atom *atom, const ByteVector &bytes) +{ + ByteVectorList data = parseData(atom, bytes); + return { + atom->name, + !data.isEmpty() ? Item(static_cast(data[0].at(0))) : Item() + }; +} + +std::pair ItemFactory::parseGnre( + const MP4::Atom *atom, const ByteVector &bytes) +{ + ByteVectorList data = parseData(atom, bytes); + if(!data.isEmpty()) { + int idx = static_cast(data[0].toShort()); + if(idx > 0) { + return { + "\251gen", + Item(StringList(ID3v1::genre(idx - 1))) + }; + } + } + return {"\251gen", Item()}; +} + +std::pair ItemFactory::parseIntPair( + const MP4::Atom *atom, const ByteVector &bytes) +{ + ByteVectorList data = parseData(atom, bytes); + if(!data.isEmpty()) { + const int a = data[0].toShort(2U); + const int b = data[0].toShort(4U); + return {atom->name, Item(a, b)}; + } + return {atom->name, Item()}; +} + +std::pair ItemFactory::parseBool( + const MP4::Atom *atom, const ByteVector &bytes) +{ + ByteVectorList data = parseData(atom, bytes); + if(!data.isEmpty()) { + bool value = !data[0].isEmpty() && data[0][0] != '\0'; + return {atom->name, Item(value)}; + } + return {atom->name, Item()}; +} + +std::pair ItemFactory::parseText( + const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags) +{ + const ByteVectorList data = parseData(atom, bytes, expectedFlags); + if(!data.isEmpty()) { + StringList value; + for(const auto &byte : data) { + value.append(String(byte, String::UTF8)); + } + return {atom->name, Item(value)}; + } + return {atom->name, Item()}; +} + +std::pair ItemFactory::parseFreeForm( + const MP4::Atom *atom, const ByteVector &bytes) +{ + const AtomDataList data = parseData2(atom, bytes, -1, true); + if(data.size() > 2) { + auto itBegin = data.begin(); + + String name = "----:"; + name += String((itBegin++)->data, String::UTF8); // data[0].data + name += ':'; + name += String((itBegin++)->data, String::UTF8); // data[1].data + + AtomDataType type = itBegin->type; // data[2].type + + for(auto it = itBegin; it != data.end(); ++it) { + if(it->type != type) { + debug("MP4: We currently don't support values with multiple types"); + break; + } + } + if(type == TypeUTF8) { + StringList value; + for(auto it = itBegin; it != data.end(); ++it) { + value.append(String(it->data, String::UTF8)); + } + Item item(value); + item.setAtomDataType(type); + return {name, item}; + } + else { + ByteVectorList value; + for(auto it = itBegin; it != data.end(); ++it) { + value.append(it->data); + } + Item item(value); + item.setAtomDataType(type); + return {name, item}; + } + } + return {atom->name, Item()}; +} + +std::pair ItemFactory::parseCovr( + const MP4::Atom *atom, const ByteVector &data) +{ + MP4::CoverArtList value; + unsigned int pos = 0; + while(pos < data.size()) { + const int length = static_cast(data.toUInt(pos)); + if(length < 12) { + debug("MP4: Too short atom"); + break; + } + + const ByteVector name = data.mid(pos + 4, 4); + const int flags = static_cast(data.toUInt(pos + 8)); + if(name != "data") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); + break; + } + if(flags == TypeJPEG || flags == TypePNG || flags == TypeBMP || + flags == TypeGIF || flags == TypeImplicit) { + value.append(MP4::CoverArt(static_cast(flags), + data.mid(pos + 16, length - 16))); + } + else { + debug("MP4: Unknown covr format " + String::number(flags)); + } + pos += length; + } + return { + atom->name, + !value.isEmpty() ? Item(value) : Item() + }; +} + + +ByteVector ItemFactory::renderAtom( + const ByteVector &name, const ByteVector &data) +{ + return ByteVector::fromUInt(data.size() + 8) + name + data; +} + +ByteVector ItemFactory::renderData( + const ByteVector &name, int flags, const ByteVectorList &data) +{ + ByteVector result; + for(const auto &byte : data) { + result.append(renderAtom("data", ByteVector::fromUInt(flags) + + ByteVector(4, '\0') + byte)); + } + return renderAtom(name, result); +} + +ByteVector ItemFactory::renderBool( + const ByteVector &name, const MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(1, item.toBool() ? '\1' : '\0')); + return renderData(name, TypeInteger, data); +} + +ByteVector ItemFactory::renderInt( + const ByteVector &name, const MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector::fromShort(item.toInt())); + return renderData(name, TypeInteger, data); +} + +ByteVector ItemFactory::renderTextOrInt( + const ByteVector &name, const MP4::Item &item) +{ + StringList value = item.toStringList(); + return value.isEmpty() ? renderInt(name, item) : renderText(name, item); +} + +ByteVector ItemFactory::renderUInt( + const ByteVector &name, const MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector::fromUInt(item.toUInt())); + return renderData(name, TypeInteger, data); +} + +ByteVector ItemFactory::renderLongLong( + const ByteVector &name, const MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector::fromLongLong(item.toLongLong())); + return renderData(name, TypeInteger, data); +} + +ByteVector ItemFactory::renderByte( + const ByteVector &name, const MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(1, item.toByte())); + return renderData(name, TypeInteger, data); +} + +ByteVector ItemFactory::renderIntPair( + const ByteVector &name, const MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(2, '\0') + + ByteVector::fromShort(item.toIntPair().first) + + ByteVector::fromShort(item.toIntPair().second) + + ByteVector(2, '\0')); + return renderData(name, TypeImplicit, data); +} + +ByteVector ItemFactory::renderIntPairNoTrailing( + const ByteVector &name, const MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(2, '\0') + + ByteVector::fromShort(item.toIntPair().first) + + ByteVector::fromShort(item.toIntPair().second)); + return renderData(name, TypeImplicit, data); +} + +ByteVector ItemFactory::renderText( + const ByteVector &name, const MP4::Item &item, int flags) +{ + ByteVectorList data; + const StringList values = item.toStringList(); + for(const auto &value : values) { + data.append(value.data(String::UTF8)); + } + return renderData(name, flags, data); +} + +ByteVector ItemFactory::renderCovr( + const ByteVector &name, const MP4::Item &item) +{ + ByteVector data; + const MP4::CoverArtList values = item.toCoverArtList(); + for(const auto &value : values) { + data.append(renderAtom("data", ByteVector::fromUInt(value.format()) + + ByteVector(4, '\0') + value.data())); + } + return renderAtom(name, data); +} + +ByteVector ItemFactory::renderFreeForm( + const String &name, const MP4::Item &item) +{ + StringList header = StringList::split(name, ":"); + if(header.size() != 3) { + debug("MP4: Invalid free-form item name \"" + name + "\""); + return ByteVector(); + } + ByteVector data; + data.append(renderAtom("mean", ByteVector::fromUInt(0) + + header[1].data(String::UTF8))); + data.append(renderAtom("name", ByteVector::fromUInt(0) + + header[2].data(String::UTF8))); + AtomDataType type = item.atomDataType(); + if(type == TypeUndefined) { + if(!item.toStringList().isEmpty()) { + type = TypeUTF8; + } + else { + type = TypeImplicit; + } + } + if(type == TypeUTF8) { + const StringList values = item.toStringList(); + for(const auto &value : values) { + data.append(renderAtom("data", ByteVector::fromUInt(type) + + ByteVector(4, '\0') + + value.data(String::UTF8))); + } + } + else { + const ByteVectorList values = item.toByteVectorList(); + for(const auto &value : values) { + data.append(renderAtom("data", ByteVector::fromUInt(type) + + ByteVector(4, '\0') + value)); + } + } + return renderAtom("----", data); +} diff --git a/taglib/mp4/mp4itemfactory.h b/taglib/mp4/mp4itemfactory.h new file mode 100644 index 00000000..e908b347 --- /dev/null +++ b/taglib/mp4/mp4itemfactory.h @@ -0,0 +1,256 @@ +/*************************************************************************** + copyright : (C) 2023 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * 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 * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4ITEMFACTORY_H +#define TAGLIB_MP4ITEMFACTORY_H + +#include +#include "taglib_export.h" +#include "mp4item.h" + +namespace TagLib { + + namespace MP4 { + + //! A factory for creating MP4 items during parsing + + /*! + * This factory abstracts away the parsing and rendering between atom data + * and MP4 items. + * + * Reimplementing this factory is the key to adding support for atom types + * not directly supported by TagLib to your application. To do so you would + * subclass this factory and reimplement nameHandlerMap() to add support + * for new atoms. If the new atoms do not have the same behavior as + * other supported atoms, it may be necessary to reimplement parseItem() and + * renderItem(). Then by setting your factory in the MP4::Tag constructor + * you can implement behavior that will allow for new atom types to be used. + * + * A custom item factory adding support for a "tsti" integer atom and a + * "tstt" text atom can be implemented like this: + * + * \code + * class CustomItemFactory : public MP4::ItemFactory { + * protected: + * NameHandlerMap nameHandlerMap() const override + * { + * return MP4::ItemFactory::nameHandlerMap() + * .insert("tsti", ItemHandlerType::Int) + * .insert("tstt", ItemHandlerType::Text); + * } + * }; + * \endcode + * + * If the custom item shall also be accessible via a property, + * namePropertyMap() can be overridden in the same way. + */ + class TAGLIB_EXPORT ItemFactory + { + public: + ItemFactory(const ItemFactory &) = delete; + ItemFactory &operator=(const ItemFactory &) = delete; + + static ItemFactory *instance(); + + /*! + * Create an MP4 item from the \a data bytes of an \a atom. + * Returns the name (in most cases atom->name) and an item, + * an invalid item on failure. + * The default implementation uses the map returned by nameHandlerMap(). + */ + virtual std::pair parseItem( + const Atom *atom, const ByteVector &data) const; + + /*! + * Render an MP4 \a item to the data bytes of an atom \a itemName. + * An empty byte vector is returned if the item is invalid or unknown. + * The default implementation uses the map returned by nameHandlerMap(). + */ + virtual ByteVector renderItem( + const String &itemName, const Item &item) const; + + /*! + * Create an MP4 item from a property with \a key and \a values. + * If the property is not supported, an invalid item is returned. + * The default implementation uses the map returned by namePropertyMap(). + */ + virtual std::pair itemFromProperty( + const String &key, const StringList &values) const; + + /*! + * Get an MP4 item as a property. + * If no property exists for \a itemName, an empty string is returned as + * the property key. + * The default implementation uses the map returned by namePropertyMap(). + */ + virtual std::pair itemToProperty( + const ByteVector &itemName, const Item &item) const; + + /*! + * Returns property key for atom \a name, empty if no property exists for + * this atom. + * The default method looks up the map created by namePropertyMap() and + * should be enough for most uses. + */ + virtual String propertyKeyForName(const ByteVector &name) const; + + /*! + * Returns atom name for property \a key, empty if no property is + * supported for this key. + * The default method uses the reverse mapping of propertyKeyForName() + * and should be enough for most uses. + */ + virtual ByteVector nameForPropertyKey(const String &key) const; + + protected: + /*! + * Type that determines the parsing and rendering between the data and + * the item representation of an atom. + */ + enum class ItemHandlerType { + Unknown, + FreeForm, + IntPair, + IntPairNoTrailing, + Bool, + Int, + TextOrInt, + UInt, + LongLong, + Byte, + Gnre, + Covr, + TextImplicit, + Text + }; + + /*! Mapping of atom name to handler type. */ + using NameHandlerMap = Map; + + /*! + * Constructs an item factory. Because this is a singleton this method is + * protected, but may be used for subclasses. + */ + ItemFactory(); + + /*! + * Destroys the frame factory. + */ + virtual ~ItemFactory(); + + /*! + * Returns mapping between atom names and handler types. + * This method is called once by handlerTypeForName() to initialize its + * internal cache. + * To add support for a new atom, it is sufficient in most cases to just + * reimplement this method and add new entries. + */ + virtual NameHandlerMap nameHandlerMap() const; + + /*! + * Returns handler type for atom \a name. + * The default method looks up the map created by nameHandlerMap() and + * should be enough for most uses. + */ + virtual ItemHandlerType handlerTypeForName(const ByteVector &name) const; + + /*! + * Returns mapping between atom names and property keys. + * This method is called once by propertyKeyForName() to initialize its + * internal cache. + * To add support for a new atom with a property, it is sufficient in most + * cases to just reimplement this method and add new entries. + */ + virtual Map namePropertyMap() const; + + // Functions used by parseItem() to create items from atom data. + static MP4::AtomDataList parseData2( + const MP4::Atom *atom, const ByteVector &data, int expectedFlags = -1, + bool freeForm = false); + static ByteVectorList parseData( + const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags = -1, + bool freeForm = false); + static std::pair parseText( + const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags = 1); + static std::pair parseFreeForm( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseInt( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseByte( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseTextOrInt( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseUInt( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseLongLong( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseGnre( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseIntPair( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseBool( + const MP4::Atom *atom, const ByteVector &bytes); + static std::pair parseCovr( + const MP4::Atom *atom, const ByteVector &data); + + // Functions used by renderItem() to render atom data for items. + static ByteVector renderAtom( + const ByteVector &name, const ByteVector &data); + static ByteVector renderData( + const ByteVector &name, int flags, const ByteVectorList &data); + static ByteVector renderText( + const ByteVector &name, const MP4::Item &item, int flags = TypeUTF8); + static ByteVector renderFreeForm( + const String &name, const MP4::Item &item); + static ByteVector renderBool( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderInt( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderTextOrInt( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderByte( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderUInt( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderLongLong( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderIntPair( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderIntPairNoTrailing( + const ByteVector &name, const MP4::Item &item); + static ByteVector renderCovr( + const ByteVector &name, const MP4::Item &item); + + private: + static ItemFactory factory; + + class ItemFactoryPrivate; + std::unique_ptr d; + }; + + } // namespace MP4 +} // namespace TagLib + +#endif diff --git a/taglib/mp4/mp4tag.cpp b/taglib/mp4/mp4tag.cpp index ce69e018..eef637a0 100644 --- a/taglib/mp4/mp4tag.cpp +++ b/taglib/mp4/mp4tag.cpp @@ -30,7 +30,7 @@ #include "tdebug.h" #include "tpropertymap.h" -#include "id3v1genres.h" +#include "mp4itemfactory.h" #include "mp4atom.h" #include "mp4coverart.h" @@ -39,18 +39,28 @@ using namespace TagLib; class MP4::Tag::TagPrivate { public: + TagPrivate(const ItemFactory *itemFactory) : + factory(itemFactory ? itemFactory + : ItemFactory::instance()) + { + } + + ~TagPrivate() = default; + + const ItemFactory *factory; TagLib::File *file { nullptr }; Atoms *atoms { nullptr }; ItemMap items; }; MP4::Tag::Tag() : - d(std::make_unique()) + d(std::make_unique(ItemFactory::instance())) { } -MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) : - d(std::make_unique()) +MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms, + const MP4::ItemFactory *factory) : + d(std::make_unique(factory)) { d->file = file; d->atoms = atoms; @@ -63,268 +73,16 @@ MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) : for(const auto &atom : std::as_const(ilst->children)) { file->seek(atom->offset + 8); - if(atom->name == "----") { - parseFreeForm(atom); - } - else if(atom->name == "trkn" || atom->name == "disk") { - parseIntPair(atom); - } - else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst" || - atom->name == "shwm") { - parseBool(atom); - } - else if(atom->name == "tmpo" || atom->name == "\251mvi" || atom->name == "\251mvc" || - atom->name == "hdvd") { - parseInt(atom); - } - else if(atom->name == "rate") { - AtomDataList data = parseData2(atom); - if(!data.isEmpty()) { - AtomData val = data[0]; - if (val.type == TypeUTF8) { - addItem(atom->name, StringList(String(val.data, String::UTF8))); - } else { - addItem(atom->name, static_cast(val.data.toShort())); - } - } - } - else if(atom->name == "tvsn" || atom->name == "tves" || atom->name == "cnID" || - atom->name == "sfID" || atom->name == "atID" || atom->name == "geID" || - atom->name == "cmID") { - parseUInt(atom); - } - else if(atom->name == "plID") { - parseLongLong(atom); - } - else if(atom->name == "stik" || atom->name == "rtng" || atom->name == "akID") { - parseByte(atom); - } - else if(atom->name == "gnre") { - parseGnre(atom); - } - else if(atom->name == "covr") { - parseCovr(atom); - } - else if(atom->name == "purl" || atom->name == "egid") { - parseText(atom, -1); - } - else { - parseText(atom); + ByteVector data = d->file->readBlock(atom->length - 8); + const auto &[name, item] = d->factory->parseItem(atom, data); + if (item.isValid()) { + addItem(name, item); } } } MP4::Tag::~Tag() = default; -MP4::AtomDataList -MP4::Tag::parseData2(const MP4::Atom *atom, int expectedFlags, bool freeForm) -{ - AtomDataList result; - ByteVector data = d->file->readBlock(atom->length - 8); - int i = 0; - unsigned int pos = 0; - while(pos < data.size()) { - const auto length = static_cast(data.toUInt(pos)); - if(length < 12) { - debug("MP4: Too short atom"); - return result; - } - - const ByteVector name = data.mid(pos + 4, 4); - const auto flags = static_cast(data.toUInt(pos + 8)); - if(freeForm && i < 2) { - if(i == 0 && name != "mean") { - debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\""); - return result; - } - if(i == 1 && name != "name") { - debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\""); - return result; - } - result.append(AtomData(static_cast(flags), data.mid(pos + 12, length - 12))); - } - else { - if(name != "data") { - debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); - return result; - } - if(expectedFlags == -1 || flags == expectedFlags) { - result.append(AtomData(static_cast(flags), data.mid(pos + 16, length - 16))); - } - } - pos += length; - i++; - } - return result; -} - -ByteVectorList -MP4::Tag::parseData(const MP4::Atom *atom, int expectedFlags, bool freeForm) -{ - const AtomDataList data = parseData2(atom, expectedFlags, freeForm); - ByteVectorList result; - for(const auto &atom : data) { - result.append(atom.data); - } - return result; -} - -void -MP4::Tag::parseInt(const MP4::Atom *atom) -{ - ByteVectorList data = parseData(atom); - if(!data.isEmpty()) { - addItem(atom->name, static_cast(data[0].toShort())); - } -} - -void -MP4::Tag::parseUInt(const MP4::Atom *atom) -{ - ByteVectorList data = parseData(atom); - if(!data.isEmpty()) { - addItem(atom->name, data[0].toUInt()); - } -} - -void -MP4::Tag::parseLongLong(const MP4::Atom *atom) -{ - ByteVectorList data = parseData(atom); - if(!data.isEmpty()) { - addItem(atom->name, data[0].toLongLong()); - } -} - -void -MP4::Tag::parseByte(const MP4::Atom *atom) -{ - ByteVectorList data = parseData(atom); - if(!data.isEmpty()) { - addItem(atom->name, static_cast(data[0].at(0))); - } -} - -void -MP4::Tag::parseGnre(const MP4::Atom *atom) -{ - ByteVectorList data = parseData(atom); - if(!data.isEmpty()) { - int idx = static_cast(data[0].toShort()); - if(idx > 0) { - addItem("\251gen", StringList(ID3v1::genre(idx - 1))); - } - } -} - -void -MP4::Tag::parseIntPair(const MP4::Atom *atom) -{ - ByteVectorList data = parseData(atom); - if(!data.isEmpty()) { - const int a = data[0].toShort(2U); - const int b = data[0].toShort(4U); - addItem(atom->name, MP4::Item(a, b)); - } -} - -void -MP4::Tag::parseBool(const MP4::Atom *atom) -{ - ByteVectorList data = parseData(atom); - if(!data.isEmpty()) { - bool value = !data[0].isEmpty() && data[0][0] != '\0'; - addItem(atom->name, value); - } -} - -void -MP4::Tag::parseText(const MP4::Atom *atom, int expectedFlags) -{ - const ByteVectorList data = parseData(atom, expectedFlags); - if(!data.isEmpty()) { - StringList value; - for(const auto &byte : data) { - value.append(String(byte, String::UTF8)); - } - addItem(atom->name, value); - } -} - -void -MP4::Tag::parseFreeForm(const MP4::Atom *atom) -{ - const AtomDataList data = parseData2(atom, -1, true); - if(data.size() > 2) { - auto itBegin = data.begin(); - - String name = "----:"; - name += String((itBegin++)->data, String::UTF8); // data[0].data - name += ':'; - name += String((itBegin++)->data, String::UTF8); // data[1].data - - AtomDataType type = itBegin->type; // data[2].type - - for(auto it = itBegin; it != data.end(); ++it) { - if(it->type != type) { - debug("MP4: We currently don't support values with multiple types"); - break; - } - } - if(type == TypeUTF8) { - StringList value; - for(auto it = itBegin; it != data.end(); ++it) { - value.append(String(it->data, String::UTF8)); - } - Item item(value); - item.setAtomDataType(type); - addItem(name, item); - } - else { - ByteVectorList value; - for(auto it = itBegin; it != data.end(); ++it) { - value.append(it->data); - } - Item item(value); - item.setAtomDataType(type); - addItem(name, item); - } - } -} - -void -MP4::Tag::parseCovr(const MP4::Atom *atom) -{ - MP4::CoverArtList value; - ByteVector data = d->file->readBlock(atom->length - 8); - unsigned int pos = 0; - while(pos < data.size()) { - const int length = static_cast(data.toUInt(pos)); - if(length < 12) { - debug("MP4: Too short atom"); - break; - } - - const ByteVector name = data.mid(pos + 4, 4); - const int flags = static_cast(data.toUInt(pos + 8)); - if(name != "data") { - debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); - break; - } - if(flags == TypeJPEG || flags == TypePNG || flags == TypeBMP || - flags == TypeGIF || flags == TypeImplicit) { - value.append(MP4::CoverArt(static_cast(flags), - data.mid(pos + 16, length - 16))); - } - else { - debug("MP4: Unknown covr format " + String::number(flags)); - } - pos += length; - } - if(!value.isEmpty()) - addItem(atom->name, value); -} - ByteVector MP4::Tag::padIlst(const ByteVector &data, int length) const { @@ -340,191 +98,12 @@ MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data) const return ByteVector::fromUInt(data.size() + 8) + name + data; } -ByteVector -MP4::Tag::renderData(const ByteVector &name, int flags, const ByteVectorList &data) const -{ - ByteVector result; - for(const auto &byte : data) { - result.append(renderAtom("data", ByteVector::fromUInt(flags) + ByteVector(4, '\0') + byte)); - } - return renderAtom(name, result); -} - -ByteVector -MP4::Tag::renderBool(const ByteVector &name, const MP4::Item &item) const -{ - ByteVectorList data; - data.append(ByteVector(1, item.toBool() ? '\1' : '\0')); - return renderData(name, TypeInteger, data); -} - -ByteVector -MP4::Tag::renderInt(const ByteVector &name, const MP4::Item &item) const -{ - ByteVectorList data; - data.append(ByteVector::fromShort(item.toInt())); - return renderData(name, TypeInteger, data); -} - -ByteVector -MP4::Tag::renderUInt(const ByteVector &name, const MP4::Item &item) const -{ - ByteVectorList data; - data.append(ByteVector::fromUInt(item.toUInt())); - return renderData(name, TypeInteger, data); -} - -ByteVector -MP4::Tag::renderLongLong(const ByteVector &name, const MP4::Item &item) const -{ - ByteVectorList data; - data.append(ByteVector::fromLongLong(item.toLongLong())); - return renderData(name, TypeInteger, data); -} - -ByteVector -MP4::Tag::renderByte(const ByteVector &name, const MP4::Item &item) const -{ - ByteVectorList data; - data.append(ByteVector(1, item.toByte())); - return renderData(name, TypeInteger, data); -} - -ByteVector -MP4::Tag::renderIntPair(const ByteVector &name, const MP4::Item &item) const -{ - ByteVectorList data; - data.append(ByteVector(2, '\0') + - ByteVector::fromShort(item.toIntPair().first) + - ByteVector::fromShort(item.toIntPair().second) + - ByteVector(2, '\0')); - return renderData(name, TypeImplicit, data); -} - -ByteVector -MP4::Tag::renderIntPairNoTrailing(const ByteVector &name, const MP4::Item &item) const -{ - ByteVectorList data; - data.append(ByteVector(2, '\0') + - ByteVector::fromShort(item.toIntPair().first) + - ByteVector::fromShort(item.toIntPair().second)); - return renderData(name, TypeImplicit, data); -} - -ByteVector -MP4::Tag::renderText(const ByteVector &name, const MP4::Item &item, int flags) const -{ - ByteVectorList data; - const StringList values = item.toStringList(); - for(const auto &value : values) { - data.append(value.data(String::UTF8)); - } - return renderData(name, flags, data); -} - -ByteVector -MP4::Tag::renderCovr(const ByteVector &name, const MP4::Item &item) const -{ - ByteVector data; - const MP4::CoverArtList values = item.toCoverArtList(); - for(const auto &value : values) { - data.append(renderAtom("data", ByteVector::fromUInt(value.format()) + - ByteVector(4, '\0') + value.data())); - } - return renderAtom(name, data); -} - -ByteVector -MP4::Tag::renderFreeForm(const String &name, const MP4::Item &item) const -{ - StringList header = StringList::split(name, ":"); - if(header.size() != 3) { - debug("MP4: Invalid free-form item name \"" + name + "\""); - return ByteVector(); - } - ByteVector data; - data.append(renderAtom("mean", ByteVector::fromUInt(0) + header[1].data(String::UTF8))); - data.append(renderAtom("name", ByteVector::fromUInt(0) + header[2].data(String::UTF8))); - AtomDataType type = item.atomDataType(); - if(type == TypeUndefined) { - if(!item.toStringList().isEmpty()) { - type = TypeUTF8; - } - else { - type = TypeImplicit; - } - } - if(type == TypeUTF8) { - const StringList values = item.toStringList(); - for(const auto &value : values) { - data.append(renderAtom("data", ByteVector::fromUInt(type) + - ByteVector(4, '\0') + value.data(String::UTF8))); - } - } - else { - const ByteVectorList values = item.toByteVectorList(); - for(const auto &value : values) { - data.append(renderAtom("data", ByteVector::fromUInt(type) + - ByteVector(4, '\0') + value)); - } - } - return renderAtom("----", data); -} - bool MP4::Tag::save() { ByteVector data; for(const auto &[name, item] : std::as_const(d->items)) { - if(name.startsWith("----")) { - data.append(renderFreeForm(name, item)); - } - else if(name == "trkn") { - data.append(renderIntPair(name.data(String::Latin1), item)); - } - else if(name == "disk") { - data.append(renderIntPairNoTrailing(name.data(String::Latin1), item)); - } - else if(name == "cpil" || name == "pgap" || name == "pcst" || - name == "shwm") { - data.append(renderBool(name.data(String::Latin1), item)); - } - else if(name == "tmpo" || name == "\251mvi" || name == "\251mvc" || - name == "hdvd") { - data.append(renderInt(name.data(String::Latin1), item)); - } - else if(name == "rate") { - StringList value = item.toStringList(); - if (value.isEmpty()) { - data.append(renderInt(name.data(String::Latin1), item)); - } - else { - data.append(renderText(name.data(String::Latin1), item)); - } - } - else if(name == "tvsn" || name == "tves" || name == "cnID" || - name == "sfID" || name == "atID" || name == "geID" || - name == "cmID") { - data.append(renderUInt(name.data(String::Latin1), item)); - } - else if(name == "plID") { - data.append(renderLongLong(name.data(String::Latin1), item)); - } - else if(name == "stik" || name == "rtng" || name == "akID") { - data.append(renderByte(name.data(String::Latin1), item)); - } - else if(name == "covr") { - data.append(renderCovr(name.data(String::Latin1), item)); - } - else if(name == "purl" || name == "egid") { - data.append(renderText(name.data(String::Latin1), item, TypeImplicit)); - } - else if(name.size() == 4){ - data.append(renderText(name.data(String::Latin1), item)); - } - else { - debug("MP4: Unknown item name \"" + name + "\""); - } + data.append(d->factory->renderItem(name, item)); } data = renderAtom("ilst", data); @@ -890,116 +469,13 @@ bool MP4::Tag::contains(const String &key) const return d->items.contains(key); } -namespace -{ - constexpr std::array keyTranslation { - std::pair("\251nam", "TITLE"), - std::pair("\251ART", "ARTIST"), - std::pair("\251alb", "ALBUM"), - std::pair("\251cmt", "COMMENT"), - std::pair("\251gen", "GENRE"), - std::pair("\251day", "DATE"), - std::pair("\251wrt", "COMPOSER"), - std::pair("\251grp", "GROUPING"), - std::pair("aART", "ALBUMARTIST"), - std::pair("trkn", "TRACKNUMBER"), - std::pair("disk", "DISCNUMBER"), - std::pair("cpil", "COMPILATION"), - std::pair("tmpo", "BPM"), - std::pair("cprt", "COPYRIGHT"), - std::pair("\251lyr", "LYRICS"), - std::pair("\251too", "ENCODEDBY"), - std::pair("soal", "ALBUMSORT"), - std::pair("soaa", "ALBUMARTISTSORT"), - std::pair("soar", "ARTISTSORT"), - std::pair("sonm", "TITLESORT"), - std::pair("soco", "COMPOSERSORT"), - std::pair("sosn", "SHOWSORT"), - std::pair("shwm", "SHOWWORKMOVEMENT"), - std::pair("pgap", "GAPLESSPLAYBACK"), - std::pair("pcst", "PODCAST"), - std::pair("catg", "PODCASTCATEGORY"), - std::pair("desc", "PODCASTDESC"), - std::pair("egid", "PODCASTID"), - std::pair("purl", "PODCASTURL"), - std::pair("tves", "TVEPISODE"), - std::pair("tven", "TVEPISODEID"), - std::pair("tvnn", "TVNETWORK"), - std::pair("tvsn", "TVSEASON"), - std::pair("tvsh", "TVSHOW"), - std::pair("\251wrk", "WORK"), - std::pair("\251mvn", "MOVEMENTNAME"), - std::pair("\251mvi", "MOVEMENTNUMBER"), - std::pair("\251mvc", "MOVEMENTCOUNT"), - std::pair("----:com.apple.iTunes:MusicBrainz Track Id", "MUSICBRAINZ_TRACKID"), - std::pair("----:com.apple.iTunes:MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID"), - std::pair("----:com.apple.iTunes:MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID"), - std::pair("----:com.apple.iTunes:MusicBrainz Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID"), - std::pair("----:com.apple.iTunes:MusicBrainz Release Group Id", "MUSICBRAINZ_RELEASEGROUPID"), - std::pair("----:com.apple.iTunes:MusicBrainz Release Track Id", "MUSICBRAINZ_RELEASETRACKID"), - std::pair("----:com.apple.iTunes:MusicBrainz Work Id", "MUSICBRAINZ_WORKID"), - std::pair("----:com.apple.iTunes:MusicBrainz Album Release Country", "RELEASECOUNTRY"), - std::pair("----:com.apple.iTunes:MusicBrainz Album Status", "RELEASESTATUS"), - std::pair("----:com.apple.iTunes:MusicBrainz Album Type", "RELEASETYPE"), - std::pair("----:com.apple.iTunes:ARTISTS", "ARTISTS"), - std::pair("----:com.apple.iTunes:originaldate", "ORIGINALDATE"), - std::pair("----:com.apple.iTunes:ASIN", "ASIN"), - std::pair("----:com.apple.iTunes:LABEL", "LABEL"), - std::pair("----:com.apple.iTunes:LYRICIST", "LYRICIST"), - std::pair("----:com.apple.iTunes:CONDUCTOR", "CONDUCTOR"), - std::pair("----:com.apple.iTunes:REMIXER", "REMIXER"), - std::pair("----:com.apple.iTunes:ENGINEER", "ENGINEER"), - std::pair("----:com.apple.iTunes:PRODUCER", "PRODUCER"), - std::pair("----:com.apple.iTunes:DJMIXER", "DJMIXER"), - std::pair("----:com.apple.iTunes:MIXER", "MIXER"), - std::pair("----:com.apple.iTunes:SUBTITLE", "SUBTITLE"), - std::pair("----:com.apple.iTunes:DISCSUBTITLE", "DISCSUBTITLE"), - std::pair("----:com.apple.iTunes:MOOD", "MOOD"), - std::pair("----:com.apple.iTunes:ISRC", "ISRC"), - std::pair("----:com.apple.iTunes:CATALOGNUMBER", "CATALOGNUMBER"), - std::pair("----:com.apple.iTunes:BARCODE", "BARCODE"), - std::pair("----:com.apple.iTunes:SCRIPT", "SCRIPT"), - std::pair("----:com.apple.iTunes:LANGUAGE", "LANGUAGE"), - std::pair("----:com.apple.iTunes:LICENSE", "LICENSE"), - std::pair("----:com.apple.iTunes:MEDIA", "MEDIA"), - }; - - String translateKey(const String &key) - { - for(const auto &[k, t] : keyTranslation) { - if(key == k) - return t; - } - - return String(); - } -} // namespace - PropertyMap MP4::Tag::properties() const { PropertyMap props; for(const auto &[k, t] : std::as_const(d->items)) { - const String key = translateKey(k); + auto [key, value] = d->factory->itemToProperty(k.data(String::Latin1), t); if(!key.isEmpty()) { - if(key == "TRACKNUMBER" || key == "DISCNUMBER") { - auto [vn, tn] = t.toIntPair(); - String value = String::number(vn); - if(tn) { - value += "/" + String::number(tn); - } - props[key] = value; - } - else if(key == "BPM" || key == "MOVEMENTNUMBER" || key == "MOVEMENTCOUNT" || - key == "TVEPISODE" || key == "TVSEASON") { - props[key] = String::number(t.toInt()); - } - else if(key == "COMPILATION" || key == "SHOWWORKMOVEMENT" || - key == "GAPLESSPLAYBACK" || key == "PODCAST") { - props[key] = String::number(t.toBool()); - } - else { - props[key] = t.toStringList(); - } + props[key] = value; } else { props.unsupportedData().append(k); @@ -1016,50 +492,18 @@ void MP4::Tag::removeUnsupportedProperties(const StringList &props) PropertyMap MP4::Tag::setProperties(const PropertyMap &props) { - static Map reverseKeyMap; - if(reverseKeyMap.isEmpty()) { - for(const auto &[k, t] : keyTranslation) { - reverseKeyMap[t] = k; - } - } - const PropertyMap origProps = properties(); for(const auto &[prop, _] : origProps) { if(!props.contains(prop) || props[prop].isEmpty()) { - d->items.erase(reverseKeyMap[prop]); + d->items.erase(d->factory->nameForPropertyKey(prop)); } } PropertyMap ignoredProps; for(const auto &[prop, val] : props) { - if(reverseKeyMap.contains(prop)) { - String name = reverseKeyMap[prop]; - if((prop == "TRACKNUMBER" || prop == "DISCNUMBER") && !val.isEmpty()) { - StringList parts = StringList::split(val.front(), "/"); - if(!parts.isEmpty()) { - int first = parts[0].toInt(); - int second = 0; - if(parts.size() > 1) { - second = parts[1].toInt(); - } - d->items[name] = MP4::Item(first, second); - } - } - else if((prop == "BPM" || prop == "MOVEMENTNUMBER" || - prop == "MOVEMENTCOUNT" || prop == "TVEPISODE" || - prop == "TVSEASON") && !val.isEmpty()) { - int value = val.front().toInt(); - d->items[name] = MP4::Item(value); - } - else if((prop == "COMPILATION" || prop == "SHOWWORKMOVEMENT" || - prop == "GAPLESSPLAYBACK" || prop == "PODCAST") && - !val.isEmpty()) { - bool value = (val.front().toInt() != 0); - d->items[name] = MP4::Item(value); - } - else { - d->items[name] = val; - } + auto [name, item] = d->factory->itemFromProperty(prop, val); + if(item.isValid()) { + d->items[name] = item; } else { ignoredProps.insert(prop, val); diff --git a/taglib/mp4/mp4tag.h b/taglib/mp4/mp4tag.h index 226ce019..e0e9f7ca 100644 --- a/taglib/mp4/mp4tag.h +++ b/taglib/mp4/mp4tag.h @@ -37,13 +37,15 @@ namespace TagLib { namespace MP4 { - using ItemMap = TagLib::Map; + + class ItemFactory; class TAGLIB_EXPORT Tag: public TagLib::Tag { public: Tag(); - Tag(TagLib::File *file, Atoms *atoms); + Tag(TagLib::File *file, Atoms *atoms, + const ItemFactory *factory = nullptr); ~Tag() override; Tag(const Tag &) = delete; Tag &operator=(const Tag &) = delete; @@ -114,36 +116,9 @@ namespace TagLib { void setTextItem(const String &key, const String &value); private: - AtomDataList parseData2(const Atom *atom, int expectedFlags = -1, - bool freeForm = false); - ByteVectorList parseData(const Atom *atom, int expectedFlags = -1, - bool freeForm = false); - void parseText(const Atom *atom, int expectedFlags = 1); - void parseFreeForm(const Atom *atom); - void parseInt(const Atom *atom); - void parseByte(const Atom *atom); - void parseUInt(const Atom *atom); - void parseLongLong(const Atom *atom); - void parseGnre(const Atom *atom); - void parseIntPair(const Atom *atom); - void parseBool(const Atom *atom); - void parseCovr(const Atom *atom); - ByteVector padIlst(const ByteVector &data, int length = -1) const; ByteVector renderAtom(const ByteVector &name, const ByteVector &data) const; - ByteVector renderData(const ByteVector &name, int flags, - const ByteVectorList &data) const; - ByteVector renderText(const ByteVector &name, const Item &item, - int flags = TypeUTF8) const; - ByteVector renderFreeForm(const String &name, const Item &item) const; - ByteVector renderBool(const ByteVector &name, const Item &item) const; - ByteVector renderInt(const ByteVector &name, const Item &item) const; - ByteVector renderByte(const ByteVector &name, const Item &item) const; - ByteVector renderUInt(const ByteVector &name, const Item &item) const; - ByteVector renderLongLong(const ByteVector &name, const Item &item) const; - ByteVector renderIntPair(const ByteVector &name, const Item &item) const; - ByteVector renderIntPairNoTrailing(const ByteVector &name, const Item &item) const; - ByteVector renderCovr(const ByteVector &name, const Item &item) const; + void updateParents(const AtomList &path, offset_t delta, int ignore = 0); void updateOffsets(offset_t delta, offset_t offset); diff --git a/tests/test_mp4.cpp b/tests/test_mp4.cpp index 5d8909da..77f8823d 100644 --- a/tests/test_mp4.cpp +++ b/tests/test_mp4.cpp @@ -33,6 +33,7 @@ #include "mp4tag.h" #include "mp4atom.h" #include "mp4file.h" +#include "mp4itemfactory.h" #include "plainfile.h" #include #include "utils.h" @@ -69,6 +70,7 @@ class TestMP4 : public CppUnit::TestFixture CPPUNIT_TEST(testEmptyValuesRemoveItems); CPPUNIT_TEST(testRemoveMetadata); CPPUNIT_TEST(testNonFullMetaAtom); + CPPUNIT_TEST(testItemFactory); CPPUNIT_TEST_SUITE_END(); public: @@ -735,6 +737,105 @@ public: CPPUNIT_ASSERT_EQUAL(StringList("FAAC 1.24"), properties["ENCODEDBY"]); } } + + void testItemFactory() + { + class CustomItemFactory : public MP4::ItemFactory { + protected: + NameHandlerMap nameHandlerMap() const override + { + return MP4::ItemFactory::nameHandlerMap() + .insert("tsti", ItemHandlerType::Int) + .insert("tstt", ItemHandlerType::Text); + } + + Map namePropertyMap() const override + { + return MP4::ItemFactory::namePropertyMap() + .insert("tsti", "TESTINTEGER"); + } + }; + + CustomItemFactory factory; + + ScopedFileCopy copy("no-tags", ".m4a"); + { + MP4::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(!f.hasMP4Tag()); + MP4::Tag *tag = f.tag(); + tag->setItem("tsti", MP4::Item(123)); + tag->setItem("tstt", MP4::Item(StringList("Test text"))); + f.save(); + } + { + MP4::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.hasMP4Tag()); + MP4::Tag *tag = f.tag(); + // Without a custom item factory, only custom text atoms with four + // letter names are possible. + MP4::Item item = tag->item("tsti"); + CPPUNIT_ASSERT(!item.isValid()); + CPPUNIT_ASSERT(item.toInt() != 123); + item = tag->item("tstt"); + CPPUNIT_ASSERT(item.isValid()); + CPPUNIT_ASSERT_EQUAL(StringList("Test text"), item.toStringList()); + f.strip(); + } + { + MP4::File f(copy.fileName().c_str(), + true, MP4::Properties::Average, &factory); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(!f.hasMP4Tag()); + MP4::Tag *tag = f.tag(); + tag->setItem("tsti", MP4::Item(123)); + tag->setItem("tstt", MP4::Item(StringList("Test text"))); + tag->setItem("trkn", MP4::Item(2, 10)); + tag->setItem("rate", MP4::Item(80)); + tag->setItem("plID", MP4::Item(1540934238LL)); + tag->setItem("rtng", MP4::Item(static_cast(2))); + f.save(); + } + { + MP4::File f(copy.fileName().c_str(), + true, MP4::Properties::Average, &factory); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.hasMP4Tag()); + MP4::Tag *tag = f.tag(); + MP4::Item item = tag->item("tsti"); + CPPUNIT_ASSERT(item.isValid()); + CPPUNIT_ASSERT_EQUAL(123, item.toInt()); + item = tag->item("tstt"); + CPPUNIT_ASSERT(item.isValid()); + CPPUNIT_ASSERT_EQUAL(StringList("Test text"), item.toStringList()); + item = tag->item("trkn"); + CPPUNIT_ASSERT(item.isValid()); + CPPUNIT_ASSERT_EQUAL(2, item.toIntPair().first); + CPPUNIT_ASSERT_EQUAL(10, item.toIntPair().second); + CPPUNIT_ASSERT_EQUAL(80, tag->item("rate").toInt()); + CPPUNIT_ASSERT_EQUAL(1540934238LL, tag->item("plID").toLongLong()); + CPPUNIT_ASSERT_EQUAL(static_cast(2), tag->item("rtng").toByte()); + PropertyMap properties = tag->properties(); + CPPUNIT_ASSERT_EQUAL(StringList("123"), properties.value("TESTINTEGER")); + CPPUNIT_ASSERT_EQUAL(StringList("2/10"), properties.value("TRACKNUMBER")); + properties["TESTINTEGER"] = StringList("456"); + tag->setProperties(properties); + f.save(); + } + { + MP4::File f(copy.fileName().c_str(), + true, MP4::Properties::Average, &factory); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.hasMP4Tag()); + MP4::Tag *tag = f.tag(); + MP4::Item item = tag->item("tsti"); + CPPUNIT_ASSERT(item.isValid()); + CPPUNIT_ASSERT_EQUAL(456, item.toInt()); + PropertyMap properties = tag->properties(); + CPPUNIT_ASSERT_EQUAL(StringList("456"), properties.value("TESTINTEGER")); + } + } }; CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4);