1 /*
2     SPDX-FileCopyrightText: 2017 Nicolas Carion
3     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5 
6 #include "thumbnailcache.hpp"
7 #include "bin/projectclip.h"
8 #include "bin/projectitemmodel.h"
9 #include "core.h"
10 #include "project/projectmanager.h"
11 #include "doc/kdenlivedoc.h"
12 #include <QDir>
13 #include <QMutexLocker>
14 #include <list>
15 
16 std::unique_ptr<ThumbnailCache> ThumbnailCache::instance;
17 std::once_flag ThumbnailCache::m_onceFlag;
18 
19 class ThumbnailCache::Cache_t
20 {
21 public:
Cache_t(int maxCost)22     Cache_t(int maxCost)
23         : m_maxCost(maxCost)
24     {
25     }
26 
contains(const QString & key) const27     bool contains(const QString &key) const { return m_cache.count(key) > 0; }
28 
remove(const QString & key)29     void remove(const QString &key)
30     {
31         if (!contains(key)) {
32             return;
33         }
34         auto it = m_cache.at(key);
35         m_currentCost -= (*it).second.second;
36         m_data.erase(it);
37         m_cache.erase(key);
38     }
39 
insert(const QString & key,const QImage & img,int cost)40     void insert(const QString &key, const QImage &img, int cost)
41     {
42         if (cost > m_maxCost) {
43             return;
44         }
45         m_data.push_front({key, {img, cost}});
46         auto it = m_data.begin();
47         m_cache[key] = it;
48         m_currentCost += cost;
49         while (m_currentCost > m_maxCost) {
50             remove(m_data.back().first);
51         }
52     }
53 
get(const QString & key)54     QImage get(const QString &key)
55     {
56         if (!contains(key)) {
57             return QImage();
58         }
59         // when a get operation occurs, we put the corresponding list item in front to remember last access
60         std::pair<QString, std::pair<QImage, int>> data;
61         auto it = m_cache.at(key);
62         std::swap(data, (*it));                                         // take data out without copy
63         QImage result = data.second.first;                              // a copy occurs here
64         m_data.erase(it);                                               // delete old iterator
65         m_cache[key] = m_data.emplace(m_data.begin(), std::move(data)); // reinsert without copy and store iterator
66         return result;
67     }
clear()68     void clear()
69     {
70         m_data.clear();
71         m_cache.clear();
72         m_currentCost = 0;
73     }
74 
75 protected:
76     int m_maxCost;
77     int m_currentCost{0};
78 
79     std::list<std::pair<QString, std::pair<QImage, int>>> m_data; // the data is stored as (key,(image, cost))
80     std::unordered_map<QString, decltype(m_data.begin())> m_cache;
81 };
82 
ThumbnailCache()83 ThumbnailCache::ThumbnailCache()
84     : m_volatileCache(new Cache_t(10000000))
85 {
86 }
87 
get()88 std::unique_ptr<ThumbnailCache> &ThumbnailCache::get()
89 {
90     std::call_once(m_onceFlag, [] { instance.reset(new ThumbnailCache()); });
91     return instance;
92 }
93 
hasThumbnail(const QString & binId,int pos,bool volatileOnly) const94 bool ThumbnailCache::hasThumbnail(const QString &binId, int pos, bool volatileOnly) const
95 {
96     QMutexLocker locker(&m_mutex);
97     bool ok = false;
98     auto key = pos < 0 ? getAudioKey(binId, &ok).constFirst() : getKey(binId, pos, &ok);
99     if (ok && m_volatileCache->contains(key)) {
100         return true;
101     }
102     if (!ok || volatileOnly) {
103         return false;
104     }
105     QDir thumbFolder = getDir(pos < 0, &ok);
106     return ok && thumbFolder.exists(key);
107 }
108 
getAudioThumbnail(const QString & binId,bool volatileOnly) const109 QImage ThumbnailCache::getAudioThumbnail(const QString &binId, bool volatileOnly) const
110 {
111     QMutexLocker locker(&m_mutex);
112     bool ok = false;
113     auto key = getAudioKey(binId, &ok).constFirst();
114     if (ok && m_volatileCache->contains(key)) {
115         return m_volatileCache->get(key);
116     }
117     if (!ok || volatileOnly) {
118         return QImage();
119     }
120     QDir thumbFolder = getDir(true, &ok);
121     if (ok && thumbFolder.exists(key)) {
122         m_storedOnDisk[binId].push_back(-1);
123         return QImage(thumbFolder.absoluteFilePath(key));
124     }
125     return QImage();
126 }
127 
getAudioThumbPath(const QString & binId) const128 const QList <QUrl> ThumbnailCache::getAudioThumbPath(const QString &binId) const
129 {
130     QMutexLocker locker(&m_mutex);
131     bool ok = false;
132     auto key = getAudioKey(binId, &ok);
133     QDir thumbFolder = getDir(true, &ok);
134     QList <QUrl> pathList;
135     if (ok) {
136         for (const QString &p : qAsConst(key)) {
137             if (thumbFolder.exists(p)) {
138                 pathList <<QUrl::fromLocalFile(thumbFolder.absoluteFilePath(p));
139             }
140         }
141     }
142     return pathList;
143 }
144 
getThumbnail(const QString & binId,int pos,bool volatileOnly) const145 QImage ThumbnailCache::getThumbnail(const QString &binId, int pos, bool volatileOnly) const
146 {
147     QMutexLocker locker(&m_mutex);
148     bool ok = false;
149     auto key = getKey(binId, pos, &ok);
150     if (ok && m_volatileCache->contains(key)) {
151         return m_volatileCache->get(key);
152     }
153     if (!ok || volatileOnly) {
154         return QImage();
155     }
156     QDir thumbFolder = getDir(false, &ok);
157     if (ok && thumbFolder.exists(key)) {
158         m_storedOnDisk[binId].push_back(pos);
159         return QImage(thumbFolder.absoluteFilePath(key));
160     }
161     return QImage();
162 }
163 
storeThumbnail(const QString & binId,int pos,const QImage & img,bool persistent)164 void ThumbnailCache::storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent)
165 {
166     QMutexLocker locker(&m_mutex);
167     bool ok = false;
168     const QString key = getKey(binId, pos, &ok);
169     if (!ok) {
170         return;
171     }
172     if (persistent) {
173         QDir thumbFolder = getDir(false, &ok);
174         if (ok) {
175             if (!img.save(thumbFolder.absoluteFilePath(key))) {
176                 qDebug() << ".............\n!!!!!!!! ERROR SAVING THUMB in: "<<thumbFolder.absoluteFilePath(key);
177             }
178             m_storedOnDisk[binId].push_back(pos);
179             // if volatile cache also contains this entry, update it
180             if (m_volatileCache->contains(key)) {
181                 m_volatileCache->remove(key);
182             } else {
183                 m_storedVolatile[binId].push_back(pos);
184             }
185             m_volatileCache->insert(key, img, (int)img.sizeInBytes());
186         }
187     } else {
188         m_volatileCache->insert(key, img, (int)img.sizeInBytes());
189         m_storedVolatile[binId].push_back(pos);
190     }
191 }
192 
saveCachedThumbs(QStringList keys)193 void ThumbnailCache::saveCachedThumbs(QStringList keys)
194 {
195     bool ok;
196     QDir thumbFolder = getDir(false, &ok);
197     if (!ok) {
198         return;
199     }
200     for (const QString &key : keys) {
201         if (!thumbFolder.exists(key) && m_volatileCache->contains(key)) {
202             QImage img = m_volatileCache->get(key);
203             if (!img.save(thumbFolder.absoluteFilePath(key))) {
204                 qDebug() << "// Error writing thumbnails to " << thumbFolder.absolutePath();
205                 break;
206             }
207         }
208     }
209 }
210 
invalidateThumbsForClip(const QString & binId)211 void ThumbnailCache::invalidateThumbsForClip(const QString &binId)
212 {
213     QMutexLocker locker(&m_mutex);
214     if (m_storedVolatile.find(binId) != m_storedVolatile.end()) {
215         bool ok = false;
216         for (int pos : m_storedVolatile.at(binId)) {
217             auto key = getKey(binId, pos, &ok);
218             if (ok) {
219                 m_volatileCache->remove(key);
220             }
221         }
222         m_storedVolatile.erase(binId);
223     }
224     bool ok = false;
225     // Video thumbs
226     QDir thumbFolder = getDir(false, &ok);
227     //QDir audioThumbFolder = getDir(true, &ok);
228     if (ok && m_storedOnDisk.find(binId) != m_storedOnDisk.end()) {
229         // Remove persistent cache
230         for (int pos : m_storedOnDisk.at(binId)) {
231             if (pos >= 0) {
232                 auto key = getKey(binId, pos, &ok);
233                 if (ok) {
234                     QFile::remove(thumbFolder.absoluteFilePath(key));
235                 }
236             }
237         }
238         m_storedOnDisk.erase(binId);
239     }
240 }
241 
clearCache()242 void ThumbnailCache::clearCache()
243 {
244     QMutexLocker locker(&m_mutex);
245     m_volatileCache->clear();
246     m_storedVolatile.clear();
247     m_storedOnDisk.clear();
248 }
249 
250 // static
getKey(const QString & binId,int pos,bool * ok)251 QString ThumbnailCache::getKey(const QString &binId, int pos, bool *ok)
252 {
253     if (binId.isEmpty()) {
254         *ok = false;
255         return QString();
256     }
257     auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
258     *ok = binClip != nullptr;
259     return *ok ? binClip->hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".jpg") : QString();
260 }
261 
262 // static
getAudioKey(const QString & binId,bool * ok)263 QStringList ThumbnailCache::getAudioKey(const QString &binId, bool *ok)
264 {
265     auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
266     *ok = binClip != nullptr;
267     if (ok) {
268         QString streams = binClip->getProducerProperty(QStringLiteral("kdenlive:active_streams"));
269         if (streams == QString::number(INT_MAX)) {
270             // activate all audio streams
271             QList <int> streamIxes = binClip->audioStreams().keys();
272             if (streamIxes.size() > 1) {
273                 QStringList streamsList;
274                 for (const int st : qAsConst(streamIxes)) {
275                     streamsList << QString("%1_%2.png").arg(binClip->hash()).arg(st);
276                 }
277                 return streamsList;
278             }
279         }
280         if (streams.size() < 2) {
281             int audio = binClip->getProducerIntProperty(QStringLiteral("audio_index"));
282             if (audio > -1) {
283                 return {QString("%1_%2.png").arg(binClip->hash()).arg(audio)};
284             }
285             return {binClip->hash() + QStringLiteral(".png")};
286         }
287         QStringList streamsList;
288         QStringList streamIndexes = streams.split(QLatin1Char(';'));
289         for (const QString &st : qAsConst(streamIndexes)) {
290             streamsList << QString("%1_%2.png").arg(binClip->hash(), st);
291         }
292         return streamsList;
293     }
294     return {};
295 }
296 
297 // static
getDir(bool audio,bool * ok)298 QDir ThumbnailCache::getDir(bool audio, bool *ok)
299 {
300     return pCore->projectManager()->cacheDir(audio, ok);
301 }
302