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:
Michael Helmling
2011-08-27 01:18:21 +02:00
parent b262180857
commit 58db919e43
11 changed files with 312 additions and 21 deletions

View File

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

View File

@ -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 &);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@ -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]);
}