From c817cc0a47e701e299f2166ad3f49284a9c0f605 Mon Sep 17 00:00:00 2001 From: Urs Fleisch Date: Tue, 26 Aug 2025 22:11:01 +0200 Subject: [PATCH] tagwriter: Support setting of complex properties A complex property can be set with -C The second parameter can be set to "" to delete complex properties with the given key. The 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 is given after "file://". There is no escape, but hex codes are supported, e.g. "\x2C" to include a ',' and \x3B to include a ';'. Examples: Set a GEOB frame in an ID3v2 tag: examples/tagwriter -C GENERALOBJECT \ 'data=file://file.bin,description=My description,fileName=file.bin,mimeType=application/octet-stream' \ file.mp3 Set an APIC frame in an ID3v2 tag (same as -p file.jpg 'My description'): examples/tagwriter -C PICTURE \ 'data=file://file.jpg,description=My description,pictureType=Front Cover,mimeType=image/jpeg' \ file.mp3 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 --- examples/tagwriter.cpp | 110 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) 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;