File indexing completed on 2023-05-30 11:30:45

0001 /**
0002  * Copyright (C) 2005, 2008 Michael Pyne <mpyne@kde.org>
0003  *
0004  * This program is free software; you can redistribute it and/or modify it under
0005  * the terms of the GNU General Public License as published by the Free Software
0006  * Foundation; either version 2 of the License, or (at your option) any later
0007  * version.
0008  *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
0012  *
0013  * You should have received a copy of the GNU General Public License along with
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.
0015  */
0016 
0017 #include "covermanager.h"
0018 
0019 #include <QByteArray>
0020 #include <QDataStream>
0021 #include <QDir>
0022 #include <QFile>
0023 #include <QHash>
0024 #include <QMap>
0025 #include <QPixmap>
0026 #include <QPixmapCache>
0027 #include <QStandardPaths>
0028 #include <QString>
0029 #include <QTemporaryFile>
0030 #include <QTimer>
0031 #include <QUrl>
0032 
0033 #include <kio/job.h>
0034 
0035 #include "juk.h"
0036 #include "coverproxy.h"
0037 #include "juk_debug.h"
0038 
0039 // This is a dictionary to map the track path to their ID.  Otherwise we'd have
0040 // to store this info with each CollectionListItem, which would break the cache
0041 // of users who upgrade, and would just generally be a big mess.
0042 typedef QHash<QString, coverKey> TrackLookupMap;
0043 
0044 static const char dragMimetype[] = "application/x-juk-coverid";
0045 
0046 const coverKey CoverManager::NoMatch = 0;
0047 
0048 // Used to save and load CoverData from a QDataStream
0049 QDataStream &operator<<(QDataStream &out, const CoverData &data);
0050 QDataStream &operator>>(QDataStream &in, CoverData &data);
0051 
0052 //
0053 // Implementation of CoverSaveHelper class
0054 //
0055 
0056 CoverSaveHelper::CoverSaveHelper(QObject *parent) :
0057     QObject(parent),
0058     m_timer(new QTimer(this))
0059 {
0060     connect(m_timer, SIGNAL(timeout()), SLOT(commitChanges()));
0061 
0062     // Wait 5 seconds before committing to avoid lots of disk activity for
0063     // rapid changes.
0064 
0065     m_timer->setSingleShot(true);
0066     m_timer->setInterval(5000);
0067 }
0068 
0069 void CoverSaveHelper::saveCovers()
0070 {
0071     m_timer->start(); // Restarts if already triggered.
0072 }
0073 
0074 void CoverSaveHelper::commitChanges()
0075 {
0076     CoverManager::saveCovers();
0077 }
0078 
0079 //
0080 // Implementation of CoverData struct
0081 //
0082 
0083 QPixmap CoverData::pixmap() const
0084 {
0085     return CoverManager::coverFromData(*this, CoverManager::FullSize);
0086 }
0087 
0088 QPixmap CoverData::thumbnail() const
0089 {
0090     return CoverManager::coverFromData(*this, CoverManager::Thumbnail);
0091 }
0092 
0093 /**
0094  * This class is responsible for actually keeping track of the storage for the
0095  * different covers and such.  It holds the covers, and the map of path names
0096  * to cover ids, and has a few utility methods to load and save the data.
0097  *
0098  * @author Michael Pyne <mpyne@kde.org>
0099  * @see CoverManager
0100  */
0101 class CoverManagerPrivate
0102 {
0103 public:
0104 
0105     /// Maps coverKey id's to CoverData
0106     CoverDataMap covers;
0107 
0108     /// Maps file names to coverKey id's.
0109     TrackLookupMap tracks;
0110 
0111     /// A map of outstanding download KJobs to their coverKey
0112     QMap<KJob*, coverKey> downloadJobs;
0113 
0114     /// A static pixmap cache is maintained for covers, with key format of:
0115     /// 'f' followed by the pathname for FullSize covers, and
0116     /// 't' followed by the pathname for Thumbnail covers.
0117     /// However only thumbnails are currently cached.
0118 
0119     CoverManagerPrivate() : m_timer(new CoverSaveHelper(0)), m_coverProxy(0)
0120     {
0121         loadCovers();
0122     }
0123 
0124     ~CoverManagerPrivate()
0125     {
0126         delete m_timer;
0127         delete m_coverProxy;
0128         saveCovers();
0129     }
0130 
0131     void requestSave()
0132     {
0133         m_timer->saveCovers();
0134     }
0135 
0136     /**
0137      * Creates the data directory for the covers if it doesn't already exist.
0138      * Must be in this class for loadCovers() and saveCovers().
0139      */
0140     void createDataDir() const;
0141 
0142     /**
0143      * Returns the next available unused coverKey that can be used for
0144      * inserting new items.
0145      *
0146      * @return unused id that can be used for new CoverData
0147      */
0148     coverKey nextId() const;
0149 
0150     void saveCovers() const;
0151 
0152     CoverProxy *coverProxy() {
0153         if(!m_coverProxy)
0154             m_coverProxy = new CoverProxy;
0155         return m_coverProxy;
0156     }
0157 
0158     private:
0159     void loadCovers();
0160 
0161     /**
0162      * @return the full path and filename of the file storing the cover
0163      * lookup map and the translations between pathnames and ids.
0164      */
0165     QString coverLocation() const;
0166 
0167     CoverSaveHelper *m_timer;
0168 
0169     CoverProxy *m_coverProxy;
0170 };
0171 
0172 // This is responsible for making sure that the CoverManagerPrivate class
0173 // gets properly destructed on shutdown.
0174 Q_GLOBAL_STATIC(CoverManagerPrivate, sd)
0175 
0176 //
0177 // Implementation of CoverManagerPrivate methods.
0178 //
0179 void CoverManagerPrivate::createDataDir() const
0180 {
0181     QDir dir;
0182     QString dirPath(QDir::cleanPath(coverLocation() + "/.."));
0183     dir.mkpath(dirPath);
0184 }
0185 
0186 void CoverManagerPrivate::saveCovers() const
0187 {
0188     // Make sure the directory exists first.
0189     createDataDir();
0190 
0191     QFile file(coverLocation());
0192 
0193     qCDebug(JUK_LOG) << "Opening covers db: " << coverLocation();
0194 
0195     if(!file.open(QIODevice::WriteOnly)) {
0196         qCCritical(JUK_LOG) << "Unable to save covers to disk!\n";
0197         return;
0198     }
0199 
0200     QDataStream out(&file);
0201 
0202     // Write out the version and count
0203     out << quint32(0) << quint32(covers.size());
0204 
0205     qCDebug(JUK_LOG) << "Writing out" << covers.size() << "covers.";
0206 
0207     // Write out the data
0208     for(const auto &it : covers) {
0209         out << quint32(it.first);
0210         out << it.second;
0211     }
0212 
0213     // Now write out the track mapping.
0214     out << quint32(tracks.count());
0215 
0216     qCDebug(JUK_LOG) << "Writing out" << tracks.count() << "tracks.";
0217 
0218     TrackLookupMap::ConstIterator trackMapIt = tracks.constBegin();
0219     while(trackMapIt != tracks.constEnd()) {
0220         out << trackMapIt.key() << quint32(trackMapIt.value());
0221         ++trackMapIt;
0222     }
0223 }
0224 
0225 void CoverManagerPrivate::loadCovers()
0226 {
0227     QFile file(coverLocation());
0228 
0229     if(!file.open(QIODevice::ReadOnly)) {
0230         // Guess we don't have any covers yet.
0231         return;
0232     }
0233 
0234     QDataStream in(&file);
0235     quint32 count, version;
0236 
0237     // First thing we'll read in will be the version.
0238     // Only version 0 is defined for now.
0239     in >> version;
0240     if(version > 0) {
0241         qCCritical(JUK_LOG) << "Cover database was created by a higher version of JuK,\n";
0242         qCCritical(JUK_LOG) << "I don't know what to do with it.\n";
0243 
0244         return;
0245     }
0246 
0247     // Read in the count next, then the data.
0248     in >> count;
0249 
0250     qCDebug(JUK_LOG) << "Loading" << count << "covers.";
0251     for(quint32 i = 0; i < count; ++i) {
0252         // Read the id, and 3 QStrings for every 1 of the count.
0253         quint32 id;
0254         CoverData data;
0255 
0256         in >> id;
0257         in >> data;
0258         data.refCount = 0;
0259 
0260         covers[(coverKey) id] = data;
0261     }
0262 
0263     in >> count;
0264     qCDebug(JUK_LOG) << "Loading" << count << "tracks";
0265     for(quint32 i = 0; i < count; ++i) {
0266         QString path;
0267         quint32 id;
0268 
0269         in >> path >> id;
0270 
0271         // If we somehow already managed to load a cover id with this path,
0272         // don't do so again.  Possible due to a coding error during 3.5
0273         // development.
0274 
0275         if(Q_LIKELY(!tracks.contains(path))) {
0276             ++covers[(coverKey) id].refCount; // Another track using this.
0277             tracks.insert(path, id);
0278         }
0279     }
0280 
0281     qCDebug(JUK_LOG) << "Tracks hash table has" << tracks.size() << "entries.";
0282 }
0283 
0284 QString CoverManagerPrivate::coverLocation() const
0285 {
0286     return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
0287             + "coverdb/covers";
0288 }
0289 
0290 coverKey CoverManagerPrivate::nextId() const
0291 {
0292     // Start from 1...
0293     coverKey key = 1;
0294 
0295     while(covers.find(key) != covers.end())
0296         ++key;
0297 
0298     return key;
0299 }
0300 
0301 //
0302 // Implementation of CoverDrag
0303 //
0304 CoverDrag::CoverDrag(coverKey id) :
0305     QMimeData()
0306 {
0307     QPixmap cover = CoverManager::coverFromId(id);
0308     setImageData(cover.toImage());
0309     setData(dragMimetype, QByteArray::number(qulonglong(id), 10));
0310 }
0311 
0312 bool CoverDrag::isCover(const QMimeData *data)
0313 {
0314     return data->hasImage() || data->hasFormat(dragMimetype);
0315 }
0316 
0317 coverKey CoverDrag::idFromData(const QMimeData *data)
0318 {
0319     bool ok = false;
0320 
0321     if(!data->hasFormat(dragMimetype))
0322         return CoverManager::NoMatch;
0323 
0324     coverKey id = data->data(dragMimetype).toULong(&ok);
0325     if(!ok)
0326         return CoverManager::NoMatch;
0327 
0328     return id;
0329 }
0330 
0331 const char *CoverDrag::mimetype()
0332 {
0333     return dragMimetype;
0334 }
0335 
0336 //
0337 // Implementation of CoverManager methods.
0338 //
0339 coverKey CoverManager::idFromMetadata(const QString &artist, const QString &album)
0340 {
0341           CoverDataMap::const_iterator it    = begin();
0342     const CoverDataMap::const_iterator endIt = end();
0343 
0344     for(; it != endIt; ++it) {
0345         if(it->second.album == album.toLower() && it->second.artist == artist.toLower())
0346             return it->first;
0347     }
0348 
0349     return NoMatch;
0350 }
0351 
0352 QPixmap CoverManager::coverFromId(coverKey id, Size size)
0353 {
0354     const auto &info = data()->covers.find(id);
0355     if(info == data()->covers.end())
0356         return QPixmap();
0357 
0358     if(size == Thumbnail)
0359         return info->second.thumbnail();
0360 
0361     return info->second.pixmap();
0362 }
0363 
0364 QPixmap CoverManager::coverFromData(const CoverData &coverData, Size size)
0365 {
0366     QString path = coverData.path;
0367 
0368     // Prepend a tag to the path to separate in the cache between full size
0369     // and thumbnail pixmaps.  If we add a different kind of pixmap in the
0370     // future we also need to add a tag letter for it.
0371     if(size == FullSize)
0372         path.prepend('f');
0373     else
0374         path.prepend('t');
0375 
0376     // Check in cache for the pixmap.
0377 
0378     QPixmap pix;
0379     if(QPixmapCache::find(path, &pix))
0380         return pix;
0381 
0382     // Not in cache, load it and add it.
0383 
0384     if(!pix.load(coverData.path))
0385         return QPixmap();
0386 
0387     // Only thumbnails are cached to avoid depleting global cache.  Caching
0388     // full size pics is not really useful as they are infrequently shown.
0389 
0390     if(size == Thumbnail) {
0391         // Double scale is faster and 99% as accurate
0392         QSize newSize(pix.size());
0393         newSize.scale(80, 80, Qt::KeepAspectRatio);
0394         pix = pix.scaled(2 * newSize)
0395                  .scaled(newSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0396         QPixmapCache::insert(path, pix);
0397     }
0398 
0399     return pix;
0400 }
0401 
0402 coverKey CoverManager::addCover(const QPixmap &large, const QString &artist, const QString &album)
0403 {
0404     qCDebug(JUK_LOG) << "Adding new pixmap to cover database.";
0405     if(large.isNull()) {
0406         qCDebug(JUK_LOG) << "The pixmap you're trying to add is NULL!";
0407         return NoMatch;
0408     }
0409 
0410     QTemporaryFile tempFile;
0411     if(!tempFile.open() || !large.save(tempFile.fileName(), "PNG")) {
0412         qCCritical(JUK_LOG) << "Unable to save pixmap to " << tempFile.fileName();
0413         return NoMatch;
0414     }
0415 
0416     return addCover(QUrl::fromLocalFile(tempFile.fileName()), artist, album);
0417 }
0418 
0419 coverKey CoverManager::addCover(const QUrl &path, const QString &artist, const QString &album)
0420 {
0421     coverKey id = data()->nextId();
0422     CoverData coverData;
0423 
0424     QString fileNameExt = path.fileName();
0425     int extPos = fileNameExt.lastIndexOf('.');
0426 
0427     fileNameExt = fileNameExt.mid(extPos);
0428     if(extPos == -1)
0429         fileNameExt = "";
0430 
0431     // Copy it to a local file first.
0432 
0433     QString ext = QString("/coverdb/coverID-%1%2").arg(id).arg(fileNameExt);
0434     coverData.path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
0435             + ext;
0436     qCDebug(JUK_LOG) << "Saving pixmap to " << coverData.path;
0437     data()->createDataDir();
0438 
0439     coverData.artist = artist.toLower();
0440     coverData.album = album.toLower();
0441     coverData.refCount = 0;
0442 
0443     data()->covers.emplace(id, coverData);
0444 
0445     // Can't use NetAccess::download() since if path is already a local file
0446     // (which is possible) then that function will return without copying, since
0447     // it assumes we merely want the file on the hard disk somewhere.
0448 
0449     KIO::FileCopyJob *job = KIO::file_copy(
0450          path, QUrl::fromLocalFile(coverData.path),
0451          -1 /* perms */, KIO::HideProgressInfo | KIO::Overwrite
0452          );
0453     QObject::connect(job, SIGNAL(result(KJob*)),
0454                      data()->coverProxy(), SLOT(handleResult(KJob*)));
0455     data()->downloadJobs.insert(job, id);
0456 
0457     job->start();
0458 
0459     data()->requestSave(); // Save changes when possible.
0460 
0461     return id;
0462 }
0463 
0464 /**
0465  * This is called when our cover downloader has completed.  Typically there
0466  * should be no issues so we just need to ensure that the newly downloaded
0467  * cover is picked up by invalidating any cache entry for it.  If it didn't
0468  * download successfully we're in kind of a pickle as we've already assigned
0469  * a coverKey, which we need to go and erase.
0470  */
0471 void CoverManager::jobComplete(KJob *job, bool completedSatisfactory)
0472 {
0473     coverKey id = NoMatch;
0474     if(data()->downloadJobs.contains(job))
0475         id = data()->downloadJobs[job];
0476 
0477     if(id == NoMatch) {
0478         qCCritical(JUK_LOG) << "No information on what download job" << job << "is.";
0479         data()->downloadJobs.remove(job);
0480         return;
0481     }
0482 
0483     if(!completedSatisfactory) {
0484         qCCritical(JUK_LOG) << "Job" << job << "failed, but not handled yet.";
0485         removeCover(id);
0486         data()->downloadJobs.remove(job);
0487         JuK::JuKInstance()->coverDownloaded(QPixmap());
0488         return;
0489     }
0490 
0491     CoverData coverData = data()->covers[id];
0492 
0493     // Make sure the new cover isn't inadvertently cached.
0494     QPixmapCache::remove(QString("f%1").arg(coverData.path));
0495     QPixmapCache::remove(QString("t%1").arg(coverData.path));
0496 
0497     JuK::JuKInstance()->coverDownloaded(coverFromData(coverData, CoverManager::Thumbnail));
0498 }
0499 
0500 bool CoverManager::hasCover(coverKey id)
0501 {
0502     return data()->covers.find(id) != data()->covers.end();
0503 }
0504 
0505 bool CoverManager::removeCover(coverKey id)
0506 {
0507     if(!hasCover(id))
0508         return false;
0509 
0510     // Remove cover from cache.
0511     CoverData coverData = coverInfo(id);
0512     QPixmapCache::remove(QString("f%1").arg(coverData.path));
0513     QPixmapCache::remove(QString("t%1").arg(coverData.path));
0514 
0515     // Remove references to files that had that track ID.
0516     QList<QString> affectedFiles = data()->tracks.keys(id);
0517     foreach (const QString &file, affectedFiles) {
0518         data()->tracks.remove(file);
0519     }
0520 
0521     // Remove covers from disk.
0522     QFile::remove(coverData.path);
0523 
0524     // Finally, forget that we ever knew about this cover.
0525     data()->covers.erase(id);
0526     data()->requestSave();
0527 
0528     return true;
0529 }
0530 
0531 bool CoverManager::replaceCover(coverKey id, const QPixmap &large)
0532 {
0533     if(!hasCover(id))
0534         return false;
0535 
0536     CoverData coverData = coverInfo(id);
0537 
0538     // Empty old pixmaps from cache.
0539     QPixmapCache::remove(QString("t%1").arg(coverData.path));
0540     QPixmapCache::remove(QString("f%1").arg(coverData.path));
0541 
0542     large.save(coverData.path, "PNG");
0543 
0544     // No save is needed, as all that has changed is the on-disk cover data,
0545     // not the list of tracks or covers.
0546 
0547     return true;
0548 }
0549 
0550 CoverManagerPrivate *CoverManager::data()
0551 {
0552     return sd;
0553 }
0554 
0555 void CoverManager::saveCovers()
0556 {
0557     data()->saveCovers();
0558 }
0559 
0560 CoverDataMapIterator CoverManager::begin()
0561 {
0562     return data()->covers.begin();
0563 }
0564 
0565 CoverDataMapIterator CoverManager::end()
0566 {
0567     return data()->covers.end();
0568 }
0569 
0570 void CoverManager::setIdForTrack(const QString &path, coverKey id)
0571 {
0572     coverKey oldId = data()->tracks.value(path, NoMatch);
0573     if(data()->tracks.contains(path) && (id == oldId))
0574         return; // We're already done.
0575 
0576     if(oldId != NoMatch) {
0577         data()->covers[oldId].refCount--;
0578         data()->tracks.remove(path);
0579 
0580         if(data()->covers[oldId].refCount == 0) {
0581             qCDebug(JUK_LOG) << "Cover " << oldId << " is unused, removing.\n";
0582             removeCover(oldId);
0583         }
0584     }
0585 
0586     if(id != NoMatch) {
0587         data()->covers[id].refCount++;
0588         data()->tracks.insert(path, id);
0589     }
0590 
0591     data()->requestSave();
0592 }
0593 
0594 coverKey CoverManager::idForTrack(const QString &path)
0595 {
0596     return data()->tracks.value(path, NoMatch);
0597 }
0598 
0599 CoverData CoverManager::coverInfo(coverKey id)
0600 {
0601     if(hasCover(id))
0602         return data()->covers[id];
0603 
0604     // TODO throw new something or other
0605     return CoverData{};
0606 }
0607 
0608 /**
0609  * Write @p data out to @p out.
0610  *
0611  * @param out the data stream to write @p data out to.
0612  * @param data the CoverData to write out.
0613  * @return the data stream that the data was written to.
0614  */
0615 QDataStream &operator<<(QDataStream &out, const CoverData &data)
0616 {
0617     out << data.artist;
0618     out << data.album;
0619     out << data.path;
0620 
0621     return out;
0622 }
0623 
0624 /**
0625  * Read @p data from @p in.
0626  *
0627  * @param in the data stream to read from.
0628  * @param data the CoverData to read into.
0629  * @return the data stream read from.
0630  */
0631 QDataStream &operator>>(QDataStream &in, CoverData &data)
0632 {
0633     in >> data.artist;
0634     in >> data.album;
0635     in >> data.path;
0636 
0637     return in;
0638 }
0639 
0640 // vim: set et sw=4 tw=0 sta: