You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdemultimedia/juk/covermanager.cpp

576 lines
15 KiB

/***************************************************************************
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 <tqpixmap.h>
#include <tqmap.h>
#include <tqstring.h>
#include <tqfile.h>
#include <tqimage.h>
#include <tqdir.h>
#include <tqdatastream.h>
#include <tqdict.h>
#include <tqcache.h>
#include <tqmime.h>
#include <tqbuffer.h>
#include <kdebug.h>
#include <kstaticdeleter.h>
#include <kstandarddirs.h>
#include <tdeglobal.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 TQDict<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 TQPixmaps for the covers so that the covers are not all kept in
// memory for no reason.
typedef TQCache<TQPixmap> CoverPixmapCache;
CoverManagerPrivate *CoverManager::m_data = 0;
// Used to save and load CoverData from a TQDataStream
TQDataStream &operator<<(TQDataStream &out, const CoverData &data);
TQDataStream &operator>>(TQDataStream &in, CoverData &data);
//
// Implementation of CoverData struct
//
TQPixmap CoverData::pixmap() const
{
return CoverManager::coverFromData(*this, CoverManager::FullSize);
}
TQPixmap 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.
*/
TQString coverLocation() const;
};
//
// Implementation of CoverManagerPrivate methods.
//
void CoverManagerPrivate::createDataDir() const
{
TQDir dir;
TQString dirPath(TQDir::cleanDirPath(coverLocation() + "/.."));
if(!dir.exists(dirPath))
TDEStandardDirs::makeDir(dirPath);
}
void CoverManagerPrivate::saveCovers() const
{
kdDebug() << k_funcinfo << endl;
// Make sure the directory exists first.
createDataDir();
TQFile file(coverLocation());
kdDebug() << "Opening covers db: " << coverLocation() << endl;
if(!file.open(IO_WriteOnly)) {
kdError() << "Unable to save covers to disk!\n";
return;
}
TQDataStream out(&file);
// Write out the version and count
out << TQ_UINT32(0) << TQ_UINT32(covers.count());
// Write out the data
for(CoverDataMap::ConstIterator it = covers.begin(); it != covers.end(); ++it) {
out << TQ_UINT32(it.key());
out << *it.data();
}
// Now write out the track mapping.
out << TQ_UINT32(tracks.count());
TQDictIterator<coverKey> trackMapIt(tracks);
while(trackMapIt.current()) {
out << trackMapIt.currentKey() << TQ_UINT32(*trackMapIt.current());
++trackMapIt;
}
}
void CoverManagerPrivate::loadCovers()
{
kdDebug() << k_funcinfo << endl;
TQFile file(coverLocation());
if(!file.open(IO_ReadOnly)) {
// Guess we don't have any covers yet.
return;
}
TQDataStream in(&file);
TQ_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(TQ_UINT32 i = 0; i < count; ++i) {
// Read the id, and 3 TQStrings for every 1 of the count.
TQ_UINT32 id;
CoverDataPtr data(new CoverData);
in >> id;
in >> *data;
data->refCount = 0;
covers[(coverKey) id] = data;
}
in >> count;
for(TQ_UINT32 i = 0; i < count; ++i) {
TQString path;
TQ_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));
}
}
}
TQString CoverManagerPrivate::coverLocation() const
{
return TDEGlobal::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, TQWidget *src) : TQDragObject(src, "coverDrag"),
m_id(id)
{
TQPixmap 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;
}
TQByteArray CoverDrag::encodedData(const char *mimetype) const
{
if(qstrcmp(CoverDrag::mimetype, mimetype) == 0) {
TQByteArray data;
TQDataStream ds(data, IO_WriteOnly);
ds << TQ_UINT32(m_id);
return data;
}
else if(qstrcmp(mimetype, "image/png") == 0) {
TQPixmap large = CoverManager::coverFromId(m_id, CoverManager::FullSize);
TQImage img = large.convertToImage();
TQByteArray data;
TQBuffer buffer(data);
buffer.open(IO_WriteOnly);
img.save(&buffer, "PNG"); // Write in PNG format.
return data;
}
return TQByteArray();
}
bool CoverDrag::canDecode(const TQMimeSource *e)
{
return e->provides(mimetype);
}
bool CoverDrag::decode(const TQMimeSource *e, coverKey &id)
{
if(!canDecode(e))
return false;
TQByteArray data = e->encodedData(mimetype);
TQDataStream ds(data, IO_ReadOnly);
TQ_UINT32 i;
ds >> i;
id = (coverKey) i;
return true;
}
//
// Implementation of CoverManager methods.
//
coverKey CoverManager::idFromMetadata(const TQString &artist, const TQString &album)
{
// Search for the string, yay! It might make sense to use a cache here,
// if so it's not hard to add a TQCache.
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;
}
TQPixmap CoverManager::coverFromId(coverKey id, Size size)
{
CoverDataPtr info = coverInfo(id);
if(!info)
return TQPixmap();
if(size == Thumbnail)
return info->thumbnail();
return info->pixmap();
}
TQPixmap CoverManager::coverFromData(const CoverData &coverData, Size size)
{
TQString 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.
TQPixmap *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 TQPixmap(coverData.path);
if(pix->isNull())
return TQPixmap();
if(size == Thumbnail) {
// Convert to image for smoothScale()
TQImage image = pix->convertToImage();
pix->convertFromImage(image.smoothScale(80, 80, TQImage::ScaleMin));
}
TQPixmap returnValue = *pix; // Save it early.
if(!data()->pixmapCache.insert(path, pix, pix->height() * pix->width()))
delete pix;
return returnValue;
}
coverKey CoverManager::addCover(const TQPixmap &large, const TQString &artist, const TQString &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!
TQString ext = TQString("/coverdb/coverID-%1.png").arg(id);
coverData->path = TDEGlobal::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(TQString("f%1").arg(coverData->path));
data()->pixmapCache.remove(TQString("t%1").arg(coverData->path));
return id;
}
coverKey CoverManager::addCover(const TQString &path, const TQString &artist, const TQString &album)
{
return addCover(TQPixmap(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(TQString("f%1").arg(coverData->path));
data()->pixmapCache.remove(TQString("t%1").arg(coverData->path));
// Remove references to files that had that track ID.
TQDictIterator<coverKey> it(data()->tracks);
for(; it.current(); ++it)
if(*it.current() == id)
data()->tracks.remove(it.currentKey());
// Remove covers from disk.
TQFile::remove(coverData->path);
// Finally, forget that we ever knew about this cover.
data()->covers.remove(id);
return true;
}
bool CoverManager::replaceCover(coverKey id, const TQPixmap &large)
{
if(!hasCover(id))
return false;
CoverDataPtr coverData = coverInfo(id);
// Empty old pixmaps from cache.
data()->pixmapCache.remove(TQString("%1%2").arg("t", coverData->path));
data()->pixmapCache.remove(TQString("%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();
}
TQValueList<coverKey> CoverManager::keys()
{
return data()->covers.keys();
}
void CoverManager::setIdForTrack(const TQString &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 TQString &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.
*/
TQDataStream &operator<<(TQDataStream &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.
*/
TQDataStream &operator>>(TQDataStream &in, CoverData &data)
{
in >> data.artist;
in >> data.album;
in >> data.path;
return in;
}