/************************************************************************** copyright : (C) 2007 by Lukáš Lalinský email : lalinsky@gmail.com **************************************************************************/ /*************************************************************************** * 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 #ifdef WITH_MP4 #include #include #include "mp4atom.h" #include "mp4tag.h" #include "id3v1genres.h" using namespace TagLib; class MP4::Tag::TagPrivate { public: TagPrivate() : file(0), atoms(0) {} ~TagPrivate() {} TagLib::File *file; Atoms *atoms; ItemListMap items; }; MP4::Tag::Tag() { d = new TagPrivate; } MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) { d = new TagPrivate; d->file = file; d->atoms = atoms; MP4::Atom *ilst = atoms->find("moov", "udta", "meta", "ilst"); if(!ilst) { //debug("Atom moov.udta.meta.ilst not found."); return; } for(unsigned int i = 0; i < ilst->children.size(); i++) { MP4::Atom *atom = ilst->children[i]; file->seek(atom->offset + 8); if(atom->name == "----") { parseFreeForm(atom, file); } else if(atom->name == "trkn" || atom->name == "disk") { parseIntPair(atom, file); } else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst") { parseBool(atom, file); } else if(atom->name == "tmpo") { parseInt(atom, file); } else if(atom->name == "gnre") { parseGnre(atom, file); } else if(atom->name == "covr") { parseCovr(atom, file); } else { parseText(atom, file); } } } MP4::Tag::~Tag() { delete d; } ByteVectorList MP4::Tag::parseData(MP4::Atom *atom, TagLib::File *file, int expectedFlags, bool freeForm) { ByteVectorList result; ByteVector data = file->readBlock(atom->length - 8); int i = 0; unsigned int pos = 0; while(pos < data.size()) { int length = data.mid(pos, 4).toUInt(); ByteVector name = data.mid(pos + 4, 4); int flags = data.mid(pos + 8, 4).toUInt(); if(freeForm && i < 2) { if(i == 0 && name != "mean") { debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\""); return result; } else if(i == 1 && name != "name") { debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\""); return result; } result.append(data.mid(pos + 12, length - 12)); } else { if(name != "data") { debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); return result; } if(expectedFlags == -1 || flags == expectedFlags) { result.append(data.mid(pos + 16, length - 16)); } } pos += length; i++; } return result; } void MP4::Tag::parseInt(MP4::Atom *atom, TagLib::File *file) { ByteVectorList data = parseData(atom, file); if(data.size()) { d->items.insert(atom->name, (int)data[0].toShort()); } } void MP4::Tag::parseGnre(MP4::Atom *atom, TagLib::File *file) { ByteVectorList data = parseData(atom, file); if(data.size()) { int idx = (int)data[0].toShort(); if(!d->items.contains("\251gen") && idx > 0) { d->items.insert("\251gen", StringList(ID3v1::genre(idx - 1))); } } } void MP4::Tag::parseIntPair(MP4::Atom *atom, TagLib::File *file) { ByteVectorList data = parseData(atom, file); if(data.size()) { int a = data[0].mid(2, 2).toShort(); int b = data[0].mid(4, 2).toShort(); d->items.insert(atom->name, MP4::Item(a, b)); } } void MP4::Tag::parseBool(MP4::Atom *atom, TagLib::File *file) { ByteVectorList data = parseData(atom, file); if(data.size()) { bool value = data[0].size() ? data[0][0] != '\0' : false; d->items.insert(atom->name, value); } } void MP4::Tag::parseText(MP4::Atom *atom, TagLib::File *file, int expectedFlags) { ByteVectorList data = parseData(atom, file, expectedFlags); if(data.size()) { StringList value; for(unsigned int i = 0; i < data.size(); i++) { value.append(String(data[i], String::UTF8)); } d->items.insert(atom->name, value); } } void MP4::Tag::parseFreeForm(MP4::Atom *atom, TagLib::File *file) { ByteVectorList data = parseData(atom, file, 1, true); if(data.size() > 2) { StringList value; for(unsigned int i = 2; i < data.size(); i++) { value.append(String(data[i], String::UTF8)); } String name = "----:" + data[0] + ':' + data[1]; d->items.insert(name, value); } } void MP4::Tag::parseCovr(MP4::Atom *atom, TagLib::File *file) { MP4::CoverArtList value; ByteVector data = file->readBlock(atom->length - 8); unsigned int pos = 0; while(pos < data.size()) { int length = data.mid(pos, 4).toUInt(); ByteVector name = data.mid(pos + 4, 4); int flags = data.mid(pos + 8, 4).toUInt(); if(name != "data") { debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); break; } if(flags == MP4::CoverArt::PNG || flags == MP4::CoverArt::JPEG) { value.append(MP4::CoverArt(MP4::CoverArt::Format(flags), data.mid(pos + 16, length - 16))); } pos += length; } if(value.size() > 0) d->items.insert(atom->name, value); } ByteVector MP4::Tag::padIlst(const ByteVector &data, int length) { if (length == -1) { length = ((data.size() + 1023) & ~1023) - data.size(); } return renderAtom("free", ByteVector(length, '\1')); } ByteVector MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data) { return ByteVector::fromUInt(data.size() + 8) + name + data; } ByteVector MP4::Tag::renderData(const ByteVector &name, int flags, const ByteVectorList &data) { ByteVector result; for(unsigned int i = 0; i < data.size(); i++) { result.append(renderAtom("data", ByteVector::fromUInt(flags) + ByteVector(4, '\0') + data[i])); } return renderAtom(name, result); } ByteVector MP4::Tag::renderBool(const ByteVector &name, MP4::Item &item) { ByteVectorList data; data.append(ByteVector(1, item.toBool() ? '\1' : '\0')); return renderData(name, 0x15, data); } ByteVector MP4::Tag::renderInt(const ByteVector &name, MP4::Item &item) { ByteVectorList data; data.append(ByteVector::fromShort(item.toInt())); return renderData(name, 0x15, data); } ByteVector MP4::Tag::renderIntPair(const ByteVector &name, MP4::Item &item) { ByteVectorList data; data.append(ByteVector(2, '\0') + ByteVector::fromShort(item.toIntPair().first) + ByteVector::fromShort(item.toIntPair().second) + ByteVector(2, '\0')); return renderData(name, 0x00, data); } ByteVector MP4::Tag::renderIntPairNoTrailing(const ByteVector &name, MP4::Item &item) { ByteVectorList data; data.append(ByteVector(2, '\0') + ByteVector::fromShort(item.toIntPair().first) + ByteVector::fromShort(item.toIntPair().second)); return renderData(name, 0x00, data); } ByteVector MP4::Tag::renderText(const ByteVector &name, MP4::Item &item, int flags) { ByteVectorList data; StringList value = item.toStringList(); for(unsigned int i = 0; i < value.size(); i++) { data.append(value[i].data(String::UTF8)); } return renderData(name, flags, data); } ByteVector MP4::Tag::renderCovr(const ByteVector &name, MP4::Item &item) { ByteVector data; MP4::CoverArtList value = item.toCoverArtList(); for(unsigned int i = 0; i < value.size(); i++) { data.append(renderAtom("data", ByteVector::fromUInt(value[i].format()) + ByteVector(4, '\0') + value[i].data())); } return renderAtom(name, data); } ByteVector MP4::Tag::renderFreeForm(const String &name, MP4::Item &item) { StringList header = StringList::split(name, ":"); if (header.size() != 3) { debug("MP4: Invalid free-form item name \"" + name + "\""); return ByteVector::null; } ByteVector data; data.append(renderAtom("mean", ByteVector::fromUInt(0) + header[1].data(String::UTF8))); data.append(renderAtom("name", ByteVector::fromUInt(0) + header[2].data(String::UTF8))); StringList value = item.toStringList(); for(unsigned int i = 0; i < value.size(); i++) { data.append(renderAtom("data", ByteVector::fromUInt(1) + ByteVector(4, '\0') + value[i].data(String::UTF8))); } return renderAtom("----", data); } bool MP4::Tag::save() { ByteVector data; for(MP4::ItemListMap::Iterator i = d->items.begin(); i != d->items.end(); i++) { const String name = i->first; if(name.startsWith("----")) { data.append(renderFreeForm(name, i->second)); } else if(name == "trkn") { data.append(renderIntPair(name.data(String::Latin1), i->second)); } else if(name == "disk") { data.append(renderIntPairNoTrailing(name.data(String::Latin1), i->second)); } else if(name == "cpil" || name == "pgap" || name == "pcst") { data.append(renderBool(name.data(String::Latin1), i->second)); } else if(name == "tmpo") { data.append(renderInt(name.data(String::Latin1), i->second)); } else if(name == "covr") { data.append(renderCovr(name.data(String::Latin1), i->second)); } else if(name.size() == 4){ data.append(renderText(name.data(String::Latin1), i->second)); } else { debug("MP4: Unknown item name \"" + name + "\""); } } data = renderAtom("ilst", data); AtomList path = d->atoms->path("moov", "udta", "meta", "ilst"); if(path.size() == 4) { saveExisting(data, path); } else { saveNew(data); } return true; } void MP4::Tag::updateParents(AtomList &path, long delta, int ignore) { for(unsigned int i = 0; i < path.size() - ignore; i++) { d->file->seek(path[i]->offset); long size = d->file->readBlock(4).toUInt(); // 64-bit if (size == 1) { d->file->seek(4, File::Current); // Skip name long long longSize = d->file->readBlock(8).toLongLong(); // Seek the offset of the 64-bit size d->file->seek(path[i]->offset + 8); d->file->writeBlock(ByteVector::fromLongLong(longSize + delta)); } // 32-bit else { d->file->seek(path[i]->offset); d->file->writeBlock(ByteVector::fromUInt(size + delta)); } } } void MP4::Tag::updateOffsets(long delta, long offset) { MP4::Atom *moov = d->atoms->find("moov"); if(moov) { MP4::AtomList stco = moov->findall("stco", true); for(unsigned int i = 0; i < stco.size(); i++) { MP4::Atom *atom = stco[i]; if(atom->offset > offset) { atom->offset += delta; } d->file->seek(atom->offset + 12); ByteVector data = d->file->readBlock(atom->length - 12); unsigned int count = data.mid(0, 4).toUInt(); d->file->seek(atom->offset + 16); int pos = 4; while(count--) { long o = data.mid(pos, 4).toUInt(); if(o > offset) { o += delta; } d->file->writeBlock(ByteVector::fromUInt(o)); pos += 4; } } MP4::AtomList co64 = moov->findall("co64", true); for(unsigned int i = 0; i < co64.size(); i++) { MP4::Atom *atom = co64[i]; if(atom->offset > offset) { atom->offset += delta; } d->file->seek(atom->offset + 12); ByteVector data = d->file->readBlock(atom->length - 12); unsigned int count = data.mid(0, 4).toUInt(); d->file->seek(atom->offset + 16); int pos = 4; while(count--) { long long o = data.mid(pos, 8).toLongLong(); if(o > offset) { o += delta; } d->file->writeBlock(ByteVector::fromLongLong(o)); pos += 8; } } } MP4::Atom *moof = d->atoms->find("moof"); if(moof) { MP4::AtomList tfhd = moof->findall("tfhd", true); for(unsigned int i = 0; i < tfhd.size(); i++) { MP4::Atom *atom = tfhd[i]; if(atom->offset > offset) { atom->offset += delta; } d->file->seek(atom->offset + 9); ByteVector data = d->file->readBlock(atom->offset - 9); unsigned int flags = (ByteVector(1, '\0') + data.mid(0, 3)).toUInt(); if(flags & 1) { long long o = data.mid(7, 8).toLongLong(); if(o > offset) { o += delta; } d->file->seek(atom->offset + 16); d->file->writeBlock(ByteVector::fromLongLong(o)); } } } } void MP4::Tag::saveNew(ByteVector &data) { data = renderAtom("meta", TagLib::ByteVector(4, '\0') + renderAtom("hdlr", TagLib::ByteVector(8, '\0') + TagLib::ByteVector("mdirappl") + TagLib::ByteVector(9, '\0')) + data + padIlst(data)); AtomList path = d->atoms->path("moov", "udta"); if(path.size() != 2) { path = d->atoms->path("moov"); data = renderAtom("udta", data); } long offset = path[path.size() - 1]->offset + 8; d->file->insert(data, offset, 0); updateParents(path, data.size()); updateOffsets(data.size(), offset); } void MP4::Tag::saveExisting(ByteVector &data, AtomList &path) { MP4::Atom *ilst = path[path.size() - 1]; long offset = ilst->offset; long length = ilst->length; MP4::Atom *meta = path[path.size() - 2]; AtomList::Iterator index = meta->children.find(ilst); // check if there is an atom before 'ilst', and possibly use it as padding if(index != meta->children.begin()) { AtomList::Iterator prevIndex = index; prevIndex--; MP4::Atom *prev = *prevIndex; if(prev->name == "free") { offset = prev->offset; length += prev->length; } } // check if there is an atom after 'ilst', and possibly use it as padding AtomList::Iterator nextIndex = index; nextIndex++; if(nextIndex != meta->children.end()) { MP4::Atom *next = *nextIndex; if(next->name == "free") { length += next->length; } } long delta = data.size() - length; if(delta > 0 || (delta < 0 && delta > -8)) { data.append(padIlst(data)); delta = data.size() - length; } else if(delta < 0) { data.append(padIlst(data, -delta - 8)); delta = 0; } d->file->insert(data, offset, length); if(delta) { updateParents(path, delta, 1); updateOffsets(delta, offset); } } String MP4::Tag::title() const { if(d->items.contains("\251nam")) return d->items["\251nam"].toStringList().toString(", "); return String::null; } String MP4::Tag::artist() const { if(d->items.contains("\251ART")) return d->items["\251ART"].toStringList().toString(", "); return String::null; } String MP4::Tag::album() const { if(d->items.contains("\251alb")) return d->items["\251alb"].toStringList().toString(", "); return String::null; } String MP4::Tag::comment() const { if(d->items.contains("\251cmt")) return d->items["\251cmt"].toStringList().toString(", "); return String::null; } String MP4::Tag::genre() const { if(d->items.contains("\251gen")) return d->items["\251gen"].toStringList().toString(", "); return String::null; } unsigned int MP4::Tag::year() const { if(d->items.contains("\251day")) return d->items["\251day"].toStringList().toString().toInt(); return 0; } unsigned int MP4::Tag::track() const { if(d->items.contains("trkn")) return d->items["trkn"].toIntPair().first; return 0; } void MP4::Tag::setTitle(const String &value) { d->items["\251nam"] = StringList(value); } void MP4::Tag::setArtist(const String &value) { d->items["\251ART"] = StringList(value); } void MP4::Tag::setAlbum(const String &value) { d->items["\251alb"] = StringList(value); } void MP4::Tag::setComment(const String &value) { d->items["\251cmt"] = StringList(value); } void MP4::Tag::setGenre(const String &value) { d->items["\251gen"] = StringList(value); } void MP4::Tag::setYear(uint value) { d->items["\251day"] = StringList(String::number(value)); } void MP4::Tag::setTrack(uint value) { d->items["trkn"] = MP4::Item(value, 0); } MP4::ItemListMap & MP4::Tag::itemListMap() { return d->items; } #endif