thumbnailcache.cpp 11.8 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2017 Nicolas Carion
Camille Moulin's avatar
Camille Moulin committed
3
    SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4
*/
5
6
7
8
9
10

#include "thumbnailcache.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "doc/kdenlivedoc.h"
11
#include "project/projectmanager.h"
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <QDir>
#include <QMutexLocker>
#include <list>

std::unique_ptr<ThumbnailCache> ThumbnailCache::instance;
std::once_flag ThumbnailCache::m_onceFlag;

class ThumbnailCache::Cache_t
{
public:
    Cache_t(int maxCost)
        : m_maxCost(maxCost)
    {
    }

    bool contains(const QString &key) const { return m_cache.count(key) > 0; }

    void remove(const QString &key)
    {
        if (!contains(key)) {
            return;
        }
        auto it = m_cache.at(key);
        m_currentCost -= (*it).second.second;
        m_data.erase(it);
        m_cache.erase(key);
    }

    void insert(const QString &key, const QImage &img, int cost)
    {
        if (cost > m_maxCost) {
            return;
        }
        m_data.push_front({key, {img, cost}});
        auto it = m_data.begin();
        m_cache[key] = it;
        m_currentCost += cost;
        while (m_currentCost > m_maxCost) {
            remove(m_data.back().first);
        }
    }

    QImage get(const QString &key)
    {
        if (!contains(key)) {
            return QImage();
        }
        // when a get operation occurs, we put the corresponding list item in front to remember last access
        std::pair<QString, std::pair<QImage, int>> data;
        auto it = m_cache.at(key);
        std::swap(data, (*it));                                         // take data out without copy
        QImage result = data.second.first;                              // a copy occurs here
        m_data.erase(it);                                               // delete old iterator
        m_cache[key] = m_data.emplace(m_data.begin(), std::move(data)); // reinsert without copy and store iterator
        return result;
    }
68
69
70
71
72
73
    void clear()
    {
        m_data.clear();
        m_cache.clear();
        m_currentCost = 0;
    }
74
75
76
77
78
79
80
81
82
83
84
85
86
    bool checkIntegrity() const
    {
        if (m_data.size() != m_cache.size()) {
            // Cache is corrupted
            return false;
        }
        for (const auto &d : m_data) {
            if (!contains(d.first)) {
                return false;
            }
        }
        return true;
    }
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

protected:
    int m_maxCost;
    int m_currentCost{0};

    std::list<std::pair<QString, std::pair<QImage, int>>> m_data; // the data is stored as (key,(image, cost))
    std::unordered_map<QString, decltype(m_data.begin())> m_cache;
};

ThumbnailCache::ThumbnailCache()
    : m_volatileCache(new Cache_t(10000000))
{
}

std::unique_ptr<ThumbnailCache> &ThumbnailCache::get()
{
    std::call_once(m_onceFlag, [] { instance.reset(new ThumbnailCache()); });
    return instance;
}

bool ThumbnailCache::hasThumbnail(const QString &binId, int pos, bool volatileOnly) const
{
109
    QMutexLocker locker(&m_mutex);
110
    bool ok = false;
Vincent Pinon's avatar
Vincent Pinon committed
111
    auto key = pos < 0 ? getAudioKey(binId, &ok).constFirst() : getKey(binId, pos, &ok);
112
    if (ok && m_volatileCache->contains(key)) {
113
114
        return true;
    }
115
    if (!ok || volatileOnly) {
116
117
        return false;
    }
118
    locker.unlock();
119
    QDir thumbFolder = getDir(pos < 0, &ok);
120
121
122
    return ok && thumbFolder.exists(key);
}

123
124
QImage ThumbnailCache::getAudioThumbnail(const QString &binId, bool volatileOnly) const
{
125
    QMutexLocker locker(&m_mutex);
126
    bool ok = false;
Vincent Pinon's avatar
Vincent Pinon committed
127
    auto key = getAudioKey(binId, &ok).constFirst();
128
129
130
131
132
133
134
135
    if (ok && m_volatileCache->contains(key)) {
        return m_volatileCache->get(key);
    }
    if (!ok || volatileOnly) {
        return QImage();
    }
    QDir thumbFolder = getDir(true, &ok);
    if (ok && thumbFolder.exists(key)) {
136
137
138
139
        if (std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), -1) != m_storedOnDisk[binId].end()) {
            m_storedOnDisk[binId].push_back(-1);
        }
        locker.unlock();
140
141
142
143
144
        return QImage(thumbFolder.absoluteFilePath(key));
    }
    return QImage();
}

145
const QList<QUrl> ThumbnailCache::getAudioThumbPath(const QString &binId) const
146
147
148
149
{
    bool ok = false;
    auto key = getAudioKey(binId, &ok);
    QDir thumbFolder = getDir(true, &ok);
150
    QList<QUrl> pathList;
151
    if (ok) {
Vincent Pinon's avatar
Vincent Pinon committed
152
        for (const QString &p : qAsConst(key)) {
153
            if (thumbFolder.exists(p)) {
154
                pathList << QUrl::fromLocalFile(thumbFolder.absoluteFilePath(p));
155
156
            }
        }
157
    }
158
    return pathList;
159
}
160
161
162
163
164
165
166

QImage ThumbnailCache::getThumbnail(QString hash, const QString &binId, int pos, bool volatileOnly) const
{
    if (hash.isEmpty()) {
        return QImage();
    }
    hash.append(QString("#%1.jpg").arg(pos));
167
    QMutexLocker locker(&m_mutex);
168
169
170
171
172
173
174
175
176
    if (m_volatileCache->contains(hash)) {
        return m_volatileCache->get(hash);
    }
    if (volatileOnly) {
        return QImage();
    }
    bool ok = false;
    QDir thumbFolder = getDir(false, &ok);
    if (ok && thumbFolder.exists(hash)) {
177
178
        if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() ||
            std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) {
179
180
            m_storedOnDisk[binId].push_back(pos);
        }
181
        locker.unlock();
182
183
        return QImage(thumbFolder.absoluteFilePath(hash));
    }
184
    locker.unlock();
185
186
    return QImage();
}
187

188
189
QImage ThumbnailCache::getThumbnail(const QString &binId, int pos, bool volatileOnly) const
{
190
    QMutexLocker locker(&m_mutex);
191
192
193
    bool ok = false;
    auto key = getKey(binId, pos, &ok);
    if (ok && m_volatileCache->contains(key)) {
194
195
        return m_volatileCache->get(key);
    }
196
    if (!ok || volatileOnly) {
197
198
        return QImage();
    }
199
    QDir thumbFolder = getDir(false, &ok);
200
    if (ok && thumbFolder.exists(key)) {
201
202
        if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() ||
            std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) {
203
204
            m_storedOnDisk[binId].push_back(pos);
        }
205
        locker.unlock();
206
207
208
209
210
211
212
        return QImage(thumbFolder.absoluteFilePath(key));
    }
    return QImage();
}

void ThumbnailCache::storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent)
{
213
    QMutexLocker locker(&m_mutex);
214
215
216
217
218
    bool ok = false;
    const QString key = getKey(binId, pos, &ok);
    if (!ok) {
        return;
    }
219
220
221
222
223
224
225
    // if volatile cache also contains this entry, update it
    if (m_volatileCache->contains(key)) {
        m_volatileCache->remove(key);
    } else {
        m_storedVolatile[binId].push_back(pos);
    }
    m_volatileCache->insert(key, img, (int)img.sizeInBytes());
226
    if (persistent) {
227
        QDir thumbFolder = getDir(false, &ok);
228
        if (ok) {
229
230
            if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() ||
                std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) {
231
232
                m_storedOnDisk[binId].push_back(pos);
            }
233
            locker.unlock();
234
            if (!img.save(thumbFolder.absoluteFilePath(key))) {
235
                qDebug() << ".............\n!!!!!!!! ERROR SAVING THUMB in: " << thumbFolder.absoluteFilePath(key);
236
            }
237
        }
238
    }
239
240
}

241
242
243
244
245
bool ThumbnailCache::checkIntegrity() const
{
    return m_volatileCache->checkIntegrity();
}

246
void ThumbnailCache::saveCachedThumbs(const std::unordered_map<QString, std::vector<int>> &keys)
247
248
{
    bool ok;
249
    QDir thumbFolder = getDir(false, &ok);
250
251
252
    if (!ok) {
        return;
    }
253
    QMutexLocker locker(&m_mutex);
254
255
    for (auto &key : keys) {
        bool ok;
256
257
258
        for (const auto &pos : key.second) {
            if (m_storedOnDisk.find(key.first) == m_storedOnDisk.end() ||
                std::find(m_storedOnDisk[key.first].begin(), m_storedOnDisk[key.first].end(), pos) == m_storedOnDisk[key.first].end()) {
259
260
261
262
263
264
265
266
267
268
269
270
271
                const QString thumbKey = getKey(key.first, pos, &ok);
                if (!ok) {
                    continue;
                }
                if (!thumbFolder.exists(thumbKey) && m_volatileCache->contains(thumbKey)) {
                    QImage img = m_volatileCache->get(thumbKey);
                    if (!img.save(thumbFolder.absoluteFilePath(thumbKey))) {
                        qDebug() << "// Error writing thumbnails to " << thumbFolder.absolutePath();
                        break;
                    } else {
                        m_storedOnDisk[key.first].push_back(pos);
                    }
                }
272
273
274
275
276
            }
        }
    }
}

277
void ThumbnailCache::invalidateThumbsForClip(const QString &binId)
278
{
279
    QMutexLocker locker(&m_mutex);
280
    if (m_storedVolatile.find(binId) != m_storedVolatile.end()) {
281
        bool ok = false;
282
        for (int pos : m_storedVolatile.at(binId)) {
283
284
285
286
            auto key = getKey(binId, pos, &ok);
            if (ok) {
                m_volatileCache->remove(key);
            }
287
288
289
290
        }
        m_storedVolatile.erase(binId);
    }
    bool ok = false;
291
    // Video thumbs
292
293
    QStringList files;
    if (m_storedOnDisk.find(binId) != m_storedOnDisk.end()) {
294
        // Remove persistent cache
295
        for (const auto &pos : m_storedOnDisk.at(binId)) {
296
            if (pos >= 0) {
297
298
                auto key = getKey(binId, pos, &ok);
                if (ok) {
299
                    files << key;
300
                }
301
            }
302
303
        }
        m_storedOnDisk.erase(binId);
304
    }
305
306
307
308
309
310
311
312
313
314
    // Release mutex before deleting files
    locker.unlock();
    if (!files.isEmpty()) {
        QDir thumbFolder = getDir(false, &ok);
        if (ok) {
            while (!files.isEmpty()) {
                thumbFolder.remove(files.takeFirst());
            }
        }
    }
315
316
317
318
}

void ThumbnailCache::clearCache()
{
319
    QMutexLocker locker(&m_mutex);
320
321
322
    m_volatileCache->clear();
    m_storedVolatile.clear();
    m_storedOnDisk.clear();
323
324
325
}

// static
326
QString ThumbnailCache::getKey(const QString &binId, int pos, bool *ok)
327
{
328
329
330
331
    if (binId.isEmpty()) {
        *ok = false;
        return QString();
    }
332
    auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
333
    *ok = binClip != nullptr && binClip->statusReady();
334
335
336
337
338
    QString result;
    if (!ok) {
        return result;
    }
    return *ok ? binClip->hashForThumbs() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".jpg") : QString();
339
340
341
}

// static
342
QStringList ThumbnailCache::getAudioKey(const QString &binId, bool *ok)
343
344
{
    auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
345
346
347
348
349
350
    if (binClip == nullptr) {
        *ok = false;
        qWarning() << "[BUG] Could not find binClip for binId" << binId;
        return {};
    } else {
        *ok = true;
351
        QString streams = binClip->getProducerProperty(QStringLiteral("kdenlive:active_streams"));
352
        if (streams == QString::number(INT_MAX)) {
353
            // activate all audio streams
354
            QList<int> streamIxes = binClip->audioStreams().keys();
355
356
            if (streamIxes.size() > 1) {
                QStringList streamsList;
Vincent Pinon's avatar
Vincent Pinon committed
357
                for (const int st : qAsConst(streamIxes)) {
358
359
360
361
362
                    streamsList << QString("%1_%2.png").arg(binClip->hash()).arg(st);
                }
                return streamsList;
            }
        }
363
        if (streams.size() < 2) {
364
365
366
367
368
369
370
371
            int audio = binClip->getProducerIntProperty(QStringLiteral("audio_index"));
            if (audio > -1) {
                return {QString("%1_%2.png").arg(binClip->hash()).arg(audio)};
            }
            return {binClip->hash() + QStringLiteral(".png")};
        }
        QStringList streamsList;
        QStringList streamIndexes = streams.split(QLatin1Char(';'));
Vincent Pinon's avatar
Vincent Pinon committed
372
        for (const QString &st : qAsConst(streamIndexes)) {
Vincent Pinon's avatar
Vincent Pinon committed
373
            streamsList << QString("%1_%2.png").arg(binClip->hash(), st);
374
        }
375
        return streamsList;
376
    }
377
378
379
}

// static
380
const QDir ThumbnailCache::getDir(bool audio, bool *ok)
381
{
382
    return pCore->projectManager()->cacheDir(audio, ok);
383
}