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