/*************************************************************************** copyright : (C) 2002 - 2008 by Scott Wheeler email : wheeler@kde.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., 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/ * ***************************************************************************/ #ifdef HAVE_CONFIG_H #include #endif #if HAVE_ZLIB #include #endif #include #include #include #include "id3v2tag.h" #include "id3v2frame.h" #include "id3v2synchdata.h" #include "tpropertymap.h" #include "frames/textidentificationframe.h" #include "frames/urllinkframe.h" #include "frames/unsynchronizedlyricsframe.h" #include "frames/commentsframe.h" #include "frames/uniquefileidentifierframe.h" #include "frames/unknownframe.h" using namespace TagLib; using namespace ID3v2; class Frame::FramePrivate { public: FramePrivate() : header(0) {} ~FramePrivate() { delete header; } Frame::Header *header; }; namespace { bool isValidFrameID(const ByteVector &frameID) { if(frameID.size() != 4) return false; for(ByteVector::ConstIterator it = frameID.begin(); it != frameID.end(); it++) { if( (*it < 'A' || *it > 'Z') && (*it < '0' || *it > '9') ) { return false; } } return true; } } //////////////////////////////////////////////////////////////////////////////// // static methods //////////////////////////////////////////////////////////////////////////////// TagLib::uint Frame::headerSize() { return Header::size(); } TagLib::uint Frame::headerSize(uint version) { return Header::size(version); } ByteVector Frame::textDelimiter(String::Type t) { ByteVector d = char(0); if(t == String::UTF16 || t == String::UTF16BE || t == String::UTF16LE) d.append(char(0)); return d; } const String Frame::instrumentPrefix("PERFORMER:"); const String Frame::commentPrefix("COMMENT:"); const String Frame::lyricsPrefix("LYRICS:"); const String Frame::urlPrefix("URL:"); //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// Frame *Frame::createTextualFrame(const String &key, const StringList &values) //static { // check if the key is contained in the key<=>frameID mapping ByteVector frameID = keyToFrameID(key); if(!frameID.isNull()) { if(frameID[0] == 'T'){ // text frame TextIdentificationFrame *frame = new TextIdentificationFrame(frameID, String::UTF8); frame->setText(values); return frame; } else if((frameID[0] == 'W') && (values.size() == 1)){ // URL frame (not WXXX); support only one value UrlLinkFrame* frame = new UrlLinkFrame(frameID); frame->setUrl(values.front()); return frame; } } if(key == "MUSICBRAINZ_TRACKID" && values.size() == 1) { UniqueFileIdentifierFrame *frame = new UniqueFileIdentifierFrame("http://musicbrainz.org", values.front().data(String::UTF8)); return frame; } // now we check if it's one of the "special" cases: // -LYRICS: depending on the number of values, use USLT or TXXX (with description=LYRICS) if((key == "LYRICS" || key.startsWith(lyricsPrefix)) && values.size() == 1){ UnsynchronizedLyricsFrame *frame = new UnsynchronizedLyricsFrame(String::UTF8); frame->setDescription(key == "LYRICS" ? key : key.substr(lyricsPrefix.size())); frame->setText(values.front()); return frame; } // -URL: depending on the number of values, use WXXX or TXXX (with description=URL) if((key == "URL" || key.startsWith(urlPrefix)) && values.size() == 1){ UserUrlLinkFrame *frame = new UserUrlLinkFrame(String::UTF8); frame->setDescription(key == "URL" ? key : key.substr(urlPrefix.size())); frame->setUrl(values.front()); return frame; } // -COMMENT: depending on the number of values, use COMM or TXXX (with description=COMMENT) if((key == "COMMENT" || key.startsWith(commentPrefix)) && values.size() == 1){ CommentsFrame *frame = new CommentsFrame(String::UTF8); if (key != "COMMENT"){ frame->setDescription(key.substr(commentPrefix.size())); } frame->setText(values.front()); return frame; } // if non of the above cases apply, we use a TXXX frame with the key as description return new UserTextIdentificationFrame(keyToTXXX(key), values, String::UTF8); } Frame::~Frame() { delete d; } ByteVector Frame::frameID() const { if(d->header) return d->header->frameID(); else return ByteVector::null; } TagLib::uint Frame::size() const { if(d->header) return d->header->frameSize(); else return 0; } void Frame::setData(const ByteVector &data) { parse(data); } void Frame::setText(const String &) { } ByteVector Frame::render() const { ByteVector fieldData = renderFields(); d->header->setFrameSize(fieldData.size()); ByteVector headerData = d->header->render(); return headerData + fieldData; } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// Frame::Frame(const ByteVector &data) { d = new FramePrivate; d->header = new Header(data); } Frame::Frame(Header *h) { d = new FramePrivate; d->header = h; } Frame::Header *Frame::header() const { return d->header; } void Frame::setHeader(Header *h, bool deleteCurrent) { if(deleteCurrent) delete d->header; d->header = h; } void Frame::parse(const ByteVector &data) { if(d->header) d->header->setData(data); else d->header = new Header(data); parseFields(fieldData(data)); } ByteVector Frame::fieldData(const ByteVector &frameData) const { uint headerSize = Header::size(d->header->version()); uint frameDataOffset = headerSize; uint frameDataLength = size(); if(d->header->compression() || d->header->dataLengthIndicator()) { frameDataLength = SynchData::toUInt(frameData.mid(headerSize, 4)); frameDataOffset += 4; } #if HAVE_ZLIB if(d->header->compression() && !d->header->encryption()) { if(frameData.size() <= frameDataOffset) { debug("Compressed frame doesn't have enough data to decode"); return ByteVector(); } z_stream stream = {}; if(inflateInit(&stream) != Z_OK) return ByteVector(); stream.avail_in = (uLongf) frameData.size() - frameDataOffset; stream.next_in = (Bytef *) frameData.data() + frameDataOffset; static const uint chunkSize = 1024; ByteVector data; ByteVector chunk(chunkSize); do { stream.avail_out = (uLongf) chunk.size(); stream.next_out = (Bytef *) chunk.data(); int result = inflate(&stream, Z_NO_FLUSH); if(result == Z_STREAM_ERROR || result == Z_NEED_DICT || result == Z_DATA_ERROR || result == Z_MEM_ERROR) { if(result != Z_STREAM_ERROR) inflateEnd(&stream); debug("Error reading compressed stream"); return ByteVector(); } data.append(stream.avail_out == 0 ? chunk : chunk.mid(0, chunk.size() - stream.avail_out)); } while(stream.avail_out == 0); inflateEnd(&stream); if(frameDataLength != data.size()) debug("frameDataLength does not match the data length returned by zlib"); return data; } else #endif return frameData.mid(frameDataOffset, frameDataLength); } String Frame::readStringField(const ByteVector &data, String::Type encoding, int *position) { int start = 0; if(!position) position = &start; ByteVector delimiter = textDelimiter(encoding); int end = data.find(delimiter, *position, delimiter.size()); if(end < *position) return String::null; String str; if(encoding == String::Latin1) str = Tag::latin1StringHandler()->parse(data.mid(*position, end - *position)); else str = String(data.mid(*position, end - *position), encoding); *position = end + delimiter.size(); return str; } String::Type Frame::checkEncoding(const StringList &fields, String::Type encoding) // static { return checkEncoding(fields, encoding, 4); } String::Type Frame::checkEncoding(const StringList &fields, String::Type encoding, uint version) // static { if((encoding == String::UTF8 || encoding == String::UTF16BE) && version != 4) return String::UTF16; if(encoding != String::Latin1) return encoding; for(StringList::ConstIterator it = fields.begin(); it != fields.end(); ++it) { if(!(*it).isLatin1()) { if(version == 4) { debug("Frame::checkEncoding() -- Rendering using UTF8."); return String::UTF8; } else { debug("Frame::checkEncoding() -- Rendering using UTF16."); return String::UTF16; } } } return String::Latin1; } String::Type Frame::checkTextEncoding(const StringList &fields, String::Type encoding) const { return checkEncoding(fields, encoding, header()->version()); } static const TagLib::uint frameTranslationSize = 51; static const char *frameTranslation[][2] = { // Text information frames { "TALB", "ALBUM"}, { "TBPM", "BPM" }, { "TCOM", "COMPOSER" }, { "TCON", "GENRE" }, { "TCOP", "COPYRIGHT" }, { "TDEN", "ENCODINGTIME" }, { "TDLY", "PLAYLISTDELAY" }, { "TDOR", "ORIGINALDATE" }, { "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", "RELEASEDATE" }, { "TDTG", "TAGGINGDATE" }, { "TENC", "ENCODEDBY" }, { "TEXT", "LYRICIST" }, { "TFLT", "FILETYPE" }, //{ "TIPL", "INVOLVEDPEOPLE" }, handled separately { "TIT1", "CONTENTGROUP" }, { "TIT2", "TITLE"}, { "TIT3", "SUBTITLE" }, { "TKEY", "INITIALKEY" }, { "TLAN", "LANGUAGE" }, { "TLEN", "LENGTH" }, //{ "TMCL", "MUSICIANCREDITS" }, handled separately { "TMED", "MEDIA" }, { "TMOO", "MOOD" }, { "TOAL", "ORIGINALALBUM" }, { "TOFN", "ORIGINALFILENAME" }, { "TOLY", "ORIGINALLYRICIST" }, { "TOPE", "ORIGINALARTIST" }, { "TOWN", "OWNER" }, { "TPE1", "ARTIST"}, { "TPE2", "ALBUMARTIST" }, // id3's spec says 'PERFORMER', but most programs use 'ALBUMARTIST' { "TPE3", "CONDUCTOR" }, { "TPE4", "REMIXER" }, // could also be ARRANGER { "TPOS", "DISCNUMBER" }, { "TPRO", "PRODUCEDNOTICE" }, { "TPUB", "LABEL" }, { "TRCK", "TRACKNUMBER" }, { "TRSN", "RADIOSTATION" }, { "TRSO", "RADIOSTATIONOWNER" }, { "TSOA", "ALBUMSORT" }, { "TSOP", "ARTISTSORT" }, { "TSOT", "TITLESORT" }, { "TSO2", "ALBUMARTISTSORT" }, // non-standard, used by iTunes { "TSRC", "ISRC" }, { "TSSE", "ENCODING" }, // URL frames { "WCOP", "COPYRIGHTURL" }, { "WOAF", "FILEWEBPAGE" }, { "WOAR", "ARTISTWEBPAGE" }, { "WOAS", "AUDIOSOURCEWEBPAGE" }, { "WORS", "RADIOSTATIONWEBPAGE" }, { "WPAY", "PAYMENTWEBPAGE" }, { "WPUB", "PUBLISHERWEBPAGE" }, //{ "WXXX", "URL"}, handled specially // Other frames { "COMM", "COMMENT" }, //{ "USLT", "LYRICS" }, handled specially }; static const TagLib::uint txxxFrameTranslationSize = 8; static const char *txxxFrameTranslation[][2] = { { "MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID" }, { "MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID" }, { "MusicBrainz Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID" }, { "MusicBrainz Release Group Id", "MUSICBRAINZ_RELEASEGROUPID" }, { "MusicBrainz Work Id", "MUSICBRAINZ_WORKID" }, { "Acoustid Id", "ACOUSTID_ID" }, { "Acoustid Fingerprint", "ACOUSTID_FINGERPRINT" }, { "MusicIP PUID", "MUSICIP_PUID" }, }; Map &idMap() { static Map m; if(m.isEmpty()) for(size_t i = 0; i < frameTranslationSize; ++i) m[frameTranslation[i][0]] = frameTranslation[i][1]; return m; } Map &txxxMap() { static Map m; if(m.isEmpty()) { for(size_t i = 0; i < txxxFrameTranslationSize; ++i) { String key = String(txxxFrameTranslation[i][0]).upper(); m[key] = txxxFrameTranslation[i][1]; } } return m; } // list of deprecated frames and their successors static const TagLib::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 }; Map &deprecationMap() { static Map depMap; if(depMap.isEmpty()) for(TagLib::uint i = 0; i < deprecatedFramesSize; ++i) depMap[deprecatedFrames[i][0]] = deprecatedFrames[i][1]; return depMap; } String Frame::frameIDToKey(const ByteVector &id) { Map &m = idMap(); if(m.contains(id)) return m[id]; if(deprecationMap().contains(id)) return m[deprecationMap()[id]]; return String::null; } ByteVector Frame::keyToFrameID(const String &s) { static Map m; if(m.isEmpty()) for(size_t i = 0; i < frameTranslationSize; ++i) m[frameTranslation[i][1]] = frameTranslation[i][0]; if(m.contains(s.upper())) return m[s]; return ByteVector::null; } String Frame::txxxToKey(const String &description) { Map &m = txxxMap(); String d = description.upper(); if(m.contains(d)) return m[d]; return d; } String Frame::keyToTXXX(const String &s) { static Map m; if(m.isEmpty()) for(size_t i = 0; i < txxxFrameTranslationSize; ++i) m[txxxFrameTranslation[i][1]] = txxxFrameTranslation[i][0]; if(m.contains(s.upper())) return m[s]; return s; } PropertyMap Frame::asProperties() const { if(dynamic_cast< const UnknownFrame *>(this)) { PropertyMap m; m.unsupportedData().append("UNKNOWN/" + frameID()); return m; } const ByteVector &id = frameID(); // workaround until this function is virtual if(id == "TXXX") return dynamic_cast< const UserTextIdentificationFrame* >(this)->asProperties(); else if(id[0] == 'T') return dynamic_cast< const TextIdentificationFrame* >(this)->asProperties(); else if(id == "WXXX") return dynamic_cast< const UserUrlLinkFrame* >(this)->asProperties(); else if(id[0] == 'W') return dynamic_cast< const UrlLinkFrame* >(this)->asProperties(); else if(id == "COMM") return dynamic_cast< const CommentsFrame* >(this)->asProperties(); else if(id == "USLT") return dynamic_cast< const UnsynchronizedLyricsFrame* >(this)->asProperties(); else if(id == "UFID") return dynamic_cast< const UniqueFileIdentifierFrame* >(this)->asProperties(); PropertyMap m; m.unsupportedData().append(id); return m; } void Frame::splitProperties(const PropertyMap &original, PropertyMap &singleFrameProperties, PropertyMap &tiplProperties, PropertyMap &tmclProperties) { singleFrameProperties.clear(); tiplProperties.clear(); tmclProperties.clear(); for(PropertyMap::ConstIterator it = original.begin(); it != original.end(); ++it) { if(TextIdentificationFrame::involvedPeopleMap().contains(it->first)) tiplProperties.insert(it->first, it->second); else if(it->first.startsWith(TextIdentificationFrame::instrumentPrefix)) tmclProperties.insert(it->first, it->second); else singleFrameProperties.insert(it->first, it->second); } } //////////////////////////////////////////////////////////////////////////////// // Frame::Header class //////////////////////////////////////////////////////////////////////////////// class Frame::Header::HeaderPrivate { public: HeaderPrivate() : frameSize(0), version(4), tagAlterPreservation(false), fileAlterPreservation(false), readOnly(false), groupingIdentity(false), compression(false), encryption(false), unsynchronisation(false), dataLengthIndicator(false) {} ByteVector frameID; uint frameSize; uint version; // flags bool tagAlterPreservation; bool fileAlterPreservation; bool readOnly; bool groupingIdentity; bool compression; bool encryption; bool unsynchronisation; bool dataLengthIndicator; }; //////////////////////////////////////////////////////////////////////////////// // static members (Frame::Header) //////////////////////////////////////////////////////////////////////////////// TagLib::uint Frame::Header::size() { return size(4); } TagLib::uint Frame::Header::size(uint version) { switch(version) { case 0: case 1: case 2: return 6; case 3: case 4: default: return 10; } } //////////////////////////////////////////////////////////////////////////////// // public members (Frame::Header) //////////////////////////////////////////////////////////////////////////////// Frame::Header::Header(const ByteVector &data, bool synchSafeInts) { d = new HeaderPrivate; setData(data, synchSafeInts); } Frame::Header::Header(const ByteVector &data, uint version) { d = new HeaderPrivate; setData(data, version); } Frame::Header::~Header() { delete d; } void Frame::Header::setData(const ByteVector &data, bool synchSafeInts) { setData(data, uint(synchSafeInts ? 4 : 3)); } void Frame::Header::setData(const ByteVector &data, uint version) { d->version = version; switch(version) { case 0: case 1: case 2: { // ID3v2.2 if(data.size() < 3) { debug("You must at least specify a frame ID."); return; } // Set the frame ID -- the first three bytes d->frameID = data.mid(0, 3); // If the full header information was not passed in, do not continue to the // steps to parse the frame size and flags. if(data.size() < 6) { d->frameSize = 0; return; } d->frameSize = data.toUInt(3, 3, true); break; } case 3: { // ID3v2.3 if(data.size() < 4) { debug("You must at least specify a frame ID."); return; } // Set the frame ID -- the first four bytes d->frameID = data.mid(0, 4); // If the full header information was not passed in, do not continue to the // steps to parse the frame size and flags. if(data.size() < 10) { d->frameSize = 0; return; } // Set the size -- the frame size is the four bytes starting at byte four in // the frame header (structure 4) d->frameSize = data.toUInt(4U); { // read the first byte of flags std::bitset<8> flags(data[8]); d->tagAlterPreservation = flags[7]; // (structure 3.3.1.a) d->fileAlterPreservation = flags[6]; // (structure 3.3.1.b) d->readOnly = flags[5]; // (structure 3.3.1.c) } { // read the second byte of flags std::bitset<8> flags(data[9]); d->compression = flags[7]; // (structure 3.3.1.i) d->encryption = flags[6]; // (structure 3.3.1.j) d->groupingIdentity = flags[5]; // (structure 3.3.1.k) } break; } case 4: default: { // ID3v2.4 if(data.size() < 4) { debug("You must at least specify a frame ID."); return; } // Set the frame ID -- the first four bytes d->frameID = data.mid(0, 4); // If the full header information was not passed in, do not continue to the // steps to parse the frame size and flags. if(data.size() < 10) { d->frameSize = 0; return; } // Set the size -- the frame size is the four bytes starting at byte four in // the frame header (structure 4) d->frameSize = SynchData::toUInt(data.mid(4, 4)); #ifndef NO_ITUNES_HACKS // iTunes writes v2.4 tags with v2.3-like frame sizes if(d->frameSize > 127) { if(!isValidFrameID(data.mid(d->frameSize + 10, 4))) { unsigned int uintSize = data.toUInt(4U); if(isValidFrameID(data.mid(uintSize + 10, 4))) { d->frameSize = uintSize; } } } #endif { // read the first byte of flags std::bitset<8> flags(data[8]); d->tagAlterPreservation = flags[6]; // (structure 4.1.1.a) d->fileAlterPreservation = flags[5]; // (structure 4.1.1.b) d->readOnly = flags[4]; // (structure 4.1.1.c) } { // read the second byte of flags std::bitset<8> flags(data[9]); d->groupingIdentity = flags[6]; // (structure 4.1.2.h) d->compression = flags[3]; // (structure 4.1.2.k) d->encryption = flags[2]; // (structure 4.1.2.m) d->unsynchronisation = flags[1]; // (structure 4.1.2.n) d->dataLengthIndicator = flags[0]; // (structure 4.1.2.p) } break; } } } ByteVector Frame::Header::frameID() const { return d->frameID; } void Frame::Header::setFrameID(const ByteVector &id) { d->frameID = id.mid(0, 4); } TagLib::uint Frame::Header::frameSize() const { return d->frameSize; } void Frame::Header::setFrameSize(uint size) { d->frameSize = size; } TagLib::uint Frame::Header::version() const { return d->version; } void Frame::Header::setVersion(TagLib::uint version) { d->version = version; } bool Frame::Header::tagAlterPreservation() const { return d->tagAlterPreservation; } void Frame::Header::setTagAlterPreservation(bool preserve) { d->tagAlterPreservation = preserve; } bool Frame::Header::fileAlterPreservation() const { return d->fileAlterPreservation; } bool Frame::Header::readOnly() const { return d->readOnly; } bool Frame::Header::groupingIdentity() const { return d->groupingIdentity; } bool Frame::Header::compression() const { return d->compression; } bool Frame::Header::encryption() const { return d->encryption; } bool Frame::Header::unsycronisation() const { return unsynchronisation(); } bool Frame::Header::unsynchronisation() const { return d->unsynchronisation; } bool Frame::Header::dataLengthIndicator() const { return d->dataLengthIndicator; } ByteVector Frame::Header::render() const { ByteVector flags(2, char(0)); // just blank for the moment ByteVector v = d->frameID + (d->version == 3 ? ByteVector::fromUInt(d->frameSize) : SynchData::fromUInt(d->frameSize)) + flags; return v; } bool Frame::Header::frameAlterPreservation() const { return fileAlterPreservation(); }