File indexing completed on 2024-12-01 04:29:21

0001 /*
0002     SPDX-FileCopyrightText: 2017 Nicolas Carion
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "thumbnailcache.hpp"
0007 #include "bin/projectclip.h"
0008 #include "bin/projectitemmodel.h"
0009 #include "core.h"
0010 #include "doc/kdenlivedoc.h"
0011 #include "project/projectmanager.h"
0012 #include <QDir>
0013 #include <QMutexLocker>
0014 #include <list>
0015 
0016 std::unique_ptr<ThumbnailCache> ThumbnailCache::instance;
0017 std::once_flag ThumbnailCache::m_onceFlag;
0018 
0019 class ThumbnailCache::Cache_t
0020 {
0021 public:
0022     Cache_t(int maxCost)
0023         : m_maxCost(maxCost)
0024     {
0025     }
0026 
0027     bool contains(const QString &key) const { return m_cache.count(key) > 0; }
0028 
0029     void remove(const QString &key)
0030     {
0031         if (!contains(key)) {
0032             return;
0033         }
0034         auto it = m_cache.at(key);
0035         m_currentCost -= (*it).second.second;
0036         // Need to erase reference to iterator before erasing what it points to.
0037         // Fixes BUG 463764.
0038         m_cache.erase(key);
0039         m_data.erase(it);
0040     }
0041 
0042     void insert(const QString &key, const QImage &img, int cost)
0043     {
0044         if (cost > m_maxCost) {
0045             return;
0046         }
0047         m_data.push_front({key, {img, cost}});
0048         auto it = m_data.begin();
0049         m_cache[key] = it;
0050         m_currentCost += cost;
0051         while (m_currentCost > m_maxCost) {
0052             remove(m_data.back().first);
0053         }
0054     }
0055 
0056     QImage get(const QString &key)
0057     {
0058         if (!contains(key)) {
0059             return QImage();
0060         }
0061         // when a get operation occurs, we put the corresponding list item in front to remember last access
0062         std::pair<QString, std::pair<QImage, int>> data;
0063         auto it = m_cache.at(key);
0064         std::swap(data, (*it));                                         // take data out without copy
0065         QImage result = data.second.first;                              // a copy occurs here
0066         m_data.erase(it);                                               // delete old iterator
0067         m_cache[key] = m_data.emplace(m_data.begin(), std::move(data)); // reinsert without copy and store iterator
0068         return result;
0069     }
0070     void clear()
0071     {
0072         m_data.clear();
0073         m_cache.clear();
0074         m_currentCost = 0;
0075     }
0076     bool checkIntegrity() const
0077     {
0078         if (m_data.size() != m_cache.size()) {
0079             // Cache is corrupted
0080             return false;
0081         }
0082         for (const auto &d : m_data) {
0083             if (!contains(d.first)) {
0084                 return false;
0085             }
0086         }
0087         return true;
0088     }
0089 
0090 protected:
0091     int m_maxCost;
0092     int m_currentCost{0};
0093 
0094     // The data is stored as (key,(image, cost)) in a std::list that serves as a
0095     // FIFO queue. If m_maxCost is exceeded, elements are removed from the
0096     // end of the list until the sum of the costs in the list is less than m_maxCost.
0097     std::list<std::pair<QString, std::pair<QImage, int>>> m_data;
0098     // m_cache keeps a mapping from the key to an iterator that represents the
0099     // item's location in m_data, like a pointer.
0100     std::unordered_map<QString, decltype(m_data.begin())> m_cache;
0101 };
0102 
0103 ThumbnailCache::ThumbnailCache()
0104     : m_volatileCache(new Cache_t(10000000))
0105 {
0106 }
0107 
0108 std::unique_ptr<ThumbnailCache> &ThumbnailCache::get()
0109 {
0110     std::call_once(m_onceFlag, [] { instance.reset(new ThumbnailCache()); });
0111     return instance;
0112 }
0113 
0114 bool ThumbnailCache::hasThumbnail(const QString &binId, int pos, bool volatileOnly) const
0115 {
0116     QMutexLocker locker(&m_mutex);
0117     bool ok = false;
0118     auto key = pos < 0 ? getAudioKey(binId, &ok).constFirst() : getKey(binId, pos, &ok);
0119     if (ok && m_volatileCache->contains(key)) {
0120         return true;
0121     }
0122     if (!ok || volatileOnly) {
0123         return false;
0124     }
0125     locker.unlock();
0126     QDir thumbFolder = getDir(pos < 0, &ok);
0127     return ok && thumbFolder.exists(key);
0128 }
0129 
0130 QImage ThumbnailCache::getAudioThumbnail(const QString &binId, bool volatileOnly) const
0131 {
0132     QMutexLocker locker(&m_mutex);
0133     bool ok = false;
0134     auto key = getAudioKey(binId, &ok).constFirst();
0135     if (ok && m_volatileCache->contains(key)) {
0136         return m_volatileCache->get(key);
0137     }
0138     if (!ok || volatileOnly) {
0139         return QImage();
0140     }
0141     QDir thumbFolder = getDir(true, &ok);
0142     if (ok && thumbFolder.exists(key)) {
0143         if (std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), -1) != m_storedOnDisk[binId].end()) {
0144             m_storedOnDisk[binId].push_back(-1);
0145         }
0146         locker.unlock();
0147         return QImage(thumbFolder.absoluteFilePath(key));
0148     }
0149     return QImage();
0150 }
0151 
0152 const QList<QUrl> ThumbnailCache::getAudioThumbPath(const QString &binId) const
0153 {
0154     bool ok = false;
0155     auto key = getAudioKey(binId, &ok);
0156     QDir thumbFolder = getDir(true, &ok);
0157     QList<QUrl> pathList;
0158     if (ok) {
0159         for (const QString &p : qAsConst(key)) {
0160             if (thumbFolder.exists(p)) {
0161                 pathList << QUrl::fromLocalFile(thumbFolder.absoluteFilePath(p));
0162             }
0163         }
0164     }
0165     return pathList;
0166 }
0167 
0168 QImage ThumbnailCache::getThumbnail(QString hash, const QString &binId, int pos, bool volatileOnly) const
0169 {
0170     if (hash.isEmpty()) {
0171         return QImage();
0172     }
0173     hash.append(QString("#%1.jpg").arg(pos));
0174     QMutexLocker locker(&m_mutex);
0175     if (m_volatileCache->contains(hash)) {
0176         return m_volatileCache->get(hash);
0177     }
0178     if (volatileOnly) {
0179         return QImage();
0180     }
0181     bool ok = false;
0182     QDir thumbFolder = getDir(false, &ok);
0183     if (ok && thumbFolder.exists(hash)) {
0184         if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() ||
0185             std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) {
0186             m_storedOnDisk[binId].push_back(pos);
0187         }
0188         locker.unlock();
0189         return QImage(thumbFolder.absoluteFilePath(hash));
0190     }
0191     locker.unlock();
0192     return QImage();
0193 }
0194 
0195 QImage ThumbnailCache::getThumbnail(const QString &binId, int pos, bool volatileOnly) const
0196 {
0197     QMutexLocker locker(&m_mutex);
0198     bool ok = false;
0199     auto key = getKey(binId, pos, &ok);
0200     if (ok && m_volatileCache->contains(key)) {
0201         return m_volatileCache->get(key);
0202     }
0203     if (!ok || volatileOnly) {
0204         return QImage();
0205     }
0206     QDir thumbFolder = getDir(false, &ok);
0207     if (ok && thumbFolder.exists(key)) {
0208         if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() ||
0209             std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) {
0210             m_storedOnDisk[binId].push_back(pos);
0211         }
0212         locker.unlock();
0213         return QImage(thumbFolder.absoluteFilePath(key));
0214     }
0215     return QImage();
0216 }
0217 
0218 void ThumbnailCache::storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent)
0219 {
0220     if (pCore->projectItemModel()->closing) {
0221         return;
0222     }
0223     QMutexLocker locker(&m_mutex);
0224     bool ok = false;
0225     const QString key = getKey(binId, pos, &ok);
0226     if (!ok) {
0227         return;
0228     }
0229     // if volatile cache also contains this entry, update it
0230     if (m_volatileCache->contains(key)) {
0231         m_volatileCache->remove(key);
0232     } else {
0233         m_storedVolatile[binId].push_back(pos);
0234     }
0235     m_volatileCache->insert(key, img, (int)img.sizeInBytes());
0236     if (persistent) {
0237         QDir thumbFolder = getDir(false, &ok);
0238         if (ok) {
0239             if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() ||
0240                 std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) {
0241                 m_storedOnDisk[binId].push_back(pos);
0242             }
0243             locker.unlock();
0244             if (!img.save(thumbFolder.absoluteFilePath(key))) {
0245                 qDebug() << ".............\n!!!!!!!! ERROR SAVING THUMB in: " << thumbFolder.absoluteFilePath(key);
0246             }
0247         }
0248     }
0249 }
0250 
0251 bool ThumbnailCache::checkIntegrity() const
0252 {
0253     return m_volatileCache->checkIntegrity();
0254 }
0255 
0256 void ThumbnailCache::saveCachedThumbs(const std::unordered_map<QString, std::vector<int>> &keys)
0257 {
0258     bool ok;
0259     QDir thumbFolder = getDir(false, &ok);
0260     if (!ok) {
0261         return;
0262     }
0263     QMutexLocker locker(&m_mutex);
0264     for (auto &key : keys) {
0265         bool ok;
0266         for (const auto &pos : key.second) {
0267             if (m_storedOnDisk.find(key.first) == m_storedOnDisk.end() ||
0268                 std::find(m_storedOnDisk[key.first].begin(), m_storedOnDisk[key.first].end(), pos) == m_storedOnDisk[key.first].end()) {
0269                 const QString thumbKey = getKey(key.first, pos, &ok);
0270                 if (!ok) {
0271                     continue;
0272                 }
0273                 if (!thumbFolder.exists(thumbKey) && m_volatileCache->contains(thumbKey)) {
0274                     QImage img = m_volatileCache->get(thumbKey);
0275                     if (!img.save(thumbFolder.absoluteFilePath(thumbKey))) {
0276                         qDebug() << "// Error writing thumbnails to " << thumbFolder.absolutePath();
0277                         break;
0278                     } else {
0279                         m_storedOnDisk[key.first].push_back(pos);
0280                     }
0281                 }
0282             }
0283         }
0284     }
0285 }
0286 
0287 void ThumbnailCache::invalidateThumbsForClip(const QString &binId)
0288 {
0289     QMutexLocker locker(&m_mutex);
0290     if (m_storedVolatile.find(binId) != m_storedVolatile.end()) {
0291         bool ok = false;
0292         for (int pos : m_storedVolatile.at(binId)) {
0293             auto key = getKey(binId, pos, &ok);
0294             if (ok) {
0295                 m_volatileCache->remove(key);
0296             }
0297         }
0298         m_storedVolatile.erase(binId);
0299     }
0300     bool ok = false;
0301     // Video thumbs
0302     QStringList files;
0303     if (m_storedOnDisk.find(binId) != m_storedOnDisk.end()) {
0304         // Remove persistent cache
0305         for (const auto &pos : m_storedOnDisk.at(binId)) {
0306             if (pos >= 0) {
0307                 auto key = getKey(binId, pos, &ok);
0308                 if (ok) {
0309                     files << key;
0310                 }
0311             }
0312         }
0313         m_storedOnDisk.erase(binId);
0314     }
0315     // Release mutex before deleting files
0316     locker.unlock();
0317     if (!files.isEmpty()) {
0318         QDir thumbFolder = getDir(false, &ok);
0319         if (ok) {
0320             while (!files.isEmpty()) {
0321                 thumbFolder.remove(files.takeFirst());
0322             }
0323         }
0324     }
0325 }
0326 
0327 void ThumbnailCache::clearCache()
0328 {
0329     QMutexLocker locker(&m_mutex);
0330     m_volatileCache->clear();
0331     m_storedVolatile.clear();
0332     m_storedOnDisk.clear();
0333 }
0334 
0335 // static
0336 QString ThumbnailCache::getKey(const QString &binId, int pos, bool *ok)
0337 {
0338     if (binId.isEmpty()) {
0339         *ok = false;
0340         return QString();
0341     }
0342     auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
0343     *ok = binClip != nullptr && binClip->statusReady();
0344     if (!*ok) {
0345         return QString();
0346     }
0347     return binClip->hashForThumbs() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".jpg");
0348 }
0349 
0350 // static
0351 QStringList ThumbnailCache::getAudioKey(const QString &binId, bool *ok)
0352 {
0353     auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
0354     if (binClip == nullptr) {
0355         *ok = false;
0356         qWarning() << "[BUG] Could not find binClip for binId" << binId;
0357         return {};
0358     } else {
0359         *ok = true;
0360         QString streams = binClip->getProducerProperty(QStringLiteral("kdenlive:active_streams"));
0361         if (streams == QString::number(INT_MAX)) {
0362             // activate all audio streams
0363             QList<int> streamIxes = binClip->audioStreams().keys();
0364             if (streamIxes.size() > 1) {
0365                 QStringList streamsList;
0366                 for (const int st : qAsConst(streamIxes)) {
0367                     streamsList << QString("%1_%2.png").arg(binClip->hash()).arg(st);
0368                 }
0369                 return streamsList;
0370             }
0371         }
0372         if (streams.size() < 2) {
0373             int audio = binClip->getProducerIntProperty(QStringLiteral("audio_index"));
0374             if (audio > -1) {
0375                 return {QString("%1_%2.png").arg(binClip->hash()).arg(audio)};
0376             }
0377             return {binClip->hash() + QStringLiteral(".png")};
0378         }
0379         QStringList streamsList;
0380         QStringList streamIndexes = streams.split(QLatin1Char(';'));
0381         for (const QString &st : qAsConst(streamIndexes)) {
0382             streamsList << QString("%1_%2.png").arg(binClip->hash(), st);
0383         }
0384         return streamsList;
0385     }
0386 }
0387 
0388 // static
0389 const QDir ThumbnailCache::getDir(bool audio, bool *ok)
0390 {
0391     return pCore->projectManager()->cacheDir(audio, ok);
0392 }