Clients can control supported MP4 atoms using an ItemFactory (#1175)

This commit is contained in:
Urs Fleisch 2023-11-23 16:39:57 +01:00 committed by GitHub
parent 9679b08120
commit c083d7ce15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1195 additions and 618 deletions

View File

@ -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

View File

@ -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<MP4::Tag> tag;
std::unique_ptr<MP4::Atoms> atoms;
std::unique_ptr<MP4::Properties> 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<FilePrivate>())
d(std::make_unique<FilePrivate>(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<FilePrivate>())
d(std::make_unique<FilePrivate>(itemFactory))
{
if(isOpen())
read(readProperties);
@ -125,7 +138,7 @@ MP4::File::read(bool readProperties)
return;
}
d->tag = std::make_unique<Tag>(this, d->atoms.get());
d->tag = std::make_unique<Tag>(this, d->atoms.get(), d->itemFactory);
if(readProperties) {
d->properties = std::make_unique<Properties>(this, d->atoms.get());
}

View File

@ -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.

View File

@ -83,6 +83,8 @@ namespace TagLib {
class ItemPrivate;
std::shared_ptr<ItemPrivate> d;
};
using ItemMap = TagLib::Map<String, Item>;
} // namespace MP4
} // namespace TagLib
#endif

View File

@ -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 <utility>
#include "tbytevector.h"
#include "tdebug.h"
#include "id3v1genres.h"
using namespace TagLib;
using namespace MP4;
class ItemFactory::ItemFactoryPrivate
{
public:
NameHandlerMap handlerTypeForName;
Map<ByteVector, String> propertyKeyForName;
Map<String, ByteVector> nameForPropertyKey;
};
ItemFactory ItemFactory::factory;
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
ItemFactory *ItemFactory::instance()
{
return &factory;
}
std::pair<String, Item> 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<ByteVector, Item> 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<unsigned int>(values.front().toInt()))};
case ItemHandlerType::LongLong:
return {name, Item(static_cast<long long>(values.front().toInt()))};
case ItemHandlerType::Byte:
return {name, Item(static_cast<unsigned char>(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<String, StringList> 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<ItemFactoryPrivate>())
{
}
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<ByteVector, String> 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<int>(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<int>(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<AtomDataType>(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<AtomDataType>(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<String, Item> ItemFactory::parseInt(
const MP4::Atom *atom, const ByteVector &bytes)
{
ByteVectorList data = parseData(atom, bytes);
return {
atom->name,
!data.isEmpty() ? Item(static_cast<int>(data[0].toShort())) : Item()
};
}
std::pair<String, Item> 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<int>(val.data.toShort()))
};
}
return {atom->name, Item()};
}
std::pair<String, Item> 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<String, Item> 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<String, Item> ItemFactory::parseByte(
const MP4::Atom *atom, const ByteVector &bytes)
{
ByteVectorList data = parseData(atom, bytes);
return {
atom->name,
!data.isEmpty() ? Item(static_cast<unsigned char>(data[0].at(0))) : Item()
};
}
std::pair<String, Item> ItemFactory::parseGnre(
const MP4::Atom *atom, const ByteVector &bytes)
{
ByteVectorList data = parseData(atom, bytes);
if(!data.isEmpty()) {
int idx = static_cast<int>(data[0].toShort());
if(idx > 0) {
return {
"\251gen",
Item(StringList(ID3v1::genre(idx - 1)))
};
}
}
return {"\251gen", Item()};
}
std::pair<String, Item> 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<String, Item> 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<String, Item> 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<String, Item> 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<String, Item> 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<int>(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<int>(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<MP4::CoverArt::Format>(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);
}

256
taglib/mp4/mp4itemfactory.h Normal file
View File

@ -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 <memory>
#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<String, Item> 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<ByteVector, Item> 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<String, StringList> 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<ByteVector, ItemHandlerType>;
/*!
* 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<ByteVector, String> 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<String, Item> parseText(
const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags = 1);
static std::pair<String, Item> parseFreeForm(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseInt(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseByte(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseTextOrInt(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseUInt(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseLongLong(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseGnre(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseIntPair(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseBool(
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> 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<ItemFactoryPrivate> d;
};
} // namespace MP4
} // namespace TagLib
#endif

View File

@ -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<TagPrivate>())
d(std::make_unique<TagPrivate>(ItemFactory::instance()))
{
}
MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) :
d(std::make_unique<TagPrivate>())
MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms,
const MP4::ItemFactory *factory) :
d(std::make_unique<TagPrivate>(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<int>(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<int>(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<int>(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<AtomDataType>(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<AtomDataType>(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<int>(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<unsigned char>(data[0].at(0)));
}
}
void
MP4::Tag::parseGnre(const MP4::Atom *atom)
{
ByteVectorList data = parseData(atom);
if(!data.isEmpty()) {
int idx = static_cast<int>(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<int>(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<int>(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<MP4::CoverArt::Format>(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<String, String> 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);

View File

@ -37,13 +37,15 @@
namespace TagLib {
namespace MP4 {
using ItemMap = TagLib::Map<String, Item>;
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);

View File

@ -33,6 +33,7 @@
#include "mp4tag.h"
#include "mp4atom.h"
#include "mp4file.h"
#include "mp4itemfactory.h"
#include "plainfile.h"
#include <cppunit/extensions/HelperMacros.h>
#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<ByteVector, String> 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<unsigned char>(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<unsigned char>(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);