520 lines
14 KiB
C++
520 lines
14 KiB
C++
/***************************************************************************
|
|
copyright : (C) 2004 by Allan Sandfeld Jensen
|
|
email : kde@carewolf.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/ *
|
|
***************************************************************************/
|
|
|
|
#if defined(__SUNPRO_CC) && (__SUNPRO_CC < 0x5130)
|
|
// Sun Studio finds multiple specializations of Map because
|
|
// it considers specializations with and without class types
|
|
// to be different; this define forces Map to use only the
|
|
// specialization with the class keyword.
|
|
# define WANT_CLASS_INSTANTIATION_OF_MAP (1)
|
|
#endif
|
|
|
|
#include "tfile.h"
|
|
#include "tstring.h"
|
|
#include "tmap.h"
|
|
#include "tpicturemap.h"
|
|
#include "tpropertymap.h"
|
|
#include "tdebug.h"
|
|
#include "tutils.h"
|
|
|
|
#include "apetag.h"
|
|
#include "apefooter.h"
|
|
#include "apeitem.h"
|
|
|
|
using namespace Strawberry_TagLib::TagLib;
|
|
using namespace APE;
|
|
|
|
namespace {
|
|
const unsigned int MinKeyLength = 2;
|
|
const unsigned int MaxKeyLength = 255;
|
|
|
|
bool isKeyValid(const ByteVector &key) {
|
|
|
|
const char *invalidKeys[] = { "ID3", "TAG", "OGGS", "MP+", nullptr };
|
|
|
|
// only allow printable ASCII including space (32..126)
|
|
|
|
for (ByteVector::ConstIterator it = key.begin(); it != key.end(); ++it) {
|
|
const int c = static_cast<unsigned char>(*it);
|
|
if (c < 32 || c > 126)
|
|
return false;
|
|
}
|
|
|
|
const String upperKey = String(key).upper();
|
|
for (size_t i = 0; invalidKeys[i] != nullptr; ++i) {
|
|
if (upperKey == invalidKeys[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
class APE::Tag::TagPrivate {
|
|
public:
|
|
TagPrivate() : file(nullptr), footerLocation(0) {}
|
|
|
|
File *file;
|
|
long long footerLocation;
|
|
|
|
Footer footer;
|
|
ItemListMap itemListMap;
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// public methods
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
APE::Tag::Tag() : d(new TagPrivate()) {}
|
|
|
|
APE::Tag::Tag(Strawberry_TagLib::TagLib::File *file, long long footerLocation) : d(new TagPrivate()) {
|
|
|
|
d->file = file;
|
|
d->footerLocation = footerLocation;
|
|
|
|
read();
|
|
}
|
|
|
|
APE::Tag::~Tag() {
|
|
delete d;
|
|
}
|
|
|
|
ByteVector APE::Tag::fileIdentifier() {
|
|
return ByteVector::fromCString("APETAGEX");
|
|
}
|
|
|
|
String APE::Tag::title() const {
|
|
|
|
if (d->itemListMap["TITLE"].isEmpty())
|
|
return String();
|
|
return d->itemListMap["TITLE"].values().toString();
|
|
|
|
}
|
|
|
|
String APE::Tag::artist() const {
|
|
|
|
if (d->itemListMap["ARTIST"].isEmpty())
|
|
return String();
|
|
return d->itemListMap["ARTIST"].values().toString();
|
|
|
|
}
|
|
|
|
String APE::Tag::album() const {
|
|
|
|
if (d->itemListMap["ALBUM"].isEmpty())
|
|
return String();
|
|
return d->itemListMap["ALBUM"].values().toString();
|
|
|
|
}
|
|
|
|
String APE::Tag::comment() const {
|
|
|
|
if (d->itemListMap["COMMENT"].isEmpty())
|
|
return String();
|
|
return d->itemListMap["COMMENT"].values().toString();
|
|
|
|
}
|
|
|
|
String APE::Tag::genre() const {
|
|
|
|
if (d->itemListMap["GENRE"].isEmpty())
|
|
return String();
|
|
return d->itemListMap["GENRE"].values().toString();
|
|
|
|
}
|
|
|
|
unsigned int APE::Tag::year() const {
|
|
|
|
if (d->itemListMap["YEAR"].isEmpty())
|
|
return 0;
|
|
return d->itemListMap["YEAR"].toString().toInt();
|
|
|
|
}
|
|
|
|
unsigned int APE::Tag::track() const {
|
|
|
|
if (d->itemListMap["TRACK"].isEmpty())
|
|
return 0;
|
|
return d->itemListMap["TRACK"].toString().toInt();
|
|
|
|
}
|
|
|
|
Strawberry_TagLib::TagLib::PictureMap APE::Tag::pictures() const {
|
|
|
|
PictureMap map;
|
|
if (d->itemListMap.contains(FRONT_COVER)) {
|
|
Item front = d->itemListMap[FRONT_COVER];
|
|
if (Item::Binary == front.type()) {
|
|
ByteVector picture = front.binaryData();
|
|
const size_t index = picture.find('\0');
|
|
if (index < picture.size()) {
|
|
ByteVector desc = picture.mid(0, index + 1);
|
|
String mime = "image/jpeg";
|
|
ByteVector data = picture.mid(index + 1);
|
|
Picture p(data, Picture::FrontCover, mime, desc);
|
|
map.insert(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (d->itemListMap.contains(BACK_COVER)) {
|
|
Item back = d->itemListMap[BACK_COVER];
|
|
if (Item::Binary == back.type()) {
|
|
ByteVector picture = back.binaryData();
|
|
const size_t index = picture.find('\0');
|
|
if (index < picture.size()) {
|
|
ByteVector desc = picture.mid(0, index + 1);
|
|
String mime = "image/jpeg";
|
|
ByteVector data = picture.mid(index + 1);
|
|
Picture p(data, Picture::BackCover, mime, desc);
|
|
map.insert(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
return PictureMap(map);
|
|
|
|
}
|
|
|
|
void APE::Tag::setTitle(const String &s) {
|
|
addValue("TITLE", s, true);
|
|
}
|
|
|
|
void APE::Tag::setArtist(const String &s) {
|
|
addValue("ARTIST", s, true);
|
|
}
|
|
|
|
void APE::Tag::setAlbum(const String &s) {
|
|
addValue("ALBUM", s, true);
|
|
}
|
|
|
|
void APE::Tag::setComment(const String &s) {
|
|
addValue("COMMENT", s, true);
|
|
}
|
|
|
|
void APE::Tag::setGenre(const String &s) {
|
|
addValue("GENRE", s, true);
|
|
}
|
|
|
|
void APE::Tag::setYear(unsigned int i) {
|
|
|
|
if (i == 0)
|
|
removeItem("YEAR");
|
|
else
|
|
addValue("YEAR", String::number(i), true);
|
|
|
|
}
|
|
|
|
void APE::Tag::setTrack(unsigned int i) {
|
|
|
|
if (i == 0)
|
|
removeItem("TRACK");
|
|
else
|
|
addValue("TRACK", String::number(i), true);
|
|
|
|
}
|
|
|
|
void APE::Tag::setPictures(const PictureMap &l) {
|
|
|
|
removeItem(FRONT_COVER);
|
|
removeItem(BACK_COVER);
|
|
|
|
for (PictureMap::ConstIterator pictureMapIt = l.begin(); pictureMapIt != l.end(); ++pictureMapIt) {
|
|
Picture::Type type = pictureMapIt->first;
|
|
if (Picture::FrontCover != type && Picture::BackCover != type) {
|
|
std::cout << "APE: Trying to add a picture with wrong type" << std::endl;
|
|
continue;
|
|
}
|
|
|
|
const char *id;
|
|
switch (type) {
|
|
case Picture::FrontCover:
|
|
id = FRONT_COVER;
|
|
break;
|
|
case Picture::BackCover:
|
|
id = BACK_COVER;
|
|
break;
|
|
default:
|
|
id = FRONT_COVER;
|
|
break;
|
|
}
|
|
|
|
PictureList list = pictureMapIt->second;
|
|
for (PictureList::ConstIterator pictureListIt = list.begin(); pictureListIt != list.end(); ++pictureListIt) {
|
|
Picture picture = *pictureListIt;
|
|
if (d->itemListMap.contains(id)) {
|
|
std::cout << "APE: Already added a picture of type "
|
|
<< id
|
|
<< " '"
|
|
<< picture.description()
|
|
<< "' "
|
|
<< "and next are being ignored"
|
|
<< std::endl;
|
|
break;
|
|
}
|
|
|
|
ByteVector data = picture.description().data(String::Latin1).append('\0').append(picture.data());
|
|
|
|
Item item;
|
|
item.setKey(id);
|
|
item.setType(Item::Binary);
|
|
item.setBinaryData(data);
|
|
setItem(item.key(), item);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
namespace {
|
|
// conversions of tag keys between what we use in PropertyMap and what's usual
|
|
// for APE tags
|
|
// usual, APE
|
|
const char *keyConversions[][2] = { { "TRACKNUMBER", "TRACK" },
|
|
{ "DATE", "YEAR" },
|
|
{ "ALBUMARTIST", "ALBUM ARTIST" },
|
|
{ "DISCNUMBER", "DISC" },
|
|
{ "REMIXER", "MIXARTIST" } };
|
|
const size_t keyConversionsSize = sizeof(keyConversions) / sizeof(keyConversions[0]);
|
|
} // namespace
|
|
|
|
PropertyMap APE::Tag::properties() const {
|
|
|
|
PropertyMap properties;
|
|
ItemListMap::ConstIterator it = itemListMap().begin();
|
|
for (; it != itemListMap().end(); ++it) {
|
|
String tagName = it->first.upper();
|
|
// if the item is Binary or Locator, or if the key is an invalid string,
|
|
// add to unsupportedData
|
|
if (it->second.type() != Item::Text || tagName.isEmpty()) {
|
|
properties.unsupportedData().append(it->first);
|
|
}
|
|
else {
|
|
// Some tags need to be handled specially
|
|
for (size_t i = 0; i < keyConversionsSize; ++i) {
|
|
if (tagName == keyConversions[i][1])
|
|
tagName = keyConversions[i][0];
|
|
}
|
|
properties[tagName].append(it->second.values());
|
|
}
|
|
}
|
|
return properties;
|
|
|
|
}
|
|
|
|
void APE::Tag::removeUnsupportedProperties(const StringList &properties) {
|
|
|
|
StringList::ConstIterator it = properties.begin();
|
|
for (; it != properties.end(); ++it)
|
|
removeItem(*it);
|
|
|
|
}
|
|
|
|
PropertyMap APE::Tag::setProperties(const PropertyMap &origProps) {
|
|
|
|
PropertyMap properties(origProps); // make a local copy that can be modified
|
|
|
|
// see comment in properties()
|
|
for (size_t i = 0; i < keyConversionsSize; ++i)
|
|
if (properties.contains(keyConversions[i][0])) {
|
|
properties.insert(keyConversions[i][1], properties[keyConversions[i][0]]);
|
|
properties.erase(keyConversions[i][0]);
|
|
}
|
|
|
|
// first check if tags need to be removed completely
|
|
StringList toRemove;
|
|
ItemListMap::ConstIterator remIt = itemListMap().begin();
|
|
for (; remIt != itemListMap().end(); ++remIt) {
|
|
String key = remIt->first.upper();
|
|
// only remove if a) key is valid, b) type is text, c) key not contained in new properties
|
|
if (!key.isEmpty() && remIt->second.type() == APE::Item::Text && !properties.contains(key))
|
|
toRemove.append(remIt->first);
|
|
}
|
|
|
|
for (StringList::ConstIterator removeIt = toRemove.begin(); removeIt != toRemove.end(); removeIt++)
|
|
removeItem(*removeIt);
|
|
|
|
// now sync in the "forward direction"
|
|
PropertyMap::ConstIterator it = properties.begin();
|
|
PropertyMap invalid;
|
|
for (; it != properties.end(); ++it) {
|
|
const String &tagName = it->first;
|
|
if (!checkKey(tagName))
|
|
invalid.insert(it->first, it->second);
|
|
else if (!(itemListMap().contains(tagName)) || !(itemListMap()[tagName].values() == it->second)) {
|
|
if (it->second.isEmpty())
|
|
removeItem(tagName);
|
|
else {
|
|
StringList::ConstIterator valueIt = it->second.begin();
|
|
addValue(tagName, *valueIt, true);
|
|
++valueIt;
|
|
for (; valueIt != it->second.end(); ++valueIt)
|
|
addValue(tagName, *valueIt, false);
|
|
}
|
|
}
|
|
}
|
|
return invalid;
|
|
}
|
|
|
|
bool APE::Tag::checkKey(const String &key) {
|
|
|
|
if (key.size() < MinKeyLength || key.size() > MaxKeyLength)
|
|
return false;
|
|
|
|
return isKeyValid(key.data(String::UTF8));
|
|
|
|
}
|
|
|
|
APE::Footer *APE::Tag::footer() const {
|
|
return &d->footer;
|
|
}
|
|
|
|
const APE::ItemListMap &APE::Tag::itemListMap() const {
|
|
return d->itemListMap;
|
|
}
|
|
|
|
void APE::Tag::removeItem(const String &key) {
|
|
d->itemListMap.erase(key.upper());
|
|
}
|
|
|
|
void APE::Tag::addValue(const String &key, const String &value, bool replace) {
|
|
|
|
if (replace)
|
|
removeItem(key);
|
|
|
|
if (value.isEmpty())
|
|
return;
|
|
|
|
// Text items may contain more than one value.
|
|
// Binary or locator items may have only one value, hence always replaced.
|
|
|
|
ItemListMap::Iterator it = d->itemListMap.find(key.upper());
|
|
|
|
if (it != d->itemListMap.end() && it->second.type() == Item::Text)
|
|
it->second.appendValue(value);
|
|
else
|
|
setItem(key, Item(key, value));
|
|
|
|
}
|
|
|
|
void APE::Tag::setData(const String &key, const ByteVector &value) {
|
|
|
|
removeItem(key);
|
|
|
|
if (value.isEmpty())
|
|
return;
|
|
|
|
setItem(key, Item(key, value, true));
|
|
|
|
}
|
|
|
|
void APE::Tag::setItem(const String &key, const Item &item) {
|
|
|
|
if (!checkKey(key)) {
|
|
debug("APE::Tag::setItem() - Couldn't set an item due to an invalid key.");
|
|
return;
|
|
}
|
|
|
|
d->itemListMap[key.upper()] = item;
|
|
|
|
}
|
|
|
|
bool APE::Tag::isEmpty() const {
|
|
return d->itemListMap.isEmpty();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// protected methods
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void APE::Tag::read() {
|
|
|
|
if (d->file && d->file->isValid()) {
|
|
|
|
d->file->seek(d->footerLocation);
|
|
d->footer.setData(d->file->readBlock(Footer::size()));
|
|
|
|
if (d->footer.tagSize() <= Footer::size() ||
|
|
d->footer.tagSize() > static_cast<unsigned long>(d->file->length()))
|
|
return;
|
|
|
|
d->file->seek(d->footerLocation + Footer::size() - d->footer.tagSize());
|
|
parse(d->file->readBlock(d->footer.tagSize() - Footer::size()));
|
|
}
|
|
|
|
}
|
|
|
|
ByteVector APE::Tag::render() const {
|
|
|
|
ByteVector data;
|
|
unsigned int itemCount = 0;
|
|
|
|
for (ItemListMap::ConstIterator it = d->itemListMap.begin(); it != d->itemListMap.end(); ++it) {
|
|
data.append(it->second.render());
|
|
itemCount++;
|
|
}
|
|
|
|
d->footer.setItemCount(itemCount);
|
|
d->footer.setTagSize(data.size() + Footer::size());
|
|
d->footer.setHeaderPresent(true);
|
|
|
|
return d->footer.renderHeader() + data + d->footer.renderFooter();
|
|
|
|
}
|
|
|
|
void APE::Tag::parse(const ByteVector &data) {
|
|
|
|
// 11 bytes is the minimum size for an APE item
|
|
|
|
if (data.size() < 11)
|
|
return;
|
|
|
|
size_t pos = 0;
|
|
|
|
for (unsigned int i = 0; i < d->footer.itemCount() && pos <= data.size() - 11; i++) {
|
|
|
|
const size_t nullPos = data.find('\0', pos + 8);
|
|
if (nullPos == ByteVector::npos()) {
|
|
debug("APE::Tag::parse() - Couldn't find a key/value separator. Stopped parsing.");
|
|
return;
|
|
}
|
|
|
|
const size_t keyLength = nullPos - pos - 8;
|
|
const size_t valLegnth = data.toUInt32LE(pos);
|
|
|
|
if (keyLength >= MinKeyLength && keyLength <= MaxKeyLength && isKeyValid(data.mid(pos + 8, keyLength))) {
|
|
APE::Item item;
|
|
item.parse(data.mid(pos));
|
|
|
|
d->itemListMap.insert(item.key().upper(), item);
|
|
}
|
|
else {
|
|
debug("APE::Tag::parse() - Skipped an item due to an invalid key.");
|
|
}
|
|
|
|
pos += keyLength + valLegnth + 9;
|
|
}
|
|
|
|
}
|