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: