File indexing completed on 2023-12-03 04:55:09

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     QMutexLocker locker(&m_mutex);
0221     bool ok = false;
0222     const QString key = getKey(binId, pos, &ok);
0223     if (!ok) {
0224         return;
0225     }
0226     // if volatile cache also contains this entry, update it
0227     if (m_volatileCache->contains(key)) {
0228         m_volatileCache->remove(key);
0229     } else {
0230         m_storedVolatile[binId].push_back(pos);
0231     }
0232     m_volatileCache->insert(key, img, (int)img.sizeInBytes());
0233     if (persistent) {
0234         QDir thumbFolder = getDir(false, &ok);
0235         if (ok) {
0236             if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() ||
0237                 std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) {
0238                 m_storedOnDisk[binId].push_back(pos);
0239             }
0240             locker.unlock();
0241             if (!img.save(thumbFolder.absoluteFilePath(key))) {
0242                 qDebug() << ".............\n!!!!!!!! ERROR SAVING THUMB in: " << thumbFolder.absoluteFilePath(key);
0243             }
0244         }
0245     }
0246 }
0247 
0248 bool ThumbnailCache::checkIntegrity() const
0249 {
0250     return m_volatileCache->checkIntegrity();
0251 }
0252 
0253 void ThumbnailCache::saveCachedThumbs(const std::unordered_map<QString, std::vector<int>> &keys)
0254 {
0255     bool ok;
0256     QDir thumbFolder = getDir(false, &ok);
0257     if (!ok) {
0258         return;
0259     }
0260     QMutexLocker locker(&m_mutex);
0261     for (auto &key : keys) {
0262         bool ok;
0263         for (const auto &pos : key.second) {
0264             if (m_storedOnDisk.find(key.first) == m_storedOnDisk.end() ||
0265                 std::find(m_storedOnDisk[key.first].begin(), m_storedOnDisk[key.first].end(), pos) == m_storedOnDisk[key.first].end()) {
0266                 const QString thumbKey = getKey(key.first, pos, &ok);
0267                 if (!ok) {
0268                     continue;
0269                 }
0270                 if (!thumbFolder.exists(thumbKey) && m_volatileCache->contains(thumbKey)) {
0271                     QImage img = m_volatileCache->get(thumbKey);
0272                     if (!img.save(thumbFolder.absoluteFilePath(thumbKey))) {
0273                         qDebug() << "// Error writing thumbnails to " << thumbFolder.absolutePath();
0274                         break;
0275                     } else {
0276                         m_storedOnDisk[key.first].push_back(pos);
0277                     }
0278                 }
0279             }
0280         }
0281     }
0282 }
0283 
0284 void ThumbnailCache::invalidateThumbsForClip(const QString &binId)
0285 {
0286     QMutexLocker locker(&m_mutex);
0287     if (m_storedVolatile.find(binId) != m_storedVolatile.end()) {
0288         bool ok = false;
0289         for (int pos : m_storedVolatile.at(binId)) {
0290             auto key = getKey(binId, pos, &ok);
0291             if (ok) {
0292                 m_volatileCache->remove(key);
0293             }
0294         }
0295         m_storedVolatile.erase(binId);
0296     }
0297     bool ok = false;
0298     // Video thumbs
0299     QStringList files;
0300     if (m_storedOnDisk.find(binId) != m_storedOnDisk.end()) {
0301         // Remove persistent cache
0302         for (const auto &pos : m_storedOnDisk.at(binId)) {
0303             if (pos >= 0) {
0304                 auto key = getKey(binId, pos, &ok);
0305                 if (ok) {
0306                     files << key;
0307                 }
0308             }
0309         }
0310         m_storedOnDisk.erase(binId);
0311     }
0312     // Release mutex before deleting files
0313     locker.unlock();
0314     if (!files.isEmpty()) {
0315         QDir thumbFolder = getDir(false, &ok);
0316         if (ok) {
0317             while (!files.isEmpty()) {
0318                 thumbFolder.remove(files.takeFirst());
0319             }
0320         }
0321     }
0322 }
0323 
0324 void ThumbnailCache::clearCache()
0325 {
0326     QMutexLocker locker(&m_mutex);
0327     m_volatileCache->clear();
0328     m_storedVolatile.clear();
0329     m_storedOnDisk.clear();
0330 }
0331 
0332 // static
0333 QString ThumbnailCache::getKey(const QString &binId, int pos, bool *ok)
0334 {
0335     if (binId.isEmpty()) {
0336         *ok = false;
0337         return QString();
0338     }
0339     auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
0340     *ok = binClip != nullptr && binClip->statusReady();
0341     if (!*ok) {
0342         return QString();
0343     }
0344     return binClip->hashForThumbs() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".jpg");
0345 }
0346 
0347 // static
0348 QStringList ThumbnailCache::getAudioKey(const QString &binId, bool *ok)
0349 {
0350     auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
0351     if (binClip == nullptr) {
0352         *ok = false;
0353         qWarning() << "[BUG] Could not find binClip for binId" << binId;
0354         return {};
0355     } else {
0356         *ok = true;
0357         QString streams = binClip->getProducerProperty(QStringLiteral("kdenlive:active_streams"));
0358         if (streams == QString::number(INT_MAX)) {
0359             // activate all audio streams
0360             QList<int> streamIxes = binClip->audioStreams().keys();
0361             if (streamIxes.size() > 1) {
0362                 QStringList streamsList;
0363                 for (const int st : qAsConst(streamIxes)) {
0364                     streamsList << QString("%1_%2.png").arg(binClip->hash()).arg(st);
0365                 }
0366                 return streamsList;
0367             }
0368         }
0369         if (streams.size() < 2) {
0370             int audio = binClip->getProducerIntProperty(QStringLiteral("audio_index"));
0371             if (audio > -1) {
0372                 return {QString("%1_%2.png").arg(binClip->hash()).arg(audio)};
0373             }
0374             return {binClip->hash() + QStringLiteral(".png")};
0375         }
0376         QStringList streamsList;
0377         QStringList streamIndexes = streams.split(QLatin1Char(';'));
0378         for (const QString &st : qAsConst(streamIndexes)) {
0379             streamsList << QString("%1_%2.png").arg(binClip->hash(), st);
0380         }
0381         return streamsList;
0382     }
0383 }
0384 
0385 // static
0386 const QDir ThumbnailCache::getDir(bool audio, bool *ok)
0387 {
0388     return pCore->projectManager()->cacheDir(audio, ok);
0389 }