Support for Matroska chapters

This commit is contained in:
Urs Fleisch
2025-11-12 16:55:21 +01:00
parent d2bf7e72d2
commit b625be5690
18 changed files with 1364 additions and 10 deletions

View File

@ -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<const TagLib::Matroska::Properties *>(file.audioProperties())) {
printf("Properties:\n");
PRINT_PRETTY("Doc Type", properties->docType().toCString(false));

View File

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

View File

@ -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> 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<Element>(id, sizeLength, dataSize);
}

View File

@ -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<Element::Id::MkTitle> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkSamplingFrequency> { using type = FloatElement; };
template <> struct GetElementTypeById<Element::Id::MkSeekHead> { using type = MkSeekHead; };
template <> struct GetElementTypeById<Element::Id::MkChapters> { using type = MkChapters; };
template <> struct GetElementTypeById<Element::Id::MkEditionEntry> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkEditionUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkEditionFlagDefault> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkEditionFlagOrdered> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterAtom> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterTimeStart> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterTimeEnd> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterFlagHidden> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterDisplay> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkChapString> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkChapLanguage> { using type = Latin1StringElement; };
template <> struct GetElementTypeById<Element::Id::VoidElement> { using type = VoidElement; };
template <Element::Id ID, typename T=typename GetElementTypeById<ID>::type>

View File

@ -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<Matroska::Chapters> EBML::MkChapters::parse()
{
auto chapters = std::make_unique<Matroska::Chapters>();
chapters->setOffset(offset);
chapters->setSize(getSize());
for(const auto &element : elements) {
if(element->getId() != Id::MkEditionEntry)
continue;
List<Matroska::Chapter> editionChapters;
Matroska::ChapterEdition::UID editionUid = 0;
bool editionIsDefault = false;
bool editionIsOrdered = false;
auto edition = element_cast<Id::MkEditionEntry>(element);
for(const auto &editionChild : *edition) {
Id id = editionChild->getId();
if(id == Id::MkEditionUID)
editionUid = element_cast<Id::MkEditionUID>(editionChild)->getValue();
else if(id == Id::MkEditionFlagDefault)
editionIsDefault = element_cast<Id::MkEditionFlagDefault>(editionChild)->getValue() != 0;
else if(id == Id::MkEditionFlagOrdered)
editionIsOrdered = element_cast<Id::MkEditionFlagOrdered>(editionChild)->getValue() != 0;
else if(id == Id::MkChapterAtom) {
Matroska::Chapter::UID chapterUid = 0;
Matroska::Chapter::Time chapterTimeStart = 0;
Matroska::Chapter::Time chapterTimeEnd = 0;
List<Matroska::Chapter::Display> chapterDisplays;
bool chapterHidden = false;
auto chapterAtom = element_cast<Id::MkChapterAtom>(editionChild);
for(const auto &chapterChild : *chapterAtom) {
Id cid = chapterChild->getId();
if(cid == Id::MkChapterUID)
chapterUid = element_cast<Id::MkChapterUID>(chapterChild)->getValue();
else if(cid == Id::MkChapterTimeStart)
chapterTimeStart = element_cast<Id::MkChapterTimeStart>(chapterChild)->getValue();
else if(cid == Id::MkChapterTimeEnd)
chapterTimeEnd = element_cast<Id::MkChapterTimeEnd>(chapterChild)->getValue();
else if(cid == Id::MkChapterFlagHidden)
chapterHidden = element_cast<Id::MkChapterFlagHidden>(chapterChild)->getValue() != 0;
else if(cid == Id::MkChapterDisplay) {
auto display = element_cast<Id::MkChapterDisplay>(chapterChild);
String displayString;
String displayLanguage;
for(const auto &displayChild : *display) {
Id did = displayChild->getId();
if(did == Id::MkChapString)
displayString = element_cast<Id::MkChapString>(displayChild)->getValue();
else if(did == Id::MkChapLanguage)
displayLanguage = element_cast<Id::MkChapLanguage>(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;
}

View File

@ -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<Matroska::Chapters> parse();
};
}
}
#endif
#endif

View File

@ -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<Id::MkChapters>(std::move(element));
if(!chapters->read(file))
return false;
}
else {
if(id == Id::VoidElement
&& seekHead
@ -103,6 +104,11 @@ std::unique_ptr<Matroska::Attachments> EBML::MkSegment::parseAttachments()
return attachments ? attachments->parse() : nullptr;
}
std::unique_ptr<Matroska::Chapters> EBML::MkSegment::parseChapters()
{
return chapters ? chapters->parse() : nullptr;
}
std::unique_ptr<Matroska::SeekHead> EBML::MkSegment::parseSeekHead()
{
return seekHead ? seekHead->parse(segmentDataOffset()) : nullptr;

View File

@ -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<Matroska::Tag> parseTag();
std::unique_ptr<Matroska::Attachments> parseAttachments();
std::unique_ptr<Matroska::Chapters> parseChapters();
std::unique_ptr<Matroska::SeekHead> parseSeekHead();
std::unique_ptr<Matroska::Cues> parseCues();
std::unique_ptr<Matroska::Segment> parseSegment();
@ -64,6 +67,7 @@ namespace TagLib {
private:
std::unique_ptr<MkTags> tags;
std::unique_ptr<MkAttachments> attachments;
std::unique_ptr<MkChapters> chapters;
std::unique_ptr<MkSeekHead> seekHead;
std::unique_ptr<MkCues> cues;
std::unique_ptr<MkInfo> info;

View File

@ -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<Display> displayList;
bool hidden = false;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::Chapter::Chapter(Time timeStart, Time timeEnd,
const List<Display> &displayList, UID uid, bool hidden) :
d(std::make_unique<ChapterPrivate>())
{
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<ChapterPrivate>(*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::Display>& Matroska::Chapter::displayList() const
{
return d->displayList;
}
Matroska::Chapter::Display::Display(const String &string, const String &language) :
d(std::make_unique<DisplayPrivate>())
{
d->string = string;
d->language = language;
}
Matroska::Chapter::Display::Display(const Display &other) :
d(std::make_unique<DisplayPrivate>(*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;
}

View File

@ -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 <memory>
#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<DisplayPrivate> d;
};
/*!
* Construct a chapter.
*/
Chapter(Time timeStart, Time timeEnd, const List<Display> &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<Display> &displayList() const;
private:
class ChapterPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ChapterPrivate> d;
};
}
}
#endif

View File

@ -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<Chapter> chapters;
UID uid = 0;
bool flagDefault = false;
bool flagOrdered = false;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::ChapterEdition::ChapterEdition(const List<Chapter> &chapterList,
bool isDefault, bool isOrdered, UID uid) :
d(std::make_unique<ChapterEditionPrivate>())
{
d->chapters = chapterList;
d->uid = uid;
d->flagDefault = isDefault;
d->flagOrdered = isOrdered;
}
Matroska::ChapterEdition::ChapterEdition(const ChapterEdition &other) :
d(std::make_unique<ChapterEditionPrivate>(*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::Chapter> &Matroska::ChapterEdition::chapterList() const
{
return d->chapters;
}

View File

@ -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<Chapter> &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<Chapter> &chapterList() const;
private:
class ChapterEditionPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ChapterEditionPrivate> d;
};
}
}
#endif

View File

@ -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 <memory>
#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<ID>(EBML::Element::Id::MkChapters)),
d(std::make_unique<ChaptersPrivate>())
{
}
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<EBML::Element::Id::MkEditionEntry>();
if(auto uid = chapterEdition.uid()) {
auto uidElement = EBML::make_unique_element<EBML::Element::Id::MkEditionUID>();
uidElement->setValue(uid);
chapterEditionElement->appendElement(std::move(uidElement));
}
auto defaultElement = EBML::make_unique_element<EBML::Element::Id::MkEditionFlagDefault>();
defaultElement->setValue(chapterEdition.isDefault());
chapterEditionElement->appendElement(std::move(defaultElement));
auto orderedElement = EBML::make_unique_element<EBML::Element::Id::MkEditionFlagOrdered>();
orderedElement->setValue(chapterEdition.isOrdered());
chapterEditionElement->appendElement(std::move(orderedElement));
for(const auto &chapter : chapterEdition.chapterList()) {
auto chapterElement = EBML::make_unique_element<EBML::Element::Id::MkChapterAtom>();
auto cuidElement = EBML::make_unique_element<EBML::Element::Id::MkChapterUID>();
auto cuid = chapter.uid();
cuidElement->setValue(cuid ? cuid : EBML::randomUID());
chapterElement->appendElement(std::move(cuidElement));
auto timeStartElement = EBML::make_unique_element<EBML::Element::Id::MkChapterTimeStart>();
timeStartElement->setValue(chapter.timeStart());
chapterElement->appendElement(std::move(timeStartElement));
auto timeEndElement = EBML::make_unique_element<EBML::Element::Id::MkChapterTimeEnd>();
timeEndElement->setValue(chapter.timeEnd());
chapterElement->appendElement(std::move(timeEndElement));
auto hiddenElement = EBML::make_unique_element<EBML::Element::Id::MkChapterFlagHidden>();
hiddenElement->setValue(chapter.isHidden());
chapterElement->appendElement(std::move(hiddenElement));
for(const auto& display : chapter.displayList()) {
auto displayElement = EBML::make_unique_element<EBML::Element::Id::MkChapterDisplay>();
auto stringElement = EBML::make_unique_element<EBML::Element::Id::MkChapString>();
stringElement->setValue(display.string());
displayElement->appendElement(std::move(stringElement));
auto languageElement = EBML::make_unique_element<EBML::Element::Id::MkChapLanguage>();
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();
}

View File

@ -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 <memory>
#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<ChapterEdition>;
//! 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<ChaptersPrivate> d;
};
}
}
#endif

View File

@ -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> tag;
std::unique_ptr<Attachments> attachments;
std::unique_ptr<Chapters> chapters;
std::unique_ptr<SeekHead> seekHead;
std::unique_ptr<Cues> cues;
std::unique_ptr<Segment> segment;
@ -179,12 +183,64 @@ StringList Matroska::File::complexPropertyKeys() const
}
}
}
if(d->chapters && !d->chapters->chapterEditionList().isEmpty()) {
keys.append("CHAPTERS");
}
return keys;
}
List<VariantMap> Matroska::File::complexProperties(const String &key) const
{
List<VariantMap> 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 List<VariantM
return true;
}
if(key.upper() == "CHAPTERS") {
chapters(true)->clear();
for(const auto &ed : value) {
List<Chapter> 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<Chapter::Display> 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<AttachedFile> &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<Chapters>();
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<Properties>(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<Element *> 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
*/

View File

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

View File

@ -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 <cppunit/extensions/HelperMacros.h>
@ -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);

View File

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