mirror of
https://github.com/taglib/taglib.git
synced 2025-05-27 13:10:26 -04:00
Clients can control supported MP4 atoms using an ItemFactory (#1175)
This commit is contained in:
parent
9679b08120
commit
c083d7ce15
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -83,6 +83,8 @@ namespace TagLib {
|
||||
class ItemPrivate;
|
||||
std::shared_ptr<ItemPrivate> d;
|
||||
};
|
||||
|
||||
using ItemMap = TagLib::Map<String, Item>;
|
||||
} // namespace MP4
|
||||
} // namespace TagLib
|
||||
#endif
|
||||
|
776
taglib/mp4/mp4itemfactory.cpp
Normal file
776
taglib/mp4/mp4itemfactory.cpp
Normal 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
256
taglib/mp4/mp4itemfactory.h
Normal 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
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user