covermanager.cpp 14.4 KB
Newer Older
1
/***************************************************************************
2
    begin                : Sun May 15 2005
3 4 5 6 7 8 9 10 11 12 13 14 15
    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.                                   *
 *                                                                         *
 ***************************************************************************/

16 17 18
#include "covermanager.h"

#include <QPixmap>
19 20
#include <QString>
#include <QFile>
21
#include <QImage>
22 23
#include <QDir>
#include <QDataStream>
24 25 26 27
#include <Q3Dict>
#include <Q3Cache>
#include <QMimeSource>
#include <QBuffer>
28 29
#include <QList>
#include <QMap>
30 31 32 33 34 35 36 37 38

#include <kdebug.h>
#include <kstaticdeleter.h>
#include <kstandarddirs.h>
#include <kglobal.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.
Laurent Montel's avatar
Laurent Montel committed
39
typedef Q3Dict<coverKey> TrackLookupMap;
40 41 42 43 44

// This is responsible for making sure that the CoverManagerPrivate class
// gets properly destructed on shutdown.
static KStaticDeleter<CoverManagerPrivate> sd;

Michael Pyne's avatar
Michael Pyne committed
45
const char *CoverDrag::mimetype = "application/x-juk-coverid";
46 47
// Caches the QPixmaps for the covers so that the covers are not all kept in
// memory for no reason.
Laurent Montel's avatar
Laurent Montel committed
48
typedef Q3Cache<QPixmap> CoverPixmapCache;
49

50 51 52 53 54 55 56 57 58 59 60 61
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
{
62
    return CoverManager::coverFromData(*this, CoverManager::FullSize);
63 64 65 66
}

QPixmap CoverData::thumbnail() const
{
67
    return CoverManager::coverFromData(*this, CoverManager::Thumbnail);
68 69 70 71 72 73 74 75 76 77 78 79 80
}

/**
 * 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:
81 82 83 84 85 86 87

    /// Maps coverKey id's to CoverDataPtrs
    CoverDataMap covers;

    /// Maps file names to coverKey id's.
    TrackLookupMap tracks;

88 89 90 91 92 93
    /// 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)
94 95
    {
        loadCovers();
96
        pixmapCache.setAutoDelete(true);
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
    }

    ~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;

118 119
    void saveCovers() const;

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
    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;
136
    QString dirPath(QDir::cleanPath(coverLocation() + "/.."));
137 138 139 140 141 142
    if(!dir.exists(dirPath))
        KStandardDirs::makeDir(dirPath);
}

void CoverManagerPrivate::saveCovers() const
{
Laurent Montel's avatar
Laurent Montel committed
143
    kDebug() << k_funcinfo << endl;
144 145 146 147 148 149

    // Make sure the directory exists first.
    createDataDir();

    QFile file(coverLocation());

Laurent Montel's avatar
Laurent Montel committed
150
    kDebug() << "Opening covers db: " << coverLocation() << endl;
151

Laurent Montel's avatar
Laurent Montel committed
152
    if(!file.open(QIODevice::WriteOnly)) {
Laurent Montel's avatar
Laurent Montel committed
153
        kError() << "Unable to save covers to disk!\n";
154 155 156 157 158 159
        return;
    }

    QDataStream out(&file);

    // Write out the version and count
Laurent Montel's avatar
Laurent Montel committed
160
    out << quint32(0) << quint32(covers.count());
161 162

    // Write out the data
163
    for(CoverDataMap::const_iterator it = covers.begin(); it != covers.end(); ++it) {
Laurent Montel's avatar
Laurent Montel committed
164
        out << quint32(it.key());
165
        out << *it.value();
166 167 168
    }

    // Now write out the track mapping.
Laurent Montel's avatar
Laurent Montel committed
169
    out << quint32(tracks.count());
170

Laurent Montel's avatar
Laurent Montel committed
171
    Q3DictIterator<coverKey> trackMapIt(tracks);
172
    while(trackMapIt.current()) {
Laurent Montel's avatar
Laurent Montel committed
173
        out << trackMapIt.currentKey() << quint32(*trackMapIt.current());
174 175 176 177 178 179
        ++trackMapIt;
    }
}

void CoverManagerPrivate::loadCovers()
{
Laurent Montel's avatar
Laurent Montel committed
180
    kDebug() << k_funcinfo << endl;
181 182 183

    QFile file(coverLocation());

Laurent Montel's avatar
Laurent Montel committed
184
    if(!file.open(QIODevice::ReadOnly)) {
185 186 187 188 189
        // Guess we don't have any covers yet.
        return;
    }

    QDataStream in(&file);
Laurent Montel's avatar
Laurent Montel committed
190
    quint32 count, version;
191 192 193 194 195

    // First thing we'll read in will be the version.
    // Only version 0 is defined for now.
    in >> version;
    if(version > 0) {
Laurent Montel's avatar
Laurent Montel committed
196 197
        kError() << "Cover database was created by a higher version of JuK,\n";
        kError() << "I don't know what to do with it.\n";
198 199 200

        return;
    }
201

202 203
    // Read in the count next, then the data.
    in >> count;
Laurent Montel's avatar
Laurent Montel committed
204
    for(quint32 i = 0; i < count; ++i) {
205
        // Read the id, and 3 QStrings for every 1 of the count.
Laurent Montel's avatar
Laurent Montel committed
206
        quint32 id;
207 208 209 210
        CoverDataPtr data(new CoverData);

        in >> id;
        in >> *data;
211
        data->refCount = 0;
212

213
        covers[(coverKey) id] = data;
214 215 216
    }

    in >> count;
Laurent Montel's avatar
Laurent Montel committed
217
    for(quint32 i = 0; i < count; ++i) {
218
        QString path;
Laurent Montel's avatar
Laurent Montel committed
219
        quint32 id;
220 221

        in >> path >> id;
222 223 224 225 226 227 228 229 230

        // 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));
        }
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    }
}

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;

246
    while(covers.contains(key))
247 248 249 250 251
        ++key;

    return key;
}

Michael Pyne's avatar
Michael Pyne committed
252 253 254
//
// Implementation of CoverDrag
//
Laurent Montel's avatar
Laurent Montel committed
255
CoverDrag::CoverDrag(coverKey id, QWidget *src) : Q3DragObject(src, "coverDrag"),
Michael Pyne's avatar
Michael Pyne committed
256 257
                                                  m_id(id)
{
258 259 260
    QPixmap cover = CoverManager::coverFromId(id);
    if(!cover.isNull())
        setPixmap(cover);
Michael Pyne's avatar
Michael Pyne committed
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
}

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;
277
        QDataStream ds(&data, QIODevice::WriteOnly);
Michael Pyne's avatar
Michael Pyne committed
278

Laurent Montel's avatar
Laurent Montel committed
279
        ds << quint32(m_id);
Michael Pyne's avatar
Michael Pyne committed
280 281 282
        return data;
    }
    else if(qstrcmp(mimetype, "image/png") == 0) {
283
        QPixmap large = CoverManager::coverFromId(m_id, CoverManager::FullSize);
284
        QImage img = large.toImage();
285
        QByteArray data;
286
        QBuffer buffer(&data);
287 288 289 290 291

        buffer.open(IO_WriteOnly);
        img.save(&buffer, "PNG"); // Write in PNG format.

        return data;
Michael Pyne's avatar
Michael Pyne committed
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
    }

    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);
308
    QDataStream ds(&data, QIODevice::ReadOnly);
Laurent Montel's avatar
Laurent Montel committed
309
    quint32 i;
Michael Pyne's avatar
Michael Pyne committed
310 311 312 313 314 315 316

    ds >> i;
    id = (coverKey) i;

    return true;
}

317 318 319 320 321 322 323
//
// 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.
324 325
    CoverDataMap::const_iterator it = begin();
    CoverDataMap::const_iterator endIt = end();
326

327
    for(; it != endIt; ++it) {
328
        if(it.value()->album == album.toLower() && it.value()->artist == artist.toLower())
329 330 331 332 333 334 335 336 337 338
            return it.key();
    }

    return NoMatch;
}

QPixmap CoverManager::coverFromId(coverKey id, Size size)
{
    CoverDataPtr info = coverInfo(id);

339 340 341
    if(!info)
        return QPixmap();

342 343 344 345 346 347
    if(size == Thumbnail)
        return info->thumbnail();

    return info->pixmap();
}

348 349 350 351 352 353 354 355 356 357 358 359 360
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.
361

362
    QPixmap *pix = data()->pixmapCache[path];
363

364
    if(pix) {
Laurent Montel's avatar
Laurent Montel committed
365
        kDebug(65432) << "Found pixmap in cover cache.\n";
366 367 368 369
        return *pix;
    }

    // Not in cache, load it and add it.
370

371 372 373 374
    pix = new QPixmap(coverData.path);
    if(pix->isNull())
        return QPixmap();

375 376
    if(size == Thumbnail)
        pix->scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation);
377 378

    QPixmap returnValue = *pix; // Save it early.
379

380 381 382 383 384 385
    if(!data()->pixmapCache.insert(path, pix, pix->height() * pix->width()))
        delete pix;

    return returnValue;
}

386 387
coverKey CoverManager::addCover(const QPixmap &large, const QString &artist, const QString &album)
{
Laurent Montel's avatar
Laurent Montel committed
388
    kDebug() << k_funcinfo << endl;
389 390 391 392 393

    coverKey id = data()->nextId();
    CoverDataPtr coverData(new CoverData);

    if(large.isNull()) {
Laurent Montel's avatar
Laurent Montel committed
394
        kDebug() << "The pixmap you're trying to add is NULL!\n";
395 396 397 398 399 400 401 402
        return NoMatch;
    }

    // Save it to file first!

    QString ext = QString("/coverdb/coverID-%1.png").arg(id);
    coverData->path = KGlobal::dirs()->saveLocation("appdata") + ext;

Laurent Montel's avatar
Laurent Montel committed
403
    kDebug() << "Saving pixmap to " << coverData->path << endl;
404 405 406
    data()->createDataDir();

    if(!large.save(coverData->path, "PNG")) {
Laurent Montel's avatar
Laurent Montel committed
407
        kError() << "Unable to save pixmap to " << coverData->path << endl;
408 409 410
        return NoMatch;
    }

Dirk Mueller's avatar
Dirk Mueller committed
411 412
    coverData->artist = artist.toLower();
    coverData->album = album.toLower();
413
    coverData->refCount = 0;
414

415
    data()->covers[id] = coverData;
416

Michael Pyne's avatar
Michael Pyne committed
417 418 419 420
    // 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));

421 422 423 424 425 426 427 428 429 430
    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)
{
431
    return data()->covers.contains(id);
432 433 434 435 436 437 438
}

bool CoverManager::removeCover(coverKey id)
{
    if(!hasCover(id))
        return false;

Michael Pyne's avatar
Michael Pyne committed
439 440 441 442
    // 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));
443 444

    // Remove references to files that had that track ID.
Laurent Montel's avatar
Laurent Montel committed
445
    Q3DictIterator<coverKey> it(data()->tracks);
446 447
    for(; it.current(); ++it)
        if(*it.current() == id)
448
            data()->tracks.remove(it.currentKey());
449

Michael Pyne's avatar
Michael Pyne committed
450 451 452 453 454 455
    // Remove covers from disk.
    QFile::remove(coverData->path);

    // Finally, forget that we ever knew about this cover.
    data()->covers.remove(id);

456 457 458 459 460 461 462 463 464
    return true;
}

bool CoverManager::replaceCover(coverKey id, const QPixmap &large)
{
    if(!hasCover(id))
        return false;

    CoverDataPtr coverData = coverInfo(id);
465 466 467 468

    // 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));
469 470 471 472 473 474 475 476 477 478 479 480 481

    large.save(coverData->path, "PNG");
    return true;
}

CoverManagerPrivate *CoverManager::data()
{
    if(!m_data)
        sd.setObject(m_data, new CoverManagerPrivate);

    return m_data;
}

482 483 484 485 486
void CoverManager::saveCovers()
{
    data()->saveCovers();
}

487 488 489 490 491
void CoverManager::shutdown()
{
    sd.destructObject();
}

492
CoverDataMapIterator CoverManager::begin()
493
{
494
    return data()->covers.constBegin();
495 496
}

497
CoverDataMapIterator CoverManager::end()
498
{
499
    return data()->covers.constEnd();
500 501
}

502
CoverList CoverManager::keys()
503
{
504
    return data()->covers.keys();
505 506 507 508
}

void CoverManager::setIdForTrack(const QString &path, coverKey id)
{
509 510 511 512 513 514
    coverKey *oldId = data()->tracks.find(path);
    if(oldId && (id == *oldId))
        return; // We're already done.

    if(oldId && *oldId != NoMatch) {
        data()->covers[*oldId]->refCount--;
515
        data()->tracks.remove(path);
Michael Pyne's avatar
Michael Pyne committed
516

517
        if(data()->covers[*oldId]->refCount == 0) {
Scott Wheeler's avatar
Scott Wheeler committed
518
            kDebug(65432) << "Cover " << *oldId << " is unused, removing.\n";
519 520
            removeCover(*oldId);
        }
Michael Pyne's avatar
Michael Pyne committed
521
    }
522 523 524

    if(id != NoMatch) {
        data()->covers[id]->refCount++;
525
        data()->tracks.insert(path, new coverKey(id));
526
    }
527 528 529 530
}

coverKey CoverManager::idForTrack(const QString &path)
{
531
    coverKey *coverPtr = data()->tracks.find(path);
532 533 534 535 536 537 538 539 540

    if(!coverPtr)
        return NoMatch;

    return *coverPtr;
}

CoverDataPtr CoverManager::coverInfo(coverKey id)
{
541 542
    if(data()->covers.contains(id))
        return data()->covers[id];
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578

    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;
}

579
// vim: set et sw=4 tw=0 sta: