#include "./mp4tag.h" #include "./mp4atom.h" #include "./mp4container.h" #include "./mp4ids.h" #include "../exceptions.h" #include #include using namespace std; using namespace CppUtilities; namespace TagParser { /*! * \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) { 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: mean = nullptr; name = nullptr; updateOnly = false; } } /*! * \class TagParser::Mp4Tag * \brief Implementation of TagParser::Tag for the MP4 container. */ bool Mp4Tag::canEncodingBeUsed(TagTextEncoding encoding) const { switch (encoding) { case TagTextEncoding::Utf8: return true; case TagTextEncoding::Utf16BigEndian: return true; default: return false; } } const TagValue &Mp4Tag::value(KnownField field) const { switch (field) { case KnownField::Genre: { const TagValue &value = FieldMapBasedTag::value(Mp4TagAtomIds::Genre); if (!value.isEmpty()) { return value; } else { return FieldMapBasedTag::value(Mp4TagAtomIds::PreDefinedGenre); } } case KnownField::EncoderSettings: return this->value(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::cdec); case KnownField::RecordLabel: { const TagValue &value = FieldMapBasedTag::value(Mp4TagAtomIds::RecordLabel); if (!value.isEmpty()) { return value; } else { return this->value(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::label); } } default: return FieldMapBasedTag::value(field); } } std::vector Mp4Tag::values(KnownField field) const { auto values = FieldMapBasedTag::values(field); const Mp4ExtendedFieldId extendedId(field); if (extendedId) { auto range = fields().equal_range(Mp4TagAtomIds::Extended); for (auto i = range.first; i != range.second; ++i) { if (extendedId.matches(i->second)) { values.emplace_back(&i->second.value()); } } } return values; } /*! * \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. */ const TagValue &Mp4Tag::value(const char *mean, const char *name) const { auto range = fields().equal_range(Mp4TagAtomIds::Extended); for (auto i = range.first; i != range.second; ++i) { if (i->second.mean() == mean && i->second.name() == name) { return i->second.value(); } } return TagValue::empty(); } Mp4Tag::IdentifierType Mp4Tag::internallyGetFieldId(KnownField field) const { using namespace Mp4TagAtomIds; switch (field) { case KnownField::Album: return Album; case KnownField::Artist: return Artist; case KnownField::Comment: return Comment; case KnownField::Year: 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; default: return 0; } } KnownField Mp4Tag::internallyGetKnownField(const IdentifierType &id) const { using namespace Mp4TagAtomIds; switch (id) { case Album: return KnownField::Album; case Artist: return KnownField::Artist; case Comment: return KnownField::Comment; case Year: return KnownField::Year; 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; default: return KnownField::Invalid; } } bool Mp4Tag::setValue(KnownField field, const TagValue &value) { switch (field) { case KnownField::Genre: switch (value.type()) { case TagDataType::StandardGenreIndex: fields().erase(Mp4TagAtomIds::Genre); return FieldMapBasedTag::setValue(Mp4TagAtomIds::PreDefinedGenre, value); default: fields().erase(Mp4TagAtomIds::PreDefinedGenre); return FieldMapBasedTag::setValue(Mp4TagAtomIds::Genre, value); } case KnownField::EncoderSettings: return setValue(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::cdec, value); case KnownField::RecordLabel: if (!this->value(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::label).isEmpty()) { setValue(Mp4TagExtendedMeanIds::iTunes, Mp4TagExtendedNameIds::label, value); } FALLTHROUGH; default: return FieldMapBasedTag::setValue(field, value); } } bool Mp4Tag::setValues(KnownField field, const std::vector &values) { const Mp4ExtendedFieldId extendedId(field); if (extendedId) { auto valuesIterator = values.cbegin(); auto range = fields().equal_range(Mp4TagAtomIds::Extended); for (; valuesIterator != values.cend() && range.first != range.second;) { if (!valuesIterator->isEmpty()) { if (extendedId.matches(range.first->second) && (!extendedId.updateOnly || !range.first->second.value().isEmpty())) { range.first->second.setValue(*valuesIterator); ++valuesIterator; } ++range.first; } else { ++valuesIterator; } } for (; valuesIterator != values.cend(); ++valuesIterator) { Mp4TagField tagField(Mp4TagAtomIds::Extended, *valuesIterator); tagField.setMean(extendedId.mean); tagField.setName(extendedId.name); fields().insert(std::make_pair(Mp4TagAtomIds::Extended, move(tagField))); } for (; range.first != range.second; ++range.first) { range.first->second.setValue(TagValue()); } } return FieldMapBasedTag::setValues(field, values); } /*! * \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. */ bool Mp4Tag::setValue(const char *mean, const char *name, const TagValue &value) { auto range = fields().equal_range(Mp4TagAtomIds::Extended); for (auto i = range.first; i != range.second; ++i) { if (i->second.mean() == mean && i->second.name() == name) { i->second.setValue(value); return true; } } fields().insert(make_pair(Mp4TagAtomIds::Extended, FieldType(mean, name, value))); return true; } bool Mp4Tag::hasField(KnownField field) const { switch (field) { case KnownField::Genre: return FieldMapBasedTag::hasField(Mp4TagAtomIds::PreDefinedGenre) || FieldMapBasedTag::hasField(Mp4TagAtomIds::Genre); default: return FieldMapBasedTag::hasField(field); } } /*! * \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 * error occurs. */ void Mp4Tag::parse(Mp4Atom &metaAtom, Diagnostics &diag) { static const string context("parsing MP4 tag"); istream &stream = metaAtom.container().stream(); BinaryReader &reader = metaAtom.container().reader(); if (metaAtom.totalSize() > numeric_limits::max()) { diag.emplace_back(DiagLevel::Critical, "Can't handle such big \"meta\" atoms.", context); throw NotImplementedException(); } m_size = static_cast(metaAtom.totalSize()); Mp4Atom *subAtom = nullptr; try { metaAtom.childById(Mp4AtomIds::HandlerReference, diag); } catch (const Failure &) { diag.emplace_back(DiagLevel::Critical, "Unable to parse child atoms of meta atom (stores hdlr and ilst atoms).", context); } if (subAtom) { stream.seekg(static_cast(subAtom->startOffset() + subAtom->headerSize())); int versionByte = reader.readByte(); if (versionByte != 0) { diag.emplace_back(DiagLevel::Warning, "Version is unknown.", context); } if (reader.readUInt24BE()) { diag.emplace_back(DiagLevel::Warning, "Flags (hdlr atom) aren't set to 0.", context); } if (reader.readInt32BE()) { diag.emplace_back(DiagLevel::Warning, "Predefined 32-bit integer (hdlr atom) ins't set to 0.", context); } std::uint64_t handlerType = reader.readUInt64BE(); 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); } m_version = numberToString(versionByte); } else { m_version.clear(); } try { subAtom = metaAtom.childById(Mp4AtomIds::ItunesList, diag); } catch (const Failure &) { diag.emplace_back(DiagLevel::Critical, "Unable to parse child atoms of meta atom (stores hdlr and ilst atoms).", context); } if (!subAtom) { diag.emplace_back(DiagLevel::Warning, "No ilst atom found (stores attached meta information).", context); 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 &) { } } } /*! * \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. * * 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) { return Mp4TagMaker(*this, diag); } /*! * \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 * error occurs. */ void Mp4Tag::make(ostream &stream, Diagnostics &diag) { prepareMaking(diag).make(stream, diag); } /*! * \class TagParser::Mp4TagMaker * \brief The Mp4TagMaker class helps writing MP4 tags. * * An instance can be obtained using the Mp4Tag::prepareMaking() method. */ /*! * \brief Prepares making the specified \a tag. * \sa See Mp4Tag::prepareMaking() for more information. */ Mp4TagMaker::Mp4TagMaker(Mp4Tag &tag, Diagnostics &diag) : m_tag(tag) , // meta head, hdlr atom m_metaSize(8 + 37) , // ilst head m_ilstSize(8) , // ensure there only one genre atom is written (prefer genre as string) m_omitPreDefinedGenre(m_tag.fields().count(m_tag.hasField(Mp4TagAtomIds::Genre))) { m_maker.reserve(m_tag.fields().size()); for (auto &field : m_tag.fields()) { if (!field.second.value().isEmpty() && (!m_omitPreDefinedGenre || field.first != Mp4TagAtomIds::PreDefinedGenre)) { try { m_maker.emplace_back(field.second.prepareMaking(diag)); m_ilstSize += m_maker.back().requiredSize(); } catch (const Failure &) { } } } if (m_ilstSize != 8) { m_metaSize += m_ilstSize; } if (m_metaSize >= numeric_limits::max()) { diag.emplace_back(DiagLevel::Critical, "Making such big tags is not implemented.", "making MP4 tag"); throw NotImplementedException(); } } /*! * \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. */ void Mp4TagMaker::make(ostream &stream, Diagnostics &diag) { // write meta head BinaryWriter writer(&stream); writer.writeUInt32BE(static_cast(m_metaSize)); writer.writeUInt32BE(Mp4AtomIds::Meta); // write hdlr atom 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 }; stream.write(reinterpret_cast(hdlrData), sizeof(hdlrData)); if (m_ilstSize != 8) { // write ilst head writer.writeUInt32BE(static_cast(m_ilstSize)); writer.writeUInt32BE(Mp4AtomIds::ItunesList); // write fields for (auto &maker : m_maker) { maker.make(stream); } } else { // no fields to be written -> no ilst to be written diag.emplace_back(DiagLevel::Warning, "Tag is empty.", "making MP4 tag"); } } } // namespace TagParser