Some preliminary work for unified dictionary tag interface support.

- toDict() and fromDict() for XiphComments
- toDict() for ID3v2 Tags
This commit is contained in:
Michael Helmling 2011-08-26 21:48:40 +02:00
parent bec3875b94
commit b262180857
11 changed files with 466 additions and 4 deletions

View File

@ -54,6 +54,7 @@ set(tag_HDRS
mpeg/xingheader.h
mpeg/id3v1/id3v1tag.h
mpeg/id3v1/id3v1genres.h
mpeg/id3v2/id3v2dicttools.h
mpeg/id3v2/id3v2extendedheader.h
mpeg/id3v2/id3v2frame.h
mpeg/id3v2/id3v2header.h
@ -137,6 +138,7 @@ set(id3v1_SRCS
)
set(id3v2_SRCS
mpeg/id3v2/id3v2dicttools.cpp
mpeg/id3v2/id3v2framefactory.cpp
mpeg/id3v2/id3v2synchdata.cpp
mpeg/id3v2/id3v2tag.cpp

View File

@ -0,0 +1,156 @@
/***************************************************************************
copyright : (C) 2011 by Michael Helmling
email : supermihi@web.de
***************************************************************************/
/***************************************************************************
* 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 "tdebug.h"
#include "id3v2dicttools.h"
#include "tmap.h"
namespace TagLib {
namespace ID3v2 {
/*!
* A map of translations frameID <-> tag used by the unified dictionary interface.
*/
static const uint numid3frames = 55;
static const char *id3frames[][2] = {
// Text information frames
{ "TALB", "ALBUM"},
{ "TBPM", "BPM" },
{ "TCOM", "COMPOSER" },
{ "TCON", "GENRE" },
{ "TCOP", "COPYRIGHT" },
{ "TDEN", "ENCODINGTIME" },
{ "TDLY", "PLAYLISTDELAY" },
{ "TDOR", "ORIGINALRELEASETIME" },
{ "TDRC", "DATE" },
// { "TRDA", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4
// { "TDAT", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4
// { "TYER", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4
// { "TIME", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4
{ "TDRL", "RELEASETIME" },
{ "TDTG", "TAGGINGTIME" },
{ "TENC", "ENCODEDBY" },
{ "TEXT", "LYRICIST" },
{ "TFLT", "FILETYPE" },
{ "TIPL", "INVOLVEDPEOPLE" },
{ "TIT1", "CONTENTGROUP" },
{ "TIT2", "TITLE"},
{ "TIT3", "SUBTITLE" },
{ "TKEY", "INITIALKEY" },
{ "TLAN", "LANGUAGE" },
{ "TLEN", "LENGTH" },
{ "TMCL", "MUSICIANCREDITS" },
{ "TMED", "MEDIATYPE" },
{ "TMOO", "MOOD" },
{ "TOAL", "ORIGINALALBUM" },
{ "TOFN", "ORIGINALFILENAME" },
{ "TOLY", "ORIGINALLYRICIST" },
{ "TOPE", "ORIGINALARTIST" },
{ "TOWN", "OWNER" },
{ "TPE1", "ARTIST"},
{ "TPE2", "PERFORMER" },
{ "TPE3", "CONDUCTOR" },
{ "TPE4", "ARRANGER" },
{ "TPOS", "DISCNUMBER" },
{ "TPRO", "PRODUCEDNOTICE" },
{ "TPUB", "PUBLISHER" },
{ "TRCK", "TRACKNUMBER" },
{ "TRSN", "RADIOSTATION" },
{ "TRSO", "RADIOSTATIONOWNER" },
{ "TSOA", "ALBUMSORT" },
{ "TSOP", "ARTISTSORT" },
{ "TSOT", "TITLESORT" },
{ "TSRC", "ISRC" },
{ "TSSE", "ENCODING" },
// URL frames
{ "WCOP", "COPYRIGHTURL" },
{ "WOAF", "FILEWEBPAGE" },
{ "WOAR", "ARTISTWEBPAGE" },
{ "WOAS", "AUDIOSOURCEWEBPAGE" },
{ "WORS", "RADIOSTATIONWEBPAGE" },
{ "WPAY", "PAYMENTWEBPAGE" },
{ "WPUB", "PUBLISHERWEBPAGE" },
{ "WXXX", "URL"},
// 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 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
"PRIV", // private frames
"APIC", // attached picture -- TODO how could we do this?
"POPM", // popularimeter
"RVA2", // relative volume
};
// list of deprecated frames and their successors
static const uint deprecatedFramesSize = 4;
static const char *deprecatedFrames[][2] = {
{"TRDA", "TDRC"}, // 2.3 -> 2.4 (http://en.wikipedia.org/wiki/ID3)
{"TDAT", "TDRC"}, // 2.3 -> 2.4
{"TYER", "TDRC"}, // 2.3 -> 2.4
{"TIME", "TDRC"}, // 2.3 -> 2.4
};
String frameIDToTagName(const ByteVector &id) {
static Map<ByteVector, String> m;
if (m.isEmpty())
for (size_t i = 0; i < numid3frames; ++i)
m[id3frames[i][0]] = id3frames[i][1];
if (m.contains(id))
return m[id];
if (deprecationMap().contains(id))
return m[deprecationMap()[id]];
debug("unknown frame ID: " + id);
return "UNKNOWNID3TAG"; //TODO: implement this nicer
}
bool isIgnored(const ByteVector& id) {
List<ByteVector> ignoredList;
if (ignoredList.isEmpty())
for (uint i = 0; i < ignoredFramesSize; ++i)
ignoredList.append(ignoredFrames[i]);
return ignoredList.contains(id);
}
FrameIDMap deprecationMap() {
static FrameIDMap depMap;
if (depMap.isEmpty())
for(uint i = 0; i < deprecatedFramesSize; ++i)
depMap[deprecatedFrames[i][0]] = deprecatedFrames[i][1];
return depMap;
}
bool isDeprecated(const ByteVector& id) {
return deprecationMap().contains(id);
}
}
}

View File

@ -0,0 +1,54 @@
/***************************************************************************
copyright : (C) 2011 by Michael Helmling
email : supermihi@web.de
***************************************************************************/
/***************************************************************************
* 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 ID3V2DICTTOOLS_H_
#define ID3V2DICTTOOLS_H_
#include "tstringlist.h"
#include "taglib_export.h"
#include "tmap.h"
namespace TagLib {
namespace ID3v2 {
/*!
* This file contains methods used by the unified dictionary interface for ID3v2 tags
* (tag name conversion, handling of un-translatable frameIDs, ...).
*/
typedef Map<ByteVector, ByteVector> FrameIDMap;
String TAGLIB_EXPORT frameIDToTagName(const ByteVector &id);
bool TAGLIB_EXPORT isIgnored(const ByteVector &);
FrameIDMap TAGLIB_EXPORT deprecationMap();
bool TAGLIB_EXPORT isDeprecated(const ByteVector&);
}
}
#endif /* ID3V2DICTTOOLS_H_ */

View File

@ -31,11 +31,15 @@
#include "id3v2extendedheader.h"
#include "id3v2footer.h"
#include "id3v2synchdata.h"
#include "id3v2dicttools.h"
#include "tbytevector.h"
#include "id3v1genres.h"
#include "frames/textidentificationframe.h"
#include "frames/commentsframe.h"
#include "frames/urllinkframe.h"
#include "frames/uniquefileidentifierframe.h"
#include "frames/unsynchronizedlyricsframe.h"
using namespace TagLib;
using namespace ID3v2;
@ -324,9 +328,115 @@ void ID3v2::Tag::removeFrame(Frame *frame, bool del)
void ID3v2::Tag::removeFrames(const ByteVector &id)
{
FrameList l = d->frameListMap[id];
for(FrameList::Iterator it = l.begin(); it != l.end(); ++it)
removeFrame(*it, true);
FrameList l = d->frameListMap[id];
for(FrameList::Iterator it = l.begin(); it != l.end(); ++it)
removeFrame(*it, true);
}
TagDict ID3v2::Tag::toDict() const
{
TagDict dict;
FrameList::ConstIterator frameIt = frameList().begin();
for (; frameIt != frameList().end(); ++frameIt) {
ByteVector id = (*frameIt)->frameID();
if (isIgnored(id)) {
debug("found ignored id3 frame " + id);
continue;
}
if (isDeprecated(id)) {
debug("found deprecated id3 frame " + id);
continue;
}
if (id[0] == 'T') {
if (id == "TXXX") {
const UserTextIdentificationFrame *uframe
= dynamic_cast< const UserTextIdentificationFrame* >(*frameIt);
String tagName = uframe->description();
StringList l(uframe->fieldList());
// this is done because taglib stores the description also as first entry
// in the field list. (why?)
//
if (l.contains(tagName))
l.erase(l.find(tagName));
// handle user text frames set by the QuodLibet / exFalso package,
// which sets the description to QuodLibet::<tagName> instead of simply
// <tagName>.
int pos = tagName.find("::");
tagName = (pos != -1) ? tagName.substr(pos+2) : tagName;
dict[tagName.upper()].append(l);
}
else {
const TextIdentificationFrame* tframe
= dynamic_cast< const TextIdentificationFrame* >(*frameIt);
String tagName = frameIDToTagName(id);
StringList l = tframe->fieldList();
if (tagName == "GENRE") {
// Special case: Support ID3v1-style genre numbers. They are not officially supported in
// ID3v2, however it seems that still a lot of programs use them.
//
for (StringList::Iterator lit = l.begin(); lit != l.end(); ++lit) {
bool ok = false;
int test = lit->toInt(&ok); // test if the genre value is an integer
if (ok) {
*lit = ID3v1::genre(test);
}
}
}
else if (tagName == "DATE") {
for (StringList::Iterator lit = l.begin(); lit != l.end(); ++lit) {
// ID3v2 specifies ISO8601 timestamps which contain a 'T' as separator between date and time.
// Since this is unusual in other formats, the T is removed.
//
int tpos = lit->find("T");
if (tpos != -1)
(*lit)[tpos] = ' ';
}
}
dict[tagName].append(l);
}
continue;
}
if (id[0] == 'W') {
if (id == "WXXX") {
const UserUrlLinkFrame *uframe = dynamic_cast< const UserUrlLinkFrame* >(*frameIt);
String tagname = uframe->description().upper();
if (tagname == "")
tagname = "URL";
dict[tagname].append(uframe->url());
}
else {
const UrlLinkFrame* uframe = dynamic_cast< const UrlLinkFrame* >(*frameIt);
dict[frameIDToTagName(id)].append(uframe->url());
}
continue;
}
if (id == "COMM") {
const CommentsFrame *cframe = dynamic_cast< const CommentsFrame* >(*frameIt);
String tagName = cframe->description().upper();
if (tagName.isEmpty())
tagName = "COMMENT";
dict[tagName].append(cframe->text());
continue;
}
if (id == "USLT") {
const UnsynchronizedLyricsFrame *uframe
= dynamic_cast< const UnsynchronizedLyricsFrame* >(*frameIt);
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;
}
ByteVector ID3v2::Tag::render() const

View File

@ -260,6 +260,16 @@ namespace TagLib {
*/
void removeFrames(const ByteVector &id);
/*!
* Implements the unified tag dictionary interface -- export function.
*/
TagDict toDict() const;
/*!
* Implements the unified tag dictionary interface -- import function.
*/
void fromDict(const TagDict &);
/*!
* Render the tag back to binary data, suitable to be written to disk.
*/

View File

@ -188,6 +188,47 @@ const Ogg::FieldListMap &Ogg::XiphComment::fieldListMap() const
return d->fieldListMap;
}
TagDict Ogg::XiphComment::toDict() const
{
return d->fieldListMap;
}
void Ogg::XiphComment::fromDict(const TagDict &tagDict)
{
// check which keys are to be deleted
StringList toRemove;
FieldListMap::ConstIterator it = d->fieldListMap.begin();
for(; it != d->fieldListMap.end(); ++it) {
if (!tagDict.contains(it->first))
toRemove.append(it->first);
}
StringList::ConstIterator removeIt = toRemove.begin();
for (; removeIt != toRemove.end(); ++removeIt)
removeField(*removeIt);
/* now go through keys in tagDict and check that the values match those in the xiph comment */
TagDict::ConstIterator tagIt = tagDict.begin();
for (; tagIt != tagDict.end(); ++tagIt)
{
if (!d->fieldListMap.contains(tagIt->first) || !(tagIt->second == d->fieldListMap[tagIt->first])) {
const StringList &sl = tagIt->second;
if(sl.size() == 0) {
// zero size string list -> remove the tag with all values
removeField(tagIt->first);
}
else {
// replace all strings in the list for the tag
StringList::ConstIterator valueIterator = sl.begin();
addField(tagIt->first, *valueIterator, true);
++valueIterator;
for(; valueIterator != sl.end(); ++valueIterator)
addField(tagIt->first, *valueIterator, false);
}
}
}
}
String Ogg::XiphComment::vendorID() const
{
return d->vendorID;

View File

@ -140,6 +140,16 @@ namespace TagLib {
*/
const FieldListMap &fieldListMap() const;
/*!
* Implements the unified tag dictionary interface -- export function.
*/
TagDict toDict() const;
/*!
* Implements the unified tag dictionary interface -- import function.
*/
void fromDict(const TagDict &);
/*!
* Returns the vendor ID of the Ogg Vorbis encoder. libvorbis 1.0 as the
* most common case always returns "Xiph.Org libVorbis I 20020717".

View File

@ -28,9 +28,17 @@
#include "taglib_export.h"
#include "tstring.h"
#include "tmap.h"
namespace TagLib {
/*!
* This is used for the unified dictionary interface: the tags of a file are
* represented as a dictionary mapping a string (the tag name) to a list of
* strings (the values).
*/
typedef Map<String, StringList> TagDict;
//! A simple, generic interface to common audio meta data fields
/*!

BIN
tests/data/test.ogg Normal file

Binary file not shown.

View File

@ -67,6 +67,7 @@ class TestID3v2 : public CppUnit::TestFixture
CPPUNIT_TEST(testDowngradeTo23);
// CPPUNIT_TEST(testUpdateFullDate22); TODO TYE+TDA should be upgraded to TDRC together
CPPUNIT_TEST(testCompressedFrameWithBrokenLength);
CPPUNIT_TEST(testDictInterface);
CPPUNIT_TEST_SUITE_END();
public:
@ -547,6 +548,29 @@ public:
CPPUNIT_ASSERT_EQUAL(TagLib::uint(86414), frame->picture().size());
}
void testDictInterface()
{
ScopedFileCopy copy("rare_frames", ".mp3");
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(String("userTextData1"), dict["USERTEXTDESCRIPTION1"][0]);
CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION1"][1]);
CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION2"][0]);
CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION2"][1]);
CPPUNIT_ASSERT_EQUAL(String("Pop"), dict["GENRE"][0]);
CPPUNIT_ASSERT_EQUAL(String("Pop"), dict["GENRE"][0]);
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]);
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v2);

View File

@ -17,6 +17,8 @@ class TestOGG : public CppUnit::TestFixture
CPPUNIT_TEST_SUITE(TestOGG);
CPPUNIT_TEST(testSimple);
CPPUNIT_TEST(testSplitPackets);
CPPUNIT_TEST(testDictInterface1);
CPPUNIT_TEST(testDictInterface2);
CPPUNIT_TEST_SUITE_END();
public:
@ -51,6 +53,51 @@ public:
delete f;
}
void testDictInterface1()
{
ScopedFileCopy copy("empty", ".ogg");
string newname = copy.fileName();
Vorbis::File *f = new Vorbis::File(newname.c_str());
CPPUNIT_ASSERT_EQUAL(uint(0), f->tag()->toDict().size());
TagDict newTags;
StringList values("value 1");
values.append("value 2");
newTags["ARTIST"] = values;
f->tag()->fromDict(newTags);
TagDict map = f->tag()->toDict();
CPPUNIT_ASSERT_EQUAL(uint(1), map.size());
CPPUNIT_ASSERT_EQUAL(uint(2), map["ARTIST"].size());
CPPUNIT_ASSERT_EQUAL(String("value 1"), map["ARTIST"][0]);
delete f;
}
void testDictInterface2()
{
ScopedFileCopy copy("test", ".ogg");
string newname = copy.fileName();
Vorbis::File *f = new Vorbis::File(newname.c_str());
TagDict tags = f->tag()->toDict();
CPPUNIT_ASSERT_EQUAL(uint(2), tags["UNUSUALTAG"].size());
CPPUNIT_ASSERT_EQUAL(String("usual value"), tags["UNUSUALTAG"][0]);
CPPUNIT_ASSERT_EQUAL(String("another value"), tags["UNUSUALTAG"][1]);
CPPUNIT_ASSERT_EQUAL(String(L"öäüoΣø"), tags["UNICODETAG"][0]);
tags["UNICODETAG"][0] = L"νεω ναλυε";
tags.erase("UNUSUALTAG");
f->tag()->fromDict(tags);
CPPUNIT_ASSERT_EQUAL(String(L"νεω ναλυε"), f->tag()->toDict()["UNICODETAG"][0]);
CPPUNIT_ASSERT_EQUAL(false, f->tag()->toDict().contains("UNUSUALTAG"));
delete f;
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestOGG);