summaryrefslogtreecommitdiffstats
path: root/juk/covermanager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'juk/covermanager.cpp')
-rw-r--r--juk/covermanager.cpp577
1 files changed, 577 insertions, 0 deletions
diff --git a/juk/covermanager.cpp b/juk/covermanager.cpp
new file mode 100644
index 00000000..1fe36cc4
--- /dev/null
+++ b/juk/covermanager.cpp
@@ -0,0 +1,577 @@
+/***************************************************************************
+ begin : Sun May 15 2005
+ copyright : (C) 2005 by Michael Pyne
+ email : michael.pyne@kdemail.net
+***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#include <qpixmap.h>
+#include <qmap.h>
+#include <qstring.h>
+#include <qfile.h>
+#include <qimage.h>
+#include <qdir.h>
+#include <qdatastream.h>
+#include <qdict.h>
+#include <qcache.h>
+#include <qmime.h>
+#include <qbuffer.h>
+
+#include <kdebug.h>
+#include <kstaticdeleter.h>
+#include <kstandarddirs.h>
+#include <kglobal.h>
+
+#include "covermanager.h"
+
+// This is a dictionary to map the track path to their ID. Otherwise we'd have
+// to store this info with each CollectionListItem, which would break the cache
+// of users who upgrade, and would just generally be a big mess.
+typedef QDict<coverKey> TrackLookupMap;
+
+// This is responsible for making sure that the CoverManagerPrivate class
+// gets properly destructed on shutdown.
+static KStaticDeleter<CoverManagerPrivate> sd;
+
+const char *CoverDrag::mimetype = "application/x-juk-coverid";
+// Caches the QPixmaps for the covers so that the covers are not all kept in
+// memory for no reason.
+typedef QCache<QPixmap> CoverPixmapCache;
+
+CoverManagerPrivate *CoverManager::m_data = 0;
+
+// Used to save and load CoverData from a QDataStream
+QDataStream &operator<<(QDataStream &out, const CoverData &data);
+QDataStream &operator>>(QDataStream &in, CoverData &data);
+
+//
+// Implementation of CoverData struct
+//
+
+QPixmap CoverData::pixmap() const
+{
+ return CoverManager::coverFromData(*this, CoverManager::FullSize);
+}
+
+QPixmap CoverData::thumbnail() const
+{
+ return CoverManager::coverFromData(*this, CoverManager::Thumbnail);
+}
+
+/**
+ * This class is responsible for actually keeping track of the storage for the
+ * different covers and such. It holds the covers, and the map of path names
+ * to cover ids, and has a few utility methods to load and save the data.
+ *
+ * @author Michael Pyne <michael.pyne@kdemail.net>
+ * @see CoverManager
+ */
+class CoverManagerPrivate
+{
+public:
+
+ /// Maps coverKey id's to CoverDataPtrs
+ CoverDataMap covers;
+
+ /// Maps file names to coverKey id's.
+ TrackLookupMap tracks;
+
+ /// A cache of the cover representations. The key format is:
+ /// 'f' followed by the pathname for FullSize covers, and
+ /// 't' followed by the pathname for Thumbnail covers.
+ CoverPixmapCache pixmapCache;
+
+ CoverManagerPrivate() : tracks(1301), pixmapCache(2 * 1024 * 768)
+ {
+ loadCovers();
+ pixmapCache.setAutoDelete(true);
+ }
+
+ ~CoverManagerPrivate()
+ {
+ saveCovers();
+ }
+
+ /**
+ * Creates the data directory for the covers if it doesn't already exist.
+ * Must be in this class for loadCovers() and saveCovers().
+ */
+ void createDataDir() const;
+
+ /**
+ * Returns the next available unused coverKey that can be used for
+ * inserting new items.
+ *
+ * @return unused id that can be used for new CoverData
+ */
+ coverKey nextId() const;
+
+ void saveCovers() const;
+
+ private:
+ void loadCovers();
+
+ /**
+ * @return the full path and filename of the file storing the cover
+ * lookup map and the translations between pathnames and ids.
+ */
+ QString coverLocation() const;
+};
+
+//
+// Implementation of CoverManagerPrivate methods.
+//
+void CoverManagerPrivate::createDataDir() const
+{
+ QDir dir;
+ QString dirPath(QDir::cleanDirPath(coverLocation() + "/.."));
+ if(!dir.exists(dirPath))
+ KStandardDirs::makeDir(dirPath);
+}
+
+void CoverManagerPrivate::saveCovers() const
+{
+ kdDebug() << k_funcinfo << endl;
+
+ // Make sure the directory exists first.
+ createDataDir();
+
+ QFile file(coverLocation());
+
+ kdDebug() << "Opening covers db: " << coverLocation() << endl;
+
+ if(!file.open(IO_WriteOnly)) {
+ kdError() << "Unable to save covers to disk!\n";
+ return;
+ }
+
+ QDataStream out(&file);
+
+ // Write out the version and count
+ out << Q_UINT32(0) << Q_UINT32(covers.count());
+
+ // Write out the data
+ for(CoverDataMap::ConstIterator it = covers.begin(); it != covers.end(); ++it) {
+ out << Q_UINT32(it.key());
+ out << *it.data();
+ }
+
+ // Now write out the track mapping.
+ out << Q_UINT32(tracks.count());
+
+ QDictIterator<coverKey> trackMapIt(tracks);
+ while(trackMapIt.current()) {
+ out << trackMapIt.currentKey() << Q_UINT32(*trackMapIt.current());
+ ++trackMapIt;
+ }
+}
+
+void CoverManagerPrivate::loadCovers()
+{
+ kdDebug() << k_funcinfo << endl;
+
+ QFile file(coverLocation());
+
+ if(!file.open(IO_ReadOnly)) {
+ // Guess we don't have any covers yet.
+ return;
+ }
+
+ QDataStream in(&file);
+ Q_UINT32 count, version;
+
+ // First thing we'll read in will be the version.
+ // Only version 0 is defined for now.
+ in >> version;
+ if(version > 0) {
+ kdError() << "Cover database was created by a higher version of JuK,\n";
+ kdError() << "I don't know what to do with it.\n";
+
+ return;
+ }
+
+ // Read in the count next, then the data.
+ in >> count;
+ for(Q_UINT32 i = 0; i < count; ++i) {
+ // Read the id, and 3 QStrings for every 1 of the count.
+ Q_UINT32 id;
+ CoverDataPtr data(new CoverData);
+
+ in >> id;
+ in >> *data;
+ data->refCount = 0;
+
+ covers[(coverKey) id] = data;
+ }
+
+ in >> count;
+ for(Q_UINT32 i = 0; i < count; ++i) {
+ QString path;
+ Q_UINT32 id;
+
+ in >> path >> id;
+
+ // If we somehow already managed to load a cover id with this path,
+ // don't do so again. Possible due to a coding error during 3.5
+ // development.
+
+ if(!tracks.find(path)) {
+ ++covers[(coverKey) id]->refCount; // Another track using this.
+ tracks.insert(path, new coverKey(id));
+ }
+ }
+}
+
+QString CoverManagerPrivate::coverLocation() const
+{
+ return KGlobal::dirs()->saveLocation("appdata") + "coverdb/covers";
+}
+
+// XXX: This could probably use some improvement, I don't like the linear
+// search for ID idea.
+coverKey CoverManagerPrivate::nextId() const
+{
+ // Start from 1...
+ coverKey key = 1;
+
+ while(covers.contains(key))
+ ++key;
+
+ return key;
+}
+
+//
+// Implementation of CoverDrag
+//
+CoverDrag::CoverDrag(coverKey id, QWidget *src) : QDragObject(src, "coverDrag"),
+ m_id(id)
+{
+ QPixmap cover = CoverManager::coverFromId(id);
+ if(!cover.isNull())
+ setPixmap(cover);
+}
+
+const char *CoverDrag::format(int i) const
+{
+ if(i == 0)
+ return mimetype;
+ if(i == 1)
+ return "image/png";
+
+ return 0;
+}
+
+QByteArray CoverDrag::encodedData(const char *mimetype) const
+{
+ if(qstrcmp(CoverDrag::mimetype, mimetype) == 0) {
+ QByteArray data;
+ QDataStream ds(data, IO_WriteOnly);
+
+ ds << Q_UINT32(m_id);
+ return data;
+ }
+ else if(qstrcmp(mimetype, "image/png") == 0) {
+ QPixmap large = CoverManager::coverFromId(m_id, CoverManager::FullSize);
+ QImage img = large.convertToImage();
+ QByteArray data;
+ QBuffer buffer(data);
+
+ buffer.open(IO_WriteOnly);
+ img.save(&buffer, "PNG"); // Write in PNG format.
+
+ return data;
+ }
+
+ return QByteArray();
+}
+
+bool CoverDrag::canDecode(const QMimeSource *e)
+{
+ return e->provides(mimetype);
+}
+
+bool CoverDrag::decode(const QMimeSource *e, coverKey &id)
+{
+ if(!canDecode(e))
+ return false;
+
+ QByteArray data = e->encodedData(mimetype);
+ QDataStream ds(data, IO_ReadOnly);
+ Q_UINT32 i;
+
+ ds >> i;
+ id = (coverKey) i;
+
+ return true;
+}
+
+//
+// Implementation of CoverManager methods.
+//
+coverKey CoverManager::idFromMetadata(const QString &artist, const QString &album)
+{
+ // Search for the string, yay! It might make sense to use a cache here,
+ // if so it's not hard to add a QCache.
+ CoverDataMap::ConstIterator it = begin();
+ CoverDataMap::ConstIterator endIt = end();
+
+ for(; it != endIt; ++it) {
+ if(it.data()->album == album.lower() && it.data()->artist == artist.lower())
+ return it.key();
+ }
+
+ return NoMatch;
+}
+
+QPixmap CoverManager::coverFromId(coverKey id, Size size)
+{
+ CoverDataPtr info = coverInfo(id);
+
+ if(!info)
+ return QPixmap();
+
+ if(size == Thumbnail)
+ return info->thumbnail();
+
+ return info->pixmap();
+}
+
+QPixmap CoverManager::coverFromData(const CoverData &coverData, Size size)
+{
+ QString path = coverData.path;
+
+ // Prepend a tag to the path to separate in the cache between full size
+ // and thumbnail pixmaps. If we add a different kind of pixmap in the
+ // future we also need to add a tag letter for it.
+ if(size == FullSize)
+ path.prepend('f');
+ else
+ path.prepend('t');
+
+ // Check in cache for the pixmap.
+ QPixmap *pix = data()->pixmapCache[path];
+ if(pix) {
+ kdDebug(65432) << "Found pixmap in cover cache.\n";
+ return *pix;
+ }
+
+ // Not in cache, load it and add it.
+ pix = new QPixmap(coverData.path);
+ if(pix->isNull())
+ return QPixmap();
+
+ if(size == Thumbnail) {
+ // Convert to image for smoothScale()
+ QImage image = pix->convertToImage();
+ pix->convertFromImage(image.smoothScale(80, 80, QImage::ScaleMin));
+ }
+
+ QPixmap returnValue = *pix; // Save it early.
+ if(!data()->pixmapCache.insert(path, pix, pix->height() * pix->width()))
+ delete pix;
+
+ return returnValue;
+}
+
+coverKey CoverManager::addCover(const QPixmap &large, const QString &artist, const QString &album)
+{
+ kdDebug() << k_funcinfo << endl;
+
+ coverKey id = data()->nextId();
+ CoverDataPtr coverData(new CoverData);
+
+ if(large.isNull()) {
+ kdDebug() << "The pixmap you're trying to add is NULL!\n";
+ return NoMatch;
+ }
+
+ // Save it to file first!
+
+ QString ext = QString("/coverdb/coverID-%1.png").arg(id);
+ coverData->path = KGlobal::dirs()->saveLocation("appdata") + ext;
+
+ kdDebug() << "Saving pixmap to " << coverData->path << endl;
+ data()->createDataDir();
+
+ if(!large.save(coverData->path, "PNG")) {
+ kdError() << "Unable to save pixmap to " << coverData->path << endl;
+ return NoMatch;
+ }
+
+ coverData->artist = artist.lower();
+ coverData->album = album.lower();
+ coverData->refCount = 0;
+
+ data()->covers[id] = coverData;
+
+ // Make sure the new cover isn't inadvertently cached.
+ data()->pixmapCache.remove(QString("f%1").arg(coverData->path));
+ data()->pixmapCache.remove(QString("t%1").arg(coverData->path));
+
+ return id;
+}
+
+coverKey CoverManager::addCover(const QString &path, const QString &artist, const QString &album)
+{
+ return addCover(QPixmap(path), artist, album);
+}
+
+bool CoverManager::hasCover(coverKey id)
+{
+ return data()->covers.contains(id);
+}
+
+bool CoverManager::removeCover(coverKey id)
+{
+ if(!hasCover(id))
+ return false;
+
+ // Remove cover from cache.
+ CoverDataPtr coverData = coverInfo(id);
+ data()->pixmapCache.remove(QString("f%1").arg(coverData->path));
+ data()->pixmapCache.remove(QString("t%1").arg(coverData->path));
+
+ // Remove references to files that had that track ID.
+ QDictIterator<coverKey> it(data()->tracks);
+ for(; it.current(); ++it)
+ if(*it.current() == id)
+ data()->tracks.remove(it.currentKey());
+
+ // Remove covers from disk.
+ QFile::remove(coverData->path);
+
+ // Finally, forget that we ever knew about this cover.
+ data()->covers.remove(id);
+
+ return true;
+}
+
+bool CoverManager::replaceCover(coverKey id, const QPixmap &large)
+{
+ if(!hasCover(id))
+ return false;
+
+ CoverDataPtr coverData = coverInfo(id);
+
+ // Empty old pixmaps from cache.
+ data()->pixmapCache.remove(QString("%1%2").arg("t", coverData->path));
+ data()->pixmapCache.remove(QString("%1%2").arg("f", coverData->path));
+
+ large.save(coverData->path, "PNG");
+ return true;
+}
+
+CoverManagerPrivate *CoverManager::data()
+{
+ if(!m_data)
+ sd.setObject(m_data, new CoverManagerPrivate);
+
+ return m_data;
+}
+
+void CoverManager::saveCovers()
+{
+ data()->saveCovers();
+}
+
+void CoverManager::shutdown()
+{
+ sd.destructObject();
+}
+
+CoverDataMap::ConstIterator CoverManager::begin()
+{
+ return data()->covers.constBegin();
+}
+
+CoverDataMap::ConstIterator CoverManager::end()
+{
+ return data()->covers.constEnd();
+}
+
+QValueList<coverKey> CoverManager::keys()
+{
+ return data()->covers.keys();
+}
+
+void CoverManager::setIdForTrack(const QString &path, coverKey id)
+{
+ coverKey *oldId = data()->tracks.find(path);
+ if(oldId && (id == *oldId))
+ return; // We're already done.
+
+ if(oldId && *oldId != NoMatch) {
+ data()->covers[*oldId]->refCount--;
+ data()->tracks.remove(path);
+
+ if(data()->covers[*oldId]->refCount == 0) {
+ kdDebug(65432) << "Cover " << *oldId << " is unused, removing.\n";
+ removeCover(*oldId);
+ }
+ }
+
+ if(id != NoMatch) {
+ data()->covers[id]->refCount++;
+ data()->tracks.insert(path, new coverKey(id));
+ }
+}
+
+coverKey CoverManager::idForTrack(const QString &path)
+{
+ coverKey *coverPtr = data()->tracks.find(path);
+
+ if(!coverPtr)
+ return NoMatch;
+
+ return *coverPtr;
+}
+
+CoverDataPtr CoverManager::coverInfo(coverKey id)
+{
+ if(data()->covers.contains(id))
+ return data()->covers[id];
+
+ return CoverDataPtr(0);
+}
+
+/**
+ * Write @p data out to @p out.
+ *
+ * @param out the data stream to write @p data out to.
+ * @param data the CoverData to write out.
+ * @return the data stream that the data was written to.
+ */
+QDataStream &operator<<(QDataStream &out, const CoverData &data)
+{
+ out << data.artist;
+ out << data.album;
+ out << data.path;
+
+ return out;
+}
+
+/**
+ * Read @p data from @p in.
+ *
+ * @param in the data stream to read from.
+ * @param data the CoverData to read into.
+ * @return the data stream read from.
+ */
+QDataStream &operator>>(QDataStream &in, CoverData &data)
+{
+ in >> data.artist;
+ in >> data.album;
+ in >> data.path;
+
+ return in;
+}
+
+// vim: set et sw=4 ts=4: