File indexing completed on 2021-12-21 13:27:52

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