mirror of
https://github.com/taglib/taglib.git
synced 2025-07-14 02:54:27 -04:00
More support for the unified dictionary interface.
Addded fromDict() function to ID3v2Tag. Added fromDict() and toDict() functions to the TagUnion class (uses the first non-empty tag). Added fromDict() and toDict() functions for the generic Tag class, only handling common tags without duplicates. Addded preliminary mp3 test case. Python3 bindings now available on my github site.
This commit is contained in:
@ -31,7 +31,7 @@ namespace TagLib {
|
||||
/*!
|
||||
* A map of translations frameID <-> tag used by the unified dictionary interface.
|
||||
*/
|
||||
static const uint numid3frames = 55;
|
||||
static const uint numid3frames = 54;
|
||||
static const char *id3frames[][2] = {
|
||||
// Text information frames
|
||||
{ "TALB", "ALBUM"},
|
||||
@ -96,11 +96,10 @@ namespace TagLib {
|
||||
// Other frames
|
||||
{ "COMM", "COMMENT" },
|
||||
{ "USLT", "LYRICS" },
|
||||
{ "UFID", "UNIQUEIDENTIFIER" },
|
||||
};
|
||||
|
||||
// list of frameIDs that are ignored by the unified dictionary interface
|
||||
static const uint ignoredFramesSize = 6;
|
||||
static const uint ignoredFramesSize = 7;
|
||||
static const char *ignoredFrames[] = {
|
||||
"TCMP", // illegal 'Part of Compilation' frame set by iTunes (see http://www.id3.org/Compliance_Issues)
|
||||
"GEOB", // no way to handle a general encapsulated object by the dict interface
|
||||
@ -108,6 +107,7 @@ namespace TagLib {
|
||||
"APIC", // attached picture -- TODO how could we do this?
|
||||
"POPM", // popularimeter
|
||||
"RVA2", // relative volume
|
||||
"UFID", // unique file identifier
|
||||
};
|
||||
|
||||
// list of deprecated frames and their successors
|
||||
@ -133,6 +133,16 @@ namespace TagLib {
|
||||
return "UNKNOWNID3TAG"; //TODO: implement this nicer
|
||||
}
|
||||
|
||||
ByteVector tagNameToFrameID(const String &s) {
|
||||
static Map<String, ByteVector> m;
|
||||
if (m.isEmpty())
|
||||
for (size_t i = 0; i < numid3frames; ++i)
|
||||
m[id3frames[i][1]] = id3frames[i][0];
|
||||
if (m.contains(s.upper()))
|
||||
return m[s];
|
||||
return "TXXX";
|
||||
}
|
||||
|
||||
bool isIgnored(const ByteVector& id) {
|
||||
List<ByteVector> ignoredList;
|
||||
if (ignoredList.isEmpty())
|
||||
|
@ -38,7 +38,9 @@ namespace TagLib {
|
||||
*/
|
||||
typedef Map<ByteVector, ByteVector> FrameIDMap;
|
||||
|
||||
String TAGLIB_EXPORT frameIDToTagName(const ByteVector &id);
|
||||
ByteVector TAGLIB_EXPORT tagNameToFrameID(const String &);
|
||||
|
||||
String TAGLIB_EXPORT frameIDToTagName(const ByteVector &);
|
||||
|
||||
bool TAGLIB_EXPORT isIgnored(const ByteVector &);
|
||||
|
||||
|
@ -425,20 +425,197 @@ TagDict ID3v2::Tag::toDict() const
|
||||
dict["LYRICS"].append(uframe->text());
|
||||
continue;
|
||||
}
|
||||
if (id == "UFID") {
|
||||
const UniqueFileIdentifierFrame *uframe
|
||||
= dynamic_cast< const UniqueFileIdentifierFrame* >(*frameIt);
|
||||
String value = uframe->identifier();
|
||||
if (!uframe->owner().isEmpty())
|
||||
value.append(" [" + uframe->owner() + "]");
|
||||
dict["UNIQUEIDENTIFIER"].append(value);
|
||||
continue;
|
||||
}
|
||||
debug("unknown frame ID: " + id);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
void ID3v2::Tag::fromDict(const TagDict &dict)
|
||||
{
|
||||
FrameList toRemove;
|
||||
// first record what frames to remove; we do not remove in-place
|
||||
// because that would invalidate FrameListMap iterators.
|
||||
//
|
||||
for (FrameListMap::ConstIterator it = frameListMap().begin(); it != frameListMap().end(); ++it) {
|
||||
if (it->second.size() == 0) // ignore empty map entries (does this ever happen?)
|
||||
continue;
|
||||
if (isDeprecated(it->first))// automatically remove deprecated frames
|
||||
toRemove.append(it->second);
|
||||
else if (it->first == "TXXX") { // handle user text frames specially
|
||||
for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
|
||||
UserTextIdentificationFrame* frame
|
||||
= dynamic_cast< UserTextIdentificationFrame* >(*fit);
|
||||
String tagName = frame->description();
|
||||
int pos = tagName.find("::");
|
||||
tagName = (pos == -1) ? tagName : tagName.substr(pos+2);
|
||||
if (!dict.contains(tagName.upper()))
|
||||
toRemove.append(frame);
|
||||
}
|
||||
}
|
||||
else if (it->first == "WXXX") { // handle user URL frames specially
|
||||
for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
|
||||
UserUrlLinkFrame* frame = dynamic_cast<ID3v2::UserUrlLinkFrame* >(*fit);
|
||||
String tagName = frame->description().upper();
|
||||
if (!(tagName == "URL") || !dict.contains("URL") || dict["URL"].size() > 1)
|
||||
toRemove.append(frame);
|
||||
}
|
||||
}
|
||||
else if (it->first == "COMM") {
|
||||
for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
|
||||
CommentsFrame* frame = dynamic_cast< CommentsFrame* >(*fit);
|
||||
String tagName = frame->description().upper();
|
||||
// policy: use comment frame only with empty description and only if a comment tag
|
||||
// is present in the dictionary and only if there's no more than one comment
|
||||
// (COMM is not specified for multiple values)
|
||||
if ( !(tagName == "") || !dict.contains("COMMENT") || dict["COMMENT"].size() > 1)
|
||||
toRemove.append(frame);
|
||||
}
|
||||
}
|
||||
else if (it->first == "USLT") {
|
||||
for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
|
||||
UnsynchronizedLyricsFrame *frame
|
||||
= dynamic_cast< UnsynchronizedLyricsFrame* >(*fit);
|
||||
String tagName = frame->description().upper();
|
||||
if ( !(tagName == "") || !dict.contains("LYRICS") || dict["LYRICS"].size() > 1)
|
||||
toRemove.append(frame);
|
||||
}
|
||||
}
|
||||
else if (it->first[0] == 'T') { // a normal text frame
|
||||
if (!dict.contains(frameIDToTagName(it->first)))
|
||||
toRemove.append(it->second);
|
||||
|
||||
} else
|
||||
debug("file contains unknown tag" + it->first + ", not touching it...");
|
||||
}
|
||||
|
||||
// now remove the frames that have been determined above
|
||||
for (FrameList::ConstIterator it = toRemove.begin(); it != toRemove.end(); it++)
|
||||
removeFrame(*it);
|
||||
|
||||
// now sync in the "forward direction"
|
||||
for (TagDict::ConstIterator it = dict.begin(); it != dict.end(); ++it) {
|
||||
const String &tagName = it->first;
|
||||
ByteVector id = tagNameToFrameID(tagName);
|
||||
if (id[0] == 'T' && id != "TXXX") {
|
||||
// the easiest case: a normal text frame
|
||||
StringList values = it->second;
|
||||
const FrameList &framelist = frameList(id);
|
||||
if (tagName == "DATE") {
|
||||
// Handle ISO8601 date format (see above)
|
||||
for (StringList::Iterator lit = values.begin(); lit != values.end(); ++lit) {
|
||||
if (lit->length() > 10 && (*lit)[10] == ' ')
|
||||
(*lit)[10] = 'T';
|
||||
}
|
||||
}
|
||||
if (framelist.size() > 0) { // there exists already a frame for this tag
|
||||
const TextIdentificationFrame *frame = dynamic_cast<const TextIdentificationFrame *>(framelist[0]);
|
||||
if (values == frame->fieldList())
|
||||
continue; // equal tag values -> everything ok
|
||||
}
|
||||
// if there was no frame for this tag, or there was one but the values aren't equal,
|
||||
// we start from scratch and create a new one
|
||||
//
|
||||
removeFrames(id);
|
||||
TextIdentificationFrame *frame = new TextIdentificationFrame(id);
|
||||
frame->setText(values);
|
||||
addFrame(frame);
|
||||
}
|
||||
else if (id == "TXXX" ||
|
||||
((id == "WXXX" || id == "COMM" || id == "USLT") && it->second.size() > 1)) {
|
||||
// In all those cases, we store the tag as TXXX frame.
|
||||
// First we search for existing TXXX frames with correct description
|
||||
FrameList existingFrames;
|
||||
FrameList l = frameList("TXXX");
|
||||
|
||||
for (FrameList::ConstIterator fit = l.begin(); fit != l.end(); fit++) {
|
||||
String desc= dynamic_cast< UserTextIdentificationFrame* >(*fit)->description();
|
||||
int pos = desc.find("::");
|
||||
String tagName = (pos == -1) ? desc.upper() : desc.substr(pos+2).upper();
|
||||
if (tagName == it->first)
|
||||
existingFrames.append(*fit);
|
||||
}
|
||||
|
||||
bool needsInsert = false;
|
||||
if (existingFrames.size() > 1) { //several tags with same key, remove all and reinsert
|
||||
for (FrameList::ConstIterator it = existingFrames.begin(); it != existingFrames.end(); ++it)
|
||||
removeFrame(*it);
|
||||
needsInsert = true;
|
||||
}
|
||||
else if (existingFrames.isEmpty()) // no frame -> needs insert
|
||||
needsInsert = true;
|
||||
else {
|
||||
if (!(dynamic_cast< UserTextIdentificationFrame*>(existingFrames[0])->fieldList() == it->second)) {
|
||||
needsInsert = true;
|
||||
removeFrame(existingFrames[0]);
|
||||
}
|
||||
}
|
||||
if (needsInsert) { // create and insert new frame
|
||||
UserTextIdentificationFrame* frame = new UserTextIdentificationFrame();
|
||||
frame->setDescription(it->first);
|
||||
frame->setText(it->second);
|
||||
addFrame(frame);
|
||||
}
|
||||
}
|
||||
else if (id == "WXXX") {
|
||||
// we know that it->second.size()==1, since the other cases are handled above
|
||||
bool needsInsert = true;
|
||||
FrameList existingFrames = frameList(id);
|
||||
if (existingFrames.size() > 1 ) // do not allow several WXXX frames
|
||||
removeFrames(id);
|
||||
else if (existingFrames.size() == 1) {
|
||||
needsInsert = !(dynamic_cast< UserUrlLinkFrame* >(existingFrames[0])->url() == it->second[0]);
|
||||
if (needsInsert)
|
||||
removeFrames(id);
|
||||
}
|
||||
if (needsInsert) {
|
||||
UserUrlLinkFrame* frame = new ID3v2::UserUrlLinkFrame();
|
||||
frame->setDescription(it->first);
|
||||
frame->setUrl(it->second[0]);
|
||||
addFrame(frame);
|
||||
}
|
||||
}
|
||||
else if (id == "COMM") {
|
||||
FrameList existingFrames = frameList(id);
|
||||
bool needsInsert = true;
|
||||
if (existingFrames.size() > 1) // do not allow several COMM frames
|
||||
removeFrames(id);
|
||||
else if (existingFrames.size() == 1) {
|
||||
needsInsert = !(dynamic_cast< CommentsFrame* >(existingFrames[0])->text() == it->second[0]);
|
||||
if (needsInsert)
|
||||
removeFrames(id);
|
||||
}
|
||||
|
||||
if (needsInsert) {
|
||||
CommentsFrame* frame = new CommentsFrame();
|
||||
frame->setDescription(""); // most software players use empty description COMM frames for comments
|
||||
frame->setText(it->second[0]);
|
||||
addFrame(frame);
|
||||
}
|
||||
}
|
||||
else if (id == "USLT") {
|
||||
FrameList existingFrames = frameList(id);
|
||||
bool needsInsert = true;
|
||||
if (existingFrames.size() > 1) // do not allow several USLT frames
|
||||
removeFrames(id);
|
||||
else if (existingFrames.size() == 1) {
|
||||
needsInsert = !(dynamic_cast< UnsynchronizedLyricsFrame* >(existingFrames[0])->text() == it->second[0]);
|
||||
if (needsInsert)
|
||||
removeFrames(id);
|
||||
}
|
||||
|
||||
if (needsInsert) {
|
||||
UnsynchronizedLyricsFrame* frame = new UnsynchronizedLyricsFrame();
|
||||
frame->setDescription("");
|
||||
frame->setText(it->second[0]);
|
||||
addFrame(frame);
|
||||
}
|
||||
}
|
||||
else
|
||||
debug("ERROR: Don't know how to translate tag " + it->first + " to ID3v2!");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ByteVector ID3v2::Tag::render() const
|
||||
{
|
||||
return render(4);
|
||||
|
@ -263,12 +263,12 @@ namespace TagLib {
|
||||
/*!
|
||||
* Implements the unified tag dictionary interface -- export function.
|
||||
*/
|
||||
TagDict toDict() const;
|
||||
virtual TagDict toDict() const;
|
||||
|
||||
/*!
|
||||
* Implements the unified tag dictionary interface -- import function.
|
||||
*/
|
||||
void fromDict(const TagDict &);
|
||||
virtual void fromDict(const TagDict &);
|
||||
|
||||
/*!
|
||||
* Render the tag back to binary data, suitable to be written to disk.
|
||||
|
@ -143,12 +143,12 @@ namespace TagLib {
|
||||
/*!
|
||||
* Implements the unified tag dictionary interface -- export function.
|
||||
*/
|
||||
TagDict toDict() const;
|
||||
virtual TagDict toDict() const;
|
||||
|
||||
/*!
|
||||
* Implements the unified tag dictionary interface -- import function.
|
||||
*/
|
||||
void fromDict(const TagDict &);
|
||||
virtual void fromDict(const TagDict &);
|
||||
|
||||
/*!
|
||||
* Returns the vendor ID of the Ogg Vorbis encoder. libvorbis 1.0 as the
|
||||
|
@ -24,7 +24,7 @@
|
||||
***************************************************************************/
|
||||
|
||||
#include "tag.h"
|
||||
|
||||
#include "tstringlist.h"
|
||||
using namespace TagLib;
|
||||
|
||||
class Tag::TagPrivate
|
||||
@ -53,6 +53,75 @@ bool Tag::isEmpty() const
|
||||
track() == 0);
|
||||
}
|
||||
|
||||
TagDict Tag::toDict() const
|
||||
{
|
||||
TagDict dict;
|
||||
if (!(title() == String::null))
|
||||
dict["TITLE"].append(title());
|
||||
if (!(artist() == String::null))
|
||||
dict["ARTIST"].append(artist());
|
||||
if (!(album() == String::null))
|
||||
dict["ALBUM"].append(album());
|
||||
if (!(comment() == String::null))
|
||||
dict["COMMENT"].append(comment());
|
||||
if (!(genre() == String::null))
|
||||
dict["GENRE"].append(genre());
|
||||
if (!(year() == 0))
|
||||
dict["DATE"].append(String::number(year()));
|
||||
if (!(track() == 0))
|
||||
dict["TRACKNUMBER"].append(String::number(track()));
|
||||
return dict;
|
||||
}
|
||||
|
||||
void Tag::fromDict(const TagDict &dict)
|
||||
{
|
||||
if (dict.contains("TITLE") and dict["TITLE"].size() >= 1)
|
||||
setTitle(dict["TITLE"].front());
|
||||
else
|
||||
setTitle(String::null);
|
||||
|
||||
if (dict.contains("ARTIST") and dict["ARTIST"].size() >= 1)
|
||||
setArtist(dict["ARTIST"].front());
|
||||
else
|
||||
setArtist(String::null);
|
||||
|
||||
if (dict.contains("ALBUM") and dict["ALBUM"].size() >= 1)
|
||||
setAlbum(dict["ALBUM"].front());
|
||||
else
|
||||
setAlbum(String::null);
|
||||
|
||||
if (dict.contains("COMMENT") and dict["COMMENT"].size() >= 1)
|
||||
setComment(dict["COMMENT"].front());
|
||||
else
|
||||
setComment(String::null);
|
||||
|
||||
if (dict.contains("GENRE") and dict["GENRE"].size() >=1)
|
||||
setGenre(dict["GENRE"].front());
|
||||
else
|
||||
setGenre(String::null);
|
||||
|
||||
if (dict.contains("DATE") and dict["DATE"].size() >= 1) {
|
||||
bool ok;
|
||||
int date = dict["DATE"].front().toInt(&ok);
|
||||
if (ok)
|
||||
setYear(date);
|
||||
else
|
||||
setYear(0);
|
||||
}
|
||||
else
|
||||
setYear(0);
|
||||
|
||||
if (dict.contains("TRACKNUMBER") and dict["TRACKNUMBER"].size() >= 1) {
|
||||
bool ok;
|
||||
int track = dict["TRACKNUMBER"].front().toInt(&ok);
|
||||
if (ok)
|
||||
setTrack(track);
|
||||
else
|
||||
setTrack(0);
|
||||
}
|
||||
else
|
||||
setYear(0);
|
||||
}
|
||||
void Tag::duplicate(const Tag *source, Tag *target, bool overwrite) // static
|
||||
{
|
||||
if(overwrite) {
|
||||
|
16
taglib/tag.h
16
taglib/tag.h
@ -58,6 +58,22 @@ namespace TagLib {
|
||||
*/
|
||||
virtual ~Tag();
|
||||
|
||||
/*!
|
||||
* Unified tag dictionary interface -- export function. Converts the tags
|
||||
* of the specific metadata format into a "human-readable" map of strings
|
||||
* to lists of strings, being as precise as possible.
|
||||
*/
|
||||
virtual TagDict toDict() const;
|
||||
|
||||
/*!
|
||||
* Unified tag dictionary interface -- import function. Converts a map
|
||||
* of strings to stringslists into the specific metadata format. Note that
|
||||
* not all formats can store arbitrary tags and values, so data might
|
||||
* be lost by this operation. Especially the default implementation handles
|
||||
* only single values of the default tags specified in this class.
|
||||
*/
|
||||
virtual void fromDict(const TagDict &);
|
||||
|
||||
/*!
|
||||
* Returns the track name; if no track name is present in the tag
|
||||
* String::null will be returned.
|
||||
|
@ -24,6 +24,7 @@
|
||||
***************************************************************************/
|
||||
|
||||
#include "tagunion.h"
|
||||
#include "tstringlist.h"
|
||||
|
||||
using namespace TagLib;
|
||||
|
||||
@ -170,6 +171,21 @@ void TagUnion::setTrack(uint i)
|
||||
{
|
||||
setUnion(Track, i);
|
||||
}
|
||||
TagDict TagUnion::toDict() const
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (d->tags[i])
|
||||
return d->tags[i]->toDict();
|
||||
TagDict dict;
|
||||
return dict;
|
||||
}
|
||||
|
||||
void TagUnion::fromDict(const TagDict &dict)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (d->tags[i])
|
||||
d->tags[i]->fromDict(dict);
|
||||
}
|
||||
|
||||
bool TagUnion::isEmpty() const
|
||||
{
|
||||
|
@ -73,6 +73,9 @@ namespace TagLib {
|
||||
virtual void setTrack(uint i);
|
||||
virtual bool isEmpty() const;
|
||||
|
||||
virtual TagDict toDict() const;
|
||||
virtual void fromDict(const TagDict &);
|
||||
|
||||
template <class T> T *access(int index, bool create)
|
||||
{
|
||||
if(!create || tag(index))
|
||||
|
BIN
tests/data/rare_frames.mp3
Normal file
BIN
tests/data/rare_frames.mp3
Normal file
Binary file not shown.
@ -554,7 +554,7 @@ public:
|
||||
string newname = copy.fileName();
|
||||
MPEG::File f(newname.c_str());
|
||||
TagDict dict = f.ID3v2Tag(false)->toDict();
|
||||
CPPUNIT_ASSERT_EQUAL(uint(7), dict.size());
|
||||
CPPUNIT_ASSERT_EQUAL(uint(6), dict.size());
|
||||
CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION1"][0]);
|
||||
CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION1"][1]);
|
||||
CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION2"][0]);
|
||||
@ -566,8 +566,6 @@ public:
|
||||
CPPUNIT_ASSERT_EQUAL(String("http://a.user.url"), dict["USERURL"][0]);
|
||||
CPPUNIT_ASSERT_EQUAL(String("http://a.user.url/with/empty/description"), dict["URL"][0]);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(String("12345678 [supermihi@web.de]"), dict["UNIQUEIDENTIFIER"][0]);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(String("A COMMENT"), dict["COMMENT"][0]);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user