diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ea44e54..f60afad9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ include(ConfigureChecks.cmake) option(WITH_APE "Build with APE, MPC, WavPack" ON) option(WITH_ASF "Build with ASF" ON) option(WITH_DSF "Build with DSF" ON) +option(WITH_MATROSKA "Build with Matroska" ON) option(WITH_MOD "Build with Tracker modules" ON) option(WITH_MP4 "Build with MP4" ON) option(WITH_RIFF "Build with AIFF, RIFF, WAV" ON) @@ -197,6 +198,9 @@ endif() if(WITH_DSF) set(TAGLIB_WITH_DSF TRUE) endif() +if(WITH_MATROSKA) + set(TAGLIB_WITH_MATROSKA TRUE) +endif() if(WITH_MOD) set(TAGLIB_WITH_MOD TRUE) endif() diff --git a/INSTALL.md b/INSTALL.md index beb4bf43..af9a7652 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -63,6 +63,7 @@ and ID3 tags cannot be disabled. The following CMake options are available: | `WITH_APE` | Build with APE, MPC, WavPack (default ON) | | `WITH_ASF` | Build with ASF (default ON) | | `WITH_DSF` | Build with DSF (default ON) | +| `WITH_MATROSKA` | Build with Matroska (default ON) | | `WITH_MOD` | Build with Tracker modules (default ON) | | `WITH_MP4` | Build with MP4 (default ON) | | `WITH_RIFF` | Build with AIFF, RIFF, WAV (default ON) | diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index 34c8993c..d825fde8 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -63,6 +63,12 @@ if(WITH_SHORTEN) ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/shorten ) endif() +if(WITH_MATROSKA) + set(tag_c_HDR_DIRS ${tag_c_HDR_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/matroska + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/matroska/ebml + ) +endif() include_directories(${tag_c_HDR_DIRS}) set(tag_c_HDRS tag_c.h) diff --git a/bindings/c/tag_c.cpp b/bindings/c/tag_c.cpp index 35453a63..48afec7d 100644 --- a/bindings/c/tag_c.cpp +++ b/bindings/c/tag_c.cpp @@ -80,6 +80,9 @@ #ifdef TAGLIB_WITH_SHORTEN #include "shortenfile.h" #endif +#ifdef TAGLIB_WITH_MATROSKA +#include "matroskafile.h" +#endif using namespace TagLib; @@ -241,6 +244,11 @@ TagLib_File *taglib_file_new_type_any_char(const T *filename, TagLib_File_Type t case TagLib_File_SHORTEN: file = new Shorten::File(filename); break; +#endif +#ifdef TAGLIB_WITH_MATROSKA + case TagLib_File_MATROSKA: + file = new Matroska::File(filename); + break; #endif default: break; diff --git a/bindings/c/tag_c.h b/bindings/c/tag_c.h index 88c8d67b..59f7163e 100644 --- a/bindings/c/tag_c.h +++ b/bindings/c/tag_c.h @@ -132,7 +132,8 @@ typedef enum { TagLib_File_Opus, TagLib_File_DSF, TagLib_File_DSDIFF, - TagLib_File_SHORTEN + TagLib_File_SHORTEN, + TagLib_File_MATROSKA } TagLib_File_Type; /*! diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2360ada0..d5862407 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,6 +2,7 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../taglib ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/toolkit ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ape + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/matroska ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v1 ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v2 @@ -38,6 +39,18 @@ target_link_libraries(framelist tag) add_executable(strip-id3v1 strip-id3v1.cpp) target_link_libraries(strip-id3v1 tag) +if(WITH_MATROSKA) + ########### next target ############### + + add_executable(matroskareader matroskareader.cpp) + target_link_libraries(matroskareader tag) + + ########### next target ############### + + add_executable(matroskawriter matroskawriter.cpp) + target_link_libraries(matroskawriter tag) +endif() + install(TARGETS tagreader tagreader_c tagwriter framelist strip-id3v1 LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} diff --git a/examples/matroskareader.cpp b/examples/matroskareader.cpp new file mode 100644 index 00000000..e44c2a35 --- /dev/null +++ b/examples/matroskareader.cpp @@ -0,0 +1,124 @@ +#include +#include "matroskafile.h" +#include "matroskatag.h" +#include "matroskasimpletag.h" +#include "matroskaattachments.h" +#include "matroskaattachedfile.h" +#include "matroskachapters.h" +#include "matroskachapteredition.h" +#include "tstring.h" +#include "tutils.h" +#include "tbytevector.h" +#define GREEN_TEXT(s) "" s "" +#define PRINT_PRETTY(label, value) printf(" " GREEN_TEXT(label) ": %s\n", value) + +int main(int argc, char *argv[]) +{ + if(argc != 2) { + printf("Usage: matroskareader FILE\n"); + return 1; + } + TagLib::Matroska::File file(argv[1]); + if(!file.isValid()) { + printf("File is not valid\n"); + return 1; + } + auto tag = dynamic_cast(file.tag()); + if(!tag) { + printf("File has no tag\n"); + return 0; + } + + const TagLib::Matroska::SimpleTagsList &list = tag->simpleTagsList(); + printf("Found %u tag(s):\n", list.size()); + + for(const TagLib::Matroska::SimpleTag &t : list) { + PRINT_PRETTY("Tag Name", t.name().toCString(true)); + + if(t.type() == TagLib::Matroska::SimpleTag::StringType) + PRINT_PRETTY("Tag Value", t.toString().toCString(true)); + else if(t.type() == TagLib::Matroska::SimpleTag::BinaryType) + PRINT_PRETTY("Tag Value", + TagLib::Utils::formatString("Binary with size %i", t.toByteVector().size()).toCString(false) + ); + + auto targetTypeValue = static_cast(t.targetTypeValue()); + PRINT_PRETTY("Target Type Value", + targetTypeValue == 0 ? "None" : TagLib::Utils::formatString("%i", targetTypeValue).toCString(false) + ); + if(auto trackUid = t.trackUid()) { + PRINT_PRETTY("Track UID", + TagLib::Utils::formatString("%llu",trackUid).toCString(false) + ); + } + const TagLib::String &language = t.language(); + PRINT_PRETTY("Language", !language.isEmpty() ? language.toCString(false) : "Not set"); + + printf("\n"); + } + + TagLib::Matroska::Attachments *attachments = file.attachments(); + if(attachments) { + const TagLib::Matroska::Attachments::AttachedFileList &list = attachments->attachedFileList(); + printf("Found %u attachment(s)\n", list.size()); + for(const auto &attachedFile : list) { + PRINT_PRETTY("Filename", attachedFile.fileName().toCString(true)); + const TagLib::String &description = attachedFile.description(); + PRINT_PRETTY("Description", !description.isEmpty() ? description.toCString(true) : "None"); + 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) + ); + PRINT_PRETTY("UID", + 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)); + PRINT_PRETTY("Doc Type Version", TagLib::String::number(properties->docTypeVersion()).toCString(false)); + PRINT_PRETTY("Codec Name", properties->codecName().toCString(true)); + PRINT_PRETTY("Bitrate", TagLib::String::number(properties->bitrate()).toCString(false)); + PRINT_PRETTY("Sample Rate", TagLib::String::number(properties->sampleRate()).toCString(false)); + PRINT_PRETTY("Channels", TagLib::String::number(properties->channels()).toCString(false)); + PRINT_PRETTY("Length [ms]", TagLib::String::number(properties->lengthInMilliseconds()).toCString(false)); + } + return 0; +} diff --git a/examples/matroskawriter.cpp b/examples/matroskawriter.cpp new file mode 100644 index 00000000..211cfe3d --- /dev/null +++ b/examples/matroskawriter.cpp @@ -0,0 +1,45 @@ +#include +#include "matroskafile.h" +#include "matroskatag.h" +#include "matroskasimpletag.h" +#include "matroskaattachments.h" +#include "matroskaattachedfile.h" +#include "tfilestream.h" +#include "tstring.h" +#include "tutils.h" + +int main(int argc, char *argv[]) +{ + if(argc != 3) { + printf("Usage: matroskawriter FILE ARTWORK\n"); + return 1; + } + TagLib::Matroska::File file(argv[1]); + if(!file.isValid()) { + printf("File is not valid\n"); + return 1; + } + auto tag = file.tag(true); + tag->clearSimpleTags(); + + tag->addSimpleTag(TagLib::Matroska::SimpleTag( + "Test Name 1", TagLib::String("Test Value 1"), + TagLib::Matroska::SimpleTag::TargetTypeValue::Track, "en")); + + tag->addSimpleTag(TagLib::Matroska::SimpleTag( + "Test Name 2", TagLib::String("Test Value 2"), + TagLib::Matroska::SimpleTag::TargetTypeValue::Album)); + tag->setTitle("Test title"); + tag->setArtist("Test artist"); + tag->setYear(1969); + + TagLib::FileStream image(argv[2]); + auto data = image.readBlock(image.length()); + auto attachments = file.attachments(true); + attachments->addAttachedFile(TagLib::Matroska::AttachedFile( + data, "cover.jpg", "image/jpeg")); + + file.save(); + + return 0; +} diff --git a/examples/tagwriter.cpp b/examples/tagwriter.cpp index cdf78cef..aca99491 100644 --- a/examples/tagwriter.cpp +++ b/examples/tagwriter.cpp @@ -71,6 +71,7 @@ void usage() std::cout << " -R " << std::endl; std::cout << " -I " << std::endl; std::cout << " -D " << std::endl; + std::cout << " -C " << std::endl; std::cout << " -p (\"\" \"\" to remove)" << std::endl; std::cout << std::endl; @@ -95,6 +96,102 @@ void checkForRejectedProperties(const TagLib::PropertyMap &tags) } } +/*! + * Create a list of variant maps from a string. + * The shorthand syntax in the string is kept simple, but should be sufficient + * for testing. Multiple maps are separated by ';', values within a map are + * assigned with key=value and separated by a ','. Types are detected, use + * double quotes to force a string. A ByteVector can be constructed from the + * contents of a file, the path is given after "file://". There is no escape + * character, use hex codes for ',' (\x2C) or ';' (\x3B). + */ +TagLib::List parseComplexPropertyValues(const TagLib::String &str) +{ + if(str.isEmpty() || str == "\"\"" || str == "''") { + return {}; + } + TagLib::List values; + const auto valueStrs = str.split(";"); + for(const auto &valueStr : valueStrs) { + TagLib::VariantMap value; + const auto keyValStrs = valueStr.split(","); + for(const auto &keyValStr : keyValStrs) { + if(int equalPos = keyValStr.find('='); equalPos != -1) { + TagLib::String key = keyValStr.substr(0, equalPos); + TagLib::String valStr = keyValStr.substr(equalPos + 1); + bool hasDot = false; + bool hasNonNumeric = false; + bool hasSign = false; + for(auto it = valStr.cbegin(); it != valStr.cend(); ++it) { + if(it == valStr.cbegin() && (*it == '-' || *it == '+')) { + hasSign = true; + } + else if(*it == '.') { + hasDot = true; + } + else if(*it < '0' || *it > '9') { + hasNonNumeric = true; + } + } + TagLib::Variant val; + if(valStr == "null") { + // keep empty variant + } + else if(valStr == "true" || valStr == "false") { + val = TagLib::Variant(valStr == "true"); + } + else if(!hasNonNumeric && hasDot) { + val = TagLib::Variant(std::stod(valStr.to8Bit())); + } + else if(!hasNonNumeric && hasSign) { + val = valStr.toLongLong(nullptr); + } + else if(!hasNonNumeric) { + val = valStr.toULongLong(nullptr); + } + else if(valStr.startsWith("file://")) { + auto filePath = valStr.substr(7 ); + if(isFile(filePath.toCString())) { + std::ifstream fs; + fs.open(filePath.toCString(), std::ios::in | std::ios::binary); + std::stringstream buffer; + buffer << fs.rdbuf(); + fs.close(); + TagLib::String buf(buffer.str()); + val = TagLib::Variant(buf.data(TagLib::String::Latin1)); + } + else { + std::cout << filePath.toCString() << " not found." << std::endl; + val = TagLib::Variant(TagLib::ByteVector()); + } + } + else { + int len = valStr.size(); + if(len >= 2 && valStr[0] == '"' && valStr[len - 1] == '"') { + valStr = valStr.substr(1, len - 2); + } + int hexPos = 0; + while((hexPos = valStr.find("\\x", hexPos)) != -1) { + char ch; + bool ok; + if(static_cast(valStr.length()) < hexPos + 4 || + (ch = static_cast( + valStr.substr(hexPos + 2, 2).toLongLong(&ok, 16)), !ok)) { + break; + } + valStr = valStr.substr(0, hexPos) + ch + valStr.substr(hexPos + 4); + ++hexPos; + } + val = TagLib::Variant(valStr); + } + value.insert(key, val); + } + } + values.append(value); + } + return values; +} + int main(int argc, char *argv[]) { TagLib::List fileList; @@ -170,6 +267,19 @@ int main(int argc, char *argv[]) checkForRejectedProperties(f.setProperties(map)); break; } + case 'C': { + if(i + 2 < argc) { + numArgsConsumed = 3; + if(!value.isEmpty()) { + TagLib::List values = parseComplexPropertyValues(argv[i + 2]); + f.setComplexProperties(value, values); + } + } + else { + usage(); + } + break; + } case 'p': { if(i + 2 < argc) { numArgsConsumed = 3; diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index 89f988aa..ce6197aa 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -64,8 +64,15 @@ if(WITH_SHORTEN) ${CMAKE_CURRENT_SOURCE_DIR}/shorten ) endif() +if(WITH_MATROSKA) + set(tag_HDR_DIRS ${tag_HDR_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/matroska + ${CMAKE_CURRENT_SOURCE_DIR}/matroska/ebml + ) +endif() include_directories(${tag_HDR_DIRS}) +set(tag_PRIVATE_HDRS) set(tag_HDRS tag.h fileref.h @@ -220,6 +227,41 @@ if(WITH_SHORTEN) shorten/shortentag.h ) endif() +if(WITH_MATROSKA) + set(tag_HDRS ${tag_HDRS} + matroska/matroskaattachedfile.h + matroska/matroskaattachments.h + matroska/matroskachapter.h + matroska/matroskachapteredition.h + matroska/matroskachapters.h + matroska/matroskaelement.h + matroska/matroskafile.h + matroska/matroskaproperties.h + matroska/matroskasimpletag.h + matroska/matroskatag.h + ) + set(tag_PRIVATE_HDRS ${tag_PRIVATE_HDRS} + matroska/ebml/ebmlbinaryelement.h + 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 + matroska/ebml/ebmlmkinfo.h + matroska/ebml/ebmlmktracks.h + matroska/ebml/ebmlmktags.h + matroska/ebml/ebmlstringelement.h + matroska/ebml/ebmluintelement.h + matroska/ebml/ebmlfloatelement.h + matroska/ebml/ebmlutils.h + matroska/ebml/ebmlvoidelement.h + matroska/matroskacues.h + matroska/matroskaseekhead.h + matroska/matroskasegment.h + ) +endif() set(mpeg_SRCS mpeg/mpegfile.cpp @@ -410,6 +452,43 @@ if(WITH_SHORTEN) ) endif() +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 + matroska/matroskaproperties.cpp + matroska/matroskaseekhead.cpp + matroska/matroskasegment.cpp + matroska/matroskasimpletag.cpp + matroska/matroskatag.cpp + ) + + set(ebml_SRCS + matroska/ebml/ebmlbinaryelement.cpp + 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 + matroska/ebml/ebmlmkinfo.cpp + matroska/ebml/ebmlmktracks.cpp + matroska/ebml/ebmlmktags.cpp + matroska/ebml/ebmlstringelement.cpp + matroska/ebml/ebmluintelement.cpp + matroska/ebml/ebmlfloatelement.cpp + matroska/ebml/ebmlutils.cpp + matroska/ebml/ebmlvoidelement.cpp + ) +endif() + set(toolkit_SRCS toolkit/tstring.cpp toolkit/tstringlist.cpp @@ -433,7 +512,7 @@ set(tag_LIB_SRCS ${vorbis_SRCS} ${oggflacs_SRCS} ${mpc_SRCS} ${ape_SRCS} ${toolkit_SRCS} ${flacs_SRCS} ${wavpack_SRCS} ${speex_SRCS} ${trueaudio_SRCS} ${riff_SRCS} ${aiff_SRCS} ${wav_SRCS} ${asf_SRCS} ${mp4_SRCS} ${mod_SRCS} ${s3m_SRCS} ${it_SRCS} ${xm_SRCS} ${opus_SRCS} - ${dsf_SRCS} ${dsdiff_SRCS} ${shorten_SRCS} + ${dsf_SRCS} ${dsdiff_SRCS} ${shorten_SRCS} ${matroska_SRCS} ${ebml_SRCS} tag.cpp tagunion.cpp fileref.cpp @@ -441,7 +520,7 @@ set(tag_LIB_SRCS tagutils.cpp ) -add_library(tag ${tag_LIB_SRCS} ${tag_HDRS}) +add_library(tag ${tag_LIB_SRCS} ${tag_HDRS} ${tag_PRIVATE_HDRS}) target_include_directories(tag INTERFACE $ diff --git a/taglib/fileref.cpp b/taglib/fileref.cpp index 2b5e5873..a8c3404f 100644 --- a/taglib/fileref.cpp +++ b/taglib/fileref.cpp @@ -77,6 +77,9 @@ #ifdef TAGLIB_WITH_SHORTEN #include "shortenfile.h" #endif +#ifdef TAGLIB_WITH_MATROSKA +#include "matroskafile.h" +#endif using namespace TagLib; @@ -220,6 +223,10 @@ namespace else if(ext == "SHN") file = new Shorten::File(stream, readAudioProperties, audioPropertiesStyle); #endif +#ifdef TAGLIB_WITH_MATROSKA + else if(ext == "MKA" || ext == "MKV" || ext == "WEBM") + file = new Matroska::File(stream, readAudioProperties); +#endif // if file is not valid, leave it to content-based detection. @@ -289,6 +296,10 @@ namespace else if(Shorten::File::isSupported(stream)) file = new Shorten::File(stream, readAudioProperties, audioPropertiesStyle); #endif +#ifdef TAGLIB_WITH_MATROSKA + else if(Matroska::File::isSupported(stream)) + file = new Matroska::File(stream, readAudioProperties, audioPropertiesStyle); +#endif // isSupported() only does a quick check, so double check the file here. @@ -513,6 +524,11 @@ StringList FileRef::defaultFileExtensions() #ifdef TAGLIB_WITH_SHORTEN l.append("shn"); #endif +#ifdef TAGLIB_WITH_MATROSKA + l.append("mkv"); + l.append("mka"); + l.append("webm"); +#endif return l; } diff --git a/taglib/matroska/ebml/ebmlbinaryelement.cpp b/taglib/matroska/ebml/ebmlbinaryelement.cpp new file mode 100644 index 00000000..98100502 --- /dev/null +++ b/taglib/matroska/ebml/ebmlbinaryelement.cpp @@ -0,0 +1,70 @@ +/*************************************************************************** + * 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 "ebmlbinaryelement.h" +#include "ebmlutils.h" +#include "tfile.h" +#include "tdebug.h" + +using namespace TagLib; + +EBML::BinaryElement::BinaryElement(Id id, int sizeLength, offset_t dataSize): + Element(id, sizeLength, dataSize) +{ +} + +EBML::BinaryElement::BinaryElement(Id id, int sizeLength, offset_t dataSize, offset_t): + Element(id, sizeLength, dataSize) +{ +} + +EBML::BinaryElement::BinaryElement(Id id): + Element(id, 0, 0) +{ +} + +const ByteVector& EBML::BinaryElement::getValue() const +{ + return value; +} + +void EBML::BinaryElement::setValue(const ByteVector& val) +{ + value = val; +} + +bool EBML::BinaryElement::read(File &file) +{ + value = file.readBlock(dataSize); + if(value.size() != dataSize) { + debug("Failed to read binary element"); + return false; + } + return true; +} + +ByteVector EBML::BinaryElement::render() +{ + ByteVector buffer = renderId(); + dataSize = value.size(); + buffer.append(renderVINT(dataSize, 0)); + buffer.append(value); + return buffer; +} diff --git a/taglib/matroska/ebml/ebmlbinaryelement.h b/taglib/matroska/ebml/ebmlbinaryelement.h new file mode 100644 index 00000000..f6a662fc --- /dev/null +++ b/taglib/matroska/ebml/ebmlbinaryelement.h @@ -0,0 +1,51 @@ +/*************************************************************************** + * 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_EBMLBINARYELEMENT_H +#define TAGLIB_EBMLBINARYELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlelement.h" +#include "tbytevector.h" + +namespace TagLib { + class File; + + namespace EBML { + class BinaryElement : public Element + { + public: + BinaryElement(Id id, int sizeLength, offset_t dataSize); + BinaryElement(Id id, int sizeLength, offset_t dataSize, offset_t); + explicit BinaryElement(Id id); + + const ByteVector &getValue() const; + void setValue(const ByteVector &val); + bool read(File &file) override; + ByteVector render() override; + + private: + ByteVector value; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlelement.cpp b/taglib/matroska/ebml/ebmlelement.cpp new file mode 100644 index 00000000..351e2870 --- /dev/null +++ b/taglib/matroska/ebml/ebmlelement.cpp @@ -0,0 +1,213 @@ +/*************************************************************************** + * 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 "ebmlelement.h" +#include "ebmlvoidelement.h" +#include "ebmlmasterelement.h" +#include "ebmlbinaryelement.h" +#include "ebmlfloatelement.h" +#include "ebmlmkseekhead.h" +#include "ebmlmksegment.h" +#include "ebmlmktags.h" +#include "ebmlmkattachments.h" +#include "ebmlmkchapters.h" +#include "ebmlmktracks.h" +#include "ebmlstringelement.h" +#include "ebmluintelement.h" +#include "ebmlutils.h" +#include "tfile.h" +#include "tdebug.h" +#include "tutils.h" + +using namespace TagLib; + +#define RETURN_ELEMENT_FOR_CASE(eid) \ + case (eid): return make_unique_element(id, sizeLength, dataSize, offset) + +std::unique_ptr EBML::Element::factory(File &file) +{ + // Get the element ID + const offset_t offset = file.tell(); + unsigned int uintId = readId(file); + if(!uintId) { + debug("Failed to parse EMBL ElementID"); + return nullptr; + } + + // Get the size length and data length + const auto &[sizeLength, dataSize] = readVINT(file); + if(!sizeLength) + return nullptr; + + // Return the subclass + // The enum switch without default will give us a warning if an ID is missing + auto id = static_cast(uintId); + switch(id) { + RETURN_ELEMENT_FOR_CASE(Id::EBMLHeader); + RETURN_ELEMENT_FOR_CASE(Id::DocType); + RETURN_ELEMENT_FOR_CASE(Id::DocTypeVersion); + RETURN_ELEMENT_FOR_CASE(Id::MkSegment); + RETURN_ELEMENT_FOR_CASE(Id::MkInfo); + RETURN_ELEMENT_FOR_CASE(Id::MkTracks); + RETURN_ELEMENT_FOR_CASE(Id::MkTags); + RETURN_ELEMENT_FOR_CASE(Id::MkAttachments); + RETURN_ELEMENT_FOR_CASE(Id::MkTag); + RETURN_ELEMENT_FOR_CASE(Id::MkTagTargets); + RETURN_ELEMENT_FOR_CASE(Id::MkSimpleTag); + RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFile); + RETURN_ELEMENT_FOR_CASE(Id::MkSeek); + RETURN_ELEMENT_FOR_CASE(Id::MkTrackEntry); + RETURN_ELEMENT_FOR_CASE(Id::MkAudio); + RETURN_ELEMENT_FOR_CASE(Id::MkTagName); + RETURN_ELEMENT_FOR_CASE(Id::MkTagString); + RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileName); + RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileDescription); + RETURN_ELEMENT_FOR_CASE(Id::MkTagLanguage); + RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileMediaType); + RETURN_ELEMENT_FOR_CASE(Id::MkCodecID); + RETURN_ELEMENT_FOR_CASE(Id::MkTagTargetTypeValue); + RETURN_ELEMENT_FOR_CASE(Id::MkTagTrackUID); + RETURN_ELEMENT_FOR_CASE(Id::MkTagsLanguageDefault); + RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileUID); + RETURN_ELEMENT_FOR_CASE(Id::MkSeekPosition); + RETURN_ELEMENT_FOR_CASE(Id::MkTimestampScale); + RETURN_ELEMENT_FOR_CASE(Id::MkBitDepth); + RETURN_ELEMENT_FOR_CASE(Id::MkChannels); + RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileData); + RETURN_ELEMENT_FOR_CASE(Id::MkSeekID); + RETURN_ELEMENT_FOR_CASE(Id::MkDuration); + RETURN_ELEMENT_FOR_CASE(Id::MkTitle); + RETURN_ELEMENT_FOR_CASE(Id::MkSamplingFrequency); + RETURN_ELEMENT_FOR_CASE(Id::MkSeekHead); + RETURN_ELEMENT_FOR_CASE(Id::VoidElement); + RETURN_ELEMENT_FOR_CASE(Id::MkCluster); + RETURN_ELEMENT_FOR_CASE(Id::MkCodecState); + RETURN_ELEMENT_FOR_CASE(Id::MkTagBinary); + RETURN_ELEMENT_FOR_CASE(Id::MkCues); + RETURN_ELEMENT_FOR_CASE(Id::MkCuePoint); + RETURN_ELEMENT_FOR_CASE(Id::MkCueTime); + RETURN_ELEMENT_FOR_CASE(Id::MkCueTrackPositions); + RETURN_ELEMENT_FOR_CASE(Id::MkCueTrack); + RETURN_ELEMENT_FOR_CASE(Id::MkCueClusterPosition); + RETURN_ELEMENT_FOR_CASE(Id::MkCueRelativePosition); + RETURN_ELEMENT_FOR_CASE(Id::MkCueDuration); + RETURN_ELEMENT_FOR_CASE(Id::MkCueBlockNumber); + 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); +} + +unsigned int EBML::Element::readId(File &file) +{ + auto buffer = file.readBlock(1); + if(buffer.size() != 1) { + debug("Failed to read VINT size"); + return 0; + } + const unsigned int numBytes = VINTSizeLength<4>(*buffer.begin()); + if(!numBytes) + return 0; + if(numBytes > 1) + buffer.append(file.readBlock(numBytes - 1)); + if(buffer.size() != numBytes) { + debug("Failed to read VINT data"); + return 0; + } + return buffer.toUInt(true); +} + +EBML::Element::Element(Id id, int sizeLength, offset_t dataSize): + id(id), sizeLength(sizeLength), dataSize(dataSize) +{ +} + +EBML::Element::Element(Id id, int sizeLength, offset_t dataSize, offset_t): + id(id), sizeLength(sizeLength), dataSize(dataSize) +{ +} + +EBML::Element::~Element() = default; + +bool EBML::Element::read(File& file) +{ + skipData(file); + return true; +} + +void EBML::Element::skipData(File &file) +{ + file.seek(dataSize, File::Position::Current); +} + +EBML::Element::Id EBML::Element::getId() const +{ + return id; +} + +offset_t EBML::Element::headSize() const +{ + return idSize(id) + sizeLength; +} + +offset_t EBML::Element::getSize() const +{ + return headSize() + dataSize; +} + +int EBML::Element::getSizeLength() const +{ + return sizeLength; +} + +int64_t EBML::Element::getDataSize() const +{ + return dataSize; +} + +ByteVector EBML::Element::render() +{ + ByteVector buffer = renderId(); + buffer.append(renderVINT(0, 0)); + return buffer; +} + +ByteVector EBML::Element::renderId() const +{ + const int numBytes = idSize(id); + static const auto byteOrder = Utils::systemByteOrder(); + const auto uintId = static_cast(id); + uint32_t data = byteOrder == Utils::LittleEndian ? Utils::byteSwap(uintId) : uintId; + return ByteVector(reinterpret_cast(&data) + (4 - numBytes), numBytes); +} diff --git a/taglib/matroska/ebml/ebmlelement.h b/taglib/matroska/ebml/ebmlelement.h new file mode 100644 index 00000000..8bccf469 --- /dev/null +++ b/taglib/matroska/ebml/ebmlelement.h @@ -0,0 +1,249 @@ +/*************************************************************************** + * 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_EBMLELEMENT_H +#define TAGLIB_EBMLELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include +#include +#include "taglib.h" + +namespace TagLib +{ + class File; + class ByteVector; + + namespace EBML { + + class Element + { + public: + enum class Id : unsigned int + { + EBMLHeader = 0x1A45DFA3, + DocType = 0x4282, + DocTypeVersion = 0x4287, + VoidElement = 0xEC, + MkSegment = 0x18538067, + MkTags = 0x1254C367, + MkTag = 0x7373, + MkTagTargets = 0x63C0, + MkTagTargetTypeValue = 0x68CA, + MkTagTrackUID = 0x63C5, + MkSimpleTag = 0x67C8, + MkTagName = 0x45A3, + MkTagLanguage = 0x447A, + MkTagBinary = 0x4485, + MkTagString = 0x4487, + MkTagsTagLanguage = 0x447A, + MkTagsLanguageDefault = 0x4484, + MkAttachments = 0x1941A469, + MkAttachedFile = 0x61A7, + MkAttachedFileDescription = 0x467E, + MkAttachedFileName = 0x466E, + MkAttachedFileMediaType = 0x4660, + MkAttachedFileData = 0x465C, + MkAttachedFileUID = 0x46AE, + MkSeekHead = 0x114D9B74, + MkSeek = 0x4DBB, + MkSeekID = 0x53AB, + MkSeekPosition = 0x53AC, + MkCluster = 0x1F43B675, + MkCodecState = 0xA4, + MkCues = 0x1C53BB6B, + MkCuePoint = 0xBB, + MkCueTime = 0xB3, + MkCueTrackPositions = 0xB7, + MkCueTrack = 0xF7, + MkCueClusterPosition = 0xF1, + MkCueRelativePosition = 0xF0, + MkCueDuration = 0xB2, + MkCueBlockNumber = 0x5378, + MkCueCodecState = 0xEA, + MkCueReference = 0xDB, + MkCueRefTime = 0x96, + MkInfo = 0x1549A966, + MkTimestampScale = 0x2AD7B1, + MkDuration = 0x4489, + MkTitle = 0x7BA9, + MkTracks = 0x1654AE6B, + MkTrackEntry = 0xAE, + MkCodecID = 0x86, + MkAudio = 0xE1, + 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); + Element(Id id, int sizeLength, offset_t dataSize, offset_t); + virtual ~Element(); + + virtual bool read(File &file); + void skipData(File &file); + Id getId() const; + offset_t headSize() const; + offset_t getSize() const; + int getSizeLength() const; + int64_t getDataSize() const; + ByteVector renderId() const; + virtual ByteVector render(); + static std::unique_ptr factory(File &file); + static unsigned int readId(File &file); + + protected: + Id id; + int sizeLength; + offset_t dataSize; + }; + + // Template specializations to ensure that elements for the different IDs + // are consistently created and cast. They provide a mapping between IDs + // and Element subclasses. The switch in factory() makes sure that the + // template for all IDs are instantiated, i.e. that every ID has its Element + // subclass mapped. + class MasterElement; + class UIntElement; + class BinaryElement; + class FloatElement; + class MkSegment; + class MkInfo; + class MkTracks; + class MkTags; + class MkAttachments; + class MkSeekHead; + class MkChapters; + class MkCues; + class VoidElement; + class UTF8StringElement; + class Latin1StringElement; + + template + struct GetElementTypeById; + + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = Latin1StringElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = MkSegment; }; + template <> struct GetElementTypeById { using type = MkInfo; }; + template <> struct GetElementTypeById { using type = MkTracks; }; + template <> struct GetElementTypeById { using type = MkTags; }; + template <> struct GetElementTypeById { using type = MkAttachments; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MasterElement; }; + template <> struct GetElementTypeById { using type = MkCues; }; + template <> struct GetElementTypeById { using type = UTF8StringElement; }; + template <> struct GetElementTypeById { using type = UTF8StringElement; }; + template <> struct GetElementTypeById { using type = UTF8StringElement; }; + template <> struct GetElementTypeById { using type = UTF8StringElement; }; + template <> struct GetElementTypeById { using type = Latin1StringElement; }; + template <> struct GetElementTypeById { using type = Latin1StringElement; }; + template <> struct GetElementTypeById { using type = Latin1StringElement; }; + 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 = UIntElement; }; + 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 = UIntElement; }; + 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 = UIntElement; }; + template <> struct GetElementTypeById { using type = UIntElement; }; + template <> struct GetElementTypeById { using type = BinaryElement; }; + template <> struct GetElementTypeById { using type = BinaryElement; }; + template <> struct GetElementTypeById { using type = BinaryElement; }; + template <> struct GetElementTypeById { using type = BinaryElement; }; + template <> struct GetElementTypeById { using type = FloatElement; }; + 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> + const T *element_cast(const std::unique_ptr &ptr) + { + return static_cast(ptr.get()); + } + + template ::type> + std::unique_ptr element_cast(std::unique_ptr &&ptr) + { + return std::unique_ptr(static_cast(ptr.release())); + } + + template ::type> + std::unique_ptr make_unique_element(Element::Id id, int sizeLength, offset_t dataSize, offset_t offset) + { + return std::make_unique(id, sizeLength, dataSize, offset); + } + + template ::type> + std::unique_ptr make_unique_element() + { + return std::make_unique(ID, 0, 0, 0); + } + + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlfloatelement.cpp b/taglib/matroska/ebml/ebmlfloatelement.cpp new file mode 100644 index 00000000..00ea199d --- /dev/null +++ b/taglib/matroska/ebml/ebmlfloatelement.cpp @@ -0,0 +1,109 @@ +/*************************************************************************** + 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 "ebmlfloatelement.h" +#include "ebmlutils.h" +#include "tbytevector.h" +#include "tfile.h" +#include "tdebug.h" + +using namespace TagLib; + +EBML::FloatElement::FloatElement(Id id, int sizeLength, offset_t dataSize): + Element(id, sizeLength, dataSize) +{ +} + +EBML::FloatElement::FloatElement(Id id, int sizeLength, offset_t dataSize, offset_t): + Element(id, sizeLength, dataSize) +{ +} + +EBML::FloatElement::FloatElement(Id id): + FloatElement(id, 0, 0) +{ +} + +EBML::FloatElement::FloatVariantType EBML::FloatElement::getValue() const +{ return value; } + +double EBML::FloatElement::getValueAsDouble(double defaultValue) const +{ + if(std::holds_alternative(value)) { + // get_if() used instead of get() to support restricted compilers + return *std::get_if(&value); + } + if(std::holds_alternative(value)) { + // get_if() used instead of get() to support restricted compilers + return *std::get_if(&value); + } + return defaultValue; +} + +void EBML::FloatElement::setValue(FloatVariantType val) +{ + value = val; +} + +bool EBML::FloatElement::read(File &file) +{ + const ByteVector buffer = file.readBlock(dataSize); + if(buffer.size() != dataSize) { + debug("Failed to read EBML Float element"); + return false; + } + + if(dataSize == 0) { + value = std::monostate(); + } + else if(dataSize == 4) { + value = buffer.toFloat32BE(0); + } + else if(dataSize == 8) { + value = buffer.toFloat64BE(0); + } + else { + debug("Invalid size for EBML Float element"); + return false; + } + return true; +} + +ByteVector EBML::FloatElement::render() +{ + ByteVector data; + if(std::holds_alternative(value)) { + // get_if() used instead of get() to support restricted compilers + data = ByteVector::fromFloat64BE(*std::get_if(&value)); + } + else if(std::holds_alternative(value)) { + // get_if() used instead of get() to support restricted compilers + data = ByteVector::fromFloat32BE(*std::get_if(&value)); + } + ByteVector buffer = renderId(); + buffer.append(renderVINT(data.size(), 0)); + buffer.append(data); + return buffer; +} diff --git a/taglib/matroska/ebml/ebmlfloatelement.h b/taglib/matroska/ebml/ebmlfloatelement.h new file mode 100644 index 00000000..92307919 --- /dev/null +++ b/taglib/matroska/ebml/ebmlfloatelement.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_EBMLFLOATELEMENT_H +#define TAGLIB_EBMLFLOATELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include +#include "ebmlelement.h" + +namespace TagLib { + class File; + + namespace EBML { + class FloatElement : public Element + { + public: + using FloatVariantType = std::variant; + + FloatElement(Id id, int sizeLength, offset_t dataSize); + FloatElement(Id id, int sizeLength, offset_t dataSize, offset_t); + explicit FloatElement(Id id); + + FloatVariantType getValue() const; + double getValueAsDouble(double defaultValue = 0.0) const; + void setValue(FloatVariantType val); + bool read(File &file) override; + ByteVector render() override; + + private: + FloatVariantType value; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmasterelement.cpp b/taglib/matroska/ebml/ebmlmasterelement.cpp new file mode 100644 index 00000000..22e5d9b7 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmasterelement.cpp @@ -0,0 +1,127 @@ +/*************************************************************************** + * 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 "ebmlmasterelement.h" +#include "ebmlvoidelement.h" +#include "ebmlutils.h" +#include "tfile.h" + +using namespace TagLib; + +EBML::MasterElement::MasterElement(Id id, int sizeLength, offset_t dataSize, offset_t offset): + Element(id, sizeLength, dataSize), offset(offset) +{ +} + +EBML::MasterElement::MasterElement(Id id): + Element(id, 0, 0), offset(0) +{ +} + +EBML::MasterElement::~MasterElement() = default; + +offset_t EBML::MasterElement::getOffset() const +{ + return offset; +} + +void EBML::MasterElement::appendElement(std::unique_ptr &&element) +{ + elements.push_back(std::move(element)); +} + +std::list>::iterator EBML::MasterElement::begin() +{ + return elements.begin(); +} + +std::list>::iterator EBML::MasterElement::end() +{ + return elements.end(); +} + +std::list>::const_iterator EBML::MasterElement::begin() const +{ + return elements.begin(); +} + +std::list>::const_iterator EBML::MasterElement::end() const +{ + return elements.end(); +} + +std::list>::const_iterator EBML::MasterElement::cbegin() const +{ + return elements.cbegin(); +} + +std::list>::const_iterator EBML::MasterElement::cend() const +{ + return elements.cend(); +} + +offset_t EBML::MasterElement::getPadding() const +{ + return padding; +} + +void EBML::MasterElement::setPadding(offset_t numBytes) +{ + padding = numBytes; +} + +offset_t EBML::MasterElement::getMinRenderSize() const +{ + return minRenderSize; +} + +void EBML::MasterElement::setMinRenderSize(offset_t minimumSize) +{ + minRenderSize = minimumSize; +} + +bool EBML::MasterElement::read(File &file) +{ + const offset_t maxOffset = file.tell() + dataSize; + std::unique_ptr element; + while((element = findNextElement(file, maxOffset))) { + if(!element->read(file)) + return false; + elements.push_back(std::move(element)); + } + return file.tell() == maxOffset; +} + +ByteVector EBML::MasterElement::render() +{ + ByteVector buffer = renderId(); + ByteVector data; + for(const auto &element : elements) + data.append(element->render()); + dataSize = data.size(); + buffer.append(renderVINT(dataSize, 0)); + buffer.append(data); + if(minRenderSize) { + if(const auto bufferSize = buffer.size(); + minRenderSize >= bufferSize + MIN_VOID_ELEMENT_SIZE) + buffer.append(VoidElement::renderSize(minRenderSize - bufferSize)); + } + return buffer; +} diff --git a/taglib/matroska/ebml/ebmlmasterelement.h b/taglib/matroska/ebml/ebmlmasterelement.h new file mode 100644 index 00000000..52cfe526 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmasterelement.h @@ -0,0 +1,68 @@ +/*************************************************************************** + * 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_EBMLMASTERELEMENT_H +#define TAGLIB_EBMLMASTERELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include + +#include "ebmlelement.h" +#include "taglib.h" + +namespace TagLib +{ + class ByteVector; + + namespace EBML { + class MasterElement : public Element + { + public: + MasterElement(Id id, int sizeLength, offset_t dataSize, offset_t offset); + explicit MasterElement(Id id); + ~MasterElement() override; + + offset_t getOffset() const; + bool read(File &file) override; + ByteVector render() override; + void appendElement(std::unique_ptr &&element); + std::list>::iterator begin(); + std::list>::iterator end(); + std::list>::const_iterator begin() const; + std::list>::const_iterator end() const; + std::list>::const_iterator cbegin() const; + std::list>::const_iterator cend() const; + offset_t getPadding() const; + void setPadding(offset_t numBytes); + offset_t getMinRenderSize() const; + void setMinRenderSize(offset_t minimumSize); + + protected: + offset_t offset; + offset_t padding = 0; + offset_t minRenderSize = 0; + std::list> elements; + }; + + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmkattachments.cpp b/taglib/matroska/ebml/ebmlmkattachments.cpp new file mode 100644 index 00000000..34941833 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkattachments.cpp @@ -0,0 +1,81 @@ +/*************************************************************************** + * 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 "ebmlmkattachments.h" +#include "ebmlstringelement.h" +#include "ebmluintelement.h" +#include "ebmlbinaryelement.h" +#include "matroskaattachments.h" +#include "matroskaattachedfile.h" + +using namespace TagLib; + +EBML::MkAttachments::MkAttachments(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkAttachments, sizeLength, dataSize, offset) +{ +} + +EBML::MkAttachments::MkAttachments(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkAttachments, sizeLength, dataSize, offset) +{ +} + +EBML::MkAttachments::MkAttachments(): + MasterElement(Id::MkAttachments, 0, 0, 0) +{ +} + +std::unique_ptr EBML::MkAttachments::parse() const +{ + auto attachments = std::make_unique(); + attachments->setOffset(offset); + attachments->setSize(getSize()); + + for(const auto &element : elements) { + if(element->getId() != Id::MkAttachedFile) + continue; + + const String *filename = nullptr; + const String *description = nullptr; + const String *mediaType = nullptr; + const ByteVector *data = nullptr; + Matroska::AttachedFile::UID uid = 0; + const auto attachedFile = element_cast(element); + for(const auto &attachedFileChild : *attachedFile) { + if(const Id id = attachedFileChild->getId(); id == Id::MkAttachedFileName) + filename = &element_cast(attachedFileChild)->getValue(); + else if(id == Id::MkAttachedFileData) + data = &element_cast(attachedFileChild)->getValue(); + else if(id == Id::MkAttachedFileDescription) + description = &element_cast(attachedFileChild)->getValue(); + else if(id == Id::MkAttachedFileMediaType) + mediaType = &element_cast(attachedFileChild)->getValue(); + else if(id == Id::MkAttachedFileUID) + uid = element_cast(attachedFileChild)->getValue(); + } + if(!(filename && data)) + continue; + + attachments->addAttachedFile(Matroska::AttachedFile( + *data, *filename, mediaType ? *mediaType : String(), + uid, description ? *description : String())); + } + return attachments; +} diff --git a/taglib/matroska/ebml/ebmlmkattachments.h b/taglib/matroska/ebml/ebmlmkattachments.h new file mode 100644 index 00000000..7c2404ea --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkattachments.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * 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_EBMLMKATTACHMENTS_H +#define TAGLIB_EBMLMKATTACHMENTS_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class Attachments; + } + + namespace EBML { + class MkAttachments : public MasterElement + { + public: + MkAttachments(int sizeLength, offset_t dataSize, offset_t offset); + MkAttachments(Id, int sizeLength, offset_t dataSize, offset_t offset); + MkAttachments(); + + std::unique_ptr parse() const; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmkchapters.cpp b/taglib/matroska/ebml/ebmlmkchapters.cpp new file mode 100644 index 00000000..d8d3c136 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkchapters.cpp @@ -0,0 +1,114 @@ +/*************************************************************************** + 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; + +EBML::MkChapters::MkChapters(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkChapters, sizeLength, dataSize, offset) +{ +} + +EBML::MkChapters::MkChapters(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkChapters, sizeLength, dataSize, offset) +{ +} + +EBML::MkChapters::MkChapters(): + MasterElement(Id::MkChapters, 0, 0, 0) +{ +} + +std::unique_ptr EBML::MkChapters::parse() const +{ + 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; + const auto edition = element_cast(element); + for(const auto &editionChild : *edition) { + if(const Id id = editionChild->getId(); 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; + const auto chapterAtom = element_cast(editionChild); + for(const auto &chapterChild : *chapterAtom) { + if(const Id cid = chapterChild->getId(); 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) { + const auto display = element_cast(chapterChild); + String displayString; + String displayLanguage; + for(const auto &displayChild : *display) { + if(const Id did = displayChild->getId(); 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..af7e63ba --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkchapters.h @@ -0,0 +1,52 @@ +/*************************************************************************** + 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); + MkChapters(Id, int sizeLength, offset_t dataSize, offset_t offset); + MkChapters(); + + std::unique_ptr parse() const; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmkcues.cpp b/taglib/matroska/ebml/ebmlmkcues.cpp new file mode 100644 index 00000000..1d60f8c7 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkcues.cpp @@ -0,0 +1,89 @@ +/*************************************************************************** + * 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 "ebmlmkcues.h" +#include "ebmluintelement.h" +#include "matroskacues.h" + +using namespace TagLib; + +EBML::MkCues::MkCues(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkCues, sizeLength, dataSize, offset) +{ +} + +EBML::MkCues::MkCues(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkCues, sizeLength, dataSize, offset) +{ +} + +EBML::MkCues::MkCues(): + MasterElement(Id::MkCues, 0, 0, 0) +{ +} + +std::unique_ptr EBML::MkCues::parse(offset_t segmentDataOffset) const +{ + auto cues = std::make_unique(segmentDataOffset); + cues->setOffset(offset); + cues->setSize(getSize()); + cues->setID(static_cast(id)); + + for(const auto &cuesChild : elements) { + if(cuesChild->getId() != Id::MkCuePoint) + continue; + const auto cuePointElement = element_cast(cuesChild); + auto cuePoint = std::make_unique(); + + for(const auto &cuePointChild : *cuePointElement) { + if(const Id id = cuePointChild->getId(); id == Id::MkCueTime) + cuePoint->setTime(element_cast(cuePointChild)->getValue()); + else if(id == Id::MkCueTrackPositions) { + auto cueTrack = std::make_unique(); + const auto cueTrackElement = element_cast(cuePointChild); + for(const auto &cueTrackChild : *cueTrackElement) { + if(const Id trackId = cueTrackChild->getId(); trackId == Id::MkCueTrack) + cueTrack->setTrackNumber(element_cast(cueTrackChild)->getValue()); + else if(trackId == Id::MkCueClusterPosition) + cueTrack->setClusterPosition(element_cast(cueTrackChild)->getValue()); + else if(trackId == Id::MkCueRelativePosition) + cueTrack->setRelativePosition(element_cast(cueTrackChild)->getValue()); + else if(trackId == Id::MkCueDuration) + cueTrack->setDuration(element_cast(cueTrackChild)->getValue()); + else if(trackId == Id::MkCueBlockNumber) + cueTrack->setBlockNumber(element_cast(cueTrackChild)->getValue()); + else if(trackId == Id::MkCueCodecState) + cueTrack->setCodecState(element_cast(cueTrackChild)->getValue()); + else if(trackId == Id::MkCueReference) { + const auto cueReference = element_cast(cueTrackChild); + for(const auto &cueReferenceChild : *cueReference) { + if(cueReferenceChild->getId() != Id::MkCueRefTime) + continue; + cueTrack->addReferenceTime(element_cast(cueReferenceChild)->getValue()); + } + } + } + cuePoint->addCueTrack(std::move(cueTrack)); + } + } + cues->addCuePoint(std::move(cuePoint)); + } + return cues; +} diff --git a/taglib/matroska/ebml/ebmlmkcues.h b/taglib/matroska/ebml/ebmlmkcues.h new file mode 100644 index 00000000..0ce3c7ab --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkcues.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * 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_EBMLMKCUES_H +#define TAGLIB_EBMLMKCUES_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class Cues; + } + + namespace EBML { + class MkCues : public MasterElement + { + public: + MkCues(int sizeLength, offset_t dataSize, offset_t offset); + MkCues(Id, int sizeLength, offset_t dataSize, offset_t offset); + MkCues(); + + std::unique_ptr parse(offset_t segmentDataOffset) const; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmkinfo.cpp b/taglib/matroska/ebml/ebmlmkinfo.cpp new file mode 100644 index 00000000..a2e4e910 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkinfo.cpp @@ -0,0 +1,72 @@ +/*************************************************************************** + 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 "ebmlmkinfo.h" +#include "ebmlstringelement.h" +#include "ebmluintelement.h" +#include "ebmlfloatelement.h" +#include "matroskaproperties.h" + +using namespace TagLib; + +EBML::MkInfo::MkInfo(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkInfo, sizeLength, dataSize, offset) +{ +} + +EBML::MkInfo::MkInfo(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkInfo, sizeLength, dataSize, offset) +{ +} + +EBML::MkInfo::MkInfo(): + MasterElement(Id::MkInfo, 0, 0, 0) +{ +} + +void EBML::MkInfo::parse(Matroska::Properties *properties) const +{ + if(!properties) + return; + + unsigned long long timestampScale = 1000000; + double duration = 0.0; + String title; + for(const auto &element : elements) { + if(const Id id = element->getId(); id == Id::MkTimestampScale) { + timestampScale = element_cast(element)->getValue(); + } + else if(id == Id::MkDuration) { + duration = element_cast(element)->getValueAsDouble(); + } + else if(id == Id::MkTitle) { + title = element_cast(element)->getValue(); + } + } + + properties->setLengthInMilliseconds( + static_cast(duration * static_cast(timestampScale) / 1000000.0)); + properties->setTitle(title); +} diff --git a/taglib/matroska/ebml/ebmlmkinfo.h b/taglib/matroska/ebml/ebmlmkinfo.h new file mode 100644 index 00000000..c381b46a --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkinfo.h @@ -0,0 +1,52 @@ +/*************************************************************************** + 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_EBMLMKINFO_H +#define TAGLIB_EBMLMKINFO_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class Properties; + } + + namespace EBML { + class MkInfo : public MasterElement + { + public: + MkInfo(int sizeLength, offset_t dataSize, offset_t offset); + MkInfo(Id, int sizeLength, offset_t dataSize, offset_t offset); + MkInfo(); + + void parse(Matroska::Properties * properties) const; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmkseekhead.cpp b/taglib/matroska/ebml/ebmlmkseekhead.cpp new file mode 100644 index 00000000..c3d6ca8f --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkseekhead.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + * 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 "ebmlmkseekhead.h" +#include "matroskaseekhead.h" +#include "ebmluintelement.h" +#include "ebmlbinaryelement.h" + +using namespace TagLib; + +EBML::MkSeekHead::MkSeekHead(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkSeekHead, sizeLength, dataSize, offset) +{ +} + +EBML::MkSeekHead::MkSeekHead(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkSeekHead, sizeLength, dataSize, offset) +{ +} + +EBML::MkSeekHead::MkSeekHead(): + MasterElement(Id::MkSeekHead, 0, 0, 0) +{ +} + +std::unique_ptr EBML::MkSeekHead::parse(offset_t segmentDataOffset) const +{ + auto seekHead = std::make_unique(segmentDataOffset); + seekHead->setOffset(offset); + seekHead->setSize(getSize() + padding); + + for(const auto &element : elements) { + if(element->getId() != Id::MkSeek) + continue; + const auto seekElement = element_cast(element); + Matroska::Element::ID entryId = 0; + offset_t offset = 0; + for(const auto &seekElementChild : *seekElement) { + if(const Id id = seekElementChild->getId(); id == Id::MkSeekID && !entryId) { + if(auto data = element_cast(seekElementChild)->getValue(); + data.size() == 4) + entryId = data.toUInt(true); + } + else if(id == Id::MkSeekPosition && !offset) + offset = element_cast(seekElementChild)->getValue(); + } + if(entryId && offset) + seekHead->addEntry(entryId, offset); + else { + seekHead.reset(); + return nullptr; + } + } + + return seekHead; +} diff --git a/taglib/matroska/ebml/ebmlmkseekhead.h b/taglib/matroska/ebml/ebmlmkseekhead.h new file mode 100644 index 00000000..13007841 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmkseekhead.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * 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_EBMLMKSEEKHEAD_H +#define TAGLIB_EBMLMKSEEKHEAD_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class SeekHead; + } + + namespace EBML { + class MkSeekHead : public MasterElement + { + public: + MkSeekHead(int sizeLength, offset_t dataSize, offset_t offset); + MkSeekHead(Id, int sizeLength, offset_t dataSize, offset_t offset); + MkSeekHead(); + + std::unique_ptr parse(offset_t segmentDataOffset) const; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmksegment.cpp b/taglib/matroska/ebml/ebmlmksegment.cpp new file mode 100644 index 00000000..4b9ba4e5 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmksegment.cpp @@ -0,0 +1,148 @@ +/*************************************************************************** + * 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 "ebmlmksegment.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" + +using namespace TagLib; + +EBML::MkSegment::MkSegment(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkSegment, sizeLength, dataSize, offset) +{ +} + +EBML::MkSegment::MkSegment(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkSegment, sizeLength, dataSize, offset) +{ +} + +EBML::MkSegment::~MkSegment() = default; + +offset_t EBML::MkSegment::segmentDataOffset() const +{ + return offset + idSize(id) + sizeLength; +} + +bool EBML::MkSegment::read(File &file) +{ + const offset_t maxOffset = file.tell() + dataSize; + std::unique_ptr element; + int i = 0; + int seekHeadIndex = -1; + while((element = findNextElement(file, maxOffset))) { + if(const Id id = element->getId(); id == Id::MkSeekHead) { + seekHeadIndex = i; + seekHead = element_cast(std::move(element)); + if(!seekHead->read(file)) + return false; + } + else if(id == Id::MkCues) { + cues = element_cast(std::move(element)); + if(!cues->read(file)) + return false; + } + else if(id == Id::MkInfo) { + info = element_cast(std::move(element)); + if(!info->read(file)) + return false; + } + else if(id == Id::MkTracks) { + tracks = element_cast(std::move(element)); + if(!tracks->read(file)) + return false; + } + else if(id == Id::MkTags) { + tags = element_cast(std::move(element)); + if(!tags->read(file)) + return false; + } + else if(id == Id::MkAttachments) { + attachments = element_cast(std::move(element)); + 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 + && seekHeadIndex == i - 1) + seekHead->setPadding(element->getSize()); + + element->skipData(file); + } + i++; + } + return true; +} + +std::unique_ptr EBML::MkSegment::parseTag() const +{ + return tags ? tags->parse() : nullptr; +} + +std::unique_ptr EBML::MkSegment::parseAttachments() const +{ + return attachments ? attachments->parse() : nullptr; +} + +std::unique_ptr EBML::MkSegment::parseChapters() const +{ + return chapters ? chapters->parse() : nullptr; +} + +std::unique_ptr EBML::MkSegment::parseSeekHead() const +{ + return seekHead ? seekHead->parse(segmentDataOffset()) : nullptr; +} + +std::unique_ptr EBML::MkSegment::parseCues() const +{ + return cues ? cues->parse(segmentDataOffset()) : nullptr; +} + +std::unique_ptr EBML::MkSegment::parseSegment() const +{ + return std::make_unique(sizeLength, dataSize, offset + idSize(id)); +} + +void EBML::MkSegment::parseInfo(Matroska::Properties *properties) const +{ + if(info) { + info->parse(properties); + } +} + +void EBML::MkSegment::parseTracks(Matroska::Properties *properties) const +{ + if(tracks) { + tracks->parse(properties); + } +} diff --git a/taglib/matroska/ebml/ebmlmksegment.h b/taglib/matroska/ebml/ebmlmksegment.h new file mode 100644 index 00000000..13000f3e --- /dev/null +++ b/taglib/matroska/ebml/ebmlmksegment.h @@ -0,0 +1,76 @@ +/*************************************************************************** + * 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_EBMLMKSEGMENT_H +#define TAGLIB_EBMLMKSEGMENT_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "ebmlmktags.h" +#include "ebmlmkattachments.h" +#include "ebmlmkchapters.h" +#include "ebmlmkseekhead.h" +#include "ebmlmkcues.h" +#include "ebmlmkinfo.h" +#include "ebmlmktracks.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class Tag; + class Attachments; + class Chapters; + class SeekHead; + class Segment; + } + + namespace EBML { + class MkSegment : public MasterElement + { + public: + MkSegment(int sizeLength, offset_t dataSize, offset_t offset); + MkSegment(Id, int sizeLength, offset_t dataSize, offset_t offset); + ~MkSegment() override; + + offset_t segmentDataOffset() const; + bool read(File &file) override; + std::unique_ptr parseTag() const; + std::unique_ptr parseAttachments() const; + std::unique_ptr parseChapters() const; + std::unique_ptr parseSeekHead() const; + std::unique_ptr parseCues() const; + std::unique_ptr parseSegment() const; + void parseInfo(Matroska::Properties *properties) const; + void parseTracks(Matroska::Properties *properties) const; + + 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; + std::unique_ptr tracks; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmktags.cpp b/taglib/matroska/ebml/ebmlmktags.cpp new file mode 100644 index 00000000..e732e26e --- /dev/null +++ b/taglib/matroska/ebml/ebmlmktags.cpp @@ -0,0 +1,119 @@ +/*************************************************************************** + * 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 "ebmlmktags.h" +#include "ebmlbinaryelement.h" +#include "ebmluintelement.h" +#include "ebmlstringelement.h" +#include "matroskatag.h" +#include "matroskasimpletag.h" +#include "tlist.h" + +using namespace TagLib; + +EBML::MkTags::MkTags(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkTags, sizeLength, dataSize, offset) +{ +} + +EBML::MkTags::MkTags(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkTags, sizeLength, dataSize, offset) +{ +} + +EBML::MkTags::MkTags(): + MasterElement(Id::MkTags, 0, 0, 0) +{ +} + +std::unique_ptr EBML::MkTags::parse() const +{ + auto mTag = std::make_unique(); + mTag->setOffset(offset); + mTag->setSize(getSize()); + mTag->setID(static_cast(id)); + + // Loop through each element + for(const auto &tagsChild : elements) { + if(tagsChild->getId() != Id::MkTag) + continue; + const auto tag = element_cast(tagsChild); + List simpleTags; + const MasterElement *targets = nullptr; + + // Identify the element and the elements + for(const auto &tagChild : *tag) { + if(const Id tagChildId = tagChild->getId(); !targets && tagChildId == Id::MkTagTargets) + targets = element_cast(tagChild); + else if(tagChildId == Id::MkSimpleTag) + simpleTags.append(element_cast(tagChild)); + } + + // Parse the element + Matroska::SimpleTag::TargetTypeValue targetTypeValue = Matroska::SimpleTag::TargetTypeValue::None; + unsigned long long trackUid = 0; + if(targets) { + for(const auto &targetsChild : *targets) { + if(const Id id = targetsChild->getId(); id == Id::MkTagTargetTypeValue + && targetTypeValue == Matroska::SimpleTag::TargetTypeValue::None) { + targetTypeValue = static_cast( + element_cast(targetsChild)->getValue() + ); + } + else if(id == Id::MkTagTrackUID) { + trackUid = element_cast(targetsChild)->getValue(); + } + } + } + + // Parse each + for(const auto simpleTag : simpleTags) { + const String *tagValueString = nullptr; + const ByteVector *tagValueBinary = nullptr; + String tagName; + String language; + bool defaultLanguageFlag = true; + + for(const auto &simpleTagChild : *simpleTag) { + if(const Id id = simpleTagChild->getId(); id == Id::MkTagName && tagName.isEmpty()) + tagName = element_cast(simpleTagChild)->getValue(); + else if(id == Id::MkTagString && !tagValueString) + tagValueString = &element_cast(simpleTagChild)->getValue(); + else if(id == Id::MkTagBinary && !tagValueBinary) + tagValueBinary = &element_cast(simpleTagChild)->getValue(); + else if(id == Id::MkTagsTagLanguage && language.isEmpty()) + language = element_cast(simpleTagChild)->getValue(); + else if(id == Id::MkTagsLanguageDefault) + defaultLanguageFlag = element_cast(simpleTagChild)->getValue() ? true : false; + } + if(tagName.isEmpty() || (tagValueString && tagValueBinary) || (!tagValueString && !tagValueBinary)) + continue; + + mTag->addSimpleTag(tagValueString + ? Matroska::SimpleTag(tagName, *tagValueString, + targetTypeValue, language, defaultLanguageFlag, + trackUid) + : Matroska::SimpleTag(tagName, *tagValueBinary, + targetTypeValue, language, defaultLanguageFlag, + trackUid)); + } + } + return mTag; +} diff --git a/taglib/matroska/ebml/ebmlmktags.h b/taglib/matroska/ebml/ebmlmktags.h new file mode 100644 index 00000000..82b9f33f --- /dev/null +++ b/taglib/matroska/ebml/ebmlmktags.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * 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_EBMLMKTAGS_H +#define TAGLIB_EBMLMKTAGS_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class Tag; + } + + namespace EBML { + class MkTags : public MasterElement + { + public: + MkTags(int sizeLength, offset_t dataSize, offset_t offset); + MkTags(Id, int sizeLength, offset_t dataSize, offset_t offset); + MkTags(); + + std::unique_ptr parse() const; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlmktracks.cpp b/taglib/matroska/ebml/ebmlmktracks.cpp new file mode 100644 index 00000000..14cf7a00 --- /dev/null +++ b/taglib/matroska/ebml/ebmlmktracks.cpp @@ -0,0 +1,86 @@ +/*************************************************************************** + 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 "ebmlmktracks.h" +#include "ebmlstringelement.h" +#include "ebmluintelement.h" +#include "ebmlfloatelement.h" +#include "matroskaproperties.h" + +using namespace TagLib; + +EBML::MkTracks::MkTracks(int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkTracks, sizeLength, dataSize, offset) +{ +} + +EBML::MkTracks::MkTracks(Id, int sizeLength, offset_t dataSize, offset_t offset): + MasterElement(Id::MkTracks, sizeLength, dataSize, offset) +{ +} + +EBML::MkTracks::MkTracks(): + MasterElement(Id::MkTracks, 0, 0, 0) +{ +} + +void EBML::MkTracks::parse(Matroska::Properties *properties) const +{ + if(!properties) + return; + + for(const auto &element : elements) { + if(element->getId() != Id::MkTrackEntry) + continue; + + String codecId; + double samplingFrequency = 0.0; + unsigned long long bitDepth = 0; + unsigned long long channels = 0; + const auto trackEntry = element_cast(element); + for(const auto &trackEntryChild : *trackEntry) { + if(const Id trackEntryChildId = trackEntryChild->getId(); trackEntryChildId == Id::MkCodecID) + codecId = element_cast(trackEntryChild)->getValue(); + else if(trackEntryChildId == Id::MkAudio) { + const auto audio = element_cast(trackEntryChild); + for(const auto &audioChild : *audio) { + if(const Id audioChildId = audioChild->getId(); audioChildId == Id::MkSamplingFrequency) + samplingFrequency = element_cast(audioChild)->getValueAsDouble(); + else if(audioChildId == Id::MkBitDepth) + bitDepth = element_cast(audioChild)->getValue(); + else if(audioChildId == Id::MkChannels) + channels = element_cast(audioChild)->getValue(); + } + } + } + if(bitDepth || channels) { + properties->setSampleRate(static_cast(samplingFrequency)); + properties->setBitsPerSample(static_cast(bitDepth)); + properties->setChannels(static_cast(channels)); + properties->setCodecName(codecId); + return; + } + } +} diff --git a/taglib/matroska/ebml/ebmlmktracks.h b/taglib/matroska/ebml/ebmlmktracks.h new file mode 100644 index 00000000..440ac21d --- /dev/null +++ b/taglib/matroska/ebml/ebmlmktracks.h @@ -0,0 +1,52 @@ +/*************************************************************************** + 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_EBMLMKTRACKS_H +#define TAGLIB_EBMLMKTRACKS_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlmasterelement.h" +#include "taglib.h" + +namespace TagLib { + namespace Matroska { + class Properties; + } + + namespace EBML { + class MkTracks : public MasterElement + { + public: + MkTracks(int sizeLength, offset_t dataSize, offset_t offset); + MkTracks(Id, int sizeLength, offset_t dataSize, offset_t offset); + MkTracks(); + + void parse(Matroska::Properties *properties) const; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlstringelement.cpp b/taglib/matroska/ebml/ebmlstringelement.cpp new file mode 100644 index 00000000..01699db3 --- /dev/null +++ b/taglib/matroska/ebml/ebmlstringelement.cpp @@ -0,0 +1,100 @@ +/*************************************************************************** + * 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 "ebmlstringelement.h" +#include +#include "tfile.h" +#include "tbytevector.h" +#include "tdebug.h" +#include "ebmlutils.h" + +using namespace TagLib; + +EBML::StringElement::StringElement( + String::Type stringEncoding, Id id, int sizeLength, offset_t dataSize): + Element(id, sizeLength, dataSize), encoding(stringEncoding) +{ +} + +const String& EBML::StringElement::getValue() const +{ + return value; +} + +void EBML::StringElement::setValue(const String& val) +{ + value = val; +} + +bool EBML::StringElement::read(File &file) +{ + ByteVector buffer = file.readBlock(dataSize); + if(buffer.size() != dataSize) { + debug("Failed to read string"); + return false; + } + + // The EBML strings aren't supposed to be null-terminated, + // but we'll check for it and strip the null terminator if found + if(const int nullByte = buffer.find('\0'); nullByte >= 0) + buffer = ByteVector(buffer.data(), nullByte); + value = String(buffer, encoding); + return true; +} + +ByteVector EBML::StringElement::render() +{ + ByteVector buffer = renderId(); + const std::string string = value.to8Bit(encoding == String::UTF8); + dataSize = string.size(); + buffer.append(renderVINT(dataSize, 0)); + buffer.append(ByteVector(string.data(), static_cast(dataSize))); + return buffer; +} + +EBML::UTF8StringElement::UTF8StringElement(Id id, int sizeLength, offset_t dataSize): + StringElement(String::UTF8, id, sizeLength, dataSize) +{ +} + +EBML::UTF8StringElement::UTF8StringElement(Id id, int sizeLength, offset_t dataSize, offset_t): + UTF8StringElement(id, sizeLength, dataSize) +{ +} + +EBML::UTF8StringElement::UTF8StringElement(Id id): + UTF8StringElement(id, 0, 0) +{ +} + +EBML::Latin1StringElement::Latin1StringElement(Id id, int sizeLength, offset_t dataSize): + StringElement(String::Latin1, id, sizeLength, dataSize) +{ +} + +EBML::Latin1StringElement::Latin1StringElement(Id id, int sizeLength, offset_t dataSize, offset_t): + Latin1StringElement(id, sizeLength, dataSize) +{ +} + +EBML::Latin1StringElement::Latin1StringElement(Id id): + Latin1StringElement(id, 0, 0) +{ +} diff --git a/taglib/matroska/ebml/ebmlstringelement.h b/taglib/matroska/ebml/ebmlstringelement.h new file mode 100644 index 00000000..51d46950 --- /dev/null +++ b/taglib/matroska/ebml/ebmlstringelement.h @@ -0,0 +1,65 @@ +/*************************************************************************** + * 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_EBMLSTRINGELEMENT_H +#define TAGLIB_EBMLSTRINGELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlelement.h" +#include "tstring.h" + +namespace TagLib { + class File; + class ByteVector; + + namespace EBML { + class StringElement : public Element + { + public: + StringElement(String::Type stringEncoding, Id id, int sizeLength, offset_t dataSize); + + const String &getValue() const; + void setValue(const String &val); + bool read(File &file) override; + ByteVector render() override; + + private: + String value; + String::Type encoding; + }; + + class UTF8StringElement : public StringElement { + public: + UTF8StringElement(Id id, int sizeLength, offset_t dataSize); + UTF8StringElement(Id id, int sizeLength, offset_t dataSize, offset_t); + explicit UTF8StringElement(Id id); + }; + + class Latin1StringElement : public StringElement { + public: + Latin1StringElement(Id id, int sizeLength, offset_t dataSize); + Latin1StringElement(Id id, int sizeLength, offset_t dataSize, offset_t); + explicit Latin1StringElement(Id id); + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmluintelement.cpp b/taglib/matroska/ebml/ebmluintelement.cpp new file mode 100644 index 00000000..e7d790c7 --- /dev/null +++ b/taglib/matroska/ebml/ebmluintelement.cpp @@ -0,0 +1,95 @@ +/*************************************************************************** + * 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 "ebmluintelement.h" +#include "ebmlutils.h" +#include "tbytevector.h" +#include "tfile.h" +#include "tutils.h" +#include "tdebug.h" + +using namespace TagLib; + +EBML::UIntElement::UIntElement(Id id, int sizeLength, offset_t dataSize): + Element(id, sizeLength, dataSize) +{ +} + +EBML::UIntElement::UIntElement(Id id, int sizeLength, offset_t dataSize, offset_t): + Element(id, sizeLength, dataSize) +{ +} + +EBML::UIntElement::UIntElement(Id id): + UIntElement(id, 0, 0) +{ +} + +unsigned long long EBML::UIntElement::getValue() const +{ + return value; +} + +void EBML::UIntElement::setValue(unsigned long long val) +{ + value = val; +} + +bool EBML::UIntElement::read(File &file) +{ + const ByteVector buffer = file.readBlock(dataSize); + if(buffer.size() != dataSize) { + debug("Failed to read EBML Uint element"); + return false; + } + value = buffer.toULongLong(true); + return true; +} + +ByteVector EBML::UIntElement::render() +{ + int dataSize = 0; + if(value <= 0xFFull) + dataSize = 1; + else if(value <= 0xFFFFull) + dataSize = 2; + else if(value <= 0xFFFFFFull) + dataSize = 3; + else if(value <= 0xFFFFFFFFull) + dataSize = 4; + else if(value <= 0xFFFFFFFFFFull) + dataSize = 5; + else if(value <= 0xFFFFFFFFFFFFull) + dataSize = 6; + else if(value <= 0xFFFFFFFFFFFFFFull) + dataSize = 7; + else if(value <= 0xFFFFFFFFFFFFFFFFull) + dataSize = 8; + + ByteVector buffer = renderId(); + buffer.append(renderVINT(dataSize, 0)); + uint64_t val = value; + static const auto byteOrder = Utils::systemByteOrder(); + if(byteOrder == Utils::LittleEndian) + val = Utils::byteSwap(val); + + buffer.append(ByteVector(reinterpret_cast(&val) + (sizeof(val) - dataSize), dataSize)); + return buffer; +} diff --git a/taglib/matroska/ebml/ebmluintelement.h b/taglib/matroska/ebml/ebmluintelement.h new file mode 100644 index 00000000..5529c434 --- /dev/null +++ b/taglib/matroska/ebml/ebmluintelement.h @@ -0,0 +1,50 @@ +/*************************************************************************** + * 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_EBMLUINTELEMENT_H +#define TAGLIB_EBMLUINTELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlelement.h" + +namespace TagLib { + class File; + + namespace EBML { + class UIntElement : public Element + { + public: + UIntElement(Id id, int sizeLength, offset_t dataSize); + UIntElement(Id id, int sizeLength, offset_t dataSize, offset_t); + explicit UIntElement(Id id); + + unsigned long long getValue() const; + void setValue(unsigned long long val); + bool read(File &file) override; + ByteVector render() override; + + private: + unsigned long long value = 0; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlutils.cpp b/taglib/matroska/ebml/ebmlutils.cpp new file mode 100644 index 00000000..60f05155 --- /dev/null +++ b/taglib/matroska/ebml/ebmlutils.cpp @@ -0,0 +1,122 @@ +/*************************************************************************** + * 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 "ebmlutils.h" +#include +#include "tbytevector.h" +#include "matroskafile.h" +#include "tutils.h" +#include "tdebug.h" + +using namespace TagLib; + +std::unique_ptr EBML::findElement(File &file, Element::Id id, offset_t maxOffset) +{ + std::unique_ptr element; + while(file.tell() < maxOffset) { + element = Element::factory(file); + if(!element || element->getId() == id) + return element; + element->skipData(file); + element.reset(); + } + return element; +} + +std::unique_ptr EBML::findNextElement(File &file, offset_t maxOffset) +{ + return file.tell() < maxOffset ? Element::factory(file) : nullptr; +} + +template +unsigned int EBML::VINTSizeLength(uint8_t firstByte) +{ + static_assert(maxSizeLength >= 1 && maxSizeLength <= 8); + if(!firstByte) { + debug("VINT with greater than 8 bytes not allowed"); + return 0; + } + uint8_t mask = 0b10000000; + unsigned int numBytes = 1; + while(!(mask & firstByte)) { + numBytes++; + mask >>= 1; + } + if(numBytes > maxSizeLength) { + debug(Utils::formatString("VINT size length exceeds %i bytes", maxSizeLength)); + return 0; + } + return numBytes; +} + +namespace TagLib::EBML { + template unsigned int VINTSizeLength<4>(uint8_t firstByte); + template unsigned int VINTSizeLength<8>(uint8_t firstByte); +} + +std::pair EBML::readVINT(File &file) +{ + auto buffer = file.readBlock(1); + if(buffer.size() != 1) { + debug("Failed to read VINT size"); + return {0, 0}; + } + unsigned int numBytes = VINTSizeLength<8>(*buffer.begin()); + if(!numBytes) + return {0, 0}; + + if(numBytes > 1) + buffer.append(file.readBlock(numBytes - 1)); + const int bitsToShift = static_cast(sizeof(uint64_t) * 8) - 7 * numBytes; + const uint64_t mask = 0xFFFFFFFFFFFFFFFF >> bitsToShift; + return { numBytes, buffer.toULongLong(true) & mask }; +} + +std::pair EBML::parseVINT(const ByteVector &buffer) +{ + if(buffer.isEmpty()) + return {0, 0}; + + unsigned int numBytes = VINTSizeLength<8>(*buffer.begin()); + if(!numBytes) + return {0, 0}; + + const int bitsToShift = static_cast(sizeof(uint64_t) * 8) - 7 * numBytes; + const uint64_t mask = 0xFFFFFFFFFFFFFFFF >> bitsToShift; + return { numBytes, buffer.toULongLong(true) & mask }; +} + +ByteVector EBML::renderVINT(uint64_t number, int minSizeLength) +{ + const int numBytes = std::max(minSizeLength, minSize(number)); + number |= 1ULL << (numBytes * 7); + static const auto byteOrder = Utils::systemByteOrder(); + if(byteOrder == Utils::LittleEndian) + number = Utils::byteSwap(number); + return ByteVector(reinterpret_cast(&number) + (sizeof(number) - numBytes), numBytes); +} + +unsigned long long EBML::randomUID() +{ + static std::random_device device; + static std::mt19937 generator(device()); + static std::uniform_int_distribution distribution; + return distribution(generator); +} diff --git a/taglib/matroska/ebml/ebmlutils.h b/taglib/matroska/ebml/ebmlutils.h new file mode 100644 index 00000000..4038e5b0 --- /dev/null +++ b/taglib/matroska/ebml/ebmlutils.h @@ -0,0 +1,78 @@ +/*************************************************************************** + * 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_EBMLUTILS_H +#define TAGLIB_EBMLUTILS_H +#ifndef DO_NOT_DOCUMENT + +#include +#include "taglib.h" +#include "ebmlelement.h" + +namespace TagLib { + class File; + class ByteVector; + + namespace EBML { + std::unique_ptr findElement(File &file, Element::Id id, offset_t maxOffset); + std::unique_ptr findNextElement(File &file, offset_t maxOffset); + + template + unsigned int VINTSizeLength(uint8_t firstByte); + + std::pair readVINT(File &file); + + std::pair parseVINT(const ByteVector &buffer); + + ByteVector renderVINT(uint64_t number, int minSizeLength); + + unsigned long long randomUID(); + + constexpr int minSize(uint64_t data) + { + if(data <= 0x7Fu) + return 1; + if(data <= 0x3FFFu) + return 2; + if(data <= 0x1FFFFFu) + return 3; + if(data <= 0xFFFFFFFu) + return 4; + if(data <= 0x7FFFFFFFFu) + return 5; + return 0; + } + + constexpr int idSize(Element::Id id) + { + const auto uintId = static_cast(id); + if(uintId <= 0xFF) + return 1; + if(uintId <= 0xFFFF) + return 2; + if(uintId <= 0xFFFFFF) + return 3; + return 4; + } + } +} + +#endif +#endif diff --git a/taglib/matroska/ebml/ebmlvoidelement.cpp b/taglib/matroska/ebml/ebmlvoidelement.cpp new file mode 100644 index 00000000..3a841c2e --- /dev/null +++ b/taglib/matroska/ebml/ebmlvoidelement.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + * 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 "ebmlvoidelement.h" +#include +#include "ebmlutils.h" +#include "tbytevector.h" + +using namespace TagLib; + +EBML::VoidElement::VoidElement(int sizeLength, offset_t dataSize): + Element(Id::VoidElement, sizeLength, dataSize) +{ +} + +EBML::VoidElement::VoidElement(Id, int sizeLength, offset_t dataSize, offset_t): + Element(Id::VoidElement, sizeLength, dataSize) +{ +} + +EBML::VoidElement::VoidElement(): + Element(Id::VoidElement, 0, 0) +{ +} + +ByteVector EBML::VoidElement::render() +{ + offset_t bytesNeeded = targetSize; + ByteVector buffer = renderId(); + bytesNeeded -= buffer.size(); + sizeLength = static_cast(std::min(bytesNeeded, static_cast(8))); + bytesNeeded -= sizeLength; + dataSize = bytesNeeded; + buffer.append(renderVINT(dataSize, sizeLength)); + if(dataSize) + buffer.append(ByteVector(static_cast(dataSize), 0)); + + return buffer; +} + +offset_t EBML::VoidElement::getTargetSize() const +{ + return targetSize; +} + +void EBML::VoidElement::setTargetSize(offset_t size) +{ + this->targetSize = std::max(size, MIN_VOID_ELEMENT_SIZE); +} + +ByteVector EBML::VoidElement::renderSize(offset_t targetSize) +{ + VoidElement element; + element.setTargetSize(targetSize); + return element.render(); +} diff --git a/taglib/matroska/ebml/ebmlvoidelement.h b/taglib/matroska/ebml/ebmlvoidelement.h new file mode 100644 index 00000000..24dd2a34 --- /dev/null +++ b/taglib/matroska/ebml/ebmlvoidelement.h @@ -0,0 +1,51 @@ +/*************************************************************************** + * 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_EBMLVOIDELEMENT_H +#define TAGLIB_EBMLVOIDELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include "ebmlelement.h" + +namespace TagLib { + class File; + + namespace EBML { + inline constexpr offset_t MIN_VOID_ELEMENT_SIZE = 2; + class VoidElement : public Element + { + public: + VoidElement(int sizeLength, offset_t dataSize); + VoidElement(Id, int sizeLength, offset_t dataSize, offset_t); + VoidElement(); + + ByteVector render() override; + offset_t getTargetSize() const; + void setTargetSize(offset_t size); + static ByteVector renderSize(offset_t targetSize); + + private: + offset_t targetSize = MIN_VOID_ELEMENT_SIZE; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/matroskaattachedfile.cpp b/taglib/matroska/matroskaattachedfile.cpp new file mode 100644 index 00000000..7957e86e --- /dev/null +++ b/taglib/matroska/matroskaattachedfile.cpp @@ -0,0 +1,99 @@ +/*************************************************************************** + * 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 "matroskaattachedfile.h" +#include "tbytevector.h" + +using namespace TagLib; + +class Matroska::AttachedFile::AttachedFilePrivate +{ +public: + AttachedFilePrivate(const ByteVector &data, const String &fileName, + const String &mediaType, UID uid, const String &description) : + fileName(fileName), description(description), mediaType(mediaType), + data(data), uid(uid) {} + ~AttachedFilePrivate() = default; + String fileName; + String description; + String mediaType; + ByteVector data; + UID uid = 0; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::AttachedFile::AttachedFile(const ByteVector &data, + const String &fileName, const String &mediaType, UID uid, + const String &description) : + d(std::make_unique(data, fileName, mediaType, uid, description)) +{ +} + +Matroska::AttachedFile::AttachedFile(const AttachedFile &other) : + d(std::make_unique(*other.d)) +{ +} + +Matroska::AttachedFile::AttachedFile(AttachedFile &&other) noexcept = default; + +Matroska::AttachedFile::~AttachedFile() = default; + +Matroska::AttachedFile &Matroska::AttachedFile::operator=(AttachedFile &&other) noexcept = default; + +Matroska::AttachedFile &Matroska::AttachedFile::operator=(const AttachedFile &other) +{ + AttachedFile(other).swap(*this); + return *this; +} + +void Matroska::AttachedFile::swap(AttachedFile &other) noexcept +{ + using std::swap; + + swap(d, other.d); +} + +const String &Matroska::AttachedFile::fileName() const +{ + return d->fileName; +} + +const String &Matroska::AttachedFile::description() const +{ + return d->description; +} + +const String &Matroska::AttachedFile::mediaType() const +{ + return d->mediaType; +} + +const ByteVector &Matroska::AttachedFile::data() const +{ + return d->data; +} + +Matroska::AttachedFile::UID Matroska::AttachedFile::uid() const +{ + return d->uid; +} diff --git a/taglib/matroska/matroskaattachedfile.h b/taglib/matroska/matroskaattachedfile.h new file mode 100644 index 00000000..2b7a2964 --- /dev/null +++ b/taglib/matroska/matroskaattachedfile.h @@ -0,0 +1,110 @@ +/*************************************************************************** + * 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_MATROSKAATTACHEDFILE_H +#define TAGLIB_MATROSKAATTACHEDFILE_H + +#include +#include "tstring.h" +#include "taglib_export.h" + +namespace TagLib { + class String; + class ByteVector; + + namespace Matroska { + //! Attached file embedded into a Matroska file. + class TAGLIB_EXPORT AttachedFile + { + public: + //! Unique identifier. + using UID = unsigned long long; + + /*! + * Construct an attached file. + */ + AttachedFile(const ByteVector &data, const String &fileName, + const String &mediaType, UID uid = 0, + const String &description = String()); + + /*! + * Construct an attached file as a copy of \a other. + */ + AttachedFile(const AttachedFile &other); + + /*! + * Construct an attached file moving from \a other. + */ + AttachedFile(AttachedFile &&other) noexcept; + + /*! + * Destroys this attached file. + */ + ~AttachedFile(); + + /*! + * Copies the contents of \a other into this object. + */ + AttachedFile &operator=(const AttachedFile &other); + + /*! + * Moves the contents of \a other into this object. + */ + AttachedFile &operator=(AttachedFile &&other) noexcept; + + /*! + * Exchanges the content of the object with the content of \a other. + */ + void swap(AttachedFile &other) noexcept; + + /*! + * Returns the filename of the attached file. + */ + const String &fileName() const; + + /*! + * Returns the human-friendly description for the attached file. + */ + const String &description() const; + + /*! + * Returns the media type of the attached file. + */ + const String &mediaType() const; + + /*! + * Returns the data of the attached file. + */ + const ByteVector &data() const; + + /*! + * Returns the UID of the attached file. + */ + UID uid() const; + + private: + class AttachedFilePrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + } +} + +#endif diff --git a/taglib/matroska/matroskaattachments.cpp b/taglib/matroska/matroskaattachments.cpp new file mode 100644 index 00000000..5bdee484 --- /dev/null +++ b/taglib/matroska/matroskaattachments.cpp @@ -0,0 +1,141 @@ +/*************************************************************************** + * 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 "matroskaattachments.h" +#include "matroskaattachedfile.h" +#include "ebmlmkattachments.h" +#include "ebmlmasterelement.h" +#include "ebmlstringelement.h" +#include "ebmlbinaryelement.h" +#include "ebmluintelement.h" +#include "ebmlutils.h" +#include "tlist.h" +#include "tbytevector.h" + +using namespace TagLib; + +class Matroska::Attachments::AttachmentsPrivate +{ +public: + AttachmentsPrivate() = default; + ~AttachmentsPrivate() = default; + AttachmentsPrivate(const AttachmentsPrivate &) = delete; + AttachmentsPrivate &operator=(const AttachmentsPrivate &) = delete; + AttachedFileList files; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::Attachments::Attachments() : + Element(static_cast(EBML::Element::Id::MkAttachments)), + d(std::make_unique()) +{ +} + +Matroska::Attachments::~Attachments() = default; + +void Matroska::Attachments::addAttachedFile(const AttachedFile &file) +{ + d->files.append(file); + setNeedsRender(true); +} + +void Matroska::Attachments::removeAttachedFile(unsigned long long uid) +{ + const auto it = std::find_if(d->files.begin(), d->files.end(), + [uid](const AttachedFile &file) { + return file.uid() == uid; + }); + if(it != d->files.end()) { + d->files.erase(it); + setNeedsRender(true); + } +} + +void Matroska::Attachments::clear() +{ + d->files.clear(); + setNeedsRender(true); +} + +const Matroska::Attachments::AttachedFileList &Matroska::Attachments::attachedFileList() const +{ + return d->files; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::Attachments::AttachedFileList &Matroska::Attachments::attachedFiles() +{ + setNeedsRender(true); + return d->files; +} + +ByteVector Matroska::Attachments::renderInternal() +{ + if(d->files.isEmpty()) { + // Avoid writing an Attachments element without AttachedFile element. + return {}; + } + + EBML::MkAttachments attachments; + for(const auto &attachedFile : std::as_const(d->files)) { + auto attachedFileElement = EBML::make_unique_element(); + + // Filename + auto fileNameElement = EBML::make_unique_element(); + fileNameElement->setValue(attachedFile.fileName()); + attachedFileElement->appendElement(std::move(fileNameElement)); + + // Media/MIME type + auto mediaTypeElement = + EBML::make_unique_element(); + mediaTypeElement->setValue(attachedFile.mediaType()); + attachedFileElement->appendElement(std::move(mediaTypeElement)); + + // Description + if(const String &description = attachedFile.description(); !description.isEmpty()) { + auto descriptionElement = + EBML::make_unique_element(); + descriptionElement->setValue(description); + attachedFileElement->appendElement(std::move(descriptionElement)); + } + + // Data + auto dataElement = EBML::make_unique_element(); + dataElement->setValue(attachedFile.data()); + attachedFileElement->appendElement(std::move(dataElement)); + + // UID + auto uidElement = EBML::make_unique_element(); + AttachedFile::UID uid = attachedFile.uid(); + if(!uid) + uid = EBML::randomUID(); + uidElement->setValue(uid); + attachedFileElement->appendElement(std::move(uidElement)); + + attachments.appendElement(std::move(attachedFileElement)); + } + return attachments.render(); +} diff --git a/taglib/matroska/matroskaattachments.h b/taglib/matroska/matroskaattachments.h new file mode 100644 index 00000000..84e6f150 --- /dev/null +++ b/taglib/matroska/matroskaattachments.h @@ -0,0 +1,83 @@ +/*************************************************************************** + * 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_MATROSKAATTACHMENTS_H +#define TAGLIB_MATROSKAATTACHMENTS_H + +#include +#include "taglib_export.h" +#include "tlist.h" +#include "matroskaelement.h" + +namespace TagLib { + class File; + + namespace EBML { + class MkAttachments; + } + + namespace Matroska { + class AttachedFile; + class File; + + //! Collection of attached files. + class TAGLIB_EXPORT Attachments +#ifndef DO_NOT_DOCUMENT + : private Element +#endif + { + public: + //! List of attached files. + using AttachedFileList = List; + + //! Construct attachments. + Attachments(); + + //! Destroy attachments. + virtual ~Attachments(); + + //! Add an attached file. + void addAttachedFile(const AttachedFile &file); + + //! Remove an attached file. + void removeAttachedFile(unsigned long long uid); + + //! Remove all attached files. + void clear(); + + //! Get list of all attached files. + const AttachedFileList &attachedFileList() const; + + private: + friend class EBML::MkAttachments; + friend class File; + class AttachmentsPrivate; + + // private Element implementation + ByteVector renderInternal() override; + AttachedFileList &attachedFiles(); + + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + } +} + +#endif diff --git a/taglib/matroska/matroskachapter.cpp b/taglib/matroska/matroskachapter.cpp new file mode 100644 index 00000000..4867ec25 --- /dev/null +++ b/taglib/matroska/matroskachapter.cpp @@ -0,0 +1,157 @@ +/*************************************************************************** + 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..df030d8a --- /dev/null +++ b/taglib/matroska/matroskachapter.h @@ -0,0 +1,178 @@ +/*************************************************************************** + 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 { + class String; + class ByteVector; + + namespace EBML { + class MkChapters; + } + + namespace Matroska { + //! Matroska chapter. + class TAGLIB_EXPORT Chapter + { + public: + //! Unique identifier. + using UID = unsigned long long; + + //! Timestamp in nanoseconds. + 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..2e9f733c --- /dev/null +++ b/taglib/matroska/matroskachapteredition.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 "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) noexcept = 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..3d8758f3 --- /dev/null +++ b/taglib/matroska/matroskachapteredition.h @@ -0,0 +1,108 @@ +/*************************************************************************** + 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: + //! Unique identifier. + 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) noexcept; + + /*! + * 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..4b88cecc --- /dev/null +++ b/taglib/matroska/matroskachapters.cpp @@ -0,0 +1,151 @@ +/*************************************************************************** + 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 "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) +{ + const 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(const 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(); + const 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..895c7603 --- /dev/null +++ b/taglib/matroska/matroskachapters.h @@ -0,0 +1,87 @@ +/*************************************************************************** + 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: + //! List of chapter editions. + 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/matroskacues.cpp b/taglib/matroska/matroskacues.cpp new file mode 100644 index 00000000..c71f3fe0 --- /dev/null +++ b/taglib/matroska/matroskacues.cpp @@ -0,0 +1,312 @@ +/*************************************************************************** + * 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 "matroskacues.h" +#include "ebmlelement.h" +#include "ebmlmkcues.h" +#include "ebmlmasterelement.h" +#include "ebmluintelement.h" +#include "tlist.h" +#include "tdebug.h" +#include "tfile.h" + +using namespace TagLib; + +Matroska::Cues::Cues(offset_t segmentDataOffset) : + Element(static_cast(EBML::Element::Id::MkCues)), + segmentDataOffset(segmentDataOffset) +{ + setNeedsRender(false); +} + +Matroska::Cues::~Cues() = default; + +ByteVector Matroska::Cues::renderInternal() +{ + const auto beforeSize = sizeRenderedOrWritten(); + EBML::MkCues cues; + cues.setMinRenderSize(beforeSize); + for(const auto &cuePoint : cuePoints) { + auto cuePointElement = EBML::make_unique_element(); + auto timestamp = EBML::make_unique_element(); + timestamp->setValue(cuePoint->getTime()); + cuePointElement->appendElement(std::move(timestamp)); + + const auto &trackList = cuePoint->cueTrackList(); + for(const auto &cueTrack : trackList) { + auto cueTrackElement = EBML::make_unique_element(); + + // Track number + auto trackNumber = EBML::make_unique_element(); + trackNumber->setValue(cueTrack->getTrackNumber()); + cueTrackElement->appendElement(std::move(trackNumber)); + + // Cluster position + auto clusterPosition = EBML::make_unique_element(); + clusterPosition->setValue(cueTrack->getClusterPosition()); + cueTrackElement->appendElement(std::move(clusterPosition)); + + // Relative position, optional + if(cueTrack->getRelativePosition().has_value()) { + auto relativePosition = EBML::make_unique_element(); + // operator*() used instead of value() to support restricted compilers + relativePosition->setValue(*cueTrack->getRelativePosition()); + cueTrackElement->appendElement(std::move(relativePosition)); + } + + // Duration, optional + if(cueTrack->getDuration().has_value()) { + auto duration = EBML::make_unique_element(); + // operator*() used instead of value() to support restricted compilers + duration->setValue(*cueTrack->getDuration()); + cueTrackElement->appendElement(std::move(duration)); + } + + // Block number, optional + if(cueTrack->getBlockNumber().has_value()) { + auto blockNumber = EBML::make_unique_element(); + // operator*() used instead of value() to support restricted compilers + blockNumber->setValue(*cueTrack->getBlockNumber()); + cueTrackElement->appendElement(std::move(blockNumber)); + } + + // Codec state, not in version 1 + if(cueTrack->getCodecState().has_value()) { + auto codecState = EBML::make_unique_element(); + // operator*() used instead of value() to support restricted compilers + codecState->setValue(*cueTrack->getCodecState()); + cueTrackElement->appendElement(std::move(codecState)); + } + + // Reference times + if(auto referenceTimes = cueTrack->referenceTimes(); !referenceTimes.isEmpty()) { + auto cueReference = EBML::make_unique_element(); + for(const auto reference : referenceTimes) { + auto refTime = EBML::make_unique_element(); + refTime->setValue(reference); + cueReference->appendElement(std::move(refTime)); + } + cueTrackElement->appendElement(std::move(cueReference)); + } + cuePointElement->appendElement(std::move(cueTrackElement)); + } + cues.appendElement(std::move(cuePointElement)); + } + return cues.render(); +} + +void Matroska::Cues::write(TagLib::File &file) +{ + if(!data().isEmpty()) + Element::write(file); +} + +bool Matroska::Cues::sizeChanged(Element &caller, offset_t delta) +{ + // Adjust own offset + if(!Element::sizeChanged(caller, delta)) + return false; + + const offset_t offset = caller.offset() - segmentDataOffset; + for(const auto &cuePoint : cuePoints) { + if(cuePoint->adjustOffset(offset, delta)) { + setNeedsRender(true); + } + } + return true; +} + +bool Matroska::Cues::isValid(TagLib::File &file) const +{ + for(const auto &cuePoint : cuePoints) { + if(!cuePoint->isValid(file, segmentDataOffset)) + return false; + } + return true; +} + +void Matroska::Cues::addCuePoint(std::unique_ptr &&cuePoint) +{ + cuePoints.push_back(std::move(cuePoint)); +} + +const Matroska::Cues::CuePointList &Matroska::Cues::cuePointList() +{ + return cuePoints; +} + +Matroska::CuePoint::CuePoint() = default; + +Matroska::CuePoint::~CuePoint() = default; + +bool Matroska::CuePoint::isValid(TagLib::File &file, offset_t segmentDataOffset) const +{ + for(const auto &track : cueTracks) { + if(!track->isValid(file, segmentDataOffset)) + return false; + } + return true; +} + +void Matroska::CuePoint::addCueTrack(std::unique_ptr &&cueTrack) +{ + cueTracks.push_back(std::move(cueTrack)); +} + +const Matroska::CuePoint::CueTrackList &Matroska::CuePoint::cueTrackList() const +{ + return cueTracks; +} + +void Matroska::CuePoint::setTime(Time timestamp) +{ + time = timestamp; +} + +Matroska::CuePoint::Time Matroska::CuePoint::getTime() const +{ + return time; +} + +bool Matroska::CuePoint::adjustOffset(offset_t offset, offset_t delta) +{ + bool ret = false; + for(const auto &cueTrack : cueTracks) + ret |= cueTrack->adjustOffset(offset, delta); + + return ret; +} + +Matroska::CueTrack::CueTrack() = default; + +Matroska::CueTrack::~CueTrack() = default; + +bool Matroska::CueTrack::isValid(TagLib::File &file, offset_t segmentDataOffset) const +{ + if(!trackNumber) { + debug("Cue track number not set"); + return false; + } + if(!clusterPosition) { + debug("Cue track cluster position not set"); + return false; + } + file.seek(segmentDataOffset + clusterPosition); + if(EBML::Element::readId(file) != static_cast(EBML::Element::Id::MkCluster)) { + debug("No cluster found at position"); + return false; + } + if(codecState.has_value() && *codecState != 0) { + // operator*() used instead of value() to support restricted compilers + file.seek(segmentDataOffset + *codecState); + if(EBML::Element::readId(file) != static_cast(EBML::Element::Id::MkCodecState)) { + debug("No codec state found at position"); + return false; + } + } + return true; +} + +void Matroska::CueTrack::setTrackNumber(unsigned long long trackNr) +{ + trackNumber = trackNr; +} + +unsigned long long Matroska::CueTrack::getTrackNumber() const +{ + return trackNumber; +} + +void Matroska::CueTrack::setClusterPosition(offset_t clusterPos) +{ + clusterPosition = clusterPos; +} + +offset_t Matroska::CueTrack::getClusterPosition() const +{ + return clusterPosition; +} + +void Matroska::CueTrack::setRelativePosition(std::optional relativePos) +{ + relativePosition = relativePos; +} + +std::optional Matroska::CueTrack::getRelativePosition() const +{ + return relativePosition; +} + +void Matroska::CueTrack::setCodecState(std::optional codecStatePos) +{ + codecState = codecStatePos; +} + +std::optional Matroska::CueTrack::getCodecState() const +{ + return codecState; +} + +void Matroska::CueTrack::setBlockNumber(std::optional blockNr) +{ + blockNumber = blockNr; +} + +std::optional Matroska::CueTrack::getBlockNumber() const +{ + return blockNumber; +} + +void Matroska::CueTrack::setDuration(std::optional segmentTicks) +{ + duration = segmentTicks; +} + +std::optional Matroska::CueTrack::getDuration() const +{ + return duration; +} + +void Matroska::CueTrack::addReferenceTime(unsigned long long refTime) +{ + refTimes.append(refTime); +} + +const Matroska::CueTrack::ReferenceTimeList &Matroska::CueTrack::referenceTimes() const +{ + return refTimes; +} + +bool Matroska::CueTrack::adjustOffset(offset_t offset, offset_t delta) +{ + bool ret = false; + if(clusterPosition > offset) { + clusterPosition += delta; + ret = true; + } + // operator*() used instead of value() to support restricted compilers + if(offset_t codecStateValue; + codecState.has_value() && (codecStateValue = *codecState) != 0 && + codecStateValue > offset) { + codecState = codecStateValue + delta; + ret = true; + } + return ret; +} diff --git a/taglib/matroska/matroskacues.h b/taglib/matroska/matroskacues.h new file mode 100644 index 00000000..e9285f69 --- /dev/null +++ b/taglib/matroska/matroskacues.h @@ -0,0 +1,116 @@ +/*************************************************************************** + * 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_MATROSKACUES_H +#define TAGLIB_MATROSKACUES_H +#ifndef DO_NOT_DOCUMENT + +#include + +#include "tlist.h" +#include "matroskaelement.h" + +namespace TagLib { + class File; + + namespace EBML { + class MkCues; + } + + namespace Matroska { + class CuePoint; + class CueTrack; + + class Cues : public Element + { + public: + using CuePointList = std::list>; + explicit Cues(offset_t segmentDataOffset); + ~Cues() override; + bool isValid(TagLib::File &file) const; + void addCuePoint(std::unique_ptr &&cuePoint); + const CuePointList &cuePointList(); + bool sizeChanged(Element &caller, offset_t delta) override; + void write(TagLib::File &file) override; + + private: + friend class EBML::MkCues; + ByteVector renderInternal() override; + + CuePointList cuePoints; + const offset_t segmentDataOffset; + }; + + class CuePoint + { + public: + using CueTrackList = std::list>; + using Time = unsigned long long; + CuePoint(); + ~CuePoint(); + bool isValid(TagLib::File &file, offset_t segmentDataOffset) const; + void addCueTrack(std::unique_ptr &&cueTrack); + const CueTrackList &cueTrackList() const; + void setTime(Time timestamp); + Time getTime() const; + bool adjustOffset(offset_t offset, offset_t delta); + + private: + CueTrackList cueTracks; + Time time = 0; + }; + + class CueTrack + { + public: + using ReferenceTimeList = List; + CueTrack(); + ~CueTrack(); + bool isValid(TagLib::File &file, offset_t segmentDataOffset) const; + void setTrackNumber(unsigned long long trackNr); + unsigned long long getTrackNumber() const; + void setClusterPosition(offset_t clusterPos); + offset_t getClusterPosition() const; + void setRelativePosition(std::optional relativePos); + std::optional getRelativePosition() const; + void setCodecState(std::optional codecStatePos); + std::optional getCodecState() const; + void setBlockNumber(std::optional blockNr); + std::optional getBlockNumber() const; + void setDuration(std::optional segmentTicks); + std::optional getDuration() const; + void addReferenceTime(unsigned long long refTime); + const ReferenceTimeList &referenceTimes() const; + bool adjustOffset(offset_t offset, offset_t delta); + + private: + unsigned long long trackNumber = 0; + offset_t clusterPosition = 0; + std::optional relativePosition; + std::optional blockNumber; + std::optional duration; + std::optional codecState; + ReferenceTimeList refTimes; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/matroskaelement.cpp b/taglib/matroska/matroskaelement.cpp new file mode 100644 index 00000000..f1f6b63a --- /dev/null +++ b/taglib/matroska/matroskaelement.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + * 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 "matroskaelement.h" +#include +#include "tlist.h" +#include "tfile.h" +#include "tbytevector.h" + +using namespace TagLib; + +class Matroska::Element::ElementPrivate +{ +public: + ElementPrivate() = default; + ~ElementPrivate() = default; + ElementPrivate(const ElementPrivate &) = delete; + ElementPrivate &operator=(const ElementPrivate &) = delete; + offset_t size = 0; + offset_t offset = 0; + ID id = 0; + ByteVector data; + List sizeListeners; + // The default write() implementation will delete an unrendered element, + // therefore rendering is required by default and needs to be explicitly set + // using setNeedsRender(false) together with overriding the write() method. + bool needsRender = true; +}; + +Matroska::Element::Element(ID id) : + e(std::make_unique()) +{ + e->id = id; +} + +Matroska::Element::~Element() = default; + +offset_t Matroska::Element::size() const +{ + return e->size; +} + +offset_t Matroska::Element::offset() const +{ + return e->offset; +} + +void Matroska::Element::setData(const ByteVector &data) +{ + e->data = data; +} + +const ByteVector &Matroska::Element::data() const +{ + return e->data; +} + +void Matroska::Element::setOffset(offset_t offset) +{ + e->offset = offset; +} + +void Matroska::Element::adjustOffset(offset_t delta) +{ + e->offset += delta; +} + +void Matroska::Element::setSize(offset_t size) +{ + e->size = size; +} + +Matroska::Element::ID Matroska::Element::id() const +{ + return e->id; +} + +void Matroska::Element::addSizeListener(Element *element) +{ + e->sizeListeners.append(element); +} + +void Matroska::Element::addSizeListeners(const List &elements) +{ + e->sizeListeners.append(elements); +} + +void Matroska::Element::setID(ID id) +{ + e->id = id; +} + +bool Matroska::Element::render() +{ + if(!needsRender()) + return true; + + const auto beforeSize = sizeRenderedOrWritten(); + const auto data = renderInternal(); + setNeedsRender(false); + if(const auto afterSize = data.size(); afterSize != beforeSize) { + if(!emitSizeChanged(afterSize - beforeSize)) { + return false; + } + } + + setData(data); + return true; +} + +void Matroska::Element::setNeedsRender(bool needsRender) +{ + e->needsRender = needsRender; +} + +bool Matroska::Element::needsRender() const +{ + return e->needsRender; +} + +bool Matroska::Element::emitSizeChanged(offset_t delta) +{ + for(const auto element : e->sizeListeners) { + if(!element->sizeChanged(*this, delta)) + return false; + } + return true; +} + +bool Matroska::Element::sizeChanged(Element &caller, offset_t delta) +{ + // The equal case is needed when multiple new elements are added + // (e.g. Attachments and Tags), they will start with the same offset + // are updated via size change handling. + if(caller.offset() <= e->offset && caller.id() != e->id) { + e->offset += delta; + } + return true; +} + +offset_t Matroska::Element::sizeRenderedOrWritten() const +{ + const offset_t dataSize = e->data.size(); + return dataSize != 0 ? dataSize : e->size; +} + +void Matroska::Element::write(File &file) +{ + file.insert(e->data, e->offset, e->size); + e->size = e->data.size(); +} diff --git a/taglib/matroska/matroskaelement.h b/taglib/matroska/matroskaelement.h new file mode 100644 index 00000000..ad5efdc8 --- /dev/null +++ b/taglib/matroska/matroskaelement.h @@ -0,0 +1,74 @@ +/*************************************************************************** + * 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_MATROSKAELEMENT_H +#define TAGLIB_MATROSKAELEMENT_H +#ifndef DO_NOT_DOCUMENT + +#include +#include "taglib_export.h" +#include "taglib.h" +#include "tlist.h" + +namespace TagLib { + class File; + class ByteVector; + + namespace Matroska { + class TAGLIB_EXPORT Element + { + public: + using ID = unsigned int; + explicit Element(ID id); + virtual ~Element(); + + offset_t size() const; + offset_t offset() const; + ID id() const; + void setOffset(offset_t offset); + void adjustOffset(offset_t delta); + void setSize(offset_t size); + void setID(ID id); + virtual bool render(); + void setNeedsRender(bool needsRender); + bool needsRender() const; + void setData(const ByteVector &data); + const ByteVector &data() const; + virtual void write(TagLib::File &file); + void addSizeListener(Element *element); + void addSizeListeners(const List &elements); + bool emitSizeChanged(offset_t delta); + virtual bool sizeChanged(Element &caller, offset_t delta); + + protected: + offset_t sizeRenderedOrWritten() const; + + private: + virtual ByteVector renderInternal() = 0; + + class ElementPrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr e; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/matroskafile.cpp b/taglib/matroska/matroskafile.cpp new file mode 100644 index 00000000..e21278be --- /dev/null +++ b/taglib/matroska/matroskafile.cpp @@ -0,0 +1,559 @@ +/*************************************************************************** + * 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 "matroskafile.h" +#include +#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" +#include "ebmlutils.h" +#include "ebmlelement.h" +#include "ebmlstringelement.h" +#include "ebmluintelement.h" +#include "ebmlmksegment.h" +#include "tlist.h" +#include "tdebug.h" +#include "tagutils.h" +#include "tpropertymap.h" + +using namespace TagLib; + +class Matroska::File::FilePrivate +{ +public: + FilePrivate() = default; + ~FilePrivate() = default; + + FilePrivate(const FilePrivate &) = delete; + FilePrivate &operator=(const FilePrivate &) = delete; + + std::unique_ptr tag; + std::unique_ptr attachments; + std::unique_ptr chapters; + std::unique_ptr seekHead; + std::unique_ptr cues; + std::unique_ptr segment; + std::unique_ptr properties; +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool Matroska::File::isSupported(IOStream *stream) +{ + const ByteVector id = Utils::readHeader(stream, 4, false); + return id.startsWith("\x1A\x45\xDF\xA3"); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::File::File(FileName file, bool readProperties, + Properties::ReadStyle readStyle) : + TagLib::File(file), + d(std::make_unique()) +{ + if(!isOpen()) { + debug("Failed to open matroska file"); + setValid(false); + return; + } + read(readProperties, readStyle); +} + +Matroska::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle readStyle) : + TagLib::File(stream), + d(std::make_unique()) +{ + if(!isOpen()) { + debug("Failed to open matroska file"); + setValid(false); + return; + } + read(readProperties, readStyle); +} + +Matroska::File::~File() = default; + +Matroska::Properties *Matroska::File::audioProperties() const +{ + return d->properties.get(); +} + +Tag *Matroska::File::tag() const +{ + return tag(true); +} + +Matroska::Tag *Matroska::File::tag(bool create) const +{ + if(!d->tag && create) { + d->tag = std::make_unique(); + if(d->properties) { + d->tag->setSegmentTitle(d->properties->title()); + } + } + return d->tag.get(); +} + +PropertyMap Matroska::File::properties() const +{ + return d->tag ? d->tag->properties() : PropertyMap(); +} + +void Matroska::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->tag) { + d->tag->removeUnsupportedProperties(properties); + } +} + +PropertyMap Matroska::File::setProperties(const PropertyMap &properties) +{ + if(!d->tag) { + d->tag = std::make_unique(); + } + return d->tag->setProperties(properties); +} + +namespace { + + String keyForAttachedFile(const Matroska::AttachedFile &attachedFile) + { + if(attachedFile.mediaType().startsWith("image/")) { + return "PICTURE"; + } + if(!attachedFile.fileName().isEmpty()) { + return attachedFile.fileName(); + } + if(!attachedFile.mediaType().isEmpty()) { + return attachedFile.mediaType(); + } + return String::fromULongLong(attachedFile.uid()); + } + + bool keyMatchesAttachedFile(const String &key, const Matroska::AttachedFile &attachedFile) + { + return !key.isEmpty() && ( + (key == "PICTURE" && attachedFile.mediaType().startsWith("image/")) || + key == attachedFile.fileName() || + key == attachedFile.mediaType() || + key == String::fromULongLong(attachedFile.uid()) + ); + } + +} + +StringList Matroska::File::complexPropertyKeys() const +{ + StringList keys = TagLib::File::complexPropertyKeys(); + if(d->attachments) { + const auto &attachedFiles = d->attachments->attachedFileList(); + for(const auto &attachedFile : attachedFiles) { + if(String key = keyForAttachedFile(attachedFile); + !key.isEmpty() && !keys.contains(key)) { + keys.append(key); + } + } + } + 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(const auto uid = edition.uid()) { + property.insert("uid", uid); + } + if(const auto isDefault = edition.isDefault()) { + property.insert("isDefault", isDefault); + } + if(const 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(const auto uid = chapter.uid()) { + chap.insert("uid", uid); + } + if(const auto isHidden = chapter.isHidden()) { + chap.insert("isHidden", isHidden); + } + chap.insert("timeStart", chapter.timeStart()); + if(const 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) { + if(keyMatchesAttachedFile(key, attachedFile)) { + VariantMap property; + property.insert("data", attachedFile.data()); + property.insert("mimeType", attachedFile.mediaType()); + property.insert("description", attachedFile.description()); + property.insert("fileName", attachedFile.fileName()); + property.insert("uid", attachedFile.uid()); + props.append(property); + } + } + } + return props; +} + +bool Matroska::File::setComplexProperties(const String &key, const List &value) +{ + if(TagLib::File::setComplexProperties(key, value)) { + return true; + } + + if(key.upper() == "CHAPTERS") { + chapters(true)->clear(); + 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)) { + it = files.erase(it); + } + else { + ++it; + } + } + + for(const auto &property : value) { + if(property.isEmpty()) + continue; + auto mimeType = property.value("mimeType").value(); + auto data = property.value("data").value(); + auto fileName = property.value("fileName").value(); + auto uid = property.value("uid").value(); + bool ok; + if(key.upper() == "PICTURE" && !mimeType.startsWith("image/")) { + mimeType = data.startsWith("\x89PNG\x0d\x0a\x1a\x0a") + ? "image/png" : "image/jpeg"; + } + else if(mimeType.isEmpty() && key.find("/") != -1) { + mimeType = key; + } + else if(fileName.isEmpty() && key.find(".") != -1) { + fileName = key; + } + else if(unsigned long long uidKey; + !uid && ((uidKey = key.toULongLong(&ok))) && ok) { + uid = uidKey; + } + if(fileName.isEmpty() && !mimeType.isEmpty()) { + const int slashPos = mimeType.rfind('/'); + String ext = mimeType.substr(slashPos + 1); + if(ext == "jpeg") { + ext = "jpg"; + } + fileName = "attachment." + ext; + } + if(!mimeType.isEmpty() && !fileName.isEmpty()) { + d->attachments->addAttachedFile(AttachedFile( + data, fileName, mimeType, uid, + property.value("description").value())); + } + } + return true; +} + +Matroska::Attachments *Matroska::File::attachments(bool create) const +{ + if(!d->attachments && create) + d->attachments = std::make_unique(); + 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) +{ + const offset_t fileLength = length(); + + // Find the EBML Header + const auto head = EBML::element_cast( + EBML::Element::factory(*this)); + if(!head || head->getId() != EBML::Element::Id::EBMLHeader) { + debug("Failed to find EBML head"); + setValid(false); + return; + } + if(readProperties) { + head->read(*this); + } + else { + head->skipData(*this); + } + + // Find the Matroska segment in the file + const std::unique_ptr segment( + EBML::element_cast( + EBML::findElement(*this, EBML::Element::Id::MkSegment, fileLength - tell()) + ) + ); + if(!segment) { + debug("Failed to find Matroska segment"); + setValid(false); + return; + } + + // Read the segment into memory from file + if(!segment->read(*this)) { + debug("Failed to read segment"); + setValid(false); + return; + } + + // Parse the elements + d->segment = segment->parseSegment(); + d->seekHead = segment->parseSeekHead(); + d->cues = segment->parseCues(); + d->tag = segment->parseTag(); + d->attachments = segment->parseAttachments(); + d->chapters = segment->parseChapters(); + + if(readProperties) { + d->properties = std::make_unique(this); + + for(const auto &element : *head) { + if(const auto id = element->getId(); id == EBML::Element::Id::DocType) { + d->properties->setDocType( + EBML::element_cast(element)->getValue()); + } + else if(id == EBML::Element::Id::DocTypeVersion) { + d->properties->setDocTypeVersion(static_cast( + EBML::element_cast(element)->getValue())); + } + } + + segment->parseInfo(d->properties.get()); + segment->parseTracks(d->properties.get()); + if(d->tag) { + d->tag->setSegmentTitle(d->properties->title()); + } + } + + if(readStyle == AudioProperties::Accurate && + ((d->seekHead && !d->seekHead->isValid(*this)) || + (d->cues && !d->cues->isValid(*this)))) { + setValid(false); + return; + } + setValid(true); +} + +bool Matroska::File::save() +{ + if(readOnly()) { + debug("Matroska::File::save() -- File is read only."); + return false; + } + if(!isValid()) { + debug("Matroska::File::save() -- File is not valid."); + return false; + } + + // 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()) { + d->attachments.reset(); + } + if(d->tag && d->tag->isEmpty() && + d->tag->size() == 0 && d->tag->offset() == 0 && + d->tag->data().isEmpty()) { + d->tag.reset(); + } + + List renderList; + List newElements; + + // List of all possible elements we can write + List elements { + d->chapters.get(), + d->attachments.get(), + d->tag.get() + }; + + /* Build render list. New elements will be added + * to the end of the file. For new elements, + * the order is from least likely to change, + * to most likely to change: + * 1. Chapters + * 2. Attachments + * 3. Tags + */ + for(auto element : elements) { + if(!element) + continue; + if(element->size()) + renderList.append(element); + else { + element->setOffset(length()); + newElements.append(element); + } + } + if(renderList.isEmpty() && newElements.isEmpty()) + return true; + + auto sortAscending = [](const auto a, const auto b) { return a->offset() < b->offset(); }; + renderList.sort(sortAscending); + renderList.append(newElements); + + // Add our new elements to the Seek Head (if the file has one) + if(d->seekHead) { + const auto segmentDataOffset = d->segment->dataOffset(); + for(const auto element : newElements) + d->seekHead->addEntry(element->id(), element->offset() - segmentDataOffset); + d->seekHead->sort(); + } + + // Set up listeners, add seek head and segment length to the end + for(auto it = renderList.begin(); it != renderList.end(); ++it) { + for(auto it2 = std::next(it); it2 != renderList.end(); ++it2) + (*it)->addSizeListener(*it2); + if(d->cues) + (*it)->addSizeListener(d->cues.get()); + if(d->seekHead) + (*it)->addSizeListener(d->seekHead.get()); + (*it)->addSizeListener(d->segment.get()); + } + if(d->cues) { + renderList.append(d->cues.get()); + d->cues->addSizeListeners(renderList); + if(d->seekHead) { + d->cues->addSizeListener(d->seekHead.get()); + } + d->cues->addSizeListener(d->segment.get()); + } + if(d->seekHead) { + renderList.append(d->seekHead.get()); + d->seekHead->addSizeListeners(renderList); + d->seekHead->addSizeListener(d->segment.get()); + } + d->segment->addSizeListeners(renderList); + renderList.append(d->segment.get()); + + // Render the elements. + // Because size changes of elements can cause segment offset updates and + // size changes in other elements, we might need multiple rounds until no more + // element needs rendering. + int renderRound = 0; + bool rendering = true; + while(rendering && renderRound < 5) { + rendering = false; + for(const auto element : renderList) { + if(element->needsRender()) { + rendering = true; + if(!element->render()) { + return false; + } + } + } + ++renderRound; + } + + // Write out to file + renderList.sort(sortAscending); + for(const auto element : renderList) + element->write(*this); + + return true; +} diff --git a/taglib/matroska/matroskafile.h b/taglib/matroska/matroskafile.h new file mode 100644 index 00000000..559be5aa --- /dev/null +++ b/taglib/matroska/matroskafile.h @@ -0,0 +1,183 @@ +/*************************************************************************** + * 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_MATROSKAFILE_H +#define TAGLIB_MATROSKAFILE_H + +#include "taglib_export.h" +#include "tfile.h" +#include "matroskaproperties.h" + +namespace TagLib::Matroska { + class Tag; + class Attachments; + class Chapters; + + /*! + * Implementation of TagLib::File for Matroska. + */ + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * Constructs a Matroska file from \a file. If \a readProperties is \c true the + * file's audio properties will also be read. + * + * If \a readStyle is \c Accurate all seek head and cues segment positions + * are verified for the isValid() state of the file. + */ + explicit File(FileName file, bool readProperties = true, + Properties::ReadStyle readStyle = Properties::Average); + + /*! + * Constructs a Matroska file from \a stream. If \a readProperties is \c true the + * file's audio properties will also be read. + * + * If \a readStyle is \c Accurate all seek head and cues segment positions + * are verified for the isValid() state of the file. + */ + explicit File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle readStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + ~File() override; + + File(const File &) = delete; + File &operator=(const File &) = delete; + + /*! + * Returns a pointer to the tag of the file. + * + * It will create a tag if one does not exist and returns a valid pointer. + * + * \note The tag is still owned by the Matroska::File and should not + * be deleted by the user. It will be deleted when the file (object) is + * destroyed. + */ + TagLib::Tag *tag() const override; + + /*! + * Returns a pointer to the Matroska tag of the file. + * + * If \a create is \c false this may return a null pointer if there is no tag. + * If \a create is \c true it will create a tag if one does not exist and + * returns a valid pointer. + * + * \note The tag is still owned by the Matroska::File and should not + * be deleted by the user. It will be deleted when the file (object) + * destroyed. + */ + Tag *tag(bool create) const; + + /*! + * Implements the reading part of the unified property interface. + */ + PropertyMap properties() const override; + + void removeUnsupportedProperties(const StringList &properties) override; + + /*! + * Implements the writing part of the unified tag dictionary interface. + */ + PropertyMap setProperties(const PropertyMap &) override; + + /*! + * Returns the keys for attached files, "PICTURE" for images, the media + * type, file name or UID for other attached files. + * The names of the binary simple tags are included too. + */ + StringList complexPropertyKeys() const override; + + /*! + * Get the pictures stored in the attachments as complex properties + * for \a key "PICTURE". Other attached files can be retrieved, by + * media type, file name or UID. + * The attached files are returned as maps with keys "data", "mimeType", + * "description", "fileName, "uid". + * Binary simple tags can be retrieved as maps with keys "data", "name", + * "targetTypeValue", "language", "defaultLanguage". + */ + List complexProperties(const String &key) const override; + + /*! + * Set attached files as complex properties \a value, e.g. pictures for + * \a key "PICTURE" with the maps in \a value having keys "data", "mimeType", + * "description", "fileName, "uid". For other attached files, the mime type, + * file name or UID can be used as the \a key. + * Maps with keys "name" (with the same value as \a key) and "data" are + * stored as binary simple tags with additional keys "targetTypeValue", + * "language", "defaultLanguage". + */ + bool setComplexProperties(const String &key, const List &value) override; + + /*! + * Returns the Matroska::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + Properties *audioProperties() const override; + + /*! + * Save the file. + * + * This returns \c true if the save was successful. + */ + 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. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + + private: + void read(bool readProperties, Properties::ReadStyle readStyle); + class FilePrivate; + friend class Properties; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; +} + +#endif diff --git a/taglib/matroska/matroskaproperties.cpp b/taglib/matroska/matroskaproperties.cpp new file mode 100644 index 00000000..fe37defb --- /dev/null +++ b/taglib/matroska/matroskaproperties.cpp @@ -0,0 +1,155 @@ +/*************************************************************************** + 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 "matroskaproperties.h" + +#include "matroskafile.h" + +using namespace TagLib; + +class Matroska::Properties::PropertiesPrivate +{ +public: + explicit PropertiesPrivate(File *file) : file(file) {} + ~PropertiesPrivate() = default; + + PropertiesPrivate(const PropertiesPrivate &) = delete; + PropertiesPrivate &operator=(const PropertiesPrivate &) = delete; + + File *file; + String codecName; + String title; + String docType; + int docTypeVersion { 0 }; + int length { 0 }; + int bitrate { -1 }; + int sampleRate { 0 }; + int channels { 0 }; + int bitsPerSample { 0 }; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::Properties::Properties(File *file, ReadStyle style) : + AudioProperties(style), + d(std::make_unique(file)) +{ +} + +Matroska::Properties::~Properties() = default; + +int Matroska::Properties::lengthInMilliseconds() const +{ + return d->length; +} + +int Matroska::Properties::bitrate() const +{ + if(d->bitrate == -1) { + d->bitrate = d->length != 0 ? static_cast(d->file->length() * 8 / d->length) : 0; + } + return d->bitrate; +} + +int Matroska::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int Matroska::Properties::channels() const +{ + return d->channels; +} + +int Matroska::Properties::bitsPerSample() const +{ + return d->bitsPerSample; +} + +String Matroska::Properties::docType() const +{ + return d->docType; +} + +int Matroska::Properties::docTypeVersion() const +{ + return d->docTypeVersion; +} + +String Matroska::Properties::codecName() const +{ + return d->codecName; +} + +String Matroska::Properties::title() const +{ + return d->title; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Matroska::Properties::setLengthInMilliseconds(int length) +{ + d->length = length; +} + +void Matroska::Properties::setSampleRate(int sampleRate) +{ + d->sampleRate = sampleRate; +} + +void Matroska::Properties::setChannels(int channels) +{ + d->channels = channels; +} + +void Matroska::Properties::setBitsPerSample(int bitsPerSample) +{ + d->bitsPerSample = bitsPerSample; +} + +void Matroska::Properties::setDocType(const String &docType) +{ + d->docType = docType; +} + +void Matroska::Properties::setDocTypeVersion(int docTypeVersion) +{ + d->docTypeVersion = docTypeVersion; +} + +void Matroska::Properties::setCodecName(const String &codecName) +{ + d->codecName = codecName; +} + +void Matroska::Properties::setTitle(const String &title) +{ + d->title = title; +} diff --git a/taglib/matroska/matroskaproperties.h b/taglib/matroska/matroskaproperties.h new file mode 100644 index 00000000..b81c7826 --- /dev/null +++ b/taglib/matroska/matroskaproperties.h @@ -0,0 +1,127 @@ +/*************************************************************************** + 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_MATROSKAPROPERTIES_H +#define TAGLIB_MATROSKAPROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib::EBML { + class MkTracks; + class MkInfo; +} + +namespace TagLib::Matroska { + class File; + + //! An implementation of Matroska audio properties + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Creates an instance of Matroska::Properties. + */ + explicit Properties(File *file, ReadStyle style = Average); + + /*! + * Destroys this Matroska::Properties instance. + */ + ~Properties() override; + + Properties(const Properties &) = delete; + Properties &operator=(const Properties &) = delete; + + /*! + * Returns the length of the file in milliseconds. + * + * \see lengthInSeconds() + */ + int lengthInMilliseconds() const override; + + /*! + * Returns the average bit rate of the file in kb/s. + */ + int bitrate() const override; + + /*! + * Returns the sample rate in Hz. + */ + int sampleRate() const override; + + /*! + * Returns the number of audio channels. + */ + int channels() const override; + + /*! + * Returns the number of bits per audio sample. + */ + int bitsPerSample() const; + + /*! + * Returns the EBML doc type, "matroska" or "webm". + */ + String docType() const; + + /*! + * Returns the EBML doc type version, typical values are 2 or 4. + */ + int docTypeVersion() const; + + /*! + * Returns the concrete codec name, for example "A_MPEG/L3" + * used in the file if available, otherwise an empty string. + */ + String codecName() const; + + /*! + * Returns the general name of the segment. + * Some applications store the title of the file here, but players should + * prioritize the tag title over the segment title. + */ + String title() const; + + private: + class PropertiesPrivate; + friend class EBML::MkInfo; + friend class EBML::MkTracks; + friend class File; + + void setLengthInMilliseconds(int length); + void setSampleRate(int sampleRate); + void setChannels(int channels); + void setBitsPerSample(int bitsPerSample); + void setDocType(const String &docType); + void setDocTypeVersion(int docTypeVersion); + void setCodecName(const String &codecName); + void setTitle(const String &title); + + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; +} + +#endif diff --git a/taglib/matroska/matroskaseekhead.cpp b/taglib/matroska/matroskaseekhead.cpp new file mode 100644 index 00000000..bc8bb0a1 --- /dev/null +++ b/taglib/matroska/matroskaseekhead.cpp @@ -0,0 +1,133 @@ +/*************************************************************************** + * 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 "matroskaseekhead.h" +#include "ebmlmkseekhead.h" +#include "ebmlbinaryelement.h" +#include "ebmluintelement.h" +#include "ebmlmasterelement.h" +#include "tfile.h" +#include "tutils.h" +#include "tdebug.h" + +using namespace TagLib; + +Matroska::SeekHead::SeekHead(offset_t segmentDataOffset) : + Element(static_cast(EBML::Element::Id::MkSeekHead)), + segmentDataOffset(segmentDataOffset) +{ + setNeedsRender(false); +} + +Matroska::SeekHead::~SeekHead() = default; + +bool Matroska::SeekHead::isValid(TagLib::File &file) const +{ + bool result = true; + for(const auto &[id, offset] : entries) { + file.seek(segmentDataOffset + offset); + if(EBML::Element::readId(file) != id) { + debug(Utils::formatString("No ID %x found at seek position", id)); + result = false; + } + } + return result; +} + +void Matroska::SeekHead::addEntry(const Element &element) +{ + entries.append({element.id(), element.offset()}); + debug("adding to seekhead"); + setNeedsRender(true); +} + +void Matroska::SeekHead::addEntry(ID id, offset_t offset) +{ + entries.append({id, offset}); + setNeedsRender(true); +} + +ByteVector Matroska::SeekHead::renderInternal() +{ + const auto beforeSize = sizeRenderedOrWritten(); + EBML::MkSeekHead seekHead; + seekHead.setMinRenderSize(beforeSize); + for(const auto &[id, position] : entries) { + auto seekElement = EBML::make_unique_element(); + auto idElement = EBML::make_unique_element(); + idElement->setValue(ByteVector::fromUInt(id, true)); + seekElement->appendElement(std::move(idElement)); + + auto positionElement = EBML::make_unique_element(); + positionElement->setValue(static_cast(position)); + seekElement->appendElement(std::move(positionElement)); + + seekHead.appendElement(std::move(seekElement)); + } + return seekHead.render(); +} + +void Matroska::SeekHead::write(File &file) +{ + if(!data().isEmpty()) + Element::write(file); +} + +void Matroska::SeekHead::sort() +{ + entries.sort([](const auto &a, const auto &b) { return a.second < b.second; }); +} + +bool Matroska::SeekHead::sizeChanged(Element &caller, offset_t delta) +{ + ID callerID = caller.id(); + if(callerID == static_cast(EBML::Element::Id::MkSegment)) { + adjustOffset(delta); + return true; + } + // The equal case is needed when multiple new elements are added + // (e.g. Attachments and Tags), they will start with the same offset + // and are updated via size change handling. + offset_t offset = caller.offset() - segmentDataOffset; + auto it = entries.begin(); + while(it != entries.end()) { + it = std::find_if(it, + entries.end(), + [offset, callerID](const auto &a) { + return a.second >= offset && a.first != callerID; + } + ); + if(it != entries.end()) { + it->second += delta; + setNeedsRender(true); + ++it; + } + } + + if(caller.data().isEmpty() && caller.size() + delta == 0) { + // The caller element is removed, remove it from the seek head. + it = std::find_if(entries.begin(), entries.end(), + [callerID](const auto &a){ return a.first == callerID; }); + if(it != entries.end()) { + entries.erase(it); + } + } + return true; +} diff --git a/taglib/matroska/matroskaseekhead.h b/taglib/matroska/matroskaseekhead.h new file mode 100644 index 00000000..56fe6f4c --- /dev/null +++ b/taglib/matroska/matroskaseekhead.h @@ -0,0 +1,55 @@ +/*************************************************************************** + * 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_MATROSKASEEKHEAD_H +#define TAGLIB_MATROSKASEEKHEAD_H +#ifndef DO_NOT_DOCUMENT + +#include "matroskaelement.h" +#include "tlist.h" + +namespace TagLib { + class File; + class ByteVector; + + namespace Matroska { + class SeekHead : public Element + { + public: + explicit SeekHead(offset_t segmentDataOffset); + ~SeekHead() override; + + bool isValid(TagLib::File &file) const; + void addEntry(const Element &element); + void addEntry(ID id, offset_t offset); + void write(TagLib::File &file) override; + void sort(); + bool sizeChanged(Element &caller, offset_t delta) override; + + private: + ByteVector renderInternal() override; + List> entries; + const offset_t segmentDataOffset; + }; + } +} + +#endif +#endif diff --git a/taglib/matroska/matroskasegment.cpp b/taglib/matroska/matroskasegment.cpp new file mode 100644 index 00000000..d4d52c1a --- /dev/null +++ b/taglib/matroska/matroskasegment.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + * 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 "matroskasegment.h" +#include "ebmlutils.h" +#include "tbytevector.h" + +using namespace TagLib; + +Matroska::Segment::Segment(offset_t sizeLength, offset_t dataSize, offset_t lengthOffset) : + Element(static_cast(EBML::Element::Id::MkSegment)), + sizeLength(sizeLength), dataSize(dataSize) +{ + setOffset(lengthOffset); + setSize(sizeLength); +} + +Matroska::Segment::~Segment() = default; + +ByteVector Matroska::Segment::renderInternal() +{ + return EBML::renderVINT(dataSize, static_cast(sizeLength)); +} + +bool Matroska::Segment::render() +{ + const auto beforeSize = sizeLength; + auto data = renderInternal(); + setNeedsRender(false); + if(auto afterSize = data.size(); afterSize != beforeSize) { + sizeLength = 8; + data = renderInternal(); + setNeedsRender(false); + afterSize = data.size(); + if(!emitSizeChanged(afterSize - beforeSize)) { + return false; + } + } + + setData(data); + return true; +} + +bool Matroska::Segment::sizeChanged(Element &, offset_t delta) +{ + dataSize += delta; + setNeedsRender(true); + return true; +} + +offset_t Matroska::Segment::dataOffset() const +{ + return offset() + sizeLength; +} diff --git a/taglib/matroska/matroskasegment.h b/taglib/matroska/matroskasegment.h new file mode 100644 index 00000000..cd6952dc --- /dev/null +++ b/taglib/matroska/matroskasegment.h @@ -0,0 +1,46 @@ +/*************************************************************************** + * 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_MATROSKASEGMENT_H +#define TAGLIB_MATROSKASEGMENT_H +#ifndef DO_NOT_DOCUMENT + +#include "matroskaelement.h" + +namespace TagLib::Matroska { + class Segment : public Element + { + public: + Segment(offset_t sizeLength, offset_t dataSize, offset_t lengthOffset); + ~Segment() override; + bool render() override; + bool sizeChanged(Element &caller, offset_t delta) override; + offset_t dataOffset() const; + + private: + ByteVector renderInternal() override; + + offset_t sizeLength; + offset_t dataSize; + }; +} + +#endif +#endif diff --git a/taglib/matroska/matroskasimpletag.cpp b/taglib/matroska/matroskasimpletag.cpp new file mode 100644 index 00000000..cd9a57d5 --- /dev/null +++ b/taglib/matroska/matroskasimpletag.cpp @@ -0,0 +1,142 @@ +/*************************************************************************** + * 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 "matroskasimpletag.h" +#include +#include "matroskatag.h" +#include "tbytevector.h" + +using namespace TagLib; + +class Matroska::SimpleTag::SimpleTagPrivate +{ +public: + SimpleTagPrivate(const String &name, const String &value, + TargetTypeValue targetTypeValue, const String &language, bool defaultLanguage, + unsigned long long trackUid) : + value(value), name(name), language(language), trackUid(trackUid), + targetTypeValue(targetTypeValue), defaultLanguageFlag(defaultLanguage) {} + SimpleTagPrivate(const String &name, const ByteVector &value, + TargetTypeValue targetTypeValue, const String &language, bool defaultLanguage, + unsigned long long trackUid) : + value(value), name(name), language(language), trackUid(trackUid), + targetTypeValue(targetTypeValue), defaultLanguageFlag(defaultLanguage) {} + + const std::variant value; + const String name; + const String language; + const unsigned long long trackUid; + const TargetTypeValue targetTypeValue; + const bool defaultLanguageFlag; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Matroska::SimpleTag::SimpleTag(const String &name, const String &value, + TargetTypeValue targetTypeValue, + const String &language, bool defaultLanguage, + unsigned long long trackUid) : + d(std::make_unique(name, value, targetTypeValue, + language, defaultLanguage, trackUid)) +{ +} + +Matroska::SimpleTag::SimpleTag(const String &name, const ByteVector &value, + TargetTypeValue targetTypeValue, + const String &language, bool defaultLanguage, + unsigned long long trackUid) : + d(std::make_unique(name, value, targetTypeValue, + language, defaultLanguage, trackUid)) +{ +} + +Matroska::SimpleTag::SimpleTag(const SimpleTag &other) : + d(std::make_unique(*other.d)) +{ +} + +Matroska::SimpleTag::SimpleTag(SimpleTag &&other) noexcept = default; + +Matroska::SimpleTag::~SimpleTag() = default; + +Matroska::SimpleTag &Matroska::SimpleTag::operator=(SimpleTag &&other) noexcept = default; + +Matroska::SimpleTag &Matroska::SimpleTag::operator=(const SimpleTag &other) +{ + SimpleTag(other).swap(*this); + return *this; +} + +void Matroska::SimpleTag::swap(SimpleTag &other) noexcept +{ + using std::swap; + + swap(d, other.d); +} + +Matroska::SimpleTag::TargetTypeValue Matroska::SimpleTag::targetTypeValue() const +{ + return d->targetTypeValue; +} + +const String &Matroska::SimpleTag::name() const +{ + return d->name; +} + +const String &Matroska::SimpleTag::language() const +{ + return d->language; +} + +bool Matroska::SimpleTag::defaultLanguageFlag() const +{ + return d->defaultLanguageFlag; +} + +unsigned long long Matroska::SimpleTag::trackUid() const +{ + return d->trackUid; +} + +Matroska::SimpleTag::ValueType Matroska::SimpleTag::type() const +{ + return std::holds_alternative(d->value) ? BinaryType : StringType; +} + +String Matroska::SimpleTag::toString() const +{ + if(std::holds_alternative(d->value)) { + // get_if() used instead of get() to support restricted compilers + return *std::get_if(&d->value); + } + return {}; +} + +ByteVector Matroska::SimpleTag::toByteVector() const +{ + if(std::holds_alternative(d->value)) { + // get_if() used instead of get() to support restricted compilers + return *std::get_if(&d->value); + } + return {}; +} diff --git a/taglib/matroska/matroskasimpletag.h b/taglib/matroska/matroskasimpletag.h new file mode 100644 index 00000000..4edd8b45 --- /dev/null +++ b/taglib/matroska/matroskasimpletag.h @@ -0,0 +1,151 @@ +/*************************************************************************** + * 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_MATROSKASIMPLETAG_H +#define TAGLIB_MATROSKASIMPLETAG_H + +#include + +#include "tstring.h" + +namespace TagLib { + class String; + class ByteVector; + + namespace Matroska { + //! Attribute of Matroska metadata. + class TAGLIB_EXPORT SimpleTag + { + public: + //! Specifies the level of other elements the tag value applies to. + enum TargetTypeValue { + None = 0, //!< Empty or omitted, everything in the segment + Shot = 10, //!< Shot + Subtrack = 20, //!< Subtrack / movement / scene + Track = 30, //!< Track / song / chapter + Part = 40, //!< Part / session + Album = 50, //!< Album / opera / concert / movie / episode + Edition = 60, //!< Edition / issue / volume / opus / season / sequel + Collection = 70 //!< Collection + }; + + //! The types the value can have. + enum ValueType { + StringType = 0, //!< Item contains text information coded in UTF-8 + BinaryType = 1 //!< Item contains binary information + }; + + /*! + * Construct a string simple tag. + */ + SimpleTag(const String &name, const String &value, + TargetTypeValue targetTypeValue = None, + const String &language = String(), bool defaultLanguage = true, + unsigned long long trackUid = 0); + + /*! + * Construct a binary simple tag. + */ + SimpleTag(const String &name, const ByteVector &value, + TargetTypeValue targetTypeValue = None, + const String &language = String(), bool defaultLanguage = true, + unsigned long long trackUid = 0); + + /*! + * Construct a simple tag as a copy of \a other. + */ + SimpleTag(const SimpleTag &other); + + /*! + * Construct a simple tag moving from \a other. + */ + SimpleTag(SimpleTag &&other) noexcept; + + /*! + * Destroys this simple tag. + */ + ~SimpleTag(); + + /*! + * Copies the contents of \a other into this item. + */ + SimpleTag &operator=(const SimpleTag &other); + + /*! + * Moves the contents of \a other into this item. + */ + SimpleTag &operator=(SimpleTag &&other) noexcept; + + /*! + * Exchanges the content of the simple tag with the content of \a other. + */ + void swap(SimpleTag &other) noexcept; + + /*! + * Returns the name of the simple tag. + */ + const String &name() const; + + /*! + * Returns the logical level of the target. + */ + TargetTypeValue targetTypeValue() const; + + /*! + * Returns the language of the tag. + */ + const String &language() const; + + /*! + * Returns if this is the default/original language to use for the tag. + */ + bool defaultLanguageFlag() const; + + /*! + * Returns the UID that identifies the track that the tags belong to, + * zero if not defined, the tag applies to all tracks + */ + unsigned long long trackUid() const; + + /*! + * Returns the type of the value. + */ + ValueType type() const; + + /*! + * Returns the StringType value. + */ + String toString() const; + + /*! + * Returns the BinaryType value. + */ + ByteVector toByteVector() const; + + private: + class SimpleTagPrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + + } +} + +#endif diff --git a/taglib/matroska/matroskatag.cpp b/taglib/matroska/matroskatag.cpp new file mode 100644 index 00000000..eaec2da7 --- /dev/null +++ b/taglib/matroska/matroskatag.cpp @@ -0,0 +1,604 @@ +/*************************************************************************** + * 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 "matroskatag.h" +#include +#include +#include +#include "ebmlmasterelement.h" +#include "ebmlstringelement.h" +#include "ebmlbinaryelement.h" +#include "ebmlmktags.h" +#include "ebmluintelement.h" +#include "ebmlutils.h" +#include "tpropertymap.h" + +using namespace TagLib; + +class Matroska::Tag::TagPrivate +{ +public: + TagPrivate() = default; + ~TagPrivate() = default; + + bool setTag(const String &key, const String &value); + String getTag(const String &key) const; + + template + int removeSimpleTags(T &&p) + { + auto &list = tags; + int numRemoved = 0; + for(auto it = list.begin(); it != list.end();) { + it = std::find_if(it, list.end(), std::forward(p)); + if(it != list.end()) { + it = list.erase(it); + numRemoved++; + } + } + return numRemoved; + } + + template + SimpleTagsList findSimpleTags(T &&p) + { + auto &list = tags; + for(auto it = list.begin(); it != list.end();) { + it = std::find_if(it, list.end(), std::forward(p)); + if(it != list.end()) { + list.append(*it); + ++it; + } + } + return list; + } + + SimpleTagsList tags; + ByteVector data; + String segmentTitle; +}; + +Matroska::Tag::Tag() : + Element(static_cast(EBML::Element::Id::MkTags)), + d(std::make_unique()) +{ +} + +Matroska::Tag::~Tag() = default; + +void Matroska::Tag::addSimpleTag(const SimpleTag &tag) +{ + d->tags.append(tag); + setNeedsRender(true); +} + +void Matroska::Tag::addSimpleTags(const SimpleTagsList& simpleTags) +{ + d->tags.append(simpleTags); + setNeedsRender(true); +} + +void Matroska::Tag::insertSimpleTag(unsigned int index, const SimpleTag &tag) +{ + if(index < d->tags.size()) { + auto it = d->tags.begin(); + std::advance(it, index); + d->tags.insert(it, tag); + } + else { + d->tags.append(tag); + } + setNeedsRender(true); +} + +void Matroska::Tag::removeSimpleTag(unsigned int index) +{ + if(index < d->tags.size()) { + auto it = d->tags.begin(); + std::advance(it, index); + d->tags.erase(it); + setNeedsRender(true); + } +} + +void Matroska::Tag::removeSimpleTag(const String &name, + SimpleTag::TargetTypeValue targetTypeValue, + unsigned long long trackUid) +{ + const auto it = std::find_if(d->tags.begin(), d->tags.end(), + [&name, targetTypeValue, trackUid](const SimpleTag &t) { + return t.name() == name && t.targetTypeValue() == targetTypeValue && + t.trackUid() == trackUid; + } + ); + if(it != d->tags.end()) { + d->tags.erase(it); + setNeedsRender(true); + } +} + +void Matroska::Tag::clearSimpleTags() +{ + d->tags.clear(); + setNeedsRender(true); +} + +const Matroska::SimpleTagsList &Matroska::Tag::simpleTagsList() const +{ + return d->tags; +} + +void Matroska::Tag::setSegmentTitle(const String &title) +{ + d->segmentTitle = title; +} + +bool Matroska::Tag::setTag(const String &key, const String &value) +{ + const bool found = d->setTag(key, value); + if(found) { + setNeedsRender(true); + } + return found; +} + +void Matroska::Tag::setTitle(const String &s) +{ + setTag("TITLE", s); +} + +void Matroska::Tag::setArtist(const String &s) +{ + setTag("ARTIST", s); +} + +void Matroska::Tag::setAlbum(const String &s) +{ + setTag("ALBUM", s); +} + +void Matroska::Tag::setComment(const String &s) +{ + setTag("COMMENT", s); +} + +void Matroska::Tag::setGenre(const String &s) +{ + setTag("GENRE", s); +} + +void Matroska::Tag::setYear(unsigned int i) +{ + setTag("DATE", i != 0 ? String::number(i) : String()); +} + +void Matroska::Tag::setTrack(unsigned int i) +{ + setTag("TRACKNUMBER", i != 0 ? String::number(i) : String()); +} + +String Matroska::Tag::title() const +{ + String s = d->getTag("TITLE"); + if(s.isEmpty()) { + return d->segmentTitle; + } + return s; +} + +String Matroska::Tag::artist() const +{ + return d->getTag("ARTIST"); +} + +String Matroska::Tag::album() const +{ + return d->getTag("ALBUM"); +} + +String Matroska::Tag::comment() const +{ + return d->getTag("COMMENT"); +} + +String Matroska::Tag::genre() const +{ + return d->getTag("GENRE"); +} + +unsigned int Matroska::Tag::year() const +{ + const auto value = d->getTag("DATE"); + if(value.isEmpty()) + return 0; + auto list = value.split("-"); + return static_cast(list.front().toInt()); +} + +unsigned int Matroska::Tag::track() const +{ + const auto value = d->getTag("TRACKNUMBER"); + if(value.isEmpty()) + return 0; + auto list = value.split("-"); + return static_cast(list.front().toInt()); +} + +bool Matroska::Tag::isEmpty() const +{ + return d->tags.isEmpty(); +} + +ByteVector Matroska::Tag::renderInternal() +{ + if(d->tags.isEmpty()) { + // Avoid writing a Tags element without Tag element. + return {}; + } + + EBML::MkTags tags; + List targetList; + + // Build target-based list + for(const auto &tag : std::as_const(d->tags)) { + auto targetTypeValue = tag.targetTypeValue(); + auto trackUid = tag.trackUid(); + auto it = std::find_if(targetList.begin(), + targetList.end(), + [&](const auto &list) { + const auto &simpleTag = list.front(); + return simpleTag.targetTypeValue() == targetTypeValue && + simpleTag.trackUid() == trackUid; + } + ); + if(it == targetList.end()) { + SimpleTagsList list; + list.append(tag); + targetList.append(list); + } + else + it->append(tag); + } + for(const auto &list : targetList) { + const auto &frontTag = list.front(); + const auto targetTypeValue = frontTag.targetTypeValue(); + const auto trackUid = frontTag.trackUid(); + auto tag = EBML::make_unique_element(); + + // Build element + auto targets = EBML::make_unique_element(); + if(targetTypeValue != SimpleTag::TargetTypeValue::None) { + auto element = EBML::make_unique_element(); + element->setValue(static_cast(targetTypeValue)); + targets->appendElement(std::move(element)); + } + if(trackUid != 0) { + auto element = EBML::make_unique_element(); + element->setValue(trackUid); + targets->appendElement(std::move(element)); + } + tag->appendElement(std::move(targets)); + + // Build element + for(const auto &simpleTag : list) { + auto t = EBML::make_unique_element(); + auto tagName = EBML::make_unique_element(); + tagName->setValue(simpleTag.name()); + t->appendElement(std::move(tagName)); + + // Tag Value + if(simpleTag.type() == SimpleTag::StringType) { + auto tagValue = EBML::make_unique_element(); + tagValue->setValue(simpleTag.toString()); + t->appendElement(std::move(tagValue)); + } + else if(simpleTag.type() == SimpleTag::BinaryType) { + auto tagValue = EBML::make_unique_element(); + tagValue->setValue(simpleTag.toByteVector()); + t->appendElement(std::move(tagValue)); + } + + // Language + auto language = EBML::make_unique_element(); + const String &lang = simpleTag.language(); + language->setValue(!lang.isEmpty() ? lang : "und"); + t->appendElement(std::move(language)); + + // Default language flag + auto dlf = EBML::make_unique_element(); + dlf->setValue(simpleTag.defaultLanguageFlag() ? 1 : 0); + t->appendElement(std::move(dlf)); + + tag->appendElement(std::move(t)); + } + tags.appendElement(std::move(tag)); + } + return tags.render(); +} + +namespace +{ + // PropertyMap key, Tag name, Target type value, strict + // If the key is the same as the name and the target type value is Track, + // no translation is needed because this is the default mapping. + // Therefore, keys like TITLE, ARTIST, GENRE, COMMENT, etc. are omitted + // unless they shall have priority over higher level tags with the same name + // when no target type value is given. The strict boolean marks + // entries which shall not be mapped without correct target type value. + // For offical tags, see https://www.matroska.org/technical/tagging.html + constexpr std::array simpleTagsTranslation { + std::tuple("TITLE", "TITLE", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("ALBUM", "TITLE", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("ARTIST", "ARTIST", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("ALBUMARTIST", "ARTIST", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("TRACKNUMBER", "PART_NUMBER", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("DISCNUMBER", "PART_NUMBER", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("TRACKTOTAL", "TOTAL_PARTS", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("DISCTOTAL", "TOTAL_PARTS", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("DATE", "DATE_RECORDED", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("TITLESORT", "TITLESORT", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("ALBUMSORT", "TITLESORT", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("ARTISTSORT", "ARTISTSORT", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("ALBUMARTISTSORT", "ARTISTSORT", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("MEDIA", "ORIGINAL_MEDIA_TYPE", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("LABEL", "LABEL_CODE", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("CATALOGNUMBER", "CATALOG_NUMBER", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("DJMIXER", "MIXED_BY", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("REMIXER", "REMIXED_BY", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("INITIALKEY", "INITIAL_KEY", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("RELEASEDATE", "DATE_RELEASED", Matroska::SimpleTag::TargetTypeValue::Album, false), + std::tuple("ENCODINGTIME", "DATE_ENCODED", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("TAGGINGDATE", "DATE_TAGGED", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("ENCODEDBY", "ENCODER", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("ENCODING", "ENCODER_SETTINGS", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("OWNER", "PURCHASE_OWNER", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_GAIN", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("REPLAYGAIN_ALBUM_GAIN", "REPLAYGAIN_GAIN", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("REPLAYGAIN_TRACK_PEAK", "REPLAYGAIN_PEAK", Matroska::SimpleTag::TargetTypeValue::Track, false), + std::tuple("REPLAYGAIN_ALBUM_PEAK", "REPLAYGAIN_PEAK", Matroska::SimpleTag::TargetTypeValue::Album, true), + std::tuple("MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ_ALBUMARTISTID", Matroska::SimpleTag::TargetTypeValue::Album, false), + std::tuple("MUSICBRAINZ_ALBUMID", "MUSICBRAINZ_ALBUMID", Matroska::SimpleTag::TargetTypeValue::Album, false), + std::tuple("MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ_RELEASEGROUPID", Matroska::SimpleTag::TargetTypeValue::Album, false), + }; + + std::tuple translateKey(const String &key) + { + auto it = std::find_if(simpleTagsTranslation.cbegin(), + simpleTagsTranslation.cend(), + [&key](const auto &t) { return key == std::get<0>(t); } + ); + if(it != simpleTagsTranslation.end()) + return { std::get<1>(*it), std::get<2>(*it), std::get<3>(*it) }; + if(!key.isEmpty()) + return { key, Matroska::SimpleTag::TargetTypeValue::Track, false }; + return { String(), Matroska::SimpleTag::TargetTypeValue::None, false }; + } + + String translateTag(const String &name, Matroska::SimpleTag::TargetTypeValue targetTypeValue) + { + const auto it = std::find_if(simpleTagsTranslation.cbegin(), + simpleTagsTranslation.cend(), + [&name, targetTypeValue](const auto &t) { + return name == std::get<1>(t) + && (targetTypeValue == std::get<2>(t) || + (targetTypeValue == Matroska::SimpleTag::TargetTypeValue::None + && !std::get<3>(t))); + } + ); + return it != simpleTagsTranslation.end() + ? String(std::get<0>(*it), String::UTF8) + : targetTypeValue == Matroska::SimpleTag::TargetTypeValue::Track || + targetTypeValue == Matroska::SimpleTag::TargetTypeValue::None + ? name + : String(); + } +} + +bool Matroska::Tag::TagPrivate::setTag(const String &key, const String &value) +{ + const auto tpl = translateKey(key); + // Workaround Clang issue - no lambda capture of structured bindings + const String &name = std::get<0>(tpl); + auto targetTypeValue = std::get<1>(tpl); + if(name.isEmpty()) + return false; + removeSimpleTags( + [&name, targetTypeValue] (const auto &t) { + return t.name() == name + && t.targetTypeValue() == targetTypeValue; + } + ); + if(!value.isEmpty()) { + tags.append(SimpleTag(name, value, targetTypeValue)); + } + return true; +} + +String Matroska::Tag::TagPrivate::getTag(const String &key) const +{ + const auto tpl = translateKey(key); + // Workaround Clang issue - no lambda capture of structured bindings + const String &name = std::get<0>(tpl); + auto targetTypeValue = std::get<1>(tpl); + bool strict = std::get<2>(tpl); + if(name.isEmpty()) + return {}; + const auto it = std::find_if(tags.begin(), tags.end(), + [&name, targetTypeValue, strict] (const SimpleTag &t) { + return t.name() == name + && t.type() == SimpleTag::StringType + && (t.targetTypeValue() == targetTypeValue || + (t.targetTypeValue() == SimpleTag::TargetTypeValue::None && !strict)) + && t.trackUid() == 0; + } + ); + return it != tags.end() ? it->toString() : String(); +} + +PropertyMap Matroska::Tag::properties() const +{ + PropertyMap properties; + for(const auto &simpleTag : std::as_const(d->tags)) { + if(simpleTag.type() == SimpleTag::StringType && simpleTag.trackUid() == 0) { + if(String key = translateTag(simpleTag.name(), simpleTag.targetTypeValue()); + !key.isEmpty()) + properties[key].append(simpleTag.toString()); + else + properties.addUnsupportedData(simpleTag.name()); + } + } + return properties; +} + +PropertyMap Matroska::Tag::setProperties(const PropertyMap &propertyMap) +{ + // Remove all simple tags which would be returned in properties() + for(auto it = d->tags.begin(); it != d->tags.end();) { + if(it->type() == SimpleTag::StringType && + it->trackUid() == 0 && + !translateTag(it->name(), it->targetTypeValue()).isEmpty()) { + it = d->tags.erase(it); + setNeedsRender(true); + } + else { + ++it; + } + } + + // Add the new properties + PropertyMap unsupportedProperties; + for(const auto &[key, values] : propertyMap) { + for(const auto &value : values) { + if(auto [name, targetTypeValue, _] = translateKey(key); + !name.isEmpty()) { + d->tags.append(SimpleTag(name, value, targetTypeValue)); + setNeedsRender(true); + } + else { + unsupportedProperties[key] = values; + } + } + } + return unsupportedProperties; +} + +void Matroska::Tag::removeUnsupportedProperties(const StringList &properties) +{ + if(d->removeSimpleTags( + [&properties](const SimpleTag &t) { + return properties.contains(t.name()); + }) > 0) { + setNeedsRender(true); + } +} + +StringList Matroska::Tag::complexPropertyKeys() const +{ + StringList keys; + for(const SimpleTag &t : std::as_const(d->tags)) { + if(t.type() != SimpleTag::StringType || + t.trackUid() != 0 || + translateTag(t.name(), t.targetTypeValue()).isEmpty()) { + keys.append(t.name()); + } + } + return keys; +} + +List Matroska::Tag::complexProperties(const String &key) const +{ + List props; + if(key.upper() != "PICTURE") { // Pictures are handled at the file level + for(const SimpleTag &t : std::as_const(d->tags)) { + if(t.name() == key && + (t.type() != SimpleTag::StringType || + t.trackUid() != 0 || + translateTag(t.name(), t.targetTypeValue()).isEmpty())) { + VariantMap property; + if(t.type() != SimpleTag::StringType) { + property.insert("data", t.toByteVector()); + } + else { + property.insert("value", t.toString()); + } + property.insert("name", t.name()); + if(t.targetTypeValue() != SimpleTag::TargetTypeValue::None) { + property.insert("targetTypeValue", t.targetTypeValue()); + } + if(t.trackUid()) { + property.insert("trackUid", t.trackUid()); + } + property.insert("language", t.language()); + property.insert("defaultLanguage", t.defaultLanguageFlag()); + props.append(property); + } + } + } + return props; +} + +bool Matroska::Tag::setComplexProperties(const String &key, const List &value) +{ + if(key.upper() == "PICTURE") { + // Pictures are handled at the file level + return false; + } + if(d->removeSimpleTags( + [&key](const SimpleTag &t) { + return t.name() == key && + (t.type() != SimpleTag::StringType || + t.trackUid() != 0 || + translateTag(t.name(), t.targetTypeValue()).isEmpty()); + }) > 0) { + setNeedsRender(true); + } + bool result = false; + for(const auto &property : value) { + if(property.value("name").value() == key && + (property.contains("data") || property.contains("value") )) { + SimpleTag::TargetTypeValue targetTypeValue; + switch(Variant targetTypeValueVar = property.value("targetTypeValue", 0); + targetTypeValueVar.type()) { + case Variant::UInt: + targetTypeValue = static_cast(targetTypeValueVar.value()); + break; + case Variant::LongLong: + targetTypeValue = static_cast(targetTypeValueVar.value()); + break; + case Variant::ULongLong: + targetTypeValue = static_cast(targetTypeValueVar.value()); + break; + default: + targetTypeValue = static_cast(targetTypeValueVar.value()); + } + auto language = property.value("language").value(); + const bool defaultLanguage = property.value("defaultLanguage", true).value(); + const auto trackUid = property.value("trackUid", 0ULL).value(); + d->tags.append(property.contains("data") + ? SimpleTag(key, property.value("data").value(), + targetTypeValue, language, defaultLanguage, trackUid) + : SimpleTag(key, property.value("value").value(), + targetTypeValue, language, defaultLanguage, trackUid)); + setNeedsRender(true); + result = true; + } + } + return result; +} diff --git a/taglib/matroska/matroskatag.h b/taglib/matroska/matroskatag.h new file mode 100644 index 00000000..21696cc0 --- /dev/null +++ b/taglib/matroska/matroskatag.h @@ -0,0 +1,150 @@ +/*************************************************************************** + * 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_MATROSKATAG_H +#define TAGLIB_MATROSKATAG_H + +#include + +#include "tag.h" +#include "tlist.h" +#include "matroskaelement.h" +#include "matroskasimpletag.h" + +namespace TagLib { + class File; + + namespace EBML { + class MkTags; + } + + namespace Matroska { + //! List of tag attributes. + using SimpleTagsList = List; + + //! Matroska tag implementation. + class TAGLIB_EXPORT Tag : public TagLib::Tag +#ifndef DO_NOT_DOCUMENT + , private Element +#endif + { + public: + /*! + * Constructs a Matroska tag. + */ + Tag(); + + ~Tag() override; + String title() const override; + String artist() const override; + String album() const override; + String comment() const override; + String genre() const override; + unsigned int year() const override; + unsigned int track() const override; + void setTitle(const String &s) override; + void setArtist(const String &s) override; + void setAlbum(const String &s) override; + void setComment(const String &s) override; + void setGenre(const String &s) override; + void setYear(unsigned int i) override; + void setTrack(unsigned int i) override; + bool isEmpty() const override; + PropertyMap properties() const override; + PropertyMap setProperties(const PropertyMap &propertyMap) override; + void removeUnsupportedProperties(const StringList &properties) override; + + /*! + * Returns the names of the binary simple tags. + */ + StringList complexPropertyKeys() const override; + + /*! + * Get the binary simple tags as maps with keys "data", "name", + * "targetTypeValue", "language", "defaultLanguage". + * The attached files such as pictures with key "PICTURE" are available + * with Matroska::File::complexProperties(). + */ + List complexProperties(const String &key) const override; + + /*! + * Set the binary simple tags as maps with keys "data", "name", + * "targetTypeValue", "language", "defaultLanguage". + * The attached files such as pictures with key "PICTURE" can be set + * with Matroska::File::setComplexProperties(). + * + * Returns \c true if \c key can be stored as binary simple tags. + */ + bool setComplexProperties(const String &key, const List &value) override; + + /*! + * Add a tag attribute. + */ + void addSimpleTag(const SimpleTag &tag); + + /*! + * Add multiple tag attributes. + */ + void addSimpleTags(const SimpleTagsList &simpleTags); + + /*! + * Insert a tag attribute at position \a index. + */ + void insertSimpleTag(unsigned int index, const SimpleTag &tag); + + /*! + * Remove a tag attribute at position \a index. + */ + void removeSimpleTag(unsigned int index); + + /*! + * Remove a tag attribute. + */ + void removeSimpleTag(const String &name, SimpleTag::TargetTypeValue targetTypeValue, + unsigned long long trackUid = 0); + + /*! + * Remove all tag attributes. + */ + void clearSimpleTags(); + + /*! + * Get list of all tag attributes. + */ + const SimpleTagsList &simpleTagsList() const; + + private: + friend class File; + friend class EBML::MkTags; + class TagPrivate; + + bool setTag(const String &key, const String &value); + void setSegmentTitle(const String &title); + + // private Element implementation + ByteVector renderInternal() override; + + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + } +} + +#endif diff --git a/taglib/taglib_config.h.cmake b/taglib/taglib_config.h.cmake index 7a3a02c4..06e51756 100644 --- a/taglib/taglib_config.h.cmake +++ b/taglib/taglib_config.h.cmake @@ -6,6 +6,7 @@ #cmakedefine TAGLIB_WITH_APE 1 #cmakedefine TAGLIB_WITH_ASF 1 #cmakedefine TAGLIB_WITH_DSF 1 +#cmakedefine TAGLIB_WITH_MATROSKA 1 #cmakedefine TAGLIB_WITH_MOD 1 #cmakedefine TAGLIB_WITH_MP4 1 #cmakedefine TAGLIB_WITH_RIFF 1 diff --git a/taglib/toolkit/propertymapping.dox b/taglib/toolkit/propertymapping.dox index 67450af8..43f32bd3 100644 --- a/taglib/toolkit/propertymapping.dox +++ b/taglib/toolkit/propertymapping.dox @@ -6,107 +6,111 @@ - Vorbis Comments are not included in the table because they use the names from the *Key* column. - %APE tags will also use these keys unless another name can be found in its column. +- %Matroska tags will also use these keys unless another name can be found in its column, + additionally, the target level is listed, e.g. /50 for Album, /30 for Track (default). -| Key | %ID3v2 | %RIFF | %MP4 | %APE | %ASF | -| -------------------------- | ------ | ----- | -------------------------------------------------------- | ----------------------- | --------------------------------- | -| ACOUSTID_FINGERPRINT | | | | | Acoustid/Fingerprint | -| ACOUSTID_ID | | | | | Acoustid/Id | -| ALBUM | TALB | IPRD | ©alb | | WM/AlbumTitle | -| ALBUMARTIST | TPE2 | | aART | ALBUM ARTIST | WM/AlbumArtist | -| ALBUMARTISTSORT | TSO2 | | soaa | | WM/AlbumArtistSortOrder | -| ALBUMSORT | TSOA | | soal | | WM/AlbumSortOrder | -| ARRANGER | | IENG | | | | -| ARTIST | TPE1 | IART | ©ART | | | -| ARTISTS | | | \----:com.apple.iTunes:ARTISTS | | WM/ARTISTS | -| ARTISTSORT | TSOP | | soar | | WM/ArtistSortOrder | -| ARTISTWEBPAGE | WOAR | IBSU | | | WM/AuthorURL | -| ASIN | | | \----:com.apple.iTunes:ASIN | | | -| AUDIOSOURCEWEBPAGE | WOAS | | | | ASIN | -| BARCODE | | | \----:com.apple.iTunes:BARCODE | | WM/Barcode | -| BPM | TBPM | IBPM | tmpo | | WM/BeatsPerMinute | -| CATALOGNUMBER | | | \----:com.apple.iTunes:CATALOGNUMBER | | WM/CatalogNo | -| COMMENT | COMM | ICMT | ©cmt | | | -| COMPILATION | TCMP | | cpil | | | -| COMPOSER | TCOM | IMUS | ©wrt | | WM/Composer | -| COMPOSERSORT | TSOC | | soco | | | -| CONDUCTOR | TPE3 | | \----:com.apple.iTunes:CONDUCTOR | | WM/Conductor | -| COPYRIGHT | TCOP | ICOP | cprt | | | -| COPYRIGHTURL | WCOP | | | | | -| DATE | TDRC | ICRD | ©day | YEAR | WM/Year | -| DISCNUMBER | TPOS | | disk | DISC | WM/PartOfSet | -| DISCSUBTITLE | TSST | PRT1 | \----:com.apple.iTunes:DISCSUBTITLE | | WM/SetSubTitle | -| DJMIXER | | | \----:com.apple.iTunes:DJMIXER | | | -| ENCODEDBY | TENC | ITCH | ©enc | | WM/EncodedBy | -| ENCODING | TSSE | ISFT | ©too | | WM/EncodingSettings | -| ENCODINGTIME | TDEN | IDIT | | | WM/EncodingTime | -| ENGINEER | | | \----:com.apple.iTunes:ENGINEER | | | -| FILETYPE | TFLT | | | | | -| FILEWEBPAGE | WOAF | | | | WM/AudioFileURL | -| GAPLESSPLAYBACK | | | pgap | | | -| GENRE | TCON | IGNR | ©gen | | WM/Genre | -| GROUPING | GRP1 | | ©grp | | | -| INITIALKEY | TKEY | | | | WM/InitialKey | -| INVOLVEDPEOPLE | TIPL | | | | | -| ISRC | TSRC | ISRC | \----:com.apple.iTunes:ISRC | | WM/ISRC | -| LABEL | TPUB | IPUB | \----:com.apple.iTunes:LABEL | | WM/Publisher | -| LANGUAGE | TLAN | ILNG | \----:com.apple.iTunes:LANGUAGE | | WM/Language | -| LENGTH | TLEN | | | | | -| LICENSE | | | \----:com.apple.iTunes:LICENSE | | | -| LYRICIST | TEXT | IWRI | \----:com.apple.iTunes:LYRICIST | | WM/Writer | -| LYRICS | USLT | | ©lyr | | WM/Lyrics | -| MEDIA | TMED | IMED | \----:com.apple.iTunes:MEDIA | | WM/Media | -| MIXER | | | \----:com.apple.iTunes:MIXER | | | -| MOOD | TMOO | | \----:com.apple.iTunes:MOOD | | WM/Mood | -| MOVEMENTCOUNT | | | ©mvc | | | -| MOVEMENTNAME | MVNM | | ©mvn | | | -| MOVEMENTNUMBER | MVIN | | ©mvi | | | -| MUSICBRAINZ_ALBUMID | | | \----:com.apple.iTunes:MusicBrainz Album Id | | MusicBrainz/Album Id | -| MUSICBRAINZ_ALBUMARTISTID | | | \----:com.apple.iTunes:MusicBrainz Album Artist Id | | MusicBrainz/Album Artist Id | -| MUSICBRAINZ_ARTISTID | | | \----:com.apple.iTunes:MusicBrainz Artist Id | | MusicBrainz/Artist Id | -| MUSICBRAINZ_RELEASEGROUPID | | | \----:com.apple.iTunes:MusicBrainz Release Group Id | | MusicBrainz/Release Group Id | -| MUSICBRAINZ_RELEASETRACKID | | | \----:com.apple.iTunes:MusicBrainz Release Track Id | | MusicBrainz/Release Track Id | -| MUSICBRAINZ_TRACKID | | | \----:com.apple.iTunes:MusicBrainz Track Id | | MusicBrainz/Track Id | -| MUSICBRAINZ_WORKID | | | \----:com.apple.iTunes:MusicBrainz Work Id | | MusicBrainz/Work Id | -| MUSICIANCREDITS | TMCL | | | | | -| MUSICIP_PUID | | | | | MusicIP/PUID | -| ORIGINALALBUM | TOAL | | | | WM/OriginalAlbumTitle | -| ORIGINALARTIST | TOPE | | | | WM/OriginalArtist | -| ORIGINALDATE | TDOR | | \----:com.apple.iTunes:ORIGINALDATE | | WM/OriginalReleaseYear | -| ORIGINALFILENAME | TOFN | | | | WM/OriginalFilename | -| ORIGINALLYRICIST | TOLY | | | | WM/OriginalLyricist | -| OWNER | TOWN | | ownr | | | -| PAYMENTWEBPAGE | WPAY | | | | | -| PERFORMER | | ISTR | | | | -| PLAYLISTDELAY | TDLY | | | | | -| PODCAST | PCST | | pcst | | | -| PODCASTCATEGORY | TCAT | | catg | | | -| PODCASTDESC | TDES | | desc | | | -| PODCASTID | TGID | | egid | | | -| PODCASTURL | WFED | | purl | | | -| PRODUCEDNOTICE | TPRO | | | | | -| PRODUCER | | | \----:com.apple.iTunes:PRODUCER | | WM/Producer | -| PUBLISHERWEBPAGE | WPUB | | | | | -| RADIOSTATION | TRSN | | | | | -| RADIOSTATIONOWNER | TRSO | | | | | -| RADIOSTATIONWEBPAGE | WORS | | | | | -| RELEASECOUNTRY | | ICNT | \----:com.apple.iTunes:MusicBrainz Album Release Country | | MusicBrainz/Album Release Country | -| RELEASEDATE | TDRL | | \----:com.apple.iTunes:RELEASEDATE | | | -| RELEASESTATUS | | | \----:com.apple.iTunes:MusicBrainz Album Status | MUSICBRAINZ_ALBUMSTATUS | MusicBrainz/Album Status | -| RELEASETYPE | | | \----:com.apple.iTunes:MusicBrainz Album Type | MUSICBRAINZ_ALBUMTYPE | MusicBrainz/Album Type | -| REMIXER | TPE4 | IEDT | \----:com.apple.iTunes:REMIXER | MIXARTIST | WM/ModifiedBy | -| SCRIPT | | | \----:com.apple.iTunes:SCRIPT | | WM/Script | -| SHOWSORT | | | sosn | | | -| SHOWWORKMOVEMENT | | | shwm | | | -| SUBTITLE | TIT3 | | \----:com.apple.iTunes:SUBTITLE | | WM/SubTitle | -| TAGGINGDATE | TDTG | | | | | -| TITLE | TIT2 | INAM | ©nam | | | -| TITLESORT | TSOT | | sonm | | WM/TitleSortOrder | -| TRACKNUMBER | TRCK | IPRT | trkn | TRACK | WM/TrackNumber | -| TVEPISODE | | | tves | | | -| TVEPISODEID | | | tven | | | -| TVNETWORK | | | tvnn | | | -| TVSEASON | | | tvsn | | | -| TVSHOW | | | tvsh | | | -| URL | WXXX | | | | | -| WORK | TIT1 | | ©wrk | | WM/ContentGroupDescription | +| Key | %ID3v2 | %RIFF | %MP4 | %APE | %ASF | %Matroska | +| -------------------------- | ------ | ----- | -------------------------------------------------------- | ----------------------- | --------------------------------- | ----------------------------- | +| ACOUSTID_FINGERPRINT | | | | | Acoustid/Fingerprint | | +| ACOUSTID_ID | | | | | Acoustid/Id | | +| ALBUM | TALB | IPRD | ©alb | | WM/AlbumTitle | TITLE/50 | +| ALBUMARTIST | TPE2 | | aART | ALBUM ARTIST | WM/AlbumArtist | ARTIST/50 | +| ALBUMARTISTSORT | TSO2 | | soaa | | WM/AlbumArtistSortOrder | ARTISTSORT/50 | +| ALBUMSORT | TSOA | | soal | | WM/AlbumSortOrder | TITLESORT/50 | +| ARRANGER | | IENG | | | | | +| ARTIST | TPE1 | IART | ©ART | | | | +| ARTISTS | | | \----:com.apple.iTunes:ARTISTS | | WM/ARTISTS | | +| ARTISTSORT | TSOP | | soar | | WM/ArtistSortOrder | | +| ARTISTWEBPAGE | WOAR | IBSU | | | WM/AuthorURL | | +| ASIN | | | \----:com.apple.iTunes:ASIN | | | | +| AUDIOSOURCEWEBPAGE | WOAS | | | | ASIN | | +| BARCODE | | | \----:com.apple.iTunes:BARCODE | | WM/Barcode | | +| BPM | TBPM | IBPM | tmpo | | WM/BeatsPerMinute | | +| CATALOGNUMBER | | | \----:com.apple.iTunes:CATALOGNUMBER | | WM/CatalogNo | CATALOG_NUMBER | +| COMMENT | COMM | ICMT | ©cmt | | | | +| COMPILATION | TCMP | | cpil | | | | +| COMPOSER | TCOM | IMUS | ©wrt | | WM/Composer | | +| COMPOSERSORT | TSOC | | soco | | | | +| CONDUCTOR | TPE3 | | \----:com.apple.iTunes:CONDUCTOR | | WM/Conductor | | +| COPYRIGHT | TCOP | ICOP | cprt | | | | +| COPYRIGHTURL | WCOP | | | | | | +| DATE | TDRC | ICRD | ©day | YEAR | WM/Year | DATE_RECORDED/30 | +| DISCNUMBER | TPOS | | disk | DISC | WM/PartOfSet | PART_NUMBER/50 | +| DISCSUBTITLE | TSST | PRT1 | \----:com.apple.iTunes:DISCSUBTITLE | | WM/SetSubTitle | | +| DISCTOTAL | | | | | | TOTAL_PARTS/50 | +| DJMIXER | | | \----:com.apple.iTunes:DJMIXER | | | MIXED_BY | +| ENCODEDBY | TENC | ITCH | ©enc | | WM/EncodedBy | ENCODER | +| ENCODING | TSSE | ISFT | ©too | | WM/EncodingSettings | ENCODER_SETTINGS | +| ENCODINGTIME | TDEN | IDIT | | | WM/EncodingTime | DATE_ENCODED | +| ENGINEER | | | \----:com.apple.iTunes:ENGINEER | | | | +| FILETYPE | TFLT | | | | | | +| FILEWEBPAGE | WOAF | | | | WM/AudioFileURL | | +| GAPLESSPLAYBACK | | | pgap | | | | +| GENRE | TCON | IGNR | ©gen | | WM/Genre | | +| GROUPING | GRP1 | | ©grp | | | | +| INITIALKEY | TKEY | | | | WM/InitialKey | INITIAL_KEY | +| INVOLVEDPEOPLE | TIPL | | | | | | +| ISRC | TSRC | ISRC | \----:com.apple.iTunes:ISRC | | WM/ISRC | | +| LABEL | TPUB | IPUB | \----:com.apple.iTunes:LABEL | | WM/Publisher | LABEL_CODE | +| LANGUAGE | TLAN | ILNG | \----:com.apple.iTunes:LANGUAGE | | WM/Language | | +| LENGTH | TLEN | | | | | | +| LICENSE | | | \----:com.apple.iTunes:LICENSE | | | | +| LYRICIST | TEXT | IWRI | \----:com.apple.iTunes:LYRICIST | | WM/Writer | | +| LYRICS | USLT | | ©lyr | | WM/Lyrics | | +| MEDIA | TMED | IMED | \----:com.apple.iTunes:MEDIA | | WM/Media | ORIGINAL_MEDIA_TYPE | +| MIXER | | | \----:com.apple.iTunes:MIXER | | | | +| MOOD | TMOO | | \----:com.apple.iTunes:MOOD | | WM/Mood | | +| MOVEMENTCOUNT | | | ©mvc | | | | +| MOVEMENTNAME | MVNM | | ©mvn | | | | +| MOVEMENTNUMBER | MVIN | | ©mvi | | | | +| MUSICBRAINZ_ALBUMID | | | \----:com.apple.iTunes:MusicBrainz Album Id | | MusicBrainz/Album Id | MUSICBRAINZ_ALBUMID/50 | +| MUSICBRAINZ_ALBUMARTISTID | | | \----:com.apple.iTunes:MusicBrainz Album Artist Id | | MusicBrainz/Album Artist Id | MUSICBRAINZ_ALBUMARTISTID/50 | +| MUSICBRAINZ_ARTISTID | | | \----:com.apple.iTunes:MusicBrainz Artist Id | | MusicBrainz/Artist Id | | +| MUSICBRAINZ_RELEASEGROUPID | | | \----:com.apple.iTunes:MusicBrainz Release Group Id | | MusicBrainz/Release Group Id | MUSICBRAINZ_RELEASEGROUPID/50 | +| MUSICBRAINZ_RELEASETRACKID | | | \----:com.apple.iTunes:MusicBrainz Release Track Id | | MusicBrainz/Release Track Id | | +| MUSICBRAINZ_TRACKID | | | \----:com.apple.iTunes:MusicBrainz Track Id | | MusicBrainz/Track Id | | +| MUSICBRAINZ_WORKID | | | \----:com.apple.iTunes:MusicBrainz Work Id | | MusicBrainz/Work Id | | +| MUSICIANCREDITS | TMCL | | | | | | +| MUSICIP_PUID | | | | | MusicIP/PUID | | +| ORIGINALALBUM | TOAL | | | | WM/OriginalAlbumTitle | | +| ORIGINALARTIST | TOPE | | | | WM/OriginalArtist | | +| ORIGINALDATE | TDOR | | \----:com.apple.iTunes:ORIGINALDATE | | WM/OriginalReleaseYear | | +| ORIGINALFILENAME | TOFN | | | | WM/OriginalFilename | | +| ORIGINALLYRICIST | TOLY | | | | WM/OriginalLyricist | | +| OWNER | TOWN | | ownr | | | PURCHASE_OWNER | +| PAYMENTWEBPAGE | WPAY | | | | | | +| PERFORMER | | ISTR | | | | | +| PLAYLISTDELAY | TDLY | | | | | | +| PODCAST | PCST | | pcst | | | | +| PODCASTCATEGORY | TCAT | | catg | | | | +| PODCASTDESC | TDES | | desc | | | | +| PODCASTID | TGID | | egid | | | | +| PODCASTURL | WFED | | purl | | | | +| PRODUCEDNOTICE | TPRO | | | | | | +| PRODUCER | | | \----:com.apple.iTunes:PRODUCER | | WM/Producer | | +| PUBLISHERWEBPAGE | WPUB | | | | | | +| RADIOSTATION | TRSN | | | | | | +| RADIOSTATIONOWNER | TRSO | | | | | | +| RADIOSTATIONWEBPAGE | WORS | | | | | | +| RELEASECOUNTRY | | ICNT | \----:com.apple.iTunes:MusicBrainz Album Release Country | | MusicBrainz/Album Release Country | | +| RELEASEDATE | TDRL | | \----:com.apple.iTunes:RELEASEDATE | | | DATE_RELEASED/30 | +| RELEASESTATUS | | | \----:com.apple.iTunes:MusicBrainz Album Status | MUSICBRAINZ_ALBUMSTATUS | MusicBrainz/Album Status | | +| RELEASETYPE | | | \----:com.apple.iTunes:MusicBrainz Album Type | MUSICBRAINZ_ALBUMTYPE | MusicBrainz/Album Type | | +| REMIXER | TPE4 | IEDT | \----:com.apple.iTunes:REMIXER | MIXARTIST | WM/ModifiedBy | REMIXED_BY | +| SCRIPT | | | \----:com.apple.iTunes:SCRIPT | | WM/Script | | +| SHOWSORT | | | sosn | | | | +| SHOWWORKMOVEMENT | | | shwm | | | | +| SUBTITLE | TIT3 | | \----:com.apple.iTunes:SUBTITLE | | WM/SubTitle | | +| TAGGINGDATE | TDTG | | | | | DATE_TAGGED | +| TITLE | TIT2 | INAM | ©nam | | | | +| TITLESORT | TSOT | | sonm | | WM/TitleSortOrder | | +| TRACKNUMBER | TRCK | IPRT | trkn | TRACK | WM/TrackNumber | PART_NUMBER/30 | +| TRACKTOTAL | | | | | | TOTAL_PARTS/30 | +| TVEPISODE | | | tves | | | | +| TVEPISODEID | | | tven | | | | +| TVNETWORK | | | tvnn | | | | +| TVSEASON | | | tvsn | | | | +| TVSHOW | | | tvsh | | | | +| URL | WXXX | | | | | | +| WORK | TIT1 | | ©wrk | | WM/ContentGroupDescription | | */ diff --git a/taglib/toolkit/tstring.cpp b/taglib/toolkit/tstring.cpp index 046de208..fab5452b 100644 --- a/taglib/toolkit/tstring.cpp +++ b/taglib/toolkit/tstring.cpp @@ -495,6 +495,30 @@ int String::toInt(bool *ok) const return static_cast(value); } +long long String::toLongLong(bool *ok, int base) const +{ + const wchar_t *beginPtr = d->data.c_str(); + wchar_t *endPtr; + errno = 0; + const long long value = ::wcstoll(beginPtr, &endPtr, base); + if(ok) { + *ok = errno == 0 && endPtr > beginPtr && *endPtr == L'\0'; + } + return value; +} + +unsigned long long String::toULongLong(bool *ok, int base) const +{ + const wchar_t *beginPtr = d->data.c_str(); + wchar_t *endPtr; + errno = 0; + const unsigned long long value = ::wcstoull(beginPtr, &endPtr, base); + if(ok) { + *ok = errno == 0 && endPtr > beginPtr && *endPtr == L'\0'; + } + return value; +} + String String::stripWhiteSpace() const { static const wchar_t *WhiteSpaceChars = L"\t\n\f\r "; @@ -527,6 +551,11 @@ String String::fromLongLong(long long n) // static return std::to_string(n); } +String String::fromULongLong(unsigned long long n) // static +{ + return std::to_string(n); +} + wchar_t &String::operator[](int i) { detach(); diff --git a/taglib/toolkit/tstring.h b/taglib/toolkit/tstring.h index f7e40815..1636a159 100644 --- a/taglib/toolkit/tstring.h +++ b/taglib/toolkit/tstring.h @@ -359,6 +359,24 @@ namespace TagLib { */ int toInt(bool *ok = nullptr) const; + /*! + * Convert the string to an integer. + * + * If the conversion was successful, it sets the value of \a *ok to + * \c true and returns the integer. Otherwise it sets \a *ok to \c false + * and the result is undefined. + */ + long long toLongLong(bool *ok = nullptr, int base = 10) const; + + /*! + * Convert the string to an integer. + * + * If the conversion was successful, it sets the value of \a *ok to + * \c true and returns the integer. Otherwise it sets \a *ok to \c false + * and the result is undefined. + */ + unsigned long long toULongLong(bool *ok = nullptr, int base = 10) const; + /*! * Returns a string with the leading and trailing whitespace stripped. */ @@ -384,6 +402,11 @@ namespace TagLib { */ static String fromLongLong(long long n); + /*! + * Converts the base-10 integer \a n to a string. + */ + static String fromULongLong(unsigned long long n); + /*! * Returns a reference to the character at position \a i. */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 25c6e1da..2d4a0c5d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -65,6 +65,11 @@ IF(WITH_SHORTEN) ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/shorten ) ENDIF() +IF(WITH_MATROSKA) + SET(test_HDR_DIRS ${test_HDR_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/matroska + ) +ENDIF() INCLUDE_DIRECTORIES(${test_HDR_DIRS}) SET(test_runner_SRCS @@ -152,6 +157,11 @@ IF(WITH_SHORTEN) test_shorten.cpp ) ENDIF() +IF(WITH_MATROSKA) + SET(test_runner_SRCS ${test_runner_SRCS} + test_matroska.cpp + ) +ENDIF() IF(BUILD_BINDINGS) SET(test_runner_SRCS ${test_runner_SRCS} test_tag_c.cpp diff --git a/tests/data/no-tags.mka b/tests/data/no-tags.mka new file mode 100644 index 00000000..b69ed2c4 Binary files /dev/null and b/tests/data/no-tags.mka differ diff --git a/tests/data/no-tags.webm b/tests/data/no-tags.webm new file mode 100644 index 00000000..8848bf25 Binary files /dev/null and b/tests/data/no-tags.webm differ diff --git a/tests/data/optimized.mkv b/tests/data/optimized.mkv new file mode 100644 index 00000000..e64084b1 Binary files /dev/null and b/tests/data/optimized.mkv differ diff --git a/tests/data/tags-before-cues.mkv b/tests/data/tags-before-cues.mkv new file mode 100644 index 00000000..98ad4708 Binary files /dev/null and b/tests/data/tags-before-cues.mkv differ diff --git a/tests/test_fileref.cpp b/tests/test_fileref.cpp index 5c3e4e0d..34adfd78 100644 --- a/tests/test_fileref.cpp +++ b/tests/test_fileref.cpp @@ -472,6 +472,11 @@ public: #endif #ifdef TAGLIB_WITH_SHORTEN CPPUNIT_ASSERT(extensions.contains("shn")); +#endif +#ifdef TAGLIB_WITH_MATROSKA + CPPUNIT_ASSERT(extensions.contains("mkv")); + CPPUNIT_ASSERT(extensions.contains("mka")); + CPPUNIT_ASSERT(extensions.contains("webm")); #endif } diff --git a/tests/test_matroska.cpp b/tests/test_matroska.cpp new file mode 100644 index 00000000..b3fd23d7 --- /dev/null +++ b/tests/test_matroska.cpp @@ -0,0 +1,1196 @@ +/*************************************************************************** + 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/ * + ***************************************************************************/ + +/* + * # Test files + * + * no-tags.mka was created from a small mp3 file using + * mkvmerge -o small.mka small.mp3 + * The tags were removed afterwards. + * + * tags-before-cues.mkv was created using + * ffmpeg -t 0.1 -s qcif -f rawvideo -pix_fmt rgb24 -r 25 \ + * -i /dev/zero zero_second.mkv + * The tags were added using Handbrake, which places them before the Cluster + * and Cues elements. The tags were rewritten to conform to the specification. + * + * optimized.mpk is the same as tags-before-cues.mkv, but optimized using + * mkclean --keep-cues --optimize tags-before-cues.mkv + * This is done to have a segment with the size encoded with fewer than 8 bytes, + * so the resizing of the segment can be verified by adding a large attachment. + * + * no-tags.webm was created using + * ffmpeg -f lavfi -i color=c=blue:s=64x64 -frames:v 1 \ + * -pix_fmt yuv420p frame.yuv + * vpxenc --codec=vp8 --width=64 --height=64 --fps=10/1 \ + * --end-usage=cq --cq-level=4 --lag-in-frames=0 --auto-alt-ref=0 \ + * --profile=0 --target-bitrate=1000 -o onems.webm frame.yuv + * Then the EMBL void element after the seek head was transformed to have + * the content size encoded with 8 bytes instead of 1 byte to have it in the + * same format as used here. + * + * # File validation + * + * The files are read with read style "Accurate" to verify the segment positions + * in seek head and cues. + * + * # Manual testing + * + * The integrity of Matroska files modified with TagLib can be verified with + * mkvalidator v0.6.0. For inspection of files, mkvinfo and mkvtoolnix-gui + * are useful. + * + * All tags of Matroska files can be read and written using properties and + * complex properties, so tagwriter and tagreader are sufficient for testing. + * + * - Set standard tags + * examples/tagwriter -t 'Track Title' -a 'Artist Name' -A 'Album Title' \ + * -c 'Comment' -g 'Genre' -y 2025 -T 1 test.mka + * + * To remove standard tags, set them to an empty string or 0 for numeric tags. + * Using the property interface, the properties listed in propertymapping.dox + * and arbitrary string tags with track target level can be written. + * + * - Insert property + * examples/tagwriter -I 'ALBUMARTIST' 'Album Artist' test.mka + * - Replace property + * examples/tagwriter -R 'ALBUMARTIST' 'Other Artist' test.mka + * - Delete property + * examples/tagwriter -D 'ALBUMARTIST' test.mka + * + * Pictures can be attached with a description + * examples/tagwriter -p file.jpg 'Picture description' test.mka + * + * Alternatively, they can be set using complex properties. A complex property + * can be set with + * + * examples/tagwriter -C FILE + * + * The second parameter can be set to "" to delete complex properties with the + * given key. To set complex property values, a simple shorthand syntax can be + * used. Multiple maps are separated by ';', values within a map are assigned + * with key=value and separated by a ','. Types are automatically detected, + * double quotes can be used to force a string. A ByteVector can be constructed + * from the contents of a file with the path given after "file://". There is + * no escape character, but hex codes are supported, e.g. "\x2C" to include a ',' + * and \x3B to include a ';'. + * + * - Set an attached file in a Matroska file: + * examples/tagwriter -C file.bin \ + * 'fileName=file.bin,data=file://file.bin,mimeType=application/octet-stream' \ + * file.mka + * + * - Set simple tag with target type in a Matroska file: + * examples/tagwriter -C PART_NUMBER \ + * 'name=PART_NUMBER,targetTypeValue=20,value="2"' file.mka + * + * - Set simple tag with binary value in a Matroska file: + * examples/tagwriter -C BINARY \ + * name=BINARY,data=file://file.bin,targetTypeValue=60 file.mka + * + * # Test coverage + * + * Not yet covered by the unit tests are: + * - MkCueDuration, MkCueBlockNumber, MkCueCodecState, MkCueReference, MkCueRefTime, + * MkBitDepth and the methods handling these elements because none of the test + * files has such elements, + * - some unused functions like EBML::parseVINT(), EBML::FloatElement::render(), + * - some error cases because they never occur with the unit tests. + */ + +#include +#include + +#include "tbytevectorlist.h" +#include "tbytevectorstream.h" +#include "tpropertymap.h" +#include "matroskafile.h" +#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 +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestMatroska : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMatroska); + CPPUNIT_TEST(testPropertiesMka); + CPPUNIT_TEST(testPropertiesMkv); + CPPUNIT_TEST(testPropertiesWebm); + CPPUNIT_TEST(testSimpleTagsAndAttachments); + CPPUNIT_TEST(testAddRemoveTagsAttachments); + CPPUNIT_TEST(testTagsWebm); + CPPUNIT_TEST(testRepeatedSave); + CPPUNIT_TEST(testPropertyInterface); + CPPUNIT_TEST(testComplexProperties); + CPPUNIT_TEST(testOpenInvalid); + CPPUNIT_TEST(testSegmentSizeChange); + CPPUNIT_TEST(testChapters); + CPPUNIT_TEST_SUITE_END(); + +public: + void testPropertiesMka() + { + Matroska::File f(TEST_FILE_PATH_C("no-tags.mka")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(444, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(223, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(String("matroska"), f.audioProperties()->docType()); + CPPUNIT_ASSERT_EQUAL(4, f.audioProperties()->docTypeVersion()); + CPPUNIT_ASSERT_EQUAL(String("A_MPEG/L3"), f.audioProperties()->codecName()); + CPPUNIT_ASSERT_EQUAL(String(""), f.audioProperties()->title()); + } + + void testPropertiesMkv() + { + Matroska::File f(TEST_FILE_PATH_C("tags-before-cues.mkv")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(120, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(227, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(String("matroska"), f.audioProperties()->docType()); + CPPUNIT_ASSERT_EQUAL(4, f.audioProperties()->docTypeVersion()); + CPPUNIT_ASSERT_EQUAL(String(""), f.audioProperties()->codecName()); + CPPUNIT_ASSERT_EQUAL(String("handbrake"), f.audioProperties()->title()); + } + + void testPropertiesWebm() + { + Matroska::File f(TEST_FILE_PATH_C("no-tags.webm")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(2816, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(String("webm"), f.audioProperties()->docType()); + CPPUNIT_ASSERT_EQUAL(4, f.audioProperties()->docTypeVersion()); + CPPUNIT_ASSERT_EQUAL(String(""), f.audioProperties()->codecName()); + CPPUNIT_ASSERT_EQUAL(String(""), f.audioProperties()->title()); + + Matroska::File noProps(TEST_FILE_PATH_C("no-tags.webm"), false); + CPPUNIT_ASSERT(!noProps.audioProperties()); + } + + void testSimpleTagsAndAttachments() + { + ScopedFileCopy copy("no-tags", ".mka"); + string newname = copy.fileName(); + { + Matroska::File f(newname.c_str()); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(!f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + auto tag = f.tag(true); + CPPUNIT_ASSERT(tag->isEmpty()); + tag->addSimpleTag(Matroska::SimpleTag( + "Test Name 2", String("Test Value 2"), + Matroska::SimpleTag::TargetTypeValue::Album)); + tag->insertSimpleTag(0, Matroska::SimpleTag( + "Test Name 1", String("Test Value 1"), + Matroska::SimpleTag::TargetTypeValue::Track, "en")); + tag->insertSimpleTag(1, Matroska::SimpleTag( + "Test Name 3", String("Test Value 3"))); + tag->removeSimpleTag(1); + tag->setTitle("Test title"); + tag->setArtist("Test artist"); + tag->setYear(1969); + auto attachments = f.attachments(true); + attachments->addAttachedFile(Matroska::AttachedFile( + "JPEG data", "cover.jpg", "image/jpeg", 5081000385627515072ULL, + "Cover")); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(false); + CPPUNIT_ASSERT(tag); + auto attachments = f.attachments(false); + CPPUNIT_ASSERT(attachments); + CPPUNIT_ASSERT(!tag->isEmpty()); + + CPPUNIT_ASSERT_EQUAL(String("Test title"), tag->title()); + CPPUNIT_ASSERT_EQUAL(String("Test artist"), tag->artist()); + CPPUNIT_ASSERT_EQUAL(1969U, tag->year()); + CPPUNIT_ASSERT_EQUAL(String(""), tag->album()); + CPPUNIT_ASSERT_EQUAL(String(""), tag->comment()); + CPPUNIT_ASSERT_EQUAL(String(""), tag->genre()); + CPPUNIT_ASSERT_EQUAL(0U, tag->track()); + + const auto &simpleTags = tag->simpleTagsList(); + CPPUNIT_ASSERT_EQUAL(5U, simpleTags.size()); + + CPPUNIT_ASSERT_EQUAL(String("en"), simpleTags[0].language()); + CPPUNIT_ASSERT_EQUAL(String("Test Name 1"), simpleTags[0].name()); + CPPUNIT_ASSERT_EQUAL(String("Test Value 1"), simpleTags[0].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[0].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[0].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[0].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[0].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[0].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[1].language()); + CPPUNIT_ASSERT_EQUAL(String("TITLE"), simpleTags[1].name()); + CPPUNIT_ASSERT_EQUAL(String("Test title"), simpleTags[1].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[1].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[1].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[1].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[1].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[1].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[2].language()); + CPPUNIT_ASSERT_EQUAL(String("ARTIST"), simpleTags[2].name()); + CPPUNIT_ASSERT_EQUAL(String("Test artist"), simpleTags[2].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[2].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[2].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[2].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[2].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[2].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[3].language()); + CPPUNIT_ASSERT_EQUAL(String("DATE_RECORDED"), simpleTags[3].name()); + CPPUNIT_ASSERT_EQUAL(String("1969"), simpleTags[3].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[3].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[3].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[3].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[3].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[3].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[4].language()); + CPPUNIT_ASSERT_EQUAL(String("Test Name 2"), simpleTags[4].name()); + CPPUNIT_ASSERT_EQUAL(String("Test Value 2"), simpleTags[4].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[4].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[4].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Album, simpleTags[4].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[4].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[4].type()); + + const auto &attachedFiles = attachments->attachedFileList(); + CPPUNIT_ASSERT_EQUAL(1U, attachedFiles.size()); + + CPPUNIT_ASSERT_EQUAL(String("Cover"), attachedFiles[0].description()); + CPPUNIT_ASSERT_EQUAL(String("cover.jpg"), attachedFiles[0].fileName()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), attachedFiles[0].mediaType()); + CPPUNIT_ASSERT_EQUAL(ByteVector("JPEG data"), attachedFiles[0].data()); + CPPUNIT_ASSERT_EQUAL(5081000385627515072ULL, attachedFiles[0].uid()); + + PropertyMap expectedProps; + expectedProps["ARTIST"] = StringList("Test artist"); + expectedProps["DATE"] = StringList("1969"); + expectedProps["TEST NAME 1"] = StringList("Test Value 1"); + expectedProps["TITLE"] = StringList("Test title"); + expectedProps.addUnsupportedData("Test Name 2"); + auto props = f.properties(); + if (expectedProps != props) { + CPPUNIT_ASSERT_EQUAL(expectedProps.toString(), props.toString()); + } + CPPUNIT_ASSERT(expectedProps == props); + CPPUNIT_ASSERT(expectedProps == tag->properties()); + + auto keys = f.complexPropertyKeys(); + CPPUNIT_ASSERT_EQUAL(StringList({"Test Name 2", "PICTURE"}), keys); + auto pictures = f.complexProperties("PICTURE"); + CPPUNIT_ASSERT_EQUAL(1U, pictures.size()); + const VariantMap expectedPic { + {"data", ByteVector("JPEG data")}, + {"mimeType", "image/jpeg"}, + {"description", "Cover"}, + {"fileName", "cover.jpg"}, + {"uid", 5081000385627515072ULL} + }; + CPPUNIT_ASSERT(List({expectedPic}) == pictures); + const VariantMap expectedComplexProps { + {"defaultLanguage", true}, + {"language", "und"}, + {"name", "Test Name 2"}, + {"value", "Test Value 2"}, + {"targetTypeValue", 50}, + }; + auto complexProps = f.complexProperties("Test Name 2"); + CPPUNIT_ASSERT(List({expectedComplexProps}) == complexProps); + + tag->clearSimpleTags(); + attachments->clear(); + 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 without tags is same as original empty file + const ByteVector origData = PlainFile(TEST_FILE_PATH_C("no-tags.mka")).readAll(); + const ByteVector fileData = PlainFile(newname.c_str()).readAll(); + CPPUNIT_ASSERT(origData == fileData); + } + + void testAddRemoveTagsAttachments() + { + ScopedFileCopy copy("no-tags", ".mka"); + string newname = copy.fileName(); + { + Matroska::File f(newname.c_str()); + f.tag(true)->setComment("C"); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT_EQUAL(String("C"), f.tag(false)->comment()); + f.tag()->setComment(""); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(!f.tag(false)); + f.attachments(true)->addAttachedFile(Matroska::AttachedFile( + ByteVector(), "", "")); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + const auto &attachedFiles = f.attachments(false)->attachedFileList(); + CPPUNIT_ASSERT_EQUAL(1U, attachedFiles.size()); + f.attachments()->removeAttachedFile(attachedFiles.front().uid()); + 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)); + } + } + + void testTagsWebm() + { + ScopedFileCopy copy("no-tags", ".webm"); + string newname = copy.fileName(); + { + Matroska::File f(newname.c_str()); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(!f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + + f.setProperties(SimplePropertyMap{ + {"ARTIST", {"First artist", "second artist"}}, + {"", {"Invalid", "Ignored"}} + }); + f.tag(false)->addSimpleTag(Matroska::SimpleTag("", ByteVector("Not valid"))); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), false, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(false); + CPPUNIT_ASSERT(tag); + CPPUNIT_ASSERT(!f.attachments(false)); + CPPUNIT_ASSERT_EQUAL(String("First artist"), tag->artist()); + CPPUNIT_ASSERT_EQUAL(StringList({"First artist", "second artist"}), f.properties().value("ARTIST")); + + tag->setAlbum("Album"); + tag->setTrack(5); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), false, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(false); + CPPUNIT_ASSERT(tag); + CPPUNIT_ASSERT_EQUAL(String("First artist"), tag->artist()); + CPPUNIT_ASSERT_EQUAL(String("Album"), tag->album()); + CPPUNIT_ASSERT_EQUAL(5U, tag->track()); + + tag->setArtist(""); + tag->removeSimpleTag("TITLE", Matroska::SimpleTag::TargetTypeValue::Album); + tag->setTrack(0); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), false, AudioProperties::Accurate); + CPPUNIT_ASSERT(!f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + } + + // Check if file without tags is same as original empty file + const ByteVector origData = PlainFile(TEST_FILE_PATH_C("no-tags.webm")).readAll(); + const ByteVector fileData = PlainFile(newname.c_str()).readAll(); + CPPUNIT_ASSERT(origData == fileData); + } + + void testRepeatedSave() + { + ScopedFileCopy copy("no-tags", ".mka"); + string newname = copy.fileName(); + String text = "01234 56789 ABCDE FGHIJ 01234 56789 ABCDE FGHIJ 01234 56789"; + { + Matroska::File f(newname.c_str()); + CPPUNIT_ASSERT(f.save()); + f.tag(true)->setTitle(text.substr(0, 23)); + CPPUNIT_ASSERT(f.save()); + f.tag(true)->setTitle(text.substr(0, 5)); + CPPUNIT_ASSERT(f.save()); + f.tag(true)->setTitle(text); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT_EQUAL(text, f.tag()->title()); + } + } + + void testPropertyInterface() + { + ScopedFileCopy copy("tags-before-cues", ".mkv"); + string newname = copy.fileName(); + + PropertyMap initialProps; + initialProps["ARTIST"] = StringList("Actors"); + initialProps["RELEASEDATE"] = StringList("2023"); + initialProps["DESCRIPTION"] = StringList("Description"); + initialProps["DIRECTOR"] = StringList("Director"); + initialProps["ENCODEDBY"] = StringList("Lavf59.27.100"); + initialProps["GENRE"] = StringList("Genre"); + initialProps["SUMMARY"] = StringList("Comment"); + initialProps["SYNOPSIS"] = StringList("Plot"); + + const VariantMap initialComplexProps { + {"defaultLanguage", true}, + {"language", "und"}, + {"name", "DURATION"}, + {"value", "00:00:00.120000000"}, + {"trackUid", 9584013959154292683ULL}, + }; + + PropertyMap newProps; + newProps["ALBUM"] = StringList("Album"); + newProps["ALBUMARTIST"] = StringList("Album Artist"); + newProps["ALBUMARTISTSORT"] = StringList("Album Artist Sort"); + newProps["ALBUMSORT"] = StringList("Album Sort"); + newProps["ARTIST"] = StringList("Artist"); + newProps["ARTISTS"] = StringList("Artists"); + newProps["ARTISTSORT"] = StringList("Artist Sort"); + newProps["ASIN"] = StringList("ASIN"); + newProps["BARCODE"] = StringList("Barcode"); + newProps["CATALOGNUMBER"] = StringList("Catalog Number 1").append("Catalog Number 2"); + newProps["COMMENT"] = StringList("Comment"); + newProps["DATE"] = StringList("2021-01-10"); + newProps["DISCNUMBER"] = StringList("3"); + newProps["DISCTOTAL"] = StringList("5"); + newProps["DJMIXER"] = StringList("Mixed by"); + newProps["ENCODEDBY"] = StringList("Encoded by"); + newProps["ENCODING"] = StringList("Encoder settings"); + newProps["ENCODINGTIME"] = StringList("Date encoded"); + newProps["GENRE"] = StringList("Genre"); + newProps["INITIALKEY"] = StringList("Initial key"); + newProps["ISRC"] = StringList("UKAAA0500001"); + newProps["LABEL"] = StringList("Label 1").append("Label 2"); + newProps["MEDIA"] = StringList("Media"); + newProps["MUSICBRAINZ_ALBUMARTISTID"] = StringList("MusicBrainz_AlbumartistID"); + newProps["MUSICBRAINZ_ALBUMID"] = StringList("MusicBrainz_AlbumID"); + newProps["MUSICBRAINZ_ARTISTID"] = StringList("MusicBrainz_ArtistID"); + newProps["MUSICBRAINZ_RELEASEGROUPID"] = StringList("MusicBrainz_ReleasegroupID"); + newProps["MUSICBRAINZ_RELEASETRACKID"] = StringList("MusicBrainz_ReleasetrackID"); + newProps["MUSICBRAINZ_TRACKID"] = StringList("MusicBrainz_TrackID"); + newProps["OWNER"] = StringList("Purchase owner"); + newProps["ORIGINALDATE"] = StringList("2021-01-09"); + newProps["RELEASECOUNTRY"] = StringList("Release Country"); + newProps["RELEASEDATE"] = StringList("2021-01-10"); + newProps["RELEASESTATUS"] = StringList("Release Status"); + newProps["RELEASETYPE"] = StringList("Release Type"); + newProps["REMIXER"] = StringList("Remixed by"); + newProps["SCRIPT"] = StringList("Script"); + newProps["TAGGINGDATE"] = StringList("2021-01-08"); + newProps["TITLE"] = StringList("Title"); + newProps["TRACKNUMBER"] = StringList("2"); + newProps["TRACKTOTAL"] = StringList("4"); + + const VariantMap newComplexProps { + {"defaultLanguage", false}, + {"language", "en"}, + {"name", "BINARY"}, + {"data", ByteVector("\x01\x02\x03\x04\x05\x06")}, + {"targetTypeValue", static_cast(Matroska::SimpleTag::TargetTypeValue::Collection)}, + }; + { + Matroska::File f(newname.c_str()); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(false); + CPPUNIT_ASSERT(tag); + CPPUNIT_ASSERT(!f.attachments(false)); + + CPPUNIT_ASSERT_EQUAL(String("handbrake"), tag->title()); + CPPUNIT_ASSERT_EQUAL(String("Actors"), tag->artist()); + CPPUNIT_ASSERT_EQUAL(String(""), tag->album()); + CPPUNIT_ASSERT_EQUAL(String(""), tag->comment()); + CPPUNIT_ASSERT_EQUAL(String("Genre"), tag->genre()); + CPPUNIT_ASSERT_EQUAL(0U, tag->track()); + + const auto &simpleTags = tag->simpleTagsList(); + CPPUNIT_ASSERT_EQUAL(9U, simpleTags.size()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[0].language()); + CPPUNIT_ASSERT_EQUAL(String("DURATION"), simpleTags[0].name()); + CPPUNIT_ASSERT_EQUAL(String("00:00:00.120000000"), simpleTags[0].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[0].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[0].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::None, simpleTags[0].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(9584013959154292683ULL, simpleTags[0].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[0].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[1].language()); + CPPUNIT_ASSERT_EQUAL(String("ARTIST"), simpleTags[1].name()); + CPPUNIT_ASSERT_EQUAL(String("Actors"), simpleTags[1].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[1].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[1].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[1].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[1].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[1].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[2].language()); + CPPUNIT_ASSERT_EQUAL(String("DESCRIPTION"), simpleTags[2].name()); + CPPUNIT_ASSERT_EQUAL(String("Description"), simpleTags[2].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[2].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[2].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[2].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[2].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[2].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[3].language()); + CPPUNIT_ASSERT_EQUAL(String("DIRECTOR"), simpleTags[3].name()); + CPPUNIT_ASSERT_EQUAL(String("Director"), simpleTags[3].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[3].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[3].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[3].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[3].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[3].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[4].language()); + CPPUNIT_ASSERT_EQUAL(String("ENCODER"), simpleTags[4].name()); + CPPUNIT_ASSERT_EQUAL(String("Lavf59.27.100"), simpleTags[4].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[4].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[4].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[4].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[4].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[4].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[5].language()); + CPPUNIT_ASSERT_EQUAL(String("GENRE"), simpleTags[5].name()); + CPPUNIT_ASSERT_EQUAL(String("Genre"), simpleTags[5].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[5].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[5].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[5].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[5].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[5].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[6].language()); + CPPUNIT_ASSERT_EQUAL(String("SUMMARY"), simpleTags[6].name()); + CPPUNIT_ASSERT_EQUAL(String("Comment"), simpleTags[6].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[6].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[6].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[6].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[6].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[6].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[7].language()); + CPPUNIT_ASSERT_EQUAL(String("SYNOPSIS"), simpleTags[7].name()); + CPPUNIT_ASSERT_EQUAL(String("Plot"), simpleTags[7].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[7].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[7].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[7].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[7].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[7].type()); + + CPPUNIT_ASSERT_EQUAL(String("und"), simpleTags[8].language()); + CPPUNIT_ASSERT_EQUAL(String("DATE_RELEASED"), simpleTags[8].name()); + CPPUNIT_ASSERT_EQUAL(String("2023"), simpleTags[8].toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[8].toByteVector()); + CPPUNIT_ASSERT_EQUAL(true, simpleTags[8].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Album, simpleTags[8].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(0ULL, simpleTags[8].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[8].type()); + + auto props = f.properties(); + if (initialProps != props) { + CPPUNIT_ASSERT_EQUAL(initialProps.toString(), props.toString()); + } + CPPUNIT_ASSERT(initialProps == props); + CPPUNIT_ASSERT(initialProps == tag->properties()); + + auto keys = f.complexPropertyKeys(); + CPPUNIT_ASSERT_EQUAL(StringList({"DURATION"}), keys); + auto complexProps = f.complexProperties("DURATION"); + CPPUNIT_ASSERT(List({initialComplexProps}) == complexProps); + + CPPUNIT_ASSERT(f.setComplexProperties("DURATION", {})); + CPPUNIT_ASSERT(f.setComplexProperties("BINARY", {newComplexProps})); + CPPUNIT_ASSERT(f.setProperties(newProps).isEmpty()); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(false); + CPPUNIT_ASSERT(tag); + CPPUNIT_ASSERT(!f.attachments(false)); + + auto props = f.properties(); + if (newProps != props) { + CPPUNIT_ASSERT_EQUAL(newProps.toString(), props.toString()); + } + CPPUNIT_ASSERT(newProps == props); + CPPUNIT_ASSERT(newProps == tag->properties()); + + auto keys = f.complexPropertyKeys(); + CPPUNIT_ASSERT_EQUAL(StringList({"BINARY"}), keys); + auto complexProps = f.complexProperties("BINARY"); + CPPUNIT_ASSERT(List({newComplexProps}) == complexProps); + + const auto &simpleTags = tag->simpleTagsList(); + StringList simpleTagNames; + for(const auto &simpleTag : simpleTags) { + simpleTagNames.append(simpleTag.name()); + } + const StringList expectedSimpleTagNames { + "BINARY", "TITLE", "ARTIST", "ARTISTSORT", "TITLESORT", + "PART_NUMBER", "TOTAL_PARTS", "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ_ALBUMID", + "MUSICBRAINZ_RELEASEGROUPID", "DATE_RELEASED", "ARTIST", "ARTISTS", "ARTISTSORT", + "ASIN", "BARCODE", "CATALOG_NUMBER", "CATALOG_NUMBER", "COMMENT", + "DATE_RECORDED", "MIXED_BY", "ENCODER", "ENCODER_SETTINGS", "DATE_ENCODED", + "GENRE", "INITIAL_KEY", "ISRC", "LABEL_CODE", "LABEL_CODE", + "ORIGINAL_MEDIA_TYPE", "MUSICBRAINZ_ARTISTID", + "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ_TRACKID", "ORIGINALDATE", + "PURCHASE_OWNER", "RELEASECOUNTRY", "RELEASESTATUS", + "RELEASETYPE", "REMIXED_BY", "SCRIPT", "DATE_TAGGED", "TITLE", + "PART_NUMBER", "TOTAL_PARTS" + }; + CPPUNIT_ASSERT_EQUAL(expectedSimpleTagNames, simpleTagNames); + + CPPUNIT_ASSERT(f.setComplexProperties("DURATION", {initialComplexProps})); + CPPUNIT_ASSERT(f.setComplexProperties("BINARY", {})); + CPPUNIT_ASSERT(f.setProperties(initialProps).isEmpty()); + 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); + } + + void testComplexProperties() + { + ScopedFileCopy copy("no-tags", ".mka"); + string newname = copy.fileName(); + const VariantMap picture { + {"data", ByteVector("JPEG data")}, + {"mimeType", "image/jpeg"}, + {"description", "Cover"}, + {"fileName", "folder.jpg"}, + {"uid", 123ULL} + }; + const VariantMap font { + {"data", ByteVector("TTF data")}, + {"mimeType", "font/ttf"}, + {"description", "Subtitle font"}, + {"fileName", "file.ttf"}, + {"uid", 456ULL} + }; + const VariantMap trackUidTag { + {"defaultLanguage", true}, + {"language", "und"}, + {"name", "DURATION"}, + {"trackUid", 8315232342706310039ULL}, + {"value", "00:00:00.120000000"}, + }; + const VariantMap targetTypeTag { + {"defaultLanguage", false}, + {"language", "en"}, + {"name", "PART_NUMBER"}, + {"targetTypeValue", 20}, + {"value", "2"}, + }; + const VariantMap binaryTag { + {"defaultLanguage", false}, + {"language", "und"}, + {"name", "THUMBNAIL"}, + {"targetTypeValue", 30}, + {"data", ByteVector("JPEG data")}, + }; + { + Matroska::File f(newname.c_str()); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(!f.tag(false)); + CPPUNIT_ASSERT(!f.attachments(false)); + CPPUNIT_ASSERT(f.complexPropertyKeys().isEmpty()); + CPPUNIT_ASSERT(f.complexProperties("PICTURE").isEmpty()); + + CPPUNIT_ASSERT(f.setComplexProperties("PICTURE", {picture})); + CPPUNIT_ASSERT(f.setComplexProperties("file.ttf", {font})); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + CPPUNIT_ASSERT(!f.tag(false)); + auto attachments = f.attachments(false); + CPPUNIT_ASSERT(attachments); + + const auto &attachedFiles = attachments->attachedFileList(); + CPPUNIT_ASSERT_EQUAL(2U, attachedFiles.size()); + CPPUNIT_ASSERT_EQUAL(picture.value("fileName").value(), attachedFiles[0].fileName()); + CPPUNIT_ASSERT_EQUAL(picture.value("data").value(), attachedFiles[0].data()); + CPPUNIT_ASSERT_EQUAL(picture.value("description").value(), attachedFiles[0].description()); + CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value(), attachedFiles[0].mediaType()); + CPPUNIT_ASSERT_EQUAL(picture.value("uid").value(), attachedFiles[0].uid()); + CPPUNIT_ASSERT_EQUAL(font.value("fileName").value(), attachedFiles[1].fileName()); + CPPUNIT_ASSERT_EQUAL(font.value("data").value(), attachedFiles[1].data()); + CPPUNIT_ASSERT_EQUAL(font.value("description").value(), attachedFiles[1].description()); + CPPUNIT_ASSERT_EQUAL(font.value("mimeType").value(), attachedFiles[1].mediaType()); + CPPUNIT_ASSERT_EQUAL(font.value("uid").value(), attachedFiles[1].uid()); + + CPPUNIT_ASSERT_EQUAL(StringList({"PICTURE", "file.ttf"}), f.complexPropertyKeys()); + CPPUNIT_ASSERT(List({picture}) == f.complexProperties("PICTURE")); + CPPUNIT_ASSERT(List({font}) == f.complexProperties("file.ttf")); + + CPPUNIT_ASSERT(f.setComplexProperties("DURATION", {trackUidTag})); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(true); + CPPUNIT_ASSERT(tag); + CPPUNIT_ASSERT(f.attachments(false)); + + const auto &simpleTags = tag->simpleTagsList(); + CPPUNIT_ASSERT_EQUAL(1U, simpleTags.size()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("value").value(), simpleTags[0].toString()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("language").value(), simpleTags[0].language()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("name").value(), simpleTags[0].name()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("defaultLanguage").value(), simpleTags[0].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("trackUid").value(), simpleTags[0].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::None, simpleTags[0].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[0].toByteVector()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[0].type()); + + CPPUNIT_ASSERT_EQUAL(StringList({ "DURATION", "PICTURE", "file.ttf"}), f.complexPropertyKeys()); + CPPUNIT_ASSERT(List({trackUidTag}) == f.complexProperties("DURATION")); + + CPPUNIT_ASSERT(f.setComplexProperties("PICTURE", {})); + CPPUNIT_ASSERT(f.setComplexProperties("file.ttf", {})); + CPPUNIT_ASSERT(f.setComplexProperties("PART_NUMBER", {targetTypeTag})); + CPPUNIT_ASSERT(f.setComplexProperties("THUMBNAIL", {binaryTag})); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(true); + CPPUNIT_ASSERT(tag); + CPPUNIT_ASSERT(!f.attachments(false)); + + const auto &simpleTags = tag->simpleTagsList(); + CPPUNIT_ASSERT_EQUAL(3U, simpleTags.size()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("value").value(), simpleTags[0].toString()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("language").value(), simpleTags[0].language()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("name").value(), simpleTags[0].name()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("defaultLanguage").value(), simpleTags[0].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(trackUidTag.value("trackUid").value(), simpleTags[0].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::None, simpleTags[0].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[0].toByteVector()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[0].type()); + + CPPUNIT_ASSERT_EQUAL(targetTypeTag.value("value").value(), simpleTags[1].toString()); + CPPUNIT_ASSERT_EQUAL(targetTypeTag.value("language").value(), simpleTags[1].language()); + CPPUNIT_ASSERT_EQUAL(targetTypeTag.value("name").value(), simpleTags[1].name()); + CPPUNIT_ASSERT_EQUAL(targetTypeTag.value("defaultLanguage").value(), simpleTags[1].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(targetTypeTag.value("trackUid").value(), simpleTags[1].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Subtrack, simpleTags[1].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), simpleTags[1].toByteVector()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::StringType, simpleTags[1].type()); + + CPPUNIT_ASSERT_EQUAL(binaryTag.value("data").value(), simpleTags[2].toByteVector()); + CPPUNIT_ASSERT_EQUAL(binaryTag.value("language").value(), simpleTags[2].language()); + CPPUNIT_ASSERT_EQUAL(binaryTag.value("name").value(), simpleTags[2].name()); + CPPUNIT_ASSERT_EQUAL(binaryTag.value("defaultLanguage").value(), simpleTags[2].defaultLanguageFlag()); + CPPUNIT_ASSERT_EQUAL(binaryTag.value("trackUid").value(), simpleTags[2].trackUid()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::TargetTypeValue::Track, simpleTags[2].targetTypeValue()); + CPPUNIT_ASSERT_EQUAL(String(), simpleTags[2].toString()); + CPPUNIT_ASSERT_EQUAL(Matroska::SimpleTag::ValueType::BinaryType, simpleTags[2].type()); + + CPPUNIT_ASSERT_EQUAL(StringList({"DURATION", "PART_NUMBER", "THUMBNAIL"}), f.complexPropertyKeys()); + CPPUNIT_ASSERT(List({trackUidTag}) == f.complexProperties("DURATION")); + CPPUNIT_ASSERT(List({targetTypeTag}) == f.complexProperties("PART_NUMBER")); + CPPUNIT_ASSERT(List({binaryTag}) == f.complexProperties("THUMBNAIL")); + + CPPUNIT_ASSERT(f.setComplexProperties("PART_NUMBER", {})); + CPPUNIT_ASSERT(f.setComplexProperties("THUMBNAIL", {})); + CPPUNIT_ASSERT(f.setComplexProperties("DURATION", {})); + 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.setComplexProperties("font/ttf", {{ + {"data", ByteVector("TTF data")}, + {"description", "Subtitle font"}, + {"fileName", "file.ttf"}, + }})); + CPPUNIT_ASSERT(f.setComplexProperties("file.otf", {{ + {"data", ByteVector("OTF data")}, + {"mimeType", "font/otf"}, + {"description", "OpenType"}, + }})); + 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_EQUAL(StringList({"file.ttf", "file.otf"}), f.complexPropertyKeys()); + auto ttfs = f.complexProperties("file.ttf"); + auto otfs = f.complexProperties("file.otf"); + CPPUNIT_ASSERT_EQUAL(1U, ttfs.size()); + CPPUNIT_ASSERT_EQUAL(1U, otfs.size()); + auto ttf = ttfs.front(); + auto otf = otfs.front(); + CPPUNIT_ASSERT_EQUAL(ByteVector("TTF data"), ttf.value("data").value()); + CPPUNIT_ASSERT_EQUAL(String("Subtitle font"), ttf.value("description").value()); + CPPUNIT_ASSERT_EQUAL(String("file.ttf"), ttf.value("fileName").value()); + CPPUNIT_ASSERT_EQUAL(String("font/ttf"), ttf.value("mimeType").value()); + CPPUNIT_ASSERT(ttf.value("uid").value() != 0ULL); + CPPUNIT_ASSERT_EQUAL(ByteVector("OTF data"), otf.value("data").value()); + CPPUNIT_ASSERT_EQUAL(String("OpenType"), otf.value("description").value()); + CPPUNIT_ASSERT_EQUAL(String("file.otf"), otf.value("fileName").value()); + CPPUNIT_ASSERT_EQUAL(String("font/otf"), otf.value("mimeType").value()); + CPPUNIT_ASSERT(otf.value("uid").value() != 0ULL); + + CPPUNIT_ASSERT(f.setComplexProperties("file.ttf", {})); + CPPUNIT_ASSERT(f.setComplexProperties("file.otf", {})); + CPPUNIT_ASSERT(f.save()); + } + + // Check if file without tags is same as original empty file + const ByteVector origData = PlainFile(TEST_FILE_PATH_C("no-tags.mka")).readAll(); + const ByteVector fileData = PlainFile(newname.c_str()).readAll(); + CPPUNIT_ASSERT(origData == fileData); + } + + void testOpenInvalid() + { + { + Matroska::File f(TEST_FILE_PATH_C("garbage.mp3")); + CPPUNIT_ASSERT(!f.isValid()); + } + { + ByteVector origData = PlainFile(TEST_FILE_PATH_C("no-tags.mka")).readAll(); + ByteVectorStream origStream(origData); + Matroska::File origFile(&origStream, true, AudioProperties::Accurate); + CPPUNIT_ASSERT(origFile.isValid()); + + ByteVector truncatedData = origData.mid(0, 4000); + ByteVectorStream truncatedStream(truncatedData); + Matroska::File truncatedFile(&truncatedStream, true, AudioProperties::Accurate); + CPPUNIT_ASSERT(!truncatedFile.isValid()); + } + } + + void testSegmentSizeChange() + { + ScopedFileCopy copy("optimized", ".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)); + auto attachments = f.attachments(true); + // Large enough for emitSizeChanged() from Matroska::Segment::render() + attachments->addAttachedFile(Matroska::AttachedFile( + ByteVector(20000, 'x'), "cover.jpg", "image/jpeg", + 5081000385627515072ULL, "Cover")); + CPPUNIT_ASSERT(f.save()); + } + { + Matroska::File f(newname.c_str(), true, AudioProperties::Accurate); + CPPUNIT_ASSERT(f.isValid()); + auto tag = f.tag(true); + CPPUNIT_ASSERT(tag); + auto attachments = f.attachments(false); + CPPUNIT_ASSERT(attachments); + CPPUNIT_ASSERT(PropertyMap(SimplePropertyMap{ + {"ARTIST", {"Actors"}}, + {"DESCRIPTION", {"Description"}}, + {"DIRECTOR", {"Director"}}, + {"ENCODEDBY", {"Lavf59.27.100"}}, + {"GENRE", {"Genre"}}, + {"RELEASEDATE", {"2023"}}, + {"SUMMARY", {"Comment"}}, + {"SYNOPSIS", {"Plot"}} + }) == tag->properties()); + + attachments->clear(); + 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)); + } + } + + 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); + + f.attachments(true)->addAttachedFile(Matroska::AttachedFile( + ByteVector("PNG data"), "folder.png", "image/png", 1763187649ULL, + "Cover")); + 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 0bf03cce..ebc36a3a 100644 --- a/tests/test_sizes.cpp +++ b/tests/test_sizes.cpp @@ -148,6 +148,15 @@ #include "trueaudiofile.h" #include "trueaudioproperties.h" #endif +#ifdef TAGLIB_WITH_MATROSKA +#include "matroskaattachedfile.h" +#include "matroskaattachments.h" +#include "matroskachapteredition.h" +#include "matroskachapters.h" +#include "matroskafile.h" +#include "matroskaproperties.h" +#include "matroskatag.h" +#endif #include @@ -297,6 +306,19 @@ public: #ifdef TAGLIB_WITH_TRUEAUDIO CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::TrueAudio::File)); CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::TrueAudio::Properties)); +#endif +#ifdef TAGLIB_WITH_MATROSKA + CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::Matroska::File)); + CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::Matroska::Properties)); + 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::ChapterEdition)); + CPPUNIT_ASSERT_EQUAL(classSize(0, false), sizeof(TagLib::Matroska::Chapter)); + CPPUNIT_ASSERT_EQUAL(classSize(0, false), sizeof(TagLib::Matroska::Chapter::Display)); + CPPUNIT_ASSERT_EQUAL(classSize(0, false), sizeof(TagLib::Matroska::SimpleTag)); + CPPUNIT_ASSERT_EQUAL(classSize(0, false), sizeof(TagLib::Matroska::AttachedFile)); #endif }