covermanager.cpp 16.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/**
 * Copyright (C) 2005, 2008 Michael Pyne <mpyne@kde.org>
 *
 * 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.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
16

17 18
#include "covermanager.h"

19
#include <QGlobalStatic>
20
#include <QTimer>
21
#include <QPixmap>
22 23
#include <QString>
#include <QFile>
24
#include <QImage>
25 26
#include <QDir>
#include <QDataStream>
27
#include <QHash>
Michael Pyne's avatar
Michael Pyne committed
28
#include <QPixmapCache>
Michael Pyne's avatar
Michael Pyne committed
29
#include <QByteArray>
30
#include <QMap>
31
#include <QTemporaryFile>
32
#include <QStandardPaths>
Michael Pyne's avatar
Michael Pyne committed
33
#include <QUrl>
34

35 36 37 38
#include <kio/job.h>

#include "juk.h"
#include "coverproxy.h"
Michael Pyne's avatar
Michael Pyne committed
39
#include "juk_debug.h"
40 41 42 43

// 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.
44
typedef QHash<QString, coverKey> TrackLookupMap;
45

46
static const char dragMimetype[] = "application/x-juk-coverid";
47

48
const coverKey CoverManager::NoMatch = 0;
Allen Winter's avatar
Allen Winter committed
49

50 51 52 53
// Used to save and load CoverData from a QDataStream
QDataStream &operator<<(QDataStream &out, const CoverData &data);
QDataStream &operator>>(QDataStream &in, CoverData &data);

54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
//
// Implementation of CoverSaveHelper class
//

CoverSaveHelper::CoverSaveHelper(QObject *parent) :
    QObject(parent),
    m_timer(new QTimer(this))
{
    connect(m_timer, SIGNAL(timeout()), SLOT(commitChanges()));

    // Wait 5 seconds before committing to avoid lots of disk activity for
    // rapid changes.

    m_timer->setSingleShot(true);
    m_timer->setInterval(5000);
}

void CoverSaveHelper::saveCovers()
{
    m_timer->start(); // Restarts if already triggered.
}

void CoverSaveHelper::commitChanges()
{
    CoverManager::saveCovers();
}

81 82 83 84 85 86
//
// Implementation of CoverData struct
//

QPixmap CoverData::pixmap() const
{
87
    return CoverManager::coverFromData(*this, CoverManager::FullSize);
88 89 90 91
}

QPixmap CoverData::thumbnail() const
{
92
    return CoverManager::coverFromData(*this, CoverManager::Thumbnail);
93 94 95 96 97 98 99
}

/**
 * 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.
 *
100
 * @author Michael Pyne <mpyne@kde.org>
101 102 103 104 105
 * @see CoverManager
 */
class CoverManagerPrivate
{
public:
106

Michael Pyne's avatar
Michael Pyne committed
107
    /// Maps coverKey id's to CoverData
108 109 110 111 112
    CoverDataMap covers;

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

113 114 115
    /// A map of outstanding download KJobs to their coverKey
    QMap<KJob*, coverKey> downloadJobs;

Michael Pyne's avatar
Michael Pyne committed
116
    /// A static pixmap cache is maintained for covers, with key format of:
117 118
    /// 'f' followed by the pathname for FullSize covers, and
    /// 't' followed by the pathname for Thumbnail covers.
Michael Pyne's avatar
Michael Pyne committed
119
    /// However only thumbnails are currently cached.
120

121
    CoverManagerPrivate() : m_timer(new CoverSaveHelper(0)), m_coverProxy(0)
122 123 124 125 126 127
    {
        loadCovers();
    }

    ~CoverManagerPrivate()
    {
128
        delete m_timer;
129
        delete m_coverProxy;
130 131 132
        saveCovers();
    }

133 134 135 136 137
    void requestSave()
    {
        m_timer->saveCovers();
    }

138 139 140 141 142 143 144 145 146 147 148 149 150 151
    /**
     * 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;

152 153
    void saveCovers() const;

154 155 156 157 158 159
    CoverProxy *coverProxy() {
        if(!m_coverProxy)
            m_coverProxy = new CoverProxy;
        return m_coverProxy;
    }

160 161 162 163 164 165 166 167
    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;
168 169

    CoverSaveHelper *m_timer;
170 171

    CoverProxy *m_coverProxy;
172 173
};

174 175
// This is responsible for making sure that the CoverManagerPrivate class
// gets properly destructed on shutdown.
176
Q_GLOBAL_STATIC(CoverManagerPrivate, sd)
177

178 179 180 181 182 183
//
// Implementation of CoverManagerPrivate methods.
//
void CoverManagerPrivate::createDataDir() const
{
    QDir dir;
184
    QString dirPath(QDir::cleanPath(coverLocation() + "/.."));
185
    dir.mkpath(dirPath);
186 187 188 189 190 191 192 193 194
}

void CoverManagerPrivate::saveCovers() const
{
    // Make sure the directory exists first.
    createDataDir();

    QFile file(coverLocation());

Michael Pyne's avatar
Michael Pyne committed
195
    qCDebug(JUK_LOG) << "Opening covers db: " << coverLocation();
196

Laurent Montel's avatar
Laurent Montel committed
197
    if(!file.open(QIODevice::WriteOnly)) {
Michael Pyne's avatar
Michael Pyne committed
198
        qCCritical(JUK_LOG) << "Unable to save covers to disk!\n";
199 200 201 202 203 204
        return;
    }

    QDataStream out(&file);

    // Write out the version and count
Michael Pyne's avatar
Michael Pyne committed
205
    out << quint32(0) << quint32(covers.size());
206

Michael Pyne's avatar
Michael Pyne committed
207
    qCDebug(JUK_LOG) << "Writing out" << covers.size() << "covers.";
208

209
    // Write out the data
Michael Pyne's avatar
Michael Pyne committed
210 211 212
    for(const auto &it : covers) {
        out << quint32(it.first);
        out << it.second;
213 214 215
    }

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

Michael Pyne's avatar
Michael Pyne committed
218
    qCDebug(JUK_LOG) << "Writing out" << tracks.count() << "tracks.";
219

220 221 222
    TrackLookupMap::ConstIterator trackMapIt = tracks.constBegin();
    while(trackMapIt != tracks.constEnd()) {
        out << trackMapIt.key() << quint32(trackMapIt.value());
223 224 225 226 227 228 229 230
        ++trackMapIt;
    }
}

void CoverManagerPrivate::loadCovers()
{
    QFile file(coverLocation());

Laurent Montel's avatar
Laurent Montel committed
231
    if(!file.open(QIODevice::ReadOnly)) {
232 233 234 235 236
        // Guess we don't have any covers yet.
        return;
    }

    QDataStream in(&file);
Laurent Montel's avatar
Laurent Montel committed
237
    quint32 count, version;
238 239 240 241 242

    // First thing we'll read in will be the version.
    // Only version 0 is defined for now.
    in >> version;
    if(version > 0) {
Michael Pyne's avatar
Michael Pyne committed
243 244
        qCCritical(JUK_LOG) << "Cover database was created by a higher version of JuK,\n";
        qCCritical(JUK_LOG) << "I don't know what to do with it.\n";
245 246 247

        return;
    }
248

249 250
    // Read in the count next, then the data.
    in >> count;
251

Michael Pyne's avatar
Michael Pyne committed
252
    qCDebug(JUK_LOG) << "Loading" << count << "covers.";
Laurent Montel's avatar
Laurent Montel committed
253
    for(quint32 i = 0; i < count; ++i) {
254
        // Read the id, and 3 QStrings for every 1 of the count.
Laurent Montel's avatar
Laurent Montel committed
255
        quint32 id;
Michael Pyne's avatar
Michael Pyne committed
256
        CoverData data;
257 258

        in >> id;
Michael Pyne's avatar
Michael Pyne committed
259 260
        in >> data;
        data.refCount = 0;
261

262
        covers[(coverKey) id] = data;
263 264 265
    }

    in >> count;
Michael Pyne's avatar
Michael Pyne committed
266
    qCDebug(JUK_LOG) << "Loading" << count << "tracks";
Laurent Montel's avatar
Laurent Montel committed
267
    for(quint32 i = 0; i < count; ++i) {
268
        QString path;
Laurent Montel's avatar
Laurent Montel committed
269
        quint32 id;
270 271

        in >> path >> id;
272 273 274 275 276

        // 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.

Michael Pyne's avatar
Michael Pyne committed
277
        if(Q_LIKELY(!tracks.contains(path))) {
Michael Pyne's avatar
Michael Pyne committed
278
            ++covers[(coverKey) id].refCount; // Another track using this.
279
            tracks.insert(path, id);
280
        }
281
    }
282

Michael Pyne's avatar
Michael Pyne committed
283
    qCDebug(JUK_LOG) << "Tracks hash table has" << tracks.size() << "entries.";
284 285 286 287
}

QString CoverManagerPrivate::coverLocation() const
{
288 289
    return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
            + "coverdb/covers";
290 291 292 293 294 295 296
}

coverKey CoverManagerPrivate::nextId() const
{
    // Start from 1...
    coverKey key = 1;

Michael Pyne's avatar
Michael Pyne committed
297
    while(covers.find(key) != covers.end())
298 299 300 301 302
        ++key;

    return key;
}

Michael Pyne's avatar
Michael Pyne committed
303 304 305
//
// Implementation of CoverDrag
//
Michael Pyne's avatar
Michael Pyne committed
306 307
CoverDrag::CoverDrag(coverKey id) :
    QMimeData()
Michael Pyne's avatar
Michael Pyne committed
308
{
309
    QPixmap cover = CoverManager::coverFromId(id);
Michael Pyne's avatar
Michael Pyne committed
310 311
    setImageData(cover.toImage());
    setData(dragMimetype, QByteArray::number(qulonglong(id), 10));
Michael Pyne's avatar
Michael Pyne committed
312 313
}

Michael Pyne's avatar
Michael Pyne committed
314
bool CoverDrag::isCover(const QMimeData *data)
Michael Pyne's avatar
Michael Pyne committed
315
{
Michael Pyne's avatar
Michael Pyne committed
316
    return data->hasImage() || data->hasFormat(dragMimetype);
Michael Pyne's avatar
Michael Pyne committed
317 318
}

Michael Pyne's avatar
Michael Pyne committed
319
coverKey CoverDrag::idFromData(const QMimeData *data)
Michael Pyne's avatar
Michael Pyne committed
320
{
Michael Pyne's avatar
Michael Pyne committed
321
    bool ok = false;
Michael Pyne's avatar
Michael Pyne committed
322

Michael Pyne's avatar
Michael Pyne committed
323 324
    if(!data->hasFormat(dragMimetype))
        return CoverManager::NoMatch;
Michael Pyne's avatar
Michael Pyne committed
325

Michael Pyne's avatar
Michael Pyne committed
326 327 328
    coverKey id = data->data(dragMimetype).toULong(&ok);
    if(!ok)
        return CoverManager::NoMatch;
Michael Pyne's avatar
Michael Pyne committed
329

Michael Pyne's avatar
Michael Pyne committed
330
    return id;
Michael Pyne's avatar
Michael Pyne committed
331 332
}

333 334 335 336 337
const char *CoverDrag::mimetype()
{
    return dragMimetype;
}

338 339 340 341 342
//
// Implementation of CoverManager methods.
//
coverKey CoverManager::idFromMetadata(const QString &artist, const QString &album)
{
Michael Pyne's avatar
Michael Pyne committed
343 344
          CoverDataMap::const_iterator it    = begin();
    const CoverDataMap::const_iterator endIt = end();
345

346
    for(; it != endIt; ++it) {
Michael Pyne's avatar
Michael Pyne committed
347 348
        if(it->second.album == album.toLower() && it->second.artist == artist.toLower())
            return it->first;
349 350 351 352 353 354 355
    }

    return NoMatch;
}

QPixmap CoverManager::coverFromId(coverKey id, Size size)
{
Michael Pyne's avatar
Michael Pyne committed
356 357
    const auto &info = data()->covers.find(id);
    if(info == data()->covers.end())
358 359
        return QPixmap();

360
    if(size == Thumbnail)
Michael Pyne's avatar
Michael Pyne committed
361
        return info->second.thumbnail();
362

Michael Pyne's avatar
Michael Pyne committed
363
    return info->second.pixmap();
364 365
}

366 367 368 369 370 371 372 373 374 375 376 377 378
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.
379

Michael Pyne's avatar
Michael Pyne committed
380 381 382
    QPixmap pix;
    if(QPixmapCache::find(path, pix))
        return pix;
383 384

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

Michael Pyne's avatar
Michael Pyne committed
386
    if(!pix.load(coverData.path))
387 388
        return QPixmap();

Michael Pyne's avatar
Michael Pyne committed
389 390
    // Only thumbnails are cached to avoid depleting global cache.  Caching
    // full size pics is not really useful as they are infrequently shown.
391

Michael Pyne's avatar
Michael Pyne committed
392
    if(size == Thumbnail) {
393 394 395 396 397
        // Double scale is faster and 99% as accurate
        QSize newSize(pix.size());
        newSize.scale(80, 80, Qt::KeepAspectRatio);
        pix = pix.scaled(2 * newSize)
                 .scaled(newSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
Michael Pyne's avatar
Michael Pyne committed
398 399
        QPixmapCache::insert(path, pix);
    }
400

Michael Pyne's avatar
Michael Pyne committed
401
    return pix;
402 403
}

404 405
coverKey CoverManager::addCover(const QPixmap &large, const QString &artist, const QString &album)
{
406
    qCDebug(JUK_LOG) << "Adding new pixmap to cover database.";
407
    if(large.isNull()) {
408
        qCDebug(JUK_LOG) << "The pixmap you're trying to add is NULL!";
409 410 411
        return NoMatch;
    }

412
    QTemporaryFile tempFile;
Michael Pyne's avatar
Michael Pyne committed
413
    if(!tempFile.open() || !large.save(tempFile.fileName(), "PNG")) {
414
        qCCritical(JUK_LOG) << "Unable to save pixmap to " << tempFile.fileName();
415 416 417
        return NoMatch;
    }

Michael Pyne's avatar
Michael Pyne committed
418
    return addCover(QUrl::fromLocalFile(tempFile.fileName()), artist, album);
419
}
420

Michael Pyne's avatar
Michael Pyne committed
421
coverKey CoverManager::addCover(const QUrl &path, const QString &artist, const QString &album)
422 423
{
    coverKey id = data()->nextId();
Michael Pyne's avatar
Michael Pyne committed
424
    CoverData coverData;
425 426 427 428 429 430 431 432 433 434 435

    QString fileNameExt = path.fileName();
    int extPos = fileNameExt.lastIndexOf('.');

    fileNameExt = fileNameExt.mid(extPos);
    if(extPos == -1)
        fileNameExt = "";

    // Copy it to a local file first.

    QString ext = QString("/coverdb/coverID-%1%2").arg(id).arg(fileNameExt);
436 437
    coverData.path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
            + ext;
Michael Pyne's avatar
Michael Pyne committed
438
    qCDebug(JUK_LOG) << "Saving pixmap to " << coverData.path;
439 440
    data()->createDataDir();

Michael Pyne's avatar
Michael Pyne committed
441 442 443
    coverData.artist = artist.toLower();
    coverData.album = album.toLower();
    coverData.refCount = 0;
444

Michael Pyne's avatar
Michael Pyne committed
445
    data()->covers.emplace(id, coverData);
446

447 448 449 450
    // Can't use NetAccess::download() since if path is already a local file
    // (which is possible) then that function will return without copying, since
    // it assumes we merely want the file on the hard disk somewhere.

451
    KIO::FileCopyJob *job = KIO::file_copy(
Michael Pyne's avatar
Michael Pyne committed
452 453
         path, QUrl::fromLocalFile(coverData.path),
         -1 /* perms */, KIO::HideProgressInfo | KIO::Overwrite
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
         );
    QObject::connect(job, SIGNAL(result(KJob*)),
                     data()->coverProxy(), SLOT(handleResult(KJob*)));
    data()->downloadJobs.insert(job, id);

    job->start();

    data()->requestSave(); // Save changes when possible.

    return id;
}

/**
 * This is called when our cover downloader has completed.  Typically there
 * should be no issues so we just need to ensure that the newly downloaded
 * cover is picked up by invalidating any cache entry for it.  If it didn't
 * download successfully we're in kind of a pickle as we've already assigned
 * a coverKey, which we need to go and erase.
 */
void CoverManager::jobComplete(KJob *job, bool completedSatisfactory)
{
    coverKey id = NoMatch;
    if(data()->downloadJobs.contains(job))
        id = data()->downloadJobs[job];

    if(id == NoMatch) {
Michael Pyne's avatar
Michael Pyne committed
480
        qCCritical(JUK_LOG) << "No information on what download job" << job << "is.";
481 482
        data()->downloadJobs.remove(job);
        return;
483 484
    }

485
    if(!completedSatisfactory) {
Michael Pyne's avatar
Michael Pyne committed
486
        qCCritical(JUK_LOG) << "Job" << job << "failed, but not handled yet.";
487 488 489 490 491
        removeCover(id);
        data()->downloadJobs.remove(job);
        JuK::JuKInstance()->coverDownloaded(QPixmap());
        return;
    }
492

Michael Pyne's avatar
Michael Pyne committed
493
    CoverData coverData = data()->covers[id];
494

Michael Pyne's avatar
Michael Pyne committed
495
    // Make sure the new cover isn't inadvertently cached.
Michael Pyne's avatar
Michael Pyne committed
496 497
    QPixmapCache::remove(QString("f%1").arg(coverData.path));
    QPixmapCache::remove(QString("t%1").arg(coverData.path));
Michael Pyne's avatar
Michael Pyne committed
498

Michael Pyne's avatar
Michael Pyne committed
499
    JuK::JuKInstance()->coverDownloaded(coverFromData(coverData, CoverManager::Thumbnail));
500 501 502 503
}

bool CoverManager::hasCover(coverKey id)
{
Michael Pyne's avatar
Michael Pyne committed
504
    return data()->covers.find(id) != data()->covers.end();
505 506 507 508 509 510 511
}

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

Michael Pyne's avatar
Michael Pyne committed
512
    // Remove cover from cache.
Michael Pyne's avatar
Michael Pyne committed
513 514 515
    CoverData coverData = coverInfo(id);
    QPixmapCache::remove(QString("f%1").arg(coverData.path));
    QPixmapCache::remove(QString("t%1").arg(coverData.path));
516 517

    // Remove references to files that had that track ID.
518 519 520 521
    QList<QString> affectedFiles = data()->tracks.keys(id);
    foreach (const QString &file, affectedFiles) {
        data()->tracks.remove(file);
    }
522

Michael Pyne's avatar
Michael Pyne committed
523
    // Remove covers from disk.
Michael Pyne's avatar
Michael Pyne committed
524
    QFile::remove(coverData.path);
Michael Pyne's avatar
Michael Pyne committed
525 526

    // Finally, forget that we ever knew about this cover.
Michael Pyne's avatar
Michael Pyne committed
527
    data()->covers.erase(id);
528
    data()->requestSave();
Michael Pyne's avatar
Michael Pyne committed
529

530 531 532 533 534 535 536 537
    return true;
}

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

Michael Pyne's avatar
Michael Pyne committed
538
    CoverData coverData = coverInfo(id);
539 540

    // Empty old pixmaps from cache.
Michael Pyne's avatar
Michael Pyne committed
541 542
    QPixmapCache::remove(QString("t%1").arg(coverData.path));
    QPixmapCache::remove(QString("f%1").arg(coverData.path));
543

Michael Pyne's avatar
Michael Pyne committed
544
    large.save(coverData.path, "PNG");
545 546 547 548

    // No save is needed, as all that has changed is the on-disk cover data,
    // not the list of tracks or covers.

549 550 551 552 553
    return true;
}

CoverManagerPrivate *CoverManager::data()
{
554
    return sd;
555 556
}

557 558 559 560 561
void CoverManager::saveCovers()
{
    data()->saveCovers();
}

562
CoverDataMapIterator CoverManager::begin()
563
{
Michael Pyne's avatar
Michael Pyne committed
564
    return data()->covers.begin();
565 566
}

567
CoverDataMapIterator CoverManager::end()
568
{
Michael Pyne's avatar
Michael Pyne committed
569
    return data()->covers.end();
570 571 572 573
}

void CoverManager::setIdForTrack(const QString &path, coverKey id)
{
574 575
    coverKey oldId = data()->tracks.value(path, NoMatch);
    if(data()->tracks.contains(path) && (id == oldId))
576 577
        return; // We're already done.

578
    if(oldId != NoMatch) {
Michael Pyne's avatar
Michael Pyne committed
579
        data()->covers[oldId].refCount--;
580
        data()->tracks.remove(path);
Michael Pyne's avatar
Michael Pyne committed
581

Michael Pyne's avatar
Michael Pyne committed
582
        if(data()->covers[oldId].refCount == 0) {
Michael Pyne's avatar
Michael Pyne committed
583
            qCDebug(JUK_LOG) << "Cover " << oldId << " is unused, removing.\n";
584
            removeCover(oldId);
585
        }
Michael Pyne's avatar
Michael Pyne committed
586
    }
587 588

    if(id != NoMatch) {
Michael Pyne's avatar
Michael Pyne committed
589
        data()->covers[id].refCount++;
590
        data()->tracks.insert(path, id);
591
    }
592 593

    data()->requestSave();
594 595 596 597
}

coverKey CoverManager::idForTrack(const QString &path)
{
598
    return data()->tracks.value(path, NoMatch);
599 600
}

Michael Pyne's avatar
Michael Pyne committed
601
CoverData CoverManager::coverInfo(coverKey id)
602
{
Michael Pyne's avatar
Michael Pyne committed
603
    if(hasCover(id))
604
        return data()->covers[id];
605

Michael Pyne's avatar
Michael Pyne committed
606 607
    // TODO throw new something or other
    return CoverData{};
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
}

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

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