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.
578 lines
15 KiB
578 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 <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 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))
|
|
KStandardDirs::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 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, 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::tqencodedData(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->tqencodedData(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, TQ_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").tqarg(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(TQString("f%1").tqarg(coverData->path));
|
|
data()->pixmapCache.remove(TQString("t%1").tqarg(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").tqarg(coverData->path));
|
|
data()->pixmapCache.remove(TQString("t%1").tqarg(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").tqarg("t", coverData->path));
|
|
data()->pixmapCache.remove(TQString("%1%2").tqarg("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;
|
|
}
|
|
|
|
// vim: set et sw=4 ts=4:
|