diff --git a/examples/matroskareader.cpp b/examples/matroskareader.cpp index c688f2e0..e44c2a35 100644 --- a/examples/matroskareader.cpp +++ b/examples/matroskareader.cpp @@ -4,6 +4,8 @@ #include "matroskasimpletag.h" #include "matroskaattachments.h" #include "matroskaattachedfile.h" +#include "matroskachapters.h" +#include "matroskachapteredition.h" #include "tstring.h" #include "tutils.h" #include "tbytevector.h" @@ -66,16 +68,48 @@ int main(int argc, char *argv[]) const TagLib::String &mediaType = attachedFile.mediaType(); PRINT_PRETTY("Media Type", !mediaType.isEmpty() ? mediaType.toCString(false) : "None"); PRINT_PRETTY("Data Size", - TagLib::Utils::formatString("%u byte(s)",attachedFile.data().size()).toCString(false) + TagLib::Utils::formatString("%u byte(s)", attachedFile.data().size()).toCString(false) ); PRINT_PRETTY("UID", - TagLib::Utils::formatString("%llu",attachedFile.uid()).toCString(false) + TagLib::Utils::formatString("%llu", attachedFile.uid()).toCString(false) ); } } else printf("File has no attachments\n"); + TagLib::Matroska::Chapters *chapters = file.chapters(); + if(chapters) { + printf("Chapters:\n"); + const TagLib::Matroska::Chapters::ChapterEditionList &editions = chapters->chapterEditionList(); + for(const auto &edition : editions) { + if(edition.uid()) { + PRINT_PRETTY("Edition UID", TagLib::Utils::formatString("%llu", edition.uid()) + .toCString(false)); + } + PRINT_PRETTY("Edition Flags", TagLib::Utils::formatString("default=%d, ordered=%d", + edition.isDefault(), edition.isOrdered()) + .toCString(false)); + printf("\n"); + for(const auto &chapter : edition.chapterList()) { + PRINT_PRETTY("Chapter UID", TagLib::Utils::formatString("%llu", chapter.uid()) + .toCString(false)); + PRINT_PRETTY("Chapter flags", TagLib::Utils::formatString("hidden=%d", chapter.isHidden()) + .toCString(false)); + PRINT_PRETTY("Start-End", TagLib::Utils::formatString("%llu-%llu", + chapter.timeStart(), chapter.timeEnd()).toCString(false)); + for(const auto &display : chapter.displayList()) { + PRINT_PRETTY("Display", display.string().toCString(false)); + PRINT_PRETTY("Language", !display.language().isEmpty() + ? display.language().toCString(false) : "Not set"); + } + printf("\n"); + } + } + } + else + printf("File has no chapters\n"); + if(auto properties = dynamic_cast(file.audioProperties())) { printf("Properties:\n"); PRINT_PRETTY("Doc Type", properties->docType().toCString(false)); diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index 68983824..24fb4a7d 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -230,6 +230,9 @@ if(WITH_MATROSKA) set(tag_HDRS ${tag_HDRS} matroska/matroskaattachedfile.h matroska/matroskaattachments.h + matroska/matroskachapter.h + matroska/matroskachapteredition.h + matroska/matroskachapters.h matroska/matroskacues.h matroska/matroskaelement.h matroska/matroskafile.h @@ -242,6 +245,7 @@ if(WITH_MATROSKA) matroska/ebml/ebmlelement.h matroska/ebml/ebmlmasterelement.h matroska/ebml/ebmlmkattachments.h + matroska/ebml/ebmlmkchapters.h matroska/ebml/ebmlmkcues.h matroska/ebml/ebmlmkseekhead.h matroska/ebml/ebmlmksegment.h @@ -449,6 +453,9 @@ if(WITH_MATROSKA) set(matroska_SRCS matroska/matroskaattachedfile.cpp matroska/matroskaattachments.cpp + matroska/matroskachapter.cpp + matroska/matroskachapteredition.cpp + matroska/matroskachapters.cpp matroska/matroskacues.cpp matroska/matroskaelement.cpp matroska/matroskafile.cpp @@ -464,6 +471,7 @@ if(WITH_MATROSKA) matroska/ebml/ebmlelement.cpp matroska/ebml/ebmlmasterelement.cpp matroska/ebml/ebmlmkattachments.cpp + matroska/ebml/ebmlmkchapters.cpp matroska/ebml/ebmlmkcues.cpp matroska/ebml/ebmlmkseekhead.cpp matroska/ebml/ebmlmksegment.cpp diff --git a/taglib/matroska/ebml/ebmlelement.cpp b/taglib/matroska/ebml/ebmlelement.cpp index 8ecea7fb..3acef51e 100644 --- a/taglib/matroska/ebml/ebmlelement.cpp +++ b/taglib/matroska/ebml/ebmlelement.cpp @@ -27,6 +27,7 @@ #include "ebmlmksegment.h" #include "ebmlmktags.h" #include "ebmlmkattachments.h" +#include "ebmlmkchapters.h" #include "ebmlmktracks.h" #include "ebmlstringelement.h" #include "ebmluintelement.h" @@ -113,6 +114,19 @@ std::unique_ptr EBML::Element::factory(File &file) RETURN_ELEMENT_FOR_CASE(Id::MkCueCodecState); RETURN_ELEMENT_FOR_CASE(Id::MkCueReference); RETURN_ELEMENT_FOR_CASE(Id::MkCueRefTime); + RETURN_ELEMENT_FOR_CASE(Id::MkChapters); + RETURN_ELEMENT_FOR_CASE(Id::MkEditionEntry); + RETURN_ELEMENT_FOR_CASE(Id::MkEditionUID); + RETURN_ELEMENT_FOR_CASE(Id::MkEditionFlagDefault); + RETURN_ELEMENT_FOR_CASE(Id::MkEditionFlagOrdered); + RETURN_ELEMENT_FOR_CASE(Id::MkChapterAtom); + RETURN_ELEMENT_FOR_CASE(Id::MkChapterUID); + RETURN_ELEMENT_FOR_CASE(Id::MkChapterTimeStart); + RETURN_ELEMENT_FOR_CASE(Id::MkChapterTimeEnd); + RETURN_ELEMENT_FOR_CASE(Id::MkChapterFlagHidden); + RETURN_ELEMENT_FOR_CASE(Id::MkChapterDisplay); + RETURN_ELEMENT_FOR_CASE(Id::MkChapString); + RETURN_ELEMENT_FOR_CASE(Id::MkChapLanguage); } return std::make_unique(id, sizeLength, dataSize); } diff --git a/taglib/matroska/ebml/ebmlelement.h b/taglib/matroska/ebml/ebmlelement.h index 7487889c..950bcbd7 100644 --- a/taglib/matroska/ebml/ebmlelement.h +++ b/taglib/matroska/ebml/ebmlelement.h @@ -86,6 +86,19 @@ namespace TagLib::EBML { MkSamplingFrequency = 0xB5, MkBitDepth = 0x6264, MkChannels = 0x9F, + MkChapters = 0x1043A770, + MkEditionEntry = 0x45B9, + MkEditionUID = 0x45BC, + MkEditionFlagDefault = 0x45DB, + MkEditionFlagOrdered = 0x45DD, + MkChapterAtom = 0xB6, + MkChapterUID = 0x73C4, + MkChapterTimeStart = 0x91, + MkChapterTimeEnd = 0x92, + MkChapterFlagHidden = 0x98, + MkChapterDisplay = 0x80, + MkChapString = 0x85, + MkChapLanguage = 0x437C, }; Element(Id id, int sizeLength, offset_t dataSize) : @@ -134,6 +147,7 @@ namespace TagLib::EBML { class MkTags; class MkAttachments; class MkSeekHead; + class MkChapters; class MkCues; class VoidElement; @@ -196,6 +210,19 @@ namespace TagLib::EBML { template <> struct GetElementTypeById { using type = UTF8StringElement; }; template <> struct GetElementTypeById { using type = FloatElement; }; template <> struct GetElementTypeById { using type = MkSeekHead; }; + template <> struct GetElementTypeById { using type = MkChapters; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = UTF8StringElement; }; + template <> struct GetElementTypeById { using type = Latin1StringElement; }; template <> struct GetElementTypeById { using type = VoidElement; }; template ::type> diff --git a/taglib/matroska/ebml/ebmlmkchapters.cpp b/taglib/matroska/ebml/ebmlmkchapters.cpp new file mode 100644 index 00000000..92c6258d --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkchapters.cpp @@ -0,0 +1,102 @@ +/*************************************************************************** + copyright : (C) 2025 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 "ebmlmkchapters.h" +#include "ebmlstringelement.h" +#include "ebmluintelement.h" +#include "matroskachapters.h" +#include "matroskachapteredition.h" + +using namespace TagLib; + +std::unique_ptr EBML::MkChapters::parse() +{ + auto chapters = std::make_unique(); + chapters->setOffset(offset); + chapters->setSize(getSize()); + + for(const auto &element : elements) { + if(element->getId() != Id::MkEditionEntry) + continue; + + List editionChapters; + Matroska::ChapterEdition::UID editionUid = 0; + bool editionIsDefault = false; + bool editionIsOrdered = false; + auto edition = element_cast(element); + for(const auto &editionChild : *edition) { + Id id = editionChild->getId(); + if(id == Id::MkEditionUID) + editionUid = element_cast(editionChild)->getValue(); + else if(id == Id::MkEditionFlagDefault) + editionIsDefault = element_cast(editionChild)->getValue() != 0; + else if(id == Id::MkEditionFlagOrdered) + editionIsOrdered = element_cast(editionChild)->getValue() != 0; + else if(id == Id::MkChapterAtom) { + Matroska::Chapter::UID chapterUid = 0; + Matroska::Chapter::Time chapterTimeStart = 0; + Matroska::Chapter::Time chapterTimeEnd = 0; + List chapterDisplays; + bool chapterHidden = false; + auto chapterAtom = element_cast(editionChild); + for(const auto &chapterChild : *chapterAtom) { + Id cid = chapterChild->getId(); + if(cid == Id::MkChapterUID) + chapterUid = element_cast(chapterChild)->getValue(); + else if(cid == Id::MkChapterTimeStart) + chapterTimeStart = element_cast(chapterChild)->getValue(); + else if(cid == Id::MkChapterTimeEnd) + chapterTimeEnd = element_cast(chapterChild)->getValue(); + else if(cid == Id::MkChapterFlagHidden) + chapterHidden = element_cast(chapterChild)->getValue() != 0; + else if(cid == Id::MkChapterDisplay) { + auto display = element_cast(chapterChild); + String displayString; + String displayLanguage; + for(const auto &displayChild : *display) { + Id did = displayChild->getId(); + if(did == Id::MkChapString) + displayString = element_cast(displayChild)->getValue(); + else if(did == Id::MkChapLanguage) + displayLanguage = element_cast(displayChild)->getValue(); + } + if(!displayString.isEmpty()) { + chapterDisplays.append(Matroska::Chapter::Display(displayString, displayLanguage)); + } + } + } + if(chapterUid) { + editionChapters.append(Matroska::Chapter( + chapterTimeStart, chapterTimeEnd, chapterDisplays, chapterUid, chapterHidden)); + } + } + } + if(!editionChapters.isEmpty()) { + chapters->addChapterEdition(Matroska::ChapterEdition( + editionChapters, editionIsDefault, editionIsOrdered, editionUid)); + } + } + return chapters; +} diff --git a/taglib/matroska/ebml/ebmlmkchapters.h b/taglib/matroska/ebml/ebmlmkchapters.h new file mode 100644 index 00000000..e8aba3a4 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkchapters.h @@ -0,0 +1,59 @@ +/*************************************************************************** + copyright : (C) 2025 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_EBMLMKCHAPTERS_H +#define TAGLIB_EBMLMKCHAPTERS_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class Chapters; + } + namespace EBML { + class MkChapters : public MasterElement + { + public: + MkChapters(int sizeLength, offset_t dataSize, offset_t offset) : + MasterElement(Id::MkChapters, sizeLength, dataSize, offset) + { + } + MkChapters(Id, int sizeLength, offset_t dataSize, offset_t offset) : + MasterElement(Id::MkChapters, sizeLength, dataSize, offset) + { + } + MkChapters() : + MasterElement(Id::MkChapters, 0, 0, 0) + { + } + std::unique_ptr parse(); + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmksegment.cpp b/taglib/matroska/ebml/ebmlmksegment.cpp index c8e068e2..1eb1e8de 100644 --- a/taglib/matroska/ebml/ebmlmksegment.cpp +++ b/taglib/matroska/ebml/ebmlmksegment.cpp @@ -19,15 +19,11 @@ ***************************************************************************/ #include "ebmlmksegment.h" -#include "ebmlmktags.h" -#include "ebmlmkattachments.h" -#include "ebmlmkseekhead.h" -#include "ebmlmkinfo.h" -#include "ebmlmktracks.h" #include "ebmlutils.h" #include "matroskafile.h" #include "matroskatag.h" #include "matroskaattachments.h" +#include "matroskachapters.h" #include "matroskacues.h" #include "matroskaseekhead.h" #include "matroskasegment.h" @@ -80,6 +76,11 @@ bool EBML::MkSegment::read(File &file) if(!attachments->read(file)) return false; } + else if(id == Id::MkChapters) { + chapters = element_cast(std::move(element)); + if(!chapters->read(file)) + return false; + } else { if(id == Id::VoidElement && seekHead @@ -103,6 +104,11 @@ std::unique_ptr EBML::MkSegment::parseAttachments() return attachments ? attachments->parse() : nullptr; } +std::unique_ptr EBML::MkSegment::parseChapters() +{ + return chapters ? chapters->parse() : nullptr; +} + std::unique_ptr EBML::MkSegment::parseSeekHead() { return seekHead ? seekHead->parse(segmentDataOffset()) : nullptr; diff --git a/taglib/matroska/ebml/ebmlmksegment.h b/taglib/matroska/ebml/ebmlmksegment.h index b2c3e95b..1b8b91ea 100644 --- a/taglib/matroska/ebml/ebmlmksegment.h +++ b/taglib/matroska/ebml/ebmlmksegment.h @@ -25,6 +25,7 @@ #include "ebmlmasterelement.h" #include "ebmlmktags.h" #include "ebmlmkattachments.h" +#include "ebmlmkchapters.h" #include "ebmlmkseekhead.h" #include "ebmlmkcues.h" #include "ebmlmkinfo.h" @@ -35,6 +36,7 @@ namespace TagLib { namespace Matroska { class Tag; class Attachments; + class Chapters; class SeekHead; class Segment; } @@ -55,6 +57,7 @@ namespace TagLib { bool read(File &file) override; std::unique_ptr parseTag(); std::unique_ptr parseAttachments(); + std::unique_ptr parseChapters(); std::unique_ptr parseSeekHead(); std::unique_ptr parseCues(); std::unique_ptr parseSegment(); @@ -64,6 +67,7 @@ namespace TagLib { private: std::unique_ptr tags; std::unique_ptr attachments; + std::unique_ptr chapters; std::unique_ptr seekHead; std::unique_ptr cues; std::unique_ptr info; diff --git a/taglib/matroska/matroskachapter.cpp b/taglib/matroska/matroskachapter.cpp new file mode 100644 index 00000000..6e4c1b4e --- /dev/null +++ b/taglib/matroska/matroskachapter.cpp @@ -0,0 +1,156 @@ +/*************************************************************************** + copyright : (C) 2025 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 "matroskachapter.h" +#include "tstring.h" +#include "tbytevector.h" + +using namespace TagLib; + +class Matroska::Chapter::Display::DisplayPrivate +{ +public: + DisplayPrivate() = default; + ~DisplayPrivate() = default; + String string; + String language; +}; + +class Matroska::Chapter::ChapterPrivate +{ +public: + ChapterPrivate() = default; + ~ChapterPrivate() = default; + UID uid = 0; + Time timeStart = 0; + Time timeEnd = 0; + List displayList; + bool hidden = false; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::Chapter::Chapter(Time timeStart, Time timeEnd, + const List &displayList, UID uid, bool hidden) : + d(std::make_unique()) +{ + d->uid = uid; + d->timeStart = timeStart; + d->timeEnd = timeEnd; + d->displayList = displayList; + d->hidden = hidden; +} + +Matroska::Chapter::Chapter(const Chapter &other) : + d(std::make_unique(*other.d)) +{ +} + +Matroska::Chapter::Chapter(Chapter &&other) noexcept = default; + +Matroska::Chapter::~Chapter() = default; + +Matroska::Chapter &Matroska::Chapter::operator=(Chapter &&other) noexcept = default; + +Matroska::Chapter &Matroska::Chapter::operator=(const Chapter &other) +{ + Chapter(other).swap(*this); + return *this; +} + +void Matroska::Chapter::swap(Chapter &other) noexcept +{ + using std::swap; + + swap(d, other.d); +} + +Matroska::Chapter::UID Matroska::Chapter::uid() const +{ + return d->uid; +} + +Matroska::Chapter::Time Matroska::Chapter::timeStart() const +{ + return d->timeStart; +} + +Matroska::Chapter::Time Matroska::Chapter::timeEnd() const +{ + return d->timeEnd; +} + +bool Matroska::Chapter::isHidden() const +{ + return d->hidden; +} + +const List& Matroska::Chapter::displayList() const +{ + return d->displayList; +} + +Matroska::Chapter::Display::Display(const String &string, const String &language) : + d(std::make_unique()) +{ + d->string = string; + d->language = language; +} + +Matroska::Chapter::Display::Display(const Display &other) : + d(std::make_unique(*other.d)) +{ +} + +Matroska::Chapter::Display::Display(Display &&other) noexcept = default; + +Matroska::Chapter::Display::~Display() = default; + +Matroska::Chapter::Display& Matroska::Chapter::Display::operator=(const Display &other) +{ + Display(other).swap(*this); + return *this; +} + +Matroska::Chapter::Display& Matroska::Chapter::Display::operator=(Display &&other) noexcept = default; + +void Matroska::Chapter::Display::swap(Display &other) noexcept +{ + using std::swap; + + swap(d, other.d); +} + +const String& Matroska::Chapter::Display::string() const +{ + return d->string; +} + +const String& Matroska::Chapter::Display::language() const +{ + return d->language; +} diff --git a/taglib/matroska/matroskachapter.h b/taglib/matroska/matroskachapter.h new file mode 100644 index 00000000..9135b54f --- /dev/null +++ b/taglib/matroska/matroskachapter.h @@ -0,0 +1,174 @@ +/*************************************************************************** + copyright : (C) 2025 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_MATROSKACHAPTER_H +#define TAGLIB_MATROSKACHAPTER_H + +#include +#include "taglib_export.h" +#include "tlist.h" + +namespace TagLib { + namespace EBML { + class MkChapters; + } + + class String; + class ByteVector; + namespace Matroska { + //! Matroska chapter. + class TAGLIB_EXPORT Chapter + { + public: + using UID = unsigned long long; + using Time = unsigned long long; + + /*! + * Contains all possible strings to use for the chapter display. + */ + class TAGLIB_EXPORT Display + { + public: + /*! + * Construct a chapter display. + */ + Display(const String &string, const String &language); + + /*! + * Construct a chapter display as a copy of \a other. + */ + Display(const Display &other); + + /*! + * Construct a chapter display moving from \a other. + */ + Display(Display &&other) noexcept; + + /*! + * Destroys this chapter display. + */ + ~Display(); + + /*! + * Copies the contents of \a other into this object. + */ + Display &operator=(const Display &other); + + /*! + * Moves the contents of \a other into this object. + */ + Display &operator=(Display &&other) noexcept; + + /*! + * Exchanges the content of the object with the content of \a other. + */ + void swap(Display &other) noexcept; + + /*! + * Returns string representing the chapter. + */ + const String &string() const; + + /*! + * Returns language corresponding to the string. + */ + const String &language() const; + + private: + class DisplayPrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + + /*! + * Construct a chapter. + */ + Chapter(Time timeStart, Time timeEnd, const List &displayList, + UID uid, bool hidden = false); + + /*! + * Construct a chapter as a copy of \a other. + */ + Chapter(const Chapter &other); + + /*! + * Construct a chapter moving from \a other. + */ + Chapter(Chapter &&other) noexcept; + + /*! + * Destroys this chapter. + */ + ~Chapter(); + + /*! + * Copies the contents of \a other into this object. + */ + Chapter &operator=(const Chapter &other); + + /*! + * Moves the contents of \a other into this object. + */ + Chapter &operator=(Chapter &&other) noexcept; + + /*! + * Exchanges the content of the object with the content of \a other. + */ + void swap(Chapter &other) noexcept; + + /*! + * Returns the UID of the chapter. + */ + UID uid() const; + + /*! + * Returns the timestamp of the start of the chapter in nanoseconds. + */ + Time timeStart() const; + + /*! + * Returns the timestamp of the start of the chapter in nanoseconds. + */ + Time timeEnd() const; + + /*! + * Check if chapter is hidden. + */ + bool isHidden() const; + + /*! + * Returns strings with language. + */ + const List &displayList() const; + + private: + class ChapterPrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + } +} + +#endif diff --git a/taglib/matroska/matroskachapteredition.cpp b/taglib/matroska/matroskachapteredition.cpp new file mode 100644 index 00000000..2a2293a5 --- /dev/null +++ b/taglib/matroska/matroskachapteredition.cpp @@ -0,0 +1,101 @@ +/*************************************************************************** + copyright : (C) 2025 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 "matroskachapter.h" +#include "matroskachapteredition.h" +#include "tstring.h" +#include "tbytevector.h" +#include "tlist.h" + +using namespace TagLib; + +class Matroska::ChapterEdition::ChapterEditionPrivate +{ +public: + ChapterEditionPrivate() = default; + ~ChapterEditionPrivate() = default; + List chapters; + UID uid = 0; + bool flagDefault = false; + bool flagOrdered = false; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::ChapterEdition::ChapterEdition(const List &chapterList, + bool isDefault, bool isOrdered, UID uid) : + d(std::make_unique()) +{ + d->chapters = chapterList; + d->uid = uid; + d->flagDefault = isDefault; + d->flagOrdered = isOrdered; +} + +Matroska::ChapterEdition::ChapterEdition(const ChapterEdition &other) : + d(std::make_unique(*other.d)) +{ +} + +Matroska::ChapterEdition::ChapterEdition(ChapterEdition&& other) noexcept = default; + +Matroska::ChapterEdition::~ChapterEdition() = default; + +Matroska::ChapterEdition &Matroska::ChapterEdition::operator=(ChapterEdition &&other) = default; + +Matroska::ChapterEdition &Matroska::ChapterEdition::operator=(const ChapterEdition &other) +{ + ChapterEdition(other).swap(*this); + return *this; +} + +void Matroska::ChapterEdition::swap(ChapterEdition &other) noexcept +{ + using std::swap; + + swap(d, other.d); +} + +Matroska::ChapterEdition::UID Matroska::ChapterEdition::uid() const +{ + return d->uid; +} + +bool Matroska::ChapterEdition::isDefault() const +{ + return d->flagDefault; +} + +bool Matroska::ChapterEdition::isOrdered() const +{ + return d->flagOrdered; +} + +const List &Matroska::ChapterEdition::chapterList() const +{ + return d->chapters; +} diff --git a/taglib/matroska/matroskachapteredition.h b/taglib/matroska/matroskachapteredition.h new file mode 100644 index 00000000..6ed4c789 --- /dev/null +++ b/taglib/matroska/matroskachapteredition.h @@ -0,0 +1,106 @@ +/*************************************************************************** + copyright : (C) 2025 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_MATROSKACHAPTEREDITION_H +#define TAGLIB_MATROSKACHAPTEREDITION_H + +#include "matroskachapter.h" + +namespace TagLib { + class String; + class ByteVector; + namespace Matroska { + //! Edition of chapters. + class TAGLIB_EXPORT ChapterEdition + { + public: + using UID = unsigned long long; + + /*! + * Construct an edition. + */ + ChapterEdition(const List &chapterList, + bool isDefault, bool isOrdered = false, UID uid = 0); + + /*! + * Construct an edition as a copy of \a other. + */ + ChapterEdition(const ChapterEdition &other); + + /*! + * Construct an edition moving from \a other. + */ + ChapterEdition(ChapterEdition &&other) noexcept; + + /*! + * Destroys this edition. + */ + ~ChapterEdition(); + + /*! + * Copies the contents of \a other into this object. + */ + ChapterEdition &operator=(const ChapterEdition &other); + + /*! + * Moves the contents of \a other into this object. + */ + ChapterEdition &operator=(ChapterEdition &&other); + + /*! + * Exchanges the content of the object with the content of \a other. + */ + void swap(ChapterEdition &other) noexcept; + + /*! + * Returns the UID of the edition. + */ + UID uid() const; + + /*! + * Check if this edition should be used as the default one. + */ + bool isDefault() const; + + /*! + * Check if the chapters can be defined multiple times and the order to + * play them is enforced. + */ + bool isOrdered() const; + + /*! + * Get the list of all chapters. + */ + const List &chapterList() const; + + private: + class ChapterEditionPrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + } +} + +#endif diff --git a/taglib/matroska/matroskachapters.cpp b/taglib/matroska/matroskachapters.cpp new file mode 100644 index 00000000..8f94525e --- /dev/null +++ b/taglib/matroska/matroskachapters.cpp @@ -0,0 +1,152 @@ +/*************************************************************************** + copyright : (C) 2025 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 "matroskachapters.h" +#include +#include "matroskachapteredition.h" +#include "ebmlstringelement.h" +#include "ebmlbinaryelement.h" +#include "ebmlmkchapters.h" +#include "ebmluintelement.h" +#include "ebmlutils.h" +#include "tlist.h" +#include "tbytevector.h" + +using namespace TagLib; + +class Matroska::Chapters::ChaptersPrivate +{ +public: + ChaptersPrivate() = default; + ~ChaptersPrivate() = default; + ChaptersPrivate(const ChaptersPrivate &) = delete; + ChaptersPrivate &operator=(const ChaptersPrivate &) = delete; + ChapterEditionList editions; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::Chapters::Chapters() : + Element(static_cast(EBML::Element::Id::MkChapters)), + d(std::make_unique()) +{ +} + +Matroska::Chapters::~Chapters() = default; + +void Matroska::Chapters::addChapterEdition(const ChapterEdition& edition) +{ + d->editions.append(edition); + setNeedsRender(true); +} + +void Matroska::Chapters::removeChapterEdition(unsigned long long uid) +{ + auto it = std::find_if(d->editions.begin(), d->editions.end(), + [uid](const ChapterEdition& file) { + return file.uid() == uid; + }); + if(it != d->editions.end()) { + d->editions.erase(it); + setNeedsRender(true); + } +} + +void Matroska::Chapters::clear() +{ + d->editions.clear(); + setNeedsRender(true); +} + +const Matroska::Chapters::ChapterEditionList &Matroska::Chapters::chapterEditionList() const +{ + return d->editions; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// +ByteVector Matroska::Chapters::renderInternal() +{ + if(d->editions.isEmpty()) { + // Avoid writing a Chapters element without ChapterEdition element. + return {}; + } + + EBML::MkChapters chapters; + for(const auto &chapterEdition : std::as_const(d->editions)) { + auto chapterEditionElement = EBML::make_unique_element(); + + if(auto uid = chapterEdition.uid()) { + auto uidElement = EBML::make_unique_element(); + uidElement->setValue(uid); + chapterEditionElement->appendElement(std::move(uidElement)); + } + auto defaultElement = EBML::make_unique_element(); + defaultElement->setValue(chapterEdition.isDefault()); + chapterEditionElement->appendElement(std::move(defaultElement)); + auto orderedElement = EBML::make_unique_element(); + orderedElement->setValue(chapterEdition.isOrdered()); + chapterEditionElement->appendElement(std::move(orderedElement)); + + for(const auto &chapter : chapterEdition.chapterList()) { + auto chapterElement = EBML::make_unique_element(); + + auto cuidElement = EBML::make_unique_element(); + auto cuid = chapter.uid(); + cuidElement->setValue(cuid ? cuid : EBML::randomUID()); + chapterElement->appendElement(std::move(cuidElement)); + auto timeStartElement = EBML::make_unique_element(); + timeStartElement->setValue(chapter.timeStart()); + chapterElement->appendElement(std::move(timeStartElement)); + auto timeEndElement = EBML::make_unique_element(); + timeEndElement->setValue(chapter.timeEnd()); + chapterElement->appendElement(std::move(timeEndElement)); + auto hiddenElement = EBML::make_unique_element(); + hiddenElement->setValue(chapter.isHidden()); + chapterElement->appendElement(std::move(hiddenElement)); + + for(const auto& display : chapter.displayList()) { + auto displayElement = EBML::make_unique_element(); + + auto stringElement = EBML::make_unique_element(); + stringElement->setValue(display.string()); + displayElement->appendElement(std::move(stringElement)); + auto languageElement = EBML::make_unique_element(); + languageElement->setValue(display.language()); + displayElement->appendElement(std::move(languageElement)); + + chapterElement->appendElement(std::move(displayElement)); + } + + chapterEditionElement->appendElement(std::move(chapterElement)); + } + + chapters.appendElement(std::move(chapterEditionElement)); + } + return chapters.render(); +} diff --git a/taglib/matroska/matroskachapters.h b/taglib/matroska/matroskachapters.h new file mode 100644 index 00000000..c235b74f --- /dev/null +++ b/taglib/matroska/matroskachapters.h @@ -0,0 +1,83 @@ +/*************************************************************************** + copyright : (C) 2025 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_MATROSKACHAPTERS_H +#define TAGLIB_MATROSKACHAPTERS_H + +#include +#include "taglib_export.h" +#include "tlist.h" +#include "matroskaelement.h" + +namespace TagLib { + class File; + namespace EBML { + class MkChapters; + } + namespace Matroska { + class ChapterEdition; + class File; + + //! Collection of chapter editions. + class TAGLIB_EXPORT Chapters +#ifndef DO_NOT_DOCUMENT + : private Element +#endif + { + public: + using ChapterEditionList = List; + //! Construct chapters. + Chapters(); + + //! Destroy chapters. + virtual ~Chapters(); + + //! Add a chapter edition. + void addChapterEdition(const ChapterEdition &edition); + + //! Remove a chapter edition. + void removeChapterEdition(unsigned long long uid); + + //! Remove all chapter editions. + void clear(); + + //! Get list of all chapter editions. + const ChapterEditionList &chapterEditionList() const; + + private: + friend class EBML::MkChapters; + friend class File; + class ChaptersPrivate; + + // private Element implementation + ByteVector renderInternal() override; + + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + } +} + +#endif diff --git a/taglib/matroska/matroskafile.cpp b/taglib/matroska/matroskafile.cpp index e0accda1..2cf83a4e 100644 --- a/taglib/matroska/matroskafile.cpp +++ b/taglib/matroska/matroskafile.cpp @@ -22,6 +22,9 @@ #include "matroskatag.h" #include "matroskaattachments.h" #include "matroskaattachedfile.h" +#include "matroskachapter.h" +#include "matroskachapteredition.h" +#include "matroskachapters.h" #include "matroskaseekhead.h" #include "matroskacues.h" #include "matroskasegment.h" @@ -50,6 +53,7 @@ public: std::unique_ptr tag; std::unique_ptr attachments; + std::unique_ptr chapters; std::unique_ptr seekHead; std::unique_ptr cues; std::unique_ptr segment; @@ -179,12 +183,64 @@ StringList Matroska::File::complexPropertyKeys() const } } } + if(d->chapters && !d->chapters->chapterEditionList().isEmpty()) { + keys.append("CHAPTERS"); + } return keys; } List Matroska::File::complexProperties(const String &key) const { List props = TagLib::File::complexProperties(key); + if(key.upper() == "CHAPTERS") { + if(d->chapters) { + for(const auto &edition : d->chapters->chapterEditionList()) { + VariantMap property; + if(auto uid = edition.uid()) { + property.insert("uid", uid); + } + if(auto isDefault = edition.isDefault()) { + property.insert("isDefault", isDefault); + } + if(auto isOrdered = edition.isOrdered()) { + property.insert("isOrdered", isOrdered); + } + if(auto chapters = edition.chapterList(); !chapters.isEmpty()) { + VariantList chaps; + for(const auto &chapter : chapters) { + VariantMap chap; + if(auto uid = chapter.uid()) { + chap.insert("uid", uid); + } + if(auto isHidden = chapter.isHidden()) { + chap.insert("isHidden", isHidden); + } + chap.insert("timeStart", chapter.timeStart()); + if(auto timeEnd = chapter.timeEnd()) { + chap.insert("timeEnd", timeEnd); + } + if(auto displays = chapter.displayList(); !displays.isEmpty()) { + VariantList disps; + for(const auto &display : displays) { + VariantMap disp; + if(auto str = display.string(); !str.isEmpty()) { + disp.insert("string", str); + } + if(auto language = display.language(); !language.isEmpty()) { + disp.insert("language", language); + } + disps.append(disp); + } + chap.insert("displays", disps); + } + chaps.append(chap); + } + property.insert("chapters", chaps); + } + props.append(property); + } + } + } if(d->attachments) { const auto &attachedFiles = d->attachments->attachedFileList(); for(const auto &attachedFile : attachedFiles) { @@ -208,6 +264,37 @@ bool Matroska::File::setComplexProperties(const String &key, const Listclear(); + for(const auto &ed : value) { + List editionChapters; + const auto chaps = ed.value("chapters").toList(); + for(const auto &chapVar : chaps) { + auto chap = chapVar.toMap(); + const auto disps = chap.value("displays").toList(); + List chapterDisplays; + for(const auto &dispVar : disps) { + auto disp = dispVar.toMap(); + chapterDisplays.append(Chapter::Display( + disp.value("string").toString(), + disp.value("language").toString())); + } + editionChapters.append(Chapter( + chap.value("timeStart").toULongLong(), + chap.value("timeEnd").toULongLong(), + chapterDisplays, + chap.value("uid", 0ULL).toULongLong(), + chap.value("isHidden", false).toBool())); + } + d->chapters->addChapterEdition(ChapterEdition( + editionChapters, + ed.value("isDefault", false).toBool(), + ed.value("isOrdered", false).toBool(), + ed.value("uid", 0ULL).toULongLong())); + } + return true; + } + List &files = attachments(true)->attachedFiles(); for(auto it = files.begin(); it != files.end();) { if(keyMatchesAttachedFile(key, *it)) { @@ -268,6 +355,13 @@ Matroska::Attachments *Matroska::File::attachments(bool create) const return d->attachments.get(); } +Matroska::Chapters *Matroska::File::chapters(bool create) const +{ + if(!d->chapters && create) + d->chapters = std::make_unique(); + return d->chapters.get(); +} + void Matroska::File::read(bool readProperties, Properties::ReadStyle readStyle) { offset_t fileLength = length(); @@ -312,6 +406,7 @@ void Matroska::File::read(bool readProperties, Properties::ReadStyle readStyle) d->cues = segment->parseCues(); d->tag = segment->parseTag(); d->attachments = segment->parseAttachments(); + d->chapters = segment->parseChapters(); if(readProperties) { d->properties = std::make_unique(this); @@ -355,8 +450,13 @@ bool Matroska::File::save() return false; } - // Do not create new attachments or tags and corresponding seek head entries - // if only empty objects were created. + // Do not create new attachments, chapters or tags and corresponding + // seek head entries if only empty objects were created. + if(d->chapters && d->chapters->chapterEditionList().isEmpty() && + d->chapters->size() == 0 && d->chapters->offset() == 0 && + d->chapters->data().isEmpty()) { + d->chapters.reset(); + } if(d->attachments && d->attachments->attachedFileList().isEmpty() && d->attachments->size() == 0 && d->attachments->offset() == 0 && d->attachments->data().isEmpty()) { @@ -373,6 +473,7 @@ bool Matroska::File::save() // List of all possible elements we can write List elements { + d->chapters.get(), d->attachments.get(), d->tag.get() }; @@ -381,7 +482,7 @@ bool Matroska::File::save() * to the end of the file. For new elements, * the order is from least likely to change, * to most likely to change: - * 1. Bookmarks (todo) + * 1. Chapters * 2. Attachments * 3. Tags */ diff --git a/taglib/matroska/matroskafile.h b/taglib/matroska/matroskafile.h index af57e5a6..71cd9e7c 100644 --- a/taglib/matroska/matroskafile.h +++ b/taglib/matroska/matroskafile.h @@ -30,6 +30,7 @@ namespace TagLib::Matroska { class Properties; class Tag; class Attachments; + class Chapters; /*! * Implementation of TagLib::File for Matroska. @@ -143,8 +144,26 @@ namespace TagLib::Matroska { */ bool save() override; + /*! + * Returns a pointer to the attachments of the file. + * + * If \a create is \c false this may return a null pointer if there are no + * attachments. + * If \a create is \c true it will create attachments if none exist and + * returns a valid pointer. + */ Attachments *attachments(bool create = false) const; + /*! + * Returns a pointer to the chapters of the file. + * + * If \a create is \c false this may return a null pointer if there are no + * chapters. + * If \a create is \c true it will create chapters if none exist and + * returns a valid pointer. + */ + Chapters *chapters(bool create = false) const; + /*! * Returns whether or not the given \a stream can be opened as a Matroska * file. diff --git a/tests/test_matroska.cpp b/tests/test_matroska.cpp index ca1cd0e0..a9d178b3 100644 --- a/tests/test_matroska.cpp +++ b/tests/test_matroska.cpp @@ -130,6 +130,9 @@ #include "matroskatag.h" #include "matroskaattachments.h" #include "matroskaattachedfile.h" +#include "matroskachapter.h" +#include "matroskachapteredition.h" +#include "matroskachapters.h" #include "matroskasimpletag.h" #include "plainfile.h" #include @@ -152,6 +155,7 @@ class TestMatroska : public CppUnit::TestFixture CPPUNIT_TEST(testComplexProperties); CPPUNIT_TEST(testOpenInvalid); CPPUNIT_TEST(testSegmentSizeChange); + CPPUNIT_TEST(testChapters); CPPUNIT_TEST_SUITE_END(); public: @@ -995,6 +999,208 @@ public: } } + void testChapters() + { + const Matroska::ChapterEdition edition1( + List{ + Matroska::Chapter( + 0, 40000, + List{ + Matroska::Chapter::Display("Chapter 1", "eng")}, + 1, false), + Matroska::Chapter( + 40000, 80000, + List{ + Matroska::Chapter::Display("Chapter 2", "eng"), + Matroska::Chapter::Display("Kapitel 2", "deu"), + }, + 2), + Matroska::Chapter( + 80000, 120000, + List{ + Matroska::Chapter::Display("Chapter 3", "und")}, + 3, true) + }, + true, false); + const VariantMap chapterEdition1 { + {"chapters", + VariantList{ + VariantMap{ + {"displays", VariantList{ + VariantMap{{"language", "eng"}, {"string", "Chapter 1"}}}}, + {"timeEnd", 40000ULL}, + {"timeStart", 0ULL}, + {"uid", 1ULL} + }, + VariantMap{ + {"displays", VariantList{ + VariantMap{{"language", "eng"}, {"string", "Chapter 2"}}, + VariantMap{{"language", "deu"}, {"string", "Kapitel 2"}}}}, + {"timeEnd", 80000ULL}, + {"timeStart", 40000ULL}, + {"uid", 2ULL} + }, + VariantMap{ + { + "displays", VariantList{ + VariantMap{{"language", "und"}, {"string", "Chapter 3"}}} + }, + {"isHidden", true}, + {"timeEnd", 120000ULL}, + {"timeStart", 80000ULL}, + {"uid", 3ULL} + } + } + }, + {"isDefault", true} + }; + const VariantMap chapterEdition2 { + {"chapters", + VariantList{ + VariantMap{ + {"displays", VariantList{ + VariantMap{{"string", "Chapter A"}}}}, + {"timeStart", 10000ULL}, + {"uid", 1234567890ULL} + }, + } + }, + {"isOrdered", true}, + {"uid", 321ULL} + }; + + ScopedFileCopy copy("tags-before-cues", ".mkv"); + string newname = copy.fileName(); + { + Matroska::File f(newname.c_str()); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + CPPUNIT_ASSERT(!f.chapters(false)); + CPPUNIT_ASSERT_EQUAL(StringList({"DURATION"}), f.complexPropertyKeys()); + CPPUNIT_ASSERT(f.complexProperties("CHAPTERS").isEmpty()); + + f.chapters(true)->addChapterEdition(edition1); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str()); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + auto chapters = f.chapters(false); + CPPUNIT_ASSERT(chapters); + CPPUNIT_ASSERT_EQUAL(StringList({"DURATION", "CHAPTERS"}), f.complexPropertyKeys()); + auto chaptersProperties = f.complexProperties("CHAPTERS"); + CPPUNIT_ASSERT_EQUAL(1U, chaptersProperties.size()); + CPPUNIT_ASSERT_EQUAL(chapterEdition1, chaptersProperties.front()); + + CPPUNIT_ASSERT_EQUAL(1U, chapters->chapterEditionList().size()); + const auto &edition = chapters->chapterEditionList().front(); + CPPUNIT_ASSERT_EQUAL(true, edition.isDefault()); + CPPUNIT_ASSERT_EQUAL(false, edition.isOrdered()); + CPPUNIT_ASSERT_EQUAL(0ULL, edition.uid()); + const auto &chapterAtoms = edition.chapterList(); + CPPUNIT_ASSERT_EQUAL(3U, chapterAtoms.size()); + CPPUNIT_ASSERT_EQUAL(1ULL, chapterAtoms[0].uid()); + CPPUNIT_ASSERT_EQUAL(false, chapterAtoms[0].isHidden()); + CPPUNIT_ASSERT_EQUAL(0ULL, chapterAtoms[0].timeStart()); + CPPUNIT_ASSERT_EQUAL(40000ULL, chapterAtoms[0].timeEnd()); + CPPUNIT_ASSERT_EQUAL(1U, chapterAtoms[0].displayList().size()); + CPPUNIT_ASSERT_EQUAL(String("Chapter 1"), chapterAtoms[0].displayList()[0].string()); + CPPUNIT_ASSERT_EQUAL(String("eng"), chapterAtoms[0].displayList()[0].language()); + CPPUNIT_ASSERT_EQUAL(2ULL, chapterAtoms[1].uid()); + CPPUNIT_ASSERT_EQUAL(false, chapterAtoms[1].isHidden()); + CPPUNIT_ASSERT_EQUAL(40000ULL, chapterAtoms[1].timeStart()); + CPPUNIT_ASSERT_EQUAL(80000ULL, chapterAtoms[1].timeEnd()); + CPPUNIT_ASSERT_EQUAL(2U, chapterAtoms[1].displayList().size()); + CPPUNIT_ASSERT_EQUAL(String("Chapter 2"), chapterAtoms[1].displayList()[0].string()); + CPPUNIT_ASSERT_EQUAL(String("eng"), chapterAtoms[1].displayList()[0].language()); + CPPUNIT_ASSERT_EQUAL(String("Kapitel 2"), chapterAtoms[1].displayList()[1].string()); + CPPUNIT_ASSERT_EQUAL(String("deu"), chapterAtoms[1].displayList()[1].language()); + CPPUNIT_ASSERT_EQUAL(3ULL, chapterAtoms[2].uid()); + CPPUNIT_ASSERT_EQUAL(true, chapterAtoms[2].isHidden()); + CPPUNIT_ASSERT_EQUAL(80000ULL, chapterAtoms[2].timeStart()); + CPPUNIT_ASSERT_EQUAL(120000ULL, chapterAtoms[2].timeEnd()); + CPPUNIT_ASSERT_EQUAL(1U, chapterAtoms[2].displayList().size()); + CPPUNIT_ASSERT_EQUAL(String("Chapter 3"), chapterAtoms[2].displayList()[0].string()); + CPPUNIT_ASSERT_EQUAL(String("und"), chapterAtoms[2].displayList()[0].language()); + + CPPUNIT_ASSERT(f.setComplexProperties("CHAPTERS", {chapterEdition2})); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + auto chapters = f.chapters(false); + CPPUNIT_ASSERT(chapters); + CPPUNIT_ASSERT_EQUAL(StringList({"DURATION", "CHAPTERS"}), f.complexPropertyKeys()); + auto chaptersProperties = f.complexProperties("CHAPTERS"); + CPPUNIT_ASSERT_EQUAL(1U, chaptersProperties.size()); + CPPUNIT_ASSERT_EQUAL(chapterEdition2, chaptersProperties.front()); + + CPPUNIT_ASSERT_EQUAL(1U, chapters->chapterEditionList().size()); + const auto &edition = chapters->chapterEditionList().front(); + CPPUNIT_ASSERT_EQUAL(false, edition.isDefault()); + CPPUNIT_ASSERT_EQUAL(true, edition.isOrdered()); + CPPUNIT_ASSERT_EQUAL(321ULL, edition.uid()); + const auto &chapterAtoms = edition.chapterList(); + CPPUNIT_ASSERT_EQUAL(1U, chapterAtoms.size()); + CPPUNIT_ASSERT_EQUAL(1234567890ULL, chapterAtoms[0].uid()); + CPPUNIT_ASSERT_EQUAL(false, chapterAtoms[0].isHidden()); + CPPUNIT_ASSERT_EQUAL(10000ULL, chapterAtoms[0].timeStart()); + CPPUNIT_ASSERT_EQUAL(0ULL, chapterAtoms[0].timeEnd()); + CPPUNIT_ASSERT_EQUAL(1U, chapterAtoms[0].displayList().size()); + CPPUNIT_ASSERT_EQUAL(String("Chapter A"), chapterAtoms[0].displayList()[0].string()); + CPPUNIT_ASSERT_EQUAL(String(), chapterAtoms[0].displayList()[0].language()); + + const Matroska::ChapterEdition edition2 = chapters->chapterEditionList().front(); + chapters->removeChapterEdition(321ULL); + chapters->addChapterEdition(edition1); + chapters->addChapterEdition(edition2); + + Matroska::AttachedFile attachedFile; + attachedFile.setFileName("folder.png"); + attachedFile.setMediaType("image/png"); + attachedFile.setDescription("Cover"); + attachedFile.setData(ByteVector("PNG data")); + attachedFile.setUID(1763187649ULL); + f.attachments(true)->addAttachedFile(attachedFile); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.tag(false)); + CPPUNIT_ASSERT(f.attachments(false)); + CPPUNIT_ASSERT(f.chapters(false)); + + CPPUNIT_ASSERT_EQUAL(StringList({"DURATION", "PICTURE", "CHAPTERS"}), + f.complexPropertyKeys()); + auto chaptersProperties = f.complexProperties("CHAPTERS"); + CPPUNIT_ASSERT_EQUAL(2U, chaptersProperties.size()); + CPPUNIT_ASSERT_EQUAL(chapterEdition1, chaptersProperties.front()); + CPPUNIT_ASSERT_EQUAL(chapterEdition2, chaptersProperties.back()); + + f.attachments()->clear(); + f.chapters()->clear(); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + } + + // Check if file with initial tags is same as original file + const ByteVector origData = PlainFile(TEST_FILE_PATH_C("tags-before-cues.mkv")).readAll(); + const ByteVector fileData = PlainFile(newname.c_str()).readAll(); + CPPUNIT_ASSERT(origData == fileData); + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestMatroska); diff --git a/tests/test_sizes.cpp b/tests/test_sizes.cpp index 550919fc..aaae162d 100644 --- a/tests/test_sizes.cpp +++ b/tests/test_sizes.cpp @@ -158,6 +158,7 @@ #include "matroskaattachedfile.h" #include "matroskaattachments.h" +#include "matroskachapters.h" using namespace std; using namespace TagLib; @@ -312,6 +313,7 @@ public: CPPUNIT_ASSERT_EQUAL(classSize(3, true), sizeof(TagLib::Matroska::Tag)); CPPUNIT_ASSERT_EQUAL(classSize(0, true), sizeof(TagLib::Matroska::Element)); CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::Matroska::Attachments)); + CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::Matroska::Chapters)); CPPUNIT_ASSERT_EQUAL(classSize(0, false), sizeof(TagLib::Matroska::SimpleTag)); CPPUNIT_ASSERT_EQUAL(classSize(0, false), sizeof(TagLib::Matroska::AttachedFile)); #endif