7 Commits

Author SHA1 Message Date
Urs Fleisch
e3de03501f Version 2.0.2 2024-08-24 06:40:41 +02:00
Urs Fleisch
1bd0d711ca Support older utfcpp versions with utf8cpp CMake target (#1243) (#1244)
This affects for example openSUSE Leap 15.6, which installs
utfcpp 3.2.1 in its own folder.
Now not only utf8::cpp, but also utf8cpp is supported as a CMake
target.
2024-08-15 12:44:17 +02:00
nekiwo
7c85fcaa81 Remove 'using namespace std' to avoid potential conflicts in example files (#1241)
Co-authored-by: nekiwo <nekiwo@users.noreply.github.com>
2024-08-05 21:54:33 +02:00
Urs Fleisch
cbe54d2f40 Support free form tags with MP4 properties (#1239) (#1240) 2024-07-29 20:24:33 +02:00
Urs Fleisch
c4ed590032 tagwriter option -p not working properly (#1236) (#1237)
The -p option of tagwriter sample does not work.
This is because the picture file is open in text mode instead of binary.
Also, the isFile function does not work on Windows in 32 bit mode with
large files. Using _stat64 instead of stat solves the problem.
2024-07-19 12:25:46 +02:00
Stephen Booth
f3fb4d83a4 Skip unknown MP4 boxes (#1231) 2024-05-18 06:45:10 +02:00
Urs Fleisch
3d4428726e Fix parsing of ID3v2.2 frames (#1228) 2024-05-18 06:43:00 +02:00
14 changed files with 190 additions and 87 deletions

View File

@@ -1,3 +1,12 @@
TagLib 2.0.2 (Aug 24, 2024)
===========================
* Fix parsing of ID3v2.2 frames.
* Tolerate MP4 files with unknown atom types as generated by Android tools.
* Support setting properties with arbitrary names in MP4 tags.
* Windows: Fix "-p" option in tagwriter example.
* Support building with older utfcpp versions.
TagLib 2.0.1 (Apr 9, 2024)
==========================

View File

@@ -93,7 +93,7 @@ endif()
# Patch version: increase it for bug fix releases.
set(TAGLIB_SOVERSION_MAJOR 2)
set(TAGLIB_SOVERSION_MINOR 0)
set(TAGLIB_SOVERSION_PATCH 1)
set(TAGLIB_SOVERSION_PATCH 2)
include(ConfigureChecks.cmake)

View File

@@ -34,7 +34,6 @@
#include "id3v1tag.h"
#include "apetag.h"
using namespace std;
using namespace TagLib;
int main(int argc, char *argv[])
@@ -44,7 +43,7 @@ int main(int argc, char *argv[])
for(int i = 1; i < argc; i++) {
cout << "******************** \"" << argv[i] << "\"********************" << endl;
std::cout << "******************** \"" << argv[i] << "\"********************" << std::endl;
MPEG::File f(argv[i]);
@@ -52,62 +51,62 @@ int main(int argc, char *argv[])
if(id3v2tag) {
cout << "ID3v2."
std::cout << "ID3v2."
<< id3v2tag->header()->majorVersion()
<< "."
<< id3v2tag->header()->revisionNumber()
<< ", "
<< id3v2tag->header()->tagSize()
<< " bytes in tag"
<< endl;
<< std::endl;
const auto &frames = id3v2tag->frameList();
for(auto it = frames.begin(); it != frames.end(); it++) {
cout << (*it)->frameID();
std::cout << (*it)->frameID();
if(auto comment = dynamic_cast<ID3v2::CommentsFrame *>(*it))
if(!comment->description().isEmpty())
cout << " [" << comment->description() << "]";
std::cout << " [" << comment->description() << "]";
cout << " - \"" << (*it)->toString() << "\"" << endl;
std::cout << " - \"" << (*it)->toString() << "\"" << std::endl;
}
}
else
cout << "file does not have a valid id3v2 tag" << endl;
std::cout << "file does not have a valid id3v2 tag" << std::endl;
cout << endl << "ID3v1" << endl;
std::cout << std::endl << "ID3v1" << std::endl;
ID3v1::Tag *id3v1tag = f.ID3v1Tag();
if(id3v1tag) {
cout << "title - \"" << id3v1tag->title() << "\"" << endl;
cout << "artist - \"" << id3v1tag->artist() << "\"" << endl;
cout << "album - \"" << id3v1tag->album() << "\"" << endl;
cout << "year - \"" << id3v1tag->year() << "\"" << endl;
cout << "comment - \"" << id3v1tag->comment() << "\"" << endl;
cout << "track - \"" << id3v1tag->track() << "\"" << endl;
cout << "genre - \"" << id3v1tag->genre() << "\"" << endl;
std::cout << "title - \"" << id3v1tag->title() << "\"" << std::endl;
std::cout << "artist - \"" << id3v1tag->artist() << "\"" << std::endl;
std::cout << "album - \"" << id3v1tag->album() << "\"" << std::endl;
std::cout << "year - \"" << id3v1tag->year() << "\"" << std::endl;
std::cout << "comment - \"" << id3v1tag->comment() << "\"" << std::endl;
std::cout << "track - \"" << id3v1tag->track() << "\"" << std::endl;
std::cout << "genre - \"" << id3v1tag->genre() << "\"" << std::endl;
}
else
cout << "file does not have a valid id3v1 tag" << endl;
std::cout << "file does not have a valid id3v1 tag" << std::endl;
APE::Tag *ape = f.APETag();
cout << endl << "APE" << endl;
std::cout << std::endl << "APE" << std::endl;
if(ape) {
const auto &items = ape->itemListMap();
for(auto it = items.begin(); it != items.end(); ++it)
{
if((*it).second.type() != APE::Item::Binary)
cout << (*it).first << " - \"" << (*it).second.toString() << "\"" << endl;
std::cout << (*it).first << " - \"" << (*it).second.toString() << "\"" << std::endl;
else
cout << (*it).first << " - Binary data (" << (*it).second.binaryData().size() << " bytes)" << endl;
std::cout << (*it).first << " - Binary data (" << (*it).second.binaryData().size() << " bytes)" << std::endl;
}
}
else
cout << "file does not have a valid APE tag" << endl;
std::cout << "file does not have a valid APE tag" << std::endl;
cout << endl;
std::cout << std::endl;
}
}

View File

@@ -32,13 +32,11 @@
#include "fileref.h"
#include "tag.h"
using namespace std;
int main(int argc, char *argv[])
{
for(int i = 1; i < argc; i++) {
cout << "******************** \"" << argv[i] << "\" ********************" << endl;
std::cout << "******************** \"" << argv[i] << "\" ********************" << std::endl;
TagLib::FileRef f(argv[i]);
@@ -46,14 +44,14 @@ int main(int argc, char *argv[])
TagLib::Tag *tag = f.tag();
cout << "-- TAG (basic) --" << endl;
cout << "title - \"" << tag->title() << "\"" << endl;
cout << "artist - \"" << tag->artist() << "\"" << endl;
cout << "album - \"" << tag->album() << "\"" << endl;
cout << "year - \"" << tag->year() << "\"" << endl;
cout << "comment - \"" << tag->comment() << "\"" << endl;
cout << "track - \"" << tag->track() << "\"" << endl;
cout << "genre - \"" << tag->genre() << "\"" << endl;
std::cout << "-- TAG (basic) --" << std::endl;
std::cout << "title - \"" << tag->title() << "\"" << std::endl;
std::cout << "artist - \"" << tag->artist() << "\"" << std::endl;
std::cout << "album - \"" << tag->album() << "\"" << std::endl;
std::cout << "year - \"" << tag->year() << "\"" << std::endl;
std::cout << "comment - \"" << tag->comment() << "\"" << std::endl;
std::cout << "track - \"" << tag->track() << "\"" << std::endl;
std::cout << "genre - \"" << tag->genre() << "\"" << std::endl;
TagLib::PropertyMap tags = f.properties();
if(!tags.isEmpty()) {
@@ -64,10 +62,10 @@ int main(int argc, char *argv[])
}
}
cout << "-- TAG (properties) --" << endl;
std::cout << "-- TAG (properties) --" << std::endl;
for(auto j = tags.cbegin(); j != tags.cend(); ++j) {
for(auto k = j->second.begin(); k != j->second.end(); ++k) {
cout << left << std::setfill(' ') << std::setw(longest) << j->first << " - " << '"' << *k << '"' << endl;
std::cout << std::left << std::setfill(' ') << std::setw(longest) << j->first << " - " << '"' << *k << '"' << std::endl;
}
}
}
@@ -76,13 +74,13 @@ int main(int argc, char *argv[])
for(const auto &name : names) {
const auto& properties = f.complexProperties(name);
for(const auto &property : properties) {
cout << name << ":" << endl;
std::cout << name << ":" << std::endl;
for(const auto &[key, value] : property) {
cout << " " << left << std::setfill(' ') << std::setw(11) << key << " - ";
std::cout << " " << std::left << std::setfill(' ') << std::setw(11) << key << " - ";
if(value.type() == TagLib::Variant::ByteVector) {
cout << "(" << value.value<TagLib::ByteVector>().size() << " bytes)" << endl;
std::cout << "(" << value.value<TagLib::ByteVector>().size() << " bytes)" << std::endl;
/* The picture could be extracted using:
ofstream picture;
std::ofstream picture;
TagLib::String fn(argv[i]);
int slashPos = fn.rfind('/');
int dotPos = fn.rfind('.');
@@ -90,13 +88,13 @@ int main(int argc, char *argv[])
fn = fn.substr(slashPos + 1, dotPos - slashPos - 1);
}
fn += ".jpg";
picture.open(fn.toCString(), ios_base::out | ios_base::binary);
picture.open(fn.toCString(), std::ios_base::out | std::ios_base::binary);
picture << value.value<TagLib::ByteVector>();
picture.close();
*/
}
else {
cout << value << endl;
std::cout << value << std::endl;
}
}
}
@@ -110,12 +108,13 @@ int main(int argc, char *argv[])
int seconds = properties->lengthInSeconds() % 60;
int minutes = (properties->lengthInSeconds() - seconds) / 60;
cout << "-- AUDIO --" << endl;
cout << "bitrate - " << properties->bitrate() << endl;
cout << "sample rate - " << properties->sampleRate() << endl;
cout << "channels - " << properties->channels() << endl;
cout << "length - " << minutes << ":" << setfill('0') << setw(2) << right << seconds << endl;
std::cout << "-- AUDIO --" << std::endl;
std::cout << "bitrate - " << properties->bitrate() << std::endl;
std::cout << "sample rate - " << properties->sampleRate() << std::endl;
std::cout << "channels - " << properties->channels() << std::endl;
std::cout << "length - " << minutes << ":" << std::setfill('0') << std::setw(2) << std::right << seconds << std::endl;
}
}
return 0;
}

View File

@@ -39,8 +39,6 @@
#include "fileref.h"
#include "tag.h"
using namespace std;
bool isArgument(const char *s)
{
return strlen(s) == 2 && s[0] == '-';
@@ -48,32 +46,33 @@ bool isArgument(const char *s)
bool isFile(const char *s)
{
struct stat st;
#ifdef _WIN32
return ::stat(s, &st) == 0 && (st.st_mode & (S_IFREG));
struct _stat64 st;
return ::_stat64(s, &st) == 0 && (st.st_mode & S_IFREG);
#else
struct stat st;
return ::stat(s, &st) == 0 && (st.st_mode & (S_IFREG | S_IFLNK));
#endif
}
void usage()
{
cout << endl;
cout << "Usage: tagwriter <fields> <files>" << endl;
cout << endl;
cout << "Where the valid fields are:" << endl;
cout << " -t <title>" << endl;
cout << " -a <artist>" << endl;
cout << " -A <album>" << endl;
cout << " -c <comment>" << endl;
cout << " -g <genre>" << endl;
cout << " -y <year>" << endl;
cout << " -T <track>" << endl;
cout << " -R <tagname> <tagvalue>" << endl;
cout << " -I <tagname> <tagvalue>" << endl;
cout << " -D <tagname>" << endl;
cout << " -p <picturefile> <description> (\"\" \"\" to remove)" << endl;
cout << endl;
std::cout << std::endl;
std::cout << "Usage: tagwriter <fields> <files>" << std::endl;
std::cout << std::endl;
std::cout << "Where the valid fields are:" << std::endl;
std::cout << " -t <title>" << std::endl;
std::cout << " -a <artist>" << std::endl;
std::cout << " -A <album>" << std::endl;
std::cout << " -c <comment>" << std::endl;
std::cout << " -g <genre>" << std::endl;
std::cout << " -y <year>" << std::endl;
std::cout << " -T <track>" << std::endl;
std::cout << " -R <tagname> <tagvalue>" << std::endl;
std::cout << " -I <tagname> <tagvalue>" << std::endl;
std::cout << " -D <tagname>" << std::endl;
std::cout << " -p <picturefile> <description> (\"\" \"\" to remove)" << std::endl;
std::cout << std::endl;
exit(1);
}
@@ -87,10 +86,10 @@ void checkForRejectedProperties(const TagLib::PropertyMap &tags)
longest = i->first.size();
}
}
cout << "-- rejected TAGs (properties) --" << endl;
std::cout << "-- rejected TAGs (properties) --" << std::endl;
for(auto i = tags.begin(); i != tags.end(); ++i) {
for(auto j = i->second.begin(); j != i->second.end(); ++j) {
cout << left << std::setw(longest) << i->first << " - " << '"' << *j << '"' << endl;
std::cout << std::left << std::setw(longest) << i->first << " - " << '"' << *j << '"' << std::endl;
}
}
}
@@ -176,12 +175,12 @@ int main(int argc, char *argv[])
numArgsConsumed = 3;
if(!value.isEmpty()) {
if(!isFile(value.toCString())) {
cout << value.toCString() << " not found." << endl;
std::cout << value.toCString() << " not found." << std::endl;
return 1;
}
ifstream picture;
picture.open(value.toCString());
stringstream buffer;
std::ifstream picture;
picture.open(value.toCString(), std::ios::in | std::ios::binary);
std::stringstream buffer;
buffer << picture.rdbuf();
picture.close();
TagLib::String buf(buffer.str());

View File

@@ -347,7 +347,7 @@ target_include_directories(tag INTERFACE
)
target_link_libraries(tag
PRIVATE $<$<TARGET_EXISTS:utf8::cpp>:utf8::cpp>
PRIVATE $<IF:$<TARGET_EXISTS:utf8::cpp>,utf8::cpp,$<$<TARGET_EXISTS:utf8cpp>:utf8cpp>>
$<$<TARGET_EXISTS:ZLIB::ZLIB>:ZLIB::ZLIB>
)

View File

@@ -95,13 +95,6 @@ MP4::Atom::Atom(File *file)
}
d->name = header.mid(4, 4);
for(int i = 0; i < 4; ++i) {
if(const char ch = d->name.at(i); (ch < ' ' || ch > '~') && ch != '\251') {
debug("MP4: Invalid atom type");
d->length = 0;
file->seek(0, File::End);
}
}
for(auto c : containers) {
if(d->name == c) {

View File

@@ -35,6 +35,12 @@
using namespace TagLib;
using namespace MP4;
namespace {
constexpr char freeFormPrefix[] = "----:com.apple.iTunes:";
} // namespace
class ItemFactory::ItemFactoryPrivate
{
public:
@@ -231,7 +237,11 @@ String ItemFactory::propertyKeyForName(const ByteVector &name) const
if(d->propertyKeyForName.isEmpty()) {
d->propertyKeyForName = namePropertyMap();
}
return d->propertyKeyForName.value(name);
String key = d->propertyKeyForName.value(name);
if(key.isEmpty() && name.startsWith(freeFormPrefix)) {
key = name.mid(std::size(freeFormPrefix) - 1);
}
return key;
}
ByteVector ItemFactory::nameForPropertyKey(const String &key) const
@@ -244,7 +254,14 @@ ByteVector ItemFactory::nameForPropertyKey(const String &key) const
d->nameForPropertyKey[t] = k;
}
}
return d->nameForPropertyKey.value(key);
ByteVector name = d->nameForPropertyKey.value(key);
if(name.isEmpty() && !key.isEmpty()) {
const auto &firstChar = key[0];
if(firstChar >= 'A' && firstChar <= 'Z') {
name = (freeFormPrefix + key).data(String::UTF8);
}
}
return name;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -843,8 +843,10 @@ void ID3v2::Tag::parse(const ByteVector &origData)
break;
}
Frame *frame = d->factory->createFrame(data.mid(frameDataPosition),
&d->header);
const ByteVector origData = data.mid(frameDataPosition);
const Header *tagHeader = &d->header;
unsigned int headerVersion = tagHeader->majorVersion();
Frame *frame = d->factory->createFrame(origData, tagHeader);
if(!frame)
return;
@@ -856,7 +858,15 @@ void ID3v2::Tag::parse(const ByteVector &origData)
return;
}
frameDataPosition += frame->size() + frame->headerSize();
if(frame->header()->version() == headerVersion) {
frameDataPosition += frame->size() + frame->headerSize();
} else {
// The frame was converted to another version, e.g. from 2.2 to 2.4.
// We must advance the frame data position according to the original
// frame, not the converted frame because its header size might differ.
Frame::Header origHeader(origData, headerVersion);
frameDataPosition += origHeader.frameSize() + origHeader.size();
}
addFrame(frame);
}

View File

@@ -28,7 +28,7 @@
#define TAGLIB_MAJOR_VERSION 2
#define TAGLIB_MINOR_VERSION 0
#define TAGLIB_PATCH_VERSION 1
#define TAGLIB_PATCH_VERSION 2
#if (defined(_MSC_VER) && _MSC_VER >= 1600)
#define TAGLIB_CONSTRUCT_BITSET(x) static_cast<unsigned long long>(x)

BIN
tests/data/itunes10.mp3 Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -101,6 +101,7 @@ class TestMP4 : public CppUnit::TestFixture
CPPUNIT_TEST(testRemoveMetadata);
CPPUNIT_TEST(testNonFullMetaAtom);
CPPUNIT_TEST(testItemFactory);
CPPUNIT_TEST(testNonPrintableAtom);
CPPUNIT_TEST_SUITE_END();
public:
@@ -441,6 +442,7 @@ public:
tags["BPM"] = StringList("123");
tags["ARTIST"] = StringList("Foo Bar");
tags["COMPILATION"] = StringList("1");
tags["REMIXEDBY"] = StringList("Remixed by");
f.setProperties(tags);
tags = f.properties();
@@ -467,6 +469,11 @@ public:
CPPUNIT_ASSERT_EQUAL(true, f.tag()->item("cpil").toBool());
CPPUNIT_ASSERT_EQUAL(StringList("1"), tags["COMPILATION"]);
CPPUNIT_ASSERT(f.tag()->contains("----:com.apple.iTunes:REMIXEDBY"));
CPPUNIT_ASSERT_EQUAL(StringList("Remixed by"),
f.tag()->item("----:com.apple.iTunes:REMIXEDBY").toStringList());
CPPUNIT_ASSERT_EQUAL(StringList("Remixed by"), tags["REMIXEDBY"]);
tags["COMPILATION"] = StringList("0");
f.setProperties(tags);
@@ -847,6 +854,25 @@ public:
CPPUNIT_ASSERT_EQUAL(StringList("456"), properties.value("TESTINTEGER"));
}
}
void testNonPrintableAtom()
{
ScopedFileCopy copy("nonprintable-atom-type", ".m4a");
{
MP4::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels());
CPPUNIT_ASSERT_EQUAL(32000, f.audioProperties()->sampleRate());
f.tag()->setTitle("TITLE");
f.save();
}
{
MP4::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasMP4Tag());
CPPUNIT_ASSERT_EQUAL(String("TITLE"), f.tag()->title());
}
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4);

View File

@@ -71,6 +71,7 @@ class TestMPEG : public CppUnit::TestFixture
CPPUNIT_TEST(testIgnoreGarbage);
CPPUNIT_TEST(testExtendedHeader);
CPPUNIT_TEST(testReadStyleFast);
CPPUNIT_TEST(testID3v22Properties);
CPPUNIT_TEST_SUITE_END();
public:
@@ -618,6 +619,56 @@ public:
}
}
void testID3v22Properties()
{
ScopedFileCopy copy("itunes10", ".mp3");
MPEG::File f(copy.fileName().c_str());
PropertyMap expectedProperties(SimplePropertyMap{
{"ALBUM", {"Album"}},
{"ALBUMARTIST", {"Album Artist"}},
{"ALBUMARTISTSORT", {"Sort Album Artist"}},
{"ALBUMSORT", {"Sort Album"}},
{"ARTIST", {"Artist"}},
{"ARTISTSORT", {"Sort Artist"}},
{"BPM", {"180"}},
{"COMMENT", {"Comments"}},
{"COMMENT:ITUNPGAP", {"1"}},
{"COMPILATION", {"1"}},
{"COMPOSER", {"Composer"}},
{"COMPOSERSORT", {"Sort Composer"}},
{"DATE", {"2011"}},
{"DISCNUMBER", {"1/2"}},
{"GENRE", {"Heavy Metal"}},
{"LYRICS", {"Lyrics"}},
{"SUBTITLE", {"Description"}},
{"TITLE", {"iTunes10MP3"}},
{"TITLESORT", {"Sort Name"}},
{"TRACKNUMBER", {"1/10"}},
{"WORK", {"Grouping"}}
});
expectedProperties.addUnsupportedData("APIC");
expectedProperties.addUnsupportedData("UNKNOWN/RVA");
PropertyMap properties = f.properties();
if (expectedProperties != properties) {
CPPUNIT_ASSERT_EQUAL(expectedProperties.toString(), properties.toString());
}
CPPUNIT_ASSERT(expectedProperties == properties);
const String PICTURE_KEY("PICTURE");
CPPUNIT_ASSERT_EQUAL(StringList(PICTURE_KEY), f.complexPropertyKeys());
auto pictures = f.complexProperties(PICTURE_KEY);
CPPUNIT_ASSERT_EQUAL(1U, pictures.size());
auto picture = pictures.front();
CPPUNIT_ASSERT_EQUAL(String("image/png"), picture.value("mimeType").toString());
CPPUNIT_ASSERT(picture.value("description").toString().isEmpty());
CPPUNIT_ASSERT_EQUAL(String("Other"), picture.value("pictureType").toString());
auto data = picture.value("data").toByteVector();
CPPUNIT_ASSERT(data.startsWith("\x89PNG\x0d\x0a\x1a\x0a"));
CPPUNIT_ASSERT_EQUAL(2315U, data.size());
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestMPEG);