From 2025c4e9e528c55e827b9648d1388476ce6ce322 Mon Sep 17 00:00:00 2001 From: Allan Sandfeld Jensen Date: Mon, 26 Jul 2004 12:58:59 +0000 Subject: [PATCH] Large update: * Give access to APEv2 features like lists and read-only fields (not enforced by TagLib). * Support one common mistake: ID3v1 tags placed after an APE-tag. In this case both tags are now read and maintained. APE-tags after an ID3v1 tag is not supported. git-svn-id: svn://anonsvn.kde.org/home/kde/trunk/kdesupport/taglib@332903 283d02a7-25f6-0310-bc7c-ecb5cbfe19da --- mpc/apetag.cpp | 261 ++++++++++++++++++++++++------------------------ mpc/apetag.h | 44 +++++++- mpc/mpcfile.cpp | 119 ++++++++++++++++------ mpc/mpcfile.h | 54 +++++++++- mpc/mpctag.h | 175 ++++++++++++++++++++++++++++++++ 5 files changed, 485 insertions(+), 168 deletions(-) create mode 100644 mpc/mpctag.h diff --git a/mpc/apetag.cpp b/mpc/apetag.cpp index 83362a2b..551540a1 100644 --- a/mpc/apetag.cpp +++ b/mpc/apetag.cpp @@ -38,24 +38,22 @@ public: long tagOffset; long tagLength; - Map items; - Map unknowns; + FieldListMap fieldListMap; + Map binaries; }; -/* -struct APE::Tag::Item -{ - Item(String key) : key(key), type(STRING), value.str(String::null), readOnly(false) {}; - const String key; - enum Type{ STRING, BINARY, URL, RESERVED } type; - union value{ - String str; - ByteVector bin; - } - bool readOnly; +APE::Item::Item(const String& str) : readOnly(false), locator(false) { + value.append(str); +} + +APE::Item::Item(const StringList& values) : readOnly(false), locator(false) { + value.append(values); } -*/ +bool APE::Item::isEmpty() const +{ + return value.isEmpty(); +} //////////////////////////////////////////////////////////////////////////////// // public methods @@ -80,22 +78,35 @@ APE::Tag::~Tag() delete d; } -static ByteVector APEItem(String key, String value) +using TagLib::uint; + +static ByteVector _render_APEItem(String key, Item item) { ByteVector data; - TagLib::uint flags = 0; + uint flags = ((item.readOnly) ? 1 : 0) | ((item.locator) ? 2 : 0); + ByteVector value; + + if (item.value.isEmpty()) return data; + + StringList::Iterator it = item.value.begin(); + value.append(it->data(String::UTF8)); + it++; + while (it != item.value.end()) { + value.append('\0'); + value.append(it->data(String::UTF8)); + it++; + } data.append(ByteVector::fromUInt(value.size(), false)); data.append(ByteVector::fromUInt(flags, false)); data.append(key.data(String::UTF8)); - data.append(char(0)); - data.append(value.data(String::UTF8)); + data.append(ByteVector('\0')); + data.append(value); return data; } -static ByteVector APEFrame(bool isHeader, TagLib::uint dataSize, TagLib::uint itemCount) -{ +static ByteVector _render_APEFrame(bool isHeader, uint dataSize, uint itemCount) { ByteVector header; TagLib::uint tagSize = 32 + dataSize; // bit 31: Has a header @@ -117,32 +128,30 @@ ByteVector APE::Tag::render() const ByteVector data; uint itemCount = 0; - { - Map::Iterator i = d->items.begin(); - while(i != d->items.end()) { - if(!i->second.isEmpty()) { - data.append(APEItem(i->first, i->second)); + { Map::Iterator i = d->fieldListMap.begin(); + while (i != d->fieldListMap.end()) { + if (!i->second.value.isEmpty()) { + data.append(_render_APEItem(i->first, i->second)); itemCount++; } i++; } } - { - Map::Iterator i = d->unknowns.begin(); - while(i != d->unknowns.end()) { - if(!i->second.isEmpty()) { - data.append(i->second); - itemCount++; + { Map::Iterator i = d->binaries.begin(); + while (i != d->binaries.end()) { + if (!i->second.isEmpty()) { + data.append(i->second); + itemCount++; } i++; } } ByteVector tag; - tag.append(APEFrame(true, data.size(), itemCount)); + tag.append(_render_APEFrame(true, data.size(), itemCount)); tag.append(data); - tag.append(APEFrame(false, data.size(), itemCount)); + tag.append(_render_APEFrame(false, data.size(), itemCount)); return tag; } @@ -154,117 +163,124 @@ ByteVector APE::Tag::fileIdentifier() String APE::Tag::title() const { - if(d->items.contains("Title")) - return d->items["Title"]; - else + if(d->fieldListMap["TITLE"].isEmpty()) return String::null; + return d->fieldListMap["TITLE"].value.front(); } String APE::Tag::artist() const { - if(d->items.contains("Artist")) - return d->items["Artist"]; - else + if(d->fieldListMap["ARTIST"].isEmpty()) return String::null; + return d->fieldListMap["ARTIST"].value.front(); } String APE::Tag::album() const { - if(d->items.contains("Album")) - return d->items["Album"]; - else + if(d->fieldListMap["ALBUM"].isEmpty()) return String::null; + return d->fieldListMap["ALBUM"].value.front(); } String APE::Tag::comment() const { - if(d->items.contains("Comment")) - return d->items["Comment"]; - else + if(d->fieldListMap["COMMENT"].isEmpty()) return String::null; + return d->fieldListMap["COMMENT"].value.front(); } String APE::Tag::genre() const { - if(d->items.contains("Genre")) - return d->items["Genre"]; - else + if(d->fieldListMap["GENRE"].isEmpty()) return String::null; + return d->fieldListMap["GENRE"].value.front(); } TagLib::uint APE::Tag::year() const { - if(d->items.contains("Year")) - return (d->items["Year"]).toInt(); - return 0; + if(d->fieldListMap["YEAR"].isEmpty()) + return 0; + return d->fieldListMap["YEAR"].value.front().toInt(); } TagLib::uint APE::Tag::track() const { - if(d->items.contains("Track")) - return (d->items["Track"]).toInt(); - return 0; + if(d->fieldListMap["TRACK"].isEmpty()) + return 0; + return d->fieldListMap["TRACK"].value.front().toInt(); } void APE::Tag::setTitle(const String &s) { - d->items["Title"] = s; + addField("TITLE", s, true); } void APE::Tag::setArtist(const String &s) { - d->items["Artist"] = s; + addField("ARTIST", s, true); } void APE::Tag::setAlbum(const String &s) { - d->items["Album"] = s; + addField("ALBUM", s, true); } void APE::Tag::setComment(const String &s) { - if(s.isEmpty()) - removeComment("Comment"); - else - d->items["Comment"] = s; + addField("COMMENT", s, true); } void APE::Tag::setGenre(const String &s) { - if(s.isEmpty()) - removeComment("Genre"); - else - d->items["Genre"] = s; + addField("GENRE", s, true); } void APE::Tag::setYear(uint i) { - if(i <= 0) - removeComment("Year"); + if(i <=0 ) + removeField("YEAR"); else - d->items["Year"] = String::number(i); + addField("YEAR", String::number(i), true); } void APE::Tag::setTrack(uint i) { - if(i <= 0) - removeComment("Track"); + if(i <=0 ) + removeField("TRACK"); else - d->items["Track"] = String::number(i); + addField("TRACK", String::number(i), true); } -void APE::Tag::removeComment(const String &key) { - Map::Iterator it = d->items.find(key); - if(it != d->items.end()) - d->items.erase(it); +const APE::FieldListMap& APE::Tag::fieldListMap() const +{ + return d->fieldListMap; } -void APE::Tag::addComment(const String &key, const String &value) -{ - if(value.isEmpty()) - removeComment(key); - else - d->items[key] = value; +void APE::Tag::removeField(const String &key) { + Map::Iterator it = d->fieldListMap.find(key.upper()); + if(it != d->fieldListMap.end()) + d->fieldListMap.erase(it); +} + +void APE::Tag::addField(const String &key, const String &value, bool replace) { + if(replace) + removeField(key); + if(!value.isEmpty()) { + Map::Iterator it = d->fieldListMap.find(key.upper()); + if (it != d->fieldListMap.end()) + d->fieldListMap[key].value.append(value); + else + addItem(key, Item(value)); + } +} + +void APE::Tag::addField(const String &key, const StringList &values) { + removeField(key); + + if(values.isEmpty()) return; + else { + addItem(key, Item(values)); + } } TagLib::uint APE::Tag::tagSize(const ByteVector &footer) @@ -277,13 +293,20 @@ TagLib::uint APE::Tag::tagSize(const ByteVector &footer) uint flags = footer.mid(20, 4).toUInt(false); - return length + (flags & (1U << 31) ? 32 : 0); + return length + ((flags & (1U << 31)) ? 32 : 0); } //////////////////////////////////////////////////////////////////////////////// // protected methods //////////////////////////////////////////////////////////////////////////////// +void APE::Tag::addItem(const String &key, const Item &item) { + removeField(key); + + Map::Iterator it = d->fieldListMap.find(key.upper()); + d->fieldListMap.insert(key, item); +} + void APE::Tag::read() { if(d->file && d->file->isValid()) { @@ -305,67 +328,41 @@ void APE::Tag::read() } } -void APE::Tag::parse(const ByteVector &data, uint count) +static StringList _parse_APEString(ByteVector val) { - uint pos = 0; - uint vallen, flags; - String key, value; - while(count > 0) { - vallen = data.mid(pos + 0, 4).toUInt(false); - flags = data.mid(pos + 4, 4).toUInt(false); - key = String(data.mid(pos + 8), String::UTF8); - - if(flags == 0) { - value = String(data.mid(pos + 8 + key.size() + 1, vallen), String::UTF8); - d->items.insert(key, value); - } - else { - d->unknowns.insert(key, data.mid(pos, 8 + key.size() + 1 + vallen)); - } - - pos += 8 + key.size() + 1 + vallen; - count--; - } + StringList value; + int pold = 0; + int p = val.find('\0'); + while (p != -1) { + value.append(String(val.mid(pold, p), String::UTF8)); + pold = p+1; + p = val.find('\0', pold); + }; + value.append(String(val.mid(pold), String::UTF8)); + + return value; } -/* void APE::Tag::parse(const ByteVector &data, uint count) { uint pos = 0; uint vallen, flags; - String key; + String key, value; while(count > 0) { - vallen = data.mid(pos + 0, 4).toUInt(false); - flags = data.mid(pos + 4, 4).toUInt(false); - key = String(data.mid(pos + 8), String::UTF8); - Item item(key); - - ByteVector value = data.mid(pos+8+key.size()+1, vallen); - - switch ((flags >> 1) & 3) { - case 0: - item.value.str = String(value, String::UTF8); - item.type = Item::STRING; - break; - case 1: - item.value.bin = value; - item.type = Item::BINARY; - break; - case 2: - item.value.str = String(value, String::UTF8); - item.type = Item::URL; - break; - case 3: - item.value.bin = value; - item.type = Item::RESERVED; - break; + vallen = data.mid(pos+0,4).toUInt(false); + flags = data.mid(pos+4,4).toUInt(false); + key = String(data.mid(pos+8), String::UTF8); + key = key.upper(); + APE::Item item; + + if (flags < 4 ) { + ByteVector val = data.mid(pos+8+key.size()+1, vallen); + d->fieldListMap.insert(key, Item(_parse_APEString(val))); + } else { + d->binaries.insert(key,data.mid(pos, 8+key.size()+1+vallen)); } - item.readOnly = (flags & 1); - - d->items.insert(key,item); pos += 8 + key.size() + 1 + vallen; count--; } } -*/ diff --git a/mpc/apetag.h b/mpc/apetag.h index bf87d533..37b102d0 100644 --- a/mpc/apetag.h +++ b/mpc/apetag.h @@ -24,15 +24,39 @@ #include "tag.h" #include "tbytevector.h" +#include "tmap.h" +#include "tstring.h" +#include "tstringlist.h" namespace TagLib { class File; - //! An APE tag implementation - namespace APE { + /*! + * A non-binary APE-item. + */ + struct Item { + Item() {}; + explicit Item(const String&); + explicit Item(const StringList&); + bool readOnly; + bool locator; // URL to external data + StringList value; + bool isEmpty() const; + }; + + /*! + * A mapping between a list of item names, or keys, and the associated item. + * + * \see APE::Tag::itemListMap() + */ + typedef Map FieldListMap; + + + //! An APE tag implementation + class Tag : public TagLib::Tag { public: @@ -87,17 +111,29 @@ namespace TagLib { virtual void setYear(uint i); virtual void setTrack(uint i); + const FieldListMap &fieldListMap() const; + /*! * Removes the \a key comment from the tag */ - void removeComment(const String &key); + void removeField(const String &key); /*! * Adds the \a key comment with \a value */ - void addComment(const String &key, const String &value); + void addField(const String &key, const String &value, bool replace = true); + + /*! + * Adds the \a key comment with \a values + */ + void addField(const String &key, const StringList &values); protected: + /*! + * Adds the \a key comment with \a item + */ + void addItem(const String &key, const Item &item); + /*! * Reads from the file specified in the constructor. */ diff --git a/mpc/mpcfile.cpp b/mpc/mpcfile.cpp index 4a9d8add..f57301fb 100644 --- a/mpc/mpcfile.cpp +++ b/mpc/mpcfile.cpp @@ -27,6 +27,7 @@ #include "id3v1tag.h" #include "id3v2header.h" #include "apetag.h" +#include "mpctag.h" using namespace TagLib; @@ -43,6 +44,7 @@ public: ID3v2Header(0), ID3v2Location(-1), ID3v2Size(0), + tag(0), properties(0), scanned(false), hasAPE(false), @@ -95,16 +97,7 @@ MPC::File::~File() TagLib::Tag *MPC::File::tag() const { - if(d->APETag) - return d->APETag; - else { - if(d->ID3v1Tag) - return d->ID3v1Tag; - else { - d->APETag = new APE::Tag; - return d->APETag; - } - } + return d->tag; } MPC::Properties *MPC::File::audioProperties() const @@ -114,27 +107,92 @@ MPC::Properties *MPC::File::audioProperties() const bool MPC::File::save() { - // Update APE tag - if(d->hasAPE && d->APETag) - insert(d->APETag->render(), d->APELocation, d->APESize); - else { - // Update ID3v1 tag + // Update ID3v1 tag - if(d->hasID3v1 && d->ID3v1Tag) { - seek(-128, End); - writeBlock(d->ID3v1Tag->render()); + if(d->ID3v1Tag) + { + if(d->hasID3v1) + { + seek(-128, End); + writeBlock(d->ID3v1Tag->render()); + } + else + { + seek(0, End); + writeBlock(d->ID3v1Tag->render()); } } + + // Update APE tag + + if(d->APETag) + { + if(d->hasAPE) + { + insert(d->APETag->render(), d->APELocation, d->APESize); + } + else + if (d->hasID3v1) + { + seek(-128, End); + insert(d->APETag->render(), tell(), 0); + } + else + { + seek(0, End); + writeBlock(d->APETag->render()); + } + } + return true; } +ID3v1::Tag *MPC::File::ID3v1Tag(bool create) +{ + if(!create || d->ID3v1Tag) + return d->ID3v1Tag; + + // no ID3v1 tag exists and we've been asked to create one + + d->ID3v1Tag = new ID3v1::Tag; + if (d->APETag) + d->tag = new CombinedTag(d->APETag, d->ID3v1Tag); + else + d->tag = d->ID3v1Tag; + return d->ID3v1Tag; +} + +APE::Tag *MPC::File::APETag(bool create) +{ + if(!create || d->APETag) + return d->APETag; + + // no APE tag exists and we've been asked to create one + + d->APETag = new APE::Tag; + if (d->ID3v1Tag) + d->tag = new CombinedTag(d->APETag, d->ID3v1Tag); + else + d->tag = d->APETag; + return d->APETag; +} + //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void MPC::File::read(bool readProperties, Properties::ReadStyle /* propertiesStyle */) { + // Look for an ID3v1 tag + + d->ID3v1Location = findID3v1(); + + if(d->ID3v1Location >= 0) { + d->ID3v1Tag = new ID3v1::Tag(this, d->ID3v1Location); + d->hasID3v1 = true; + } + // Look for an APE tag findAPE(); @@ -144,16 +202,16 @@ void MPC::File::read(bool readProperties, Properties::ReadStyle /* propertiesSty d->hasAPE = true; } - // Look for an ID3v1 tag - - if(!d->hasAPE) { - d->ID3v1Location = findID3v1(); - - if(d->ID3v1Location >= 0) { - d->ID3v1Tag = new ID3v1::Tag(this, d->ID3v1Location); - d->hasID3v1 = true; - } - } + if (d->hasID3v1 && d->hasAPE) + d->tag = new CombinedTag(d->APETag, d->ID3v1Tag); + else + if (d->hasID3v1) + d->tag = d->ID3v1Tag; + else + if (d->hasAPE) + d->tag = d->APETag; + else + d->tag = d->APETag = new APE::Tag(); // Look for and skip an ID3v2 tag @@ -184,7 +242,10 @@ bool MPC::File::findAPE() if(!isValid()) return false; - seek(-32, End); + if (d->hasID3v1) + seek(-160, End); + else + seek(-32, End); long p = tell(); ByteVector footer = readBlock(32); diff --git a/mpc/mpcfile.h b/mpc/mpcfile.h index 9b5f57a7..900cadb4 100644 --- a/mpc/mpcfile.h +++ b/mpc/mpcfile.h @@ -30,13 +30,17 @@ namespace TagLib { class Tag; + namespace ID3v1 { class Tag; } + namespace APE { class Tag; } + //! An implementation of MPC metadata /*! * This is implementation of MPC metadata. * * This supports ID3v1 and APE (v1 and v2) style comments as well as reading stream - * properties from the file. ID3v2 tags will be skipped and ignored. + * properties from the file. ID3v2 tags are invalid in MPC-files, but will be skipped + * and ignored. */ namespace MPC { @@ -48,11 +52,27 @@ namespace TagLib { * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing * the abstract TagLib::File API as well as providing some additional * information specific to MPC files. + * The only invalid tag combination supported is an ID3v1 tag after an APE tag. */ class File : public TagLib::File { public: + /*! + * This set of flags is used for various operations and is suitable for + * being OR-ed together. + */ + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches ID3v1 tags. + ID3v1 = 0x0001, + //! Matches APE tags. + APE = 0x0004, + //! Matches APE behind ID3v1 + APEID3 = 0x0005 + }; + /*! * Contructs an MPC file from \a file. If \a readProperties is true the * file's audio properties will also be read using \a propertiesStyle. If @@ -67,8 +87,8 @@ namespace TagLib { virtual ~File(); /*! - * Returns the Tag for this file. This will be either an APE or - * ID3v1 tag. + * Returns the Tag for this file. This will be an APE tag, an ID3v1 tag + * or a combination of the two. */ virtual TagLib::Tag *tag() const; @@ -83,6 +103,34 @@ namespace TagLib { */ virtual bool save(); + /*! + * Returns a pointer to the ID3v1 tag of the file. + * + * If \a create is false (the default) this will return a null pointer + * if there is no valid ID3v1 tag. If \a create is true it will create + * an ID3v1 tag if one does not exist. If there is already an APE tag, the + * new ID3v1 tag will be placed after it. + * + * \note The Tag is still owned by the APE::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + */ + ID3v1::Tag *ID3v1Tag(bool create = false); + + /*! + * Returns a pointer to the APE tag of the file. + * + * If \a create is false (the default) this will return a null pointer + * if there is no valid APE tag. If \a create is true it will create + * a APE tag if one does not exist. If there is already an ID3v1 tag, thes + * new APE tag will be placed before it. + * + * \note The Tag is still owned by the APE::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + */ + APE::Tag *APETag(bool create = false); + private: File(const File &); File &operator=(const File &); diff --git a/mpc/mpctag.h b/mpc/mpctag.h new file mode 100644 index 00000000..2b567bb3 --- /dev/null +++ b/mpc/mpctag.h @@ -0,0 +1,175 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + ***************************************************************************/ + +#ifndef DO_NOT_DOCUMENT // Tell Doxygen not to document this header + +#ifndef TAGLIB_MPCTAG_H +#define TAGLIB_MPCTAG_H + +//////////////////////////////////////////////////////////////////////////////// +// Note that this header is not installed. +//////////////////////////////////////////////////////////////////////////////// + +#include +#include + +namespace TagLib { + + namespace MPC { + + /*! + * A union of APE and ID3v1 tags. + */ + class CombinedTag : public TagLib::Tag + { + public: + CombinedTag(APE::Tag *ape = 0, ID3v1::Tag *id3v1 = 0) : + TagLib::Tag(), + ape(ape), id3v1(id3v1) {} + + virtual String title() const { + if(ape && !ape->title().isEmpty()) + return ape->title(); + + if(id3v1) + return id3v1->title(); + + return String::null; + } + + virtual String artist() const { + if(ape && !ape->artist().isEmpty()) + return ape->artist(); + + if(id3v1) + return id3v1->artist(); + + return String::null; + } + + virtual String album() const { + if(ape && !ape->album().isEmpty()) + return ape->album(); + + if(id3v1) + return id3v1->album(); + + return String::null; + } + + virtual String comment() const { + if(ape && !ape->comment().isEmpty()) + return ape->comment(); + + if(id3v1) + return id3v1->comment(); + + return String::null; + } + + virtual String genre() const { + if(ape && !ape->genre().isEmpty()) + return ape->genre(); + + if(id3v1) + return id3v1->genre(); + + return String::null; + } + + virtual uint year() const { + if(ape && ape->year() > 0) + return ape->year(); + + if(id3v1) + return id3v1->year(); + + return 0; + } + + virtual uint track() const { + if(ape && ape->track() > 0) + return ape->track(); + + if(id3v1) + return id3v1->track(); + + return 0; + } + + virtual void setTitle(const String &s) { + if(ape) + ape->setTitle(s); + if(id3v1) + id3v1->setTitle(s); + } + + virtual void setArtist(const String &s) { + if(ape) + ape->setArtist(s); + if(id3v1) + id3v1->setArtist(s); + } + + virtual void setAlbum(const String &s) { + if(ape) + ape->setAlbum(s); + if(id3v1) + id3v1->setAlbum(s); + } + + virtual void setComment(const String &s) { + if(ape) + ape->setComment(s); + if(id3v1) + id3v1->setComment(s); + } + + virtual void setGenre(const String &s) { + if(ape) + ape->setGenre(s); + if(id3v1) + id3v1->setGenre(s); + } + + virtual void setYear(uint i) { + if(ape) + ape->setYear(i); + if(id3v1) + id3v1->setYear(i); + } + + virtual void setTrack(uint i) { + if(ape) + ape->setTrack(i); + if(id3v1) + id3v1->setTrack(i); + } + + private: + APE::Tag *ape; + ID3v1::Tag *id3v1; + }; + } +} + +#endif +#endif -- 2.40.0