tagparser/mp4/mp4tag.cpp

483 lines
17 KiB
C++
Raw Normal View History

2015-09-06 19:57:33 +02:00
#include "./mp4tag.h"
#include "./mp4atom.h"
2018-03-07 01:17:50 +01:00
#include "./mp4container.h"
#include "./mp4ids.h"
2015-09-06 15:42:18 +02:00
2015-09-06 19:57:33 +02:00
#include "../exceptions.h"
2015-04-22 19:22:01 +02:00
#include <c++utilities/conversion/stringconversion.h>
2018-03-07 01:17:50 +01:00
#include <c++utilities/io/binarywriter.h>
2015-04-22 19:22:01 +02:00
using namespace std;
2019-06-10 22:49:11 +02:00
using namespace CppUtilities;
2015-04-22 19:22:01 +02:00
namespace TagParser {
2015-04-22 19:22:01 +02:00
/*!
* \class TagParser::Mp4ExtendedFieldId
* \brief The Mp4ExtendedFieldId specifies parameter for an extended field denoted via Mp4TagAtomIds::Extended.
*/
/*!
* \brief Constructs a new instance for the specified \a field.
* \remarks The instance will be invalid if no extended field parameter for \a field are known.
*/
Mp4ExtendedFieldId::Mp4ExtendedFieldId(KnownField field)
{
2018-03-07 01:17:50 +01:00
switch (field) {
case KnownField::EncoderSettings:
mean = Mp4TagExtendedMeanIds::iTunes;
name = Mp4TagExtendedNameIds::cdec;
updateOnly = false;
break;
case KnownField::RecordLabel:
mean = Mp4TagExtendedMeanIds::iTunes;
name = Mp4TagExtendedNameIds::label;
updateOnly = true; // set record label via extended field only if extended field is already present
break;
default:;
}
}
2015-04-22 19:22:01 +02:00
/*!
* \class TagParser::Mp4Tag
* \brief Implementation of TagParser::Tag for the MP4 container.
2015-04-22 19:22:01 +02:00
*/
bool Mp4Tag::canEncodingBeUsed(TagTextEncoding encoding) const
{
2018-03-07 01:17:50 +01:00
switch (encoding) {
2015-04-22 19:22:01 +02:00
case TagTextEncoding::Utf8:
return true;
case TagTextEncoding::Utf16BigEndian:
return true;
default:
return false;
}
}
const TagValue &Mp4Tag::value(KnownField field) const
{
2018-03-07 01:17:50 +01:00
switch (field) {
2015-04-22 19:22:01 +02:00
case KnownField::Genre: {
const TagValue &value = FieldMapBasedTag<Mp4Tag>::value(Mp4TagAtomIds::Genre);
2018-03-07 01:17:50 +01:00
if (!value.isEmpty()) {
2015-04-22 19:22:01 +02:00
return value;
} else {
return FieldMapBasedTag<Mp4Tag>::value(Mp4TagAtomIds::PreDefinedGenre);
2015-04-22 19:22:01 +02:00
}
2018-03-07 01:17:50 +01:00
}
case KnownField::EncoderSettings:
return this->value(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::cdec);
case KnownField::RecordLabel: {
const TagValue &value = FieldMapBasedTag<Mp4Tag>::value(Mp4TagAtomIds::RecordLabel);
2018-03-07 01:17:50 +01:00
if (!value.isEmpty()) {
return value;
} else {
return this->value(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::label);
}
}
2015-04-22 19:22:01 +02:00
default:
return FieldMapBasedTag<Mp4Tag>::value(field);
2015-04-22 19:22:01 +02:00
}
}
std::vector<const TagValue *> Mp4Tag::values(KnownField field) const
{
auto values = FieldMapBasedTag<Mp4Tag>::values(field);
2021-03-20 21:26:25 +01:00
if (const auto extendedId = Mp4ExtendedFieldId(field)) {
auto range = fields().equal_range(Mp4TagAtomIds::Extended);
2018-03-07 01:17:50 +01:00
for (auto i = range.first; i != range.second; ++i) {
2021-03-20 21:26:25 +01:00
const auto &extendedField = i->second;
if (extendedId.matches(extendedField)) {
values.emplace_back(&extendedField.value());
for (const auto &additionalData : extendedField.additionalData()) {
values.emplace_back(&additionalData.value);
}
}
}
}
return values;
}
2015-04-22 19:22:01 +02:00
/*!
* \brief Returns the value of the field with the specified \a mean and \a name attributes.
* \remarks
* - If there are multiple fields with specified \a mean and \a name only the first value will be returned.
2015-04-22 19:22:01 +02:00
*/
const TagValue &Mp4Tag::value(std::string_view mean, std::string_view name) const
2015-04-22 19:22:01 +02:00
{
auto range = fields().equal_range(Mp4TagAtomIds::Extended);
2018-03-07 01:17:50 +01:00
for (auto i = range.first; i != range.second; ++i) {
if (i->second.mean() == mean && i->second.name() == name) {
2015-04-22 19:22:01 +02:00
return i->second.value();
}
}
return TagValue::empty();
}
Mp4Tag::IdentifierType Mp4Tag::internallyGetFieldId(KnownField field) const
2015-04-22 19:22:01 +02:00
{
using namespace Mp4TagAtomIds;
2018-03-07 01:17:50 +01:00
switch (field) {
case KnownField::Album:
return Album;
case KnownField::Artist:
return Artist;
case KnownField::Comment:
return Comment;
case KnownField::RecordDate:
2018-03-07 01:17:50 +01:00
return Year;
case KnownField::Title:
return Title;
case KnownField::Genre:
return Genre;
case KnownField::TrackPosition:
return TrackPosition;
case KnownField::DiskPosition:
return DiskPosition;
case KnownField::Composer:
return Composer;
case KnownField::Encoder:
return Encoder;
case KnownField::Bpm:
return Bpm;
case KnownField::Cover:
return Cover;
case KnownField::Rating:
return Rating;
case KnownField::Grouping:
return Grouping;
case KnownField::Description:
return Description;
case KnownField::Lyrics:
return Lyrics;
case KnownField::RecordLabel:
return RecordLabel;
case KnownField::Performers:
return Performers;
case KnownField::Lyricist:
return Lyricist;
case KnownField::AlbumArtist:
return AlbumArtist;
2018-03-07 01:17:50 +01:00
default:
return 0;
2015-04-22 19:22:01 +02:00
}
// do not forget to extend Mp4Tag::internallyGetKnownField() and Mp4TagField::appropriateRawDataType() as well
2015-04-22 19:22:01 +02:00
}
KnownField Mp4Tag::internallyGetKnownField(const IdentifierType &id) const
2015-04-22 19:22:01 +02:00
{
using namespace Mp4TagAtomIds;
2018-03-07 01:17:50 +01:00
switch (id) {
case Album:
return KnownField::Album;
case Artist:
return KnownField::Artist;
case Comment:
return KnownField::Comment;
case Year:
return KnownField::RecordDate;
2018-03-07 01:17:50 +01:00
case Title:
return KnownField::Title;
case PreDefinedGenre:
case Genre:
return KnownField::Genre;
case TrackPosition:
return KnownField::TrackPosition;
case DiskPosition:
return KnownField::DiskPosition;
case Composer:
return KnownField::Composer;
case Encoder:
return KnownField::Encoder;
case Bpm:
return KnownField::Bpm;
case Cover:
return KnownField::Cover;
case Rating:
return KnownField::Rating;
case Grouping:
return KnownField::Grouping;
case Description:
return KnownField::Description;
case Lyrics:
return KnownField::Lyrics;
case RecordLabel:
return KnownField::RecordLabel;
case Performers:
return KnownField::Performers;
case Lyricist:
return KnownField::Lyricist;
case AlbumArtist:
return KnownField::AlbumArtist;
2018-03-07 01:17:50 +01:00
default:
return KnownField::Invalid;
2015-04-22 19:22:01 +02:00
}
// do not forget to extend Mp4Tag::internallyGetFieldId() and Mp4TagField::appropriateRawDataType() as well
2015-04-22 19:22:01 +02:00
}
/*!
* \brief Adds values from additional data atoms as well.
*/
void Mp4Tag::internallyGetValuesFromField(const Mp4Tag::FieldType &field, std::vector<const TagValue *> &values) const
{
if (!field.value().isEmpty()) {
values.emplace_back(&field.value());
}
for (const auto &value : field.additionalData()) {
if (!value.value.isEmpty()) {
values.emplace_back(&value.value);
}
}
}
2015-04-22 19:22:01 +02:00
bool Mp4Tag::setValue(KnownField field, const TagValue &value)
{
2018-03-07 01:17:50 +01:00
switch (field) {
2015-04-22 19:22:01 +02:00
case KnownField::Genre:
2018-03-07 01:17:50 +01:00
switch (value.type()) {
2015-04-22 19:22:01 +02:00
case TagDataType::StandardGenreIndex:
2015-08-10 00:02:03 +02:00
fields().erase(Mp4TagAtomIds::Genre);
return FieldMapBasedTag<Mp4Tag>::setValue(Mp4TagAtomIds::PreDefinedGenre, value);
2015-04-22 19:22:01 +02:00
default:
2015-08-10 00:02:03 +02:00
fields().erase(Mp4TagAtomIds::PreDefinedGenre);
return FieldMapBasedTag<Mp4Tag>::setValue(Mp4TagAtomIds::Genre, value);
2015-04-22 19:22:01 +02:00
}
case KnownField::EncoderSettings:
return setValue(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::cdec, value);
case KnownField::RecordLabel:
2018-03-07 01:17:50 +01:00
if (!this->value(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::label).isEmpty()) {
setValue(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::label, value);
}
2019-06-12 20:40:45 +02:00
[[fallthrough]];
2015-04-22 19:22:01 +02:00
default:
return FieldMapBasedTag<Mp4Tag>::setValue(field, value);
2015-04-22 19:22:01 +02:00
}
}
bool Mp4Tag::setValues(KnownField field, const std::vector<TagValue> &values)
{
2021-03-20 21:26:25 +01:00
if (const auto extendedId = Mp4ExtendedFieldId(field)) {
auto valuesIterator = values.cbegin();
auto range = fields().equal_range(Mp4TagAtomIds::Extended);
2018-03-07 01:17:50 +01:00
for (; valuesIterator != values.cend() && range.first != range.second;) {
if (!valuesIterator->isEmpty()) {
2021-03-20 21:26:25 +01:00
auto &extendedField = range.first->second;
if (extendedId.matches(extendedField) && (!extendedId.updateOnly || !extendedField.value().isEmpty())) {
extendedField.clearValue();
extendedField.setValue(*valuesIterator);
// note: Not sure which extended tag fields support multiple data atoms and which don't. Let's simply use
// only one data atom per extended field here and get rid of any possibly assigned additional data
// atoms.
++valuesIterator;
}
++range.first;
} else {
++valuesIterator;
}
}
2018-03-07 01:17:50 +01:00
for (; valuesIterator != values.cend(); ++valuesIterator) {
2021-01-30 19:21:18 +01:00
fields().emplace(std::piecewise_construct, std::forward_as_tuple(Mp4TagAtomIds::Extended),
std::forward_as_tuple(extendedId.mean, extendedId.name, *valuesIterator));
}
2018-03-07 01:17:50 +01:00
for (; range.first != range.second; ++range.first) {
2021-01-30 19:21:18 +01:00
range.first->second.clearValue();
}
}
return FieldMapBasedTag<Mp4Tag>::setValues(field, values);
}
2015-04-22 19:22:01 +02:00
/*!
* \brief Assigns the given \a value to the field with the specified \a mean and \a name attributes.
* \remarks
* - If there are multiple fields with specified \a mean and \a name only the first will be altered.
* - If no field is present, a new one will be created.
* - If \a value is empty, the field will be removed.
2015-04-22 19:22:01 +02:00
*/
bool Mp4Tag::setValue(std::string_view mean, std::string_view name, const TagValue &value)
2015-04-22 19:22:01 +02:00
{
auto range = fields().equal_range(Mp4TagAtomIds::Extended);
2018-03-07 01:17:50 +01:00
for (auto i = range.first; i != range.second; ++i) {
if (i->second.mean() == mean && i->second.name() == name) {
2015-04-22 19:22:01 +02:00
i->second.setValue(value);
return true;
}
}
fields().insert(make_pair(Mp4TagAtomIds::Extended, FieldType(mean, name, value)));
2015-04-22 19:22:01 +02:00
return true;
}
bool Mp4Tag::hasField(KnownField field) const
{
2018-03-07 01:17:50 +01:00
switch (field) {
2015-04-22 19:22:01 +02:00
case KnownField::Genre:
2018-03-07 01:17:50 +01:00
return FieldMapBasedTag<Mp4Tag>::hasField(Mp4TagAtomIds::PreDefinedGenre) || FieldMapBasedTag<Mp4Tag>::hasField(Mp4TagAtomIds::Genre);
2015-04-22 19:22:01 +02:00
default:
return FieldMapBasedTag<Mp4Tag>::hasField(field);
2015-04-22 19:22:01 +02:00
}
}
/*!
* \brief Parses tag information from the specified \a metaAtom.
*
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws TagParser::Failure or a derived exception when a parsing
2015-04-22 19:22:01 +02:00
* error occurs.
*/
void Mp4Tag::parse(Mp4Atom &metaAtom, Diagnostics &diag)
2015-04-22 19:22:01 +02:00
{
static const string context("parsing MP4 tag");
istream &stream = metaAtom.container().stream();
BinaryReader &reader = metaAtom.container().reader();
2019-03-13 19:06:42 +01:00
if (metaAtom.totalSize() > numeric_limits<std::uint32_t>::max()) {
diag.emplace_back(DiagLevel::Critical, "Can't handle such big \"meta\" atoms.", context);
throw NotImplementedException();
}
2019-03-13 19:06:42 +01:00
m_size = static_cast<std::uint32_t>(metaAtom.totalSize());
2015-04-22 19:22:01 +02:00
Mp4Atom *subAtom = nullptr;
try {
metaAtom.childById(Mp4AtomIds::HandlerReference, diag);
2018-03-07 01:17:50 +01:00
} catch (const Failure &) {
diag.emplace_back(DiagLevel::Critical, "Unable to parse child atoms of meta atom (stores hdlr and ilst atoms).", context);
2015-04-22 19:22:01 +02:00
}
2018-03-07 01:17:50 +01:00
if (subAtom) {
stream.seekg(static_cast<streamoff>(subAtom->startOffset() + subAtom->headerSize()));
2015-04-22 19:22:01 +02:00
int versionByte = reader.readByte();
2018-03-07 01:17:50 +01:00
if (versionByte != 0) {
diag.emplace_back(DiagLevel::Warning, "Version is unknown.", context);
2015-04-22 19:22:01 +02:00
}
2018-03-07 01:17:50 +01:00
if (reader.readUInt24BE()) {
diag.emplace_back(DiagLevel::Warning, "Flags (hdlr atom) aren't set to 0.", context);
2015-04-22 19:22:01 +02:00
}
2018-03-07 01:17:50 +01:00
if (reader.readInt32BE()) {
diag.emplace_back(DiagLevel::Warning, "Predefined 32-bit integer (hdlr atom) ins't set to 0.", context);
2015-04-22 19:22:01 +02:00
}
2019-03-13 19:06:42 +01:00
std::uint64_t handlerType = reader.readUInt64BE();
2018-03-07 01:17:50 +01:00
if (/*(((handlerType & 0xFFFFFFFF00000000) >> 32) != 0x6D647461) && */ (handlerType != 0x6d6469726170706c)) {
diag.emplace_back(DiagLevel::Warning, "Handler type (value in hdlr atom) is unknown. Trying to parse meta information anyhow.", context);
2015-04-22 19:22:01 +02:00
}
2019-06-10 22:49:11 +02:00
m_version = numberToString(versionByte);
2015-04-22 19:22:01 +02:00
} else {
m_version.clear();
}
try {
subAtom = metaAtom.childById(Mp4AtomIds::ItunesList, diag);
2018-03-07 01:17:50 +01:00
} catch (const Failure &) {
diag.emplace_back(DiagLevel::Critical, "Unable to parse child atoms of meta atom (stores hdlr and ilst atoms).", context);
2015-04-22 19:22:01 +02:00
}
if (!subAtom) {
diag.emplace_back(DiagLevel::Warning, "No ilst atom found (stores attached meta information).", context);
2015-04-22 19:22:01 +02:00
throw NoDataFoundException();
}
for (auto *child = subAtom->firstChild(); child; child = child->nextSibling()) {
Mp4TagField tagField;
try {
child->parse(diag);
tagField.reparse(*child, diag);
fields().emplace(child->id(), move(tagField));
} catch (const Failure &) {
}
}
2015-04-22 19:22:01 +02:00
}
2015-12-10 13:50:46 +01:00
/*!
* \brief Prepares making.
* \returns Returns a Mp4TagMaker object which can be used to actually make the tag.
* \remarks The tag must NOT be mutated after making is prepared when it is intended to actually
* make the tag using the make method of the returned object.
* \throws Throws TagParser::Failure or a derived exception when a making error occurs.
2015-12-10 13:50:46 +01:00
*
* This method might be useful when it is necessary to know the size of the tag before making it.
* \sa make()
*/
Mp4TagMaker Mp4Tag::prepareMaking(Diagnostics &diag)
2015-12-10 13:50:46 +01:00
{
return Mp4TagMaker(*this, diag);
2015-12-10 13:50:46 +01:00
}
2015-04-22 19:22:01 +02:00
/*!
* \brief Writes tag information to the specified \a stream.
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws TagParser::Failure or a derived exception when a making
2015-04-22 19:22:01 +02:00
* error occurs.
*/
void Mp4Tag::make(ostream &stream, Diagnostics &diag)
2015-04-22 19:22:01 +02:00
{
prepareMaking(diag).make(stream, diag);
2015-12-10 13:50:46 +01:00
}
2016-08-04 00:16:19 +02:00
/*!
* \class TagParser::Mp4TagMaker
2016-08-04 00:16:19 +02:00
* \brief The Mp4TagMaker class helps writing MP4 tags.
*
* An instance can be obtained using the Mp4Tag::prepareMaking() method.
*/
2015-12-10 13:50:46 +01:00
/*!
* \brief Prepares making the specified \a tag.
* \sa See Mp4Tag::prepareMaking() for more information.
*/
2018-03-07 01:17:50 +01:00
Mp4TagMaker::Mp4TagMaker(Mp4Tag &tag, Diagnostics &diag)
: m_tag(tag)
,
2015-12-10 13:50:46 +01:00
// meta head, hdlr atom
2018-03-07 01:17:50 +01:00
m_metaSize(8 + 37)
,
2015-12-10 13:50:46 +01:00
// ilst head
2018-03-07 01:17:50 +01:00
m_ilstSize(8)
,
2015-12-10 13:50:46 +01:00
// ensure there only one genre atom is written (prefer genre as string)
m_omitPreDefinedGenre(m_tag.fields().count(m_tag.hasField(Mp4TagAtomIds::Genre)))
2015-12-10 13:50:46 +01:00
{
m_maker.reserve(m_tag.fields().size());
2018-03-07 01:17:50 +01:00
for (auto &field : m_tag.fields()) {
if (!field.second.value().isEmpty() && (!m_omitPreDefinedGenre || field.first != Mp4TagAtomIds::PreDefinedGenre)) {
try {
m_ilstSize += m_maker.emplace_back(field.second.prepareMaking(diag)).requiredSize();
2018-03-07 01:17:50 +01:00
} catch (const Failure &) {
}
2015-12-10 13:50:46 +01:00
}
}
2018-03-07 01:17:50 +01:00
if (m_ilstSize != 8) {
2015-12-10 13:50:46 +01:00
m_metaSize += m_ilstSize;
}
2019-03-13 19:06:42 +01:00
if (m_metaSize >= numeric_limits<std::uint32_t>::max()) {
diag.emplace_back(DiagLevel::Critical, "Making such big tags is not implemented.", "making MP4 tag");
throw NotImplementedException();
}
2015-12-10 13:50:46 +01:00
}
/*!
* \brief Saves the tag (specified when constructing the object) to the
* specified \a stream.
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws Assumes the data is already validated and thus does NOT
* throw TagParser::Failure or a derived exception.
2015-12-10 13:50:46 +01:00
*/
void Mp4TagMaker::make(ostream &stream, Diagnostics &diag)
2015-12-10 13:50:46 +01:00
{
// write meta head
BinaryWriter writer(&stream);
2019-03-13 19:06:42 +01:00
writer.writeUInt32BE(static_cast<std::uint32_t>(m_metaSize));
2015-12-10 13:50:46 +01:00
writer.writeUInt32BE(Mp4AtomIds::Meta);
2015-04-22 19:22:01 +02:00
// write hdlr atom
2019-03-13 19:06:42 +01:00
static const std::uint8_t hdlrData[37] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
2015-04-22 19:22:01 +02:00
stream.write(reinterpret_cast<const char *>(hdlrData), sizeof(hdlrData));
2018-03-07 01:17:50 +01:00
if (m_ilstSize != 8) {
2015-12-10 13:50:46 +01:00
// write ilst head
2019-03-13 19:06:42 +01:00
writer.writeUInt32BE(static_cast<std::uint32_t>(m_ilstSize));
2015-12-10 13:50:46 +01:00
writer.writeUInt32BE(Mp4AtomIds::ItunesList);
// write fields
2018-03-07 01:17:50 +01:00
for (auto &maker : m_maker) {
2015-12-10 13:50:46 +01:00
maker.make(stream);
2015-04-22 19:22:01 +02:00
}
2015-12-10 13:50:46 +01:00
} else {
// no fields to be written -> no ilst to be written
diag.emplace_back(DiagLevel::Warning, "Tag is empty.", "making MP4 tag");
2015-04-22 19:22:01 +02:00
}
}
2018-03-07 01:17:50 +01:00
} // namespace TagParser