Files
z-bar-qt/Plugins/ZShell/Models/wallpaperdb.cpp
T
zach 9edbcbd610
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
2026-10-26 02:51:28 +01:00

747 lines
16 KiB
C++

#include "wallpaperdb.hpp"
#include "wallpaperutil.hpp"
#include <QtQuick/qquickimageprovider.h>
#include <qbytearray.h>
#include <qdiriterator.h>
#include <qelapsedtimer.h>
#include <qfuturewatcher.h>
#include <qimage.h>
#include <qimagereader.h>
#include <qlogging.h>
#include <qqmlengine.h>
#include <qsqldatabase.h>
#include <qsqlerror.h>
#include <qsqlquery.h>
#include <qtconcurrentrun.h>
#include <quuid.h>
#include <algorithm>
Q_LOGGING_CATEGORY(dbLog, "ZShell.Internal.wallpaper.db", QtWarningMsg)
namespace ZShell::models {
class WallpaperImageProvider : public QQuickImageProvider {
public:
explicit WallpaperImageProvider(const QString& dbPath)
: QQuickImageProvider(QQuickImageProvider::Image), m_dbPath(dbPath) {}
QImage requestImage(
const QString& id, QSize* size, const QSize& requestedSize) override {
const QString path =
QString::fromUtf8(QByteArray::fromHex(id.toUtf8()));
if (path.isEmpty() || !openThreadDb()) {
return {};
}
QSqlQuery query(QSqlDatabase::database(m_threadUuid));
query.prepare("SELECT cache FROM wallpapers WHERE path = ?");
query.addBindValue(path);
if (!query.exec() || !query.next()) {
return {};
}
const QByteArray bytes = query.value(0).toByteArray();
if (bytes.isEmpty()) {
return {};
}
QImage image = QImage::fromData(bytes, "JPEG");
if (image.isNull()) {
return {};
}
if (size) {
*size = image.size();
}
if (requestedSize.isValid() && !requestedSize.isEmpty() &&
(image.width() > requestedSize.width() ||
image.height() > requestedSize.height())) {
image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
return image;
}
private:
bool openThreadDb() {
if (!m_threadUuid.isEmpty()) {
return true;
}
if (m_dbPath.isEmpty()) {
return false;
}
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
auto db = QSqlDatabase::addDatabase("QSQLITE", uuid);
db.setConnectOptions("QSQLITE_OPEN_READONLY");
db.setDatabaseName(m_dbPath);
if (!db.open()) {
return false;
}
QSqlQuery query(db);
query.exec("PRAGMA journal_mode=WAL");
m_threadUuid = uuid;
return true;
}
const QString m_dbPath;
thread_local static QString m_threadUuid;
};
thread_local QString WallpaperImageProvider::m_threadUuid;
WallpaperEntry::WallpaperEntry(
const QString& path, const QString& relativePath, QObject* parent)
: QObject(parent)
, m_fileInfo(path)
, m_path(path)
, m_relativePath(relativePath) {}
QString WallpaperEntry::path() const {
return m_path;
}
QString WallpaperEntry::relativePath() const {
return m_relativePath;
}
QString WallpaperEntry::name() const {
return m_fileInfo.fileName();
}
QString WallpaperEntry::baseName() const {
return m_fileInfo.baseName();
}
QString WallpaperEntry::parentDir() const {
return m_fileInfo.absolutePath();
}
QString WallpaperEntry::suffix() const {
return m_fileInfo.completeSuffix();
}
qint64 WallpaperEntry::size() const {
return m_fileInfo.size();
}
bool WallpaperEntry::isImage() const {
return !hasVideoExtension(m_path);
}
bool WallpaperEntry::isVideo() const {
return hasVideoExtension(m_path);
}
bool WallpaperEntry::hardware() const {
return m_hardware;
}
bool WallpaperEntry::cached() const {
return !m_thumbnailPath.isEmpty();
}
QString WallpaperEntry::thumbnailPath() const {
if (!m_thumbnailPath.isEmpty()) {
return m_thumbnailPath;
}
return isImage() ? m_path : QString();
}
void WallpaperEntry::setRelativePath(const QString& path) {
if (m_relativePath == path) {
return;
}
m_relativePath = path;
emit relativePathChanged();
}
void WallpaperEntry::setHardware(bool hardware) {
if (m_hardware == hardware) {
return;
}
m_hardware = hardware;
emit hardwareChanged();
}
void WallpaperEntry::setThumbnailPath(const QString& path) {
if (m_thumbnailPath == path) {
return;
}
m_thumbnailPath = path;
emit thumbnailPathChanged();
}
WallpaperDb::WallpaperDb(QObject* parent) : QObject(parent) {
m_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
m_dbPath = QDir::homePath() + "/.local/state/zshell/wallpapers.sqlite";
m_queueTimer.setInterval(300);
m_queueTimer.setTimerType(Qt::CoarseTimer);
QObject::connect(
&m_queueTimer, &QTimer::timeout, this, &WallpaperDb::processQueue);
m_watchTimer.setInterval(400);
m_watchTimer.setSingleShot(true);
QObject::connect(
&m_watcher, &QFileSystemWatcher::directoryChanged, this, [this]() {
m_watchTimer.start();
});
QObject::connect(
&m_watchTimer, &QTimer::timeout, this, &WallpaperDb::scanDir);
}
WallpaperDb::~WallpaperDb() {
if (QSqlDatabase::contains(m_uuid)) {
{
auto db = QSqlDatabase::database(m_uuid, false);
if (db.isValid()) {
db.close();
}
}
QSqlDatabase::removeDatabase(m_uuid);
}
}
QString WallpaperDb::path() const {
return m_path;
}
void WallpaperDb::setPath(const QString& path) {
if (m_path == path) {
return;
}
m_path = path;
m_dir = QDir(path);
emit pathChanged();
for (const auto& dir : m_watcher.directories()) {
m_watcher.removePath(dir);
}
scanDir();
}
QString WallpaperDb::dbPath() const {
return m_dbPath;
}
void WallpaperDb::setDbPath(const QString& path) {
if (m_dbPath == path) {
return;
}
m_dbPath = path;
emit dbPathChanged();
if (!m_path.isEmpty()) {
scanDir();
}
}
bool WallpaperDb::requireHardwareCodec() const {
return m_requireHardwareCodec;
}
void WallpaperDb::setRequireHardwareCodec(bool requireHardwareCodec) {
if (m_requireHardwareCodec == requireHardwareCodec) {
return;
}
m_requireHardwareCodec = requireHardwareCodec;
emit requireHardwareCodecChanged();
rebuildEntries();
}
QQmlListProperty<WallpaperEntry> WallpaperDb::entries() {
return QQmlListProperty<WallpaperEntry>(this, &m_entries);
}
QString WallpaperDb::thumbnailUrl(const QString& path) {
return QStringLiteral("image:
+ QString::fromUtf8(path.toUtf8().toHex());
}
QSize WallpaperDb::cacheSize() const {
return m_cacheSize;
}
void WallpaperDb::setCacheSize(const QSize& cacheSize) {
if (m_cacheSize == cacheSize) {
return;
}
const QSize prev = m_cacheSize;
const bool hadPrevious = !prev.isEmpty();
m_cacheSize = cacheSize;
emit cacheSizeChanged();
if (hadPrevious && qint64(cacheSize.width()) * cacheSize.height() >
qint64(prev.width()) * prev.height()) {
const auto db = QSqlDatabase::database(m_uuid, false);
for (auto* entry : std::as_const(m_byPath)) {
if (!entry->cached()) {
continue;
}
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL"
" WHERE path = ?");
query.addBindValue(entry->path());
query.exec();
}
entry->setThumbnailPath(QString());
queueMetadata(entry, entry->isVideo());
}
rebuildEntries();
}
if (!m_queue.isEmpty() && !m_queueTimer.isActive()) {
m_queueTimer.start();
}
}
void WallpaperDb::registerProvider() {
if (m_provider != nullptr) {
return;
}
auto* engine = qmlEngine(this);
if (engine == nullptr) {
return;
}
m_provider = new WallpaperImageProvider(m_dbPath);
engine->addImageProvider(QStringLiteral("wallpapers"), m_provider);
}
void WallpaperDb::openDb() {
if (m_dbPath.isEmpty()) {
return;
}
if (QSqlDatabase::contains(m_uuid)) {
{
auto db = QSqlDatabase::database(m_uuid, false);
if (db.isOpen() && db.databaseName() == m_dbPath) {
registerProvider();
return;
}
if (db.isValid()) {
db.close();
}
}
QSqlDatabase::removeDatabase(m_uuid);
}
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
db.setDatabaseName(m_dbPath);
if (!db.open()) {
qWarning(dbLog) << "WallpaperDb: could not open database" << m_dbPath
<< db.lastError();
return;
}
QSqlQuery query(db);
query.exec("PRAGMA journal_mode=WAL");
query.exec(
"CREATE TABLE IF NOT EXISTS wallpapers("
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)");
query.exec("SELECT cache FROM wallpapers LIMIT 1");
if (query.lastError().isValid()) {
query.exec("DROP TABLE IF EXISTS wallpapers");
query.exec(
"CREATE TABLE wallpapers("
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)");
} else {
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB");
}
registerProvider();
}
void WallpaperDb::scanDir() {
openDb();
watchDir(m_dir.absolutePath());
if (!m_dir.exists()) {
if (!m_entries.isEmpty()) {
m_entries.clear();
emit entriesChanged();
}
return;
}
auto* watcher = new QFutureWatcher<ScanResult>(this);
QObject::connect(
watcher, &QFutureWatcher<ScanResult>::finished, this, [this, watcher]() {
applyScan(watcher->result());
watcher->deleteLater();
});
const QDir dir = m_dir;
QHash<QString, QPair<qint64, bool>> knownCodecs;
for (const auto* entry : std::as_const(m_byPath)) {
if (entry->isVideo()) {
knownCodecs.insert(
entry->path(), qMakePair(entry->size(), entry->hardware()));
}
}
watcher->setFuture(QtConcurrent::run([dir, knownCodecs]() {
ScanResult result;
result.dir = dir;
QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories);
while (it.hasNext()) {
const QString path = it.next();
const QFileInfo info(path);
if (info.isDir()) {
continue;
}
if (hasVideoExtension(path)) {
FileEntry file;
file.info = info;
const auto known = knownCodecs.constFind(path);
if (known != knownCodecs.constEnd() &&
known.value().first == info.size()) {
file.hardware = known.value().second;
} else {
file.hardware = probeHardwareCodec(path);
}
result.files.append(file);
} else {
QImageReader reader(path);
if (reader.canRead()) {
FileEntry file;
file.info = info;
result.files.append(file);
}
}
}
return result;
}));
}
void WallpaperDb::watchDir(const QString& dir) {
if (dir.isEmpty() || m_watcher.directories().contains(dir)) {
return;
}
if (!QDir(dir).exists()) {
return;
}
m_watcher.addPath(dir);
}
void WallpaperDb::applyScan(const ScanResult& result) {
qCInfo(
dbLog,
"WallpaperDb: scan of %s found %d files (cache target %dx%d)",
m_dir.absolutePath().toUtf8().constData(),
int(result.files.size()),
m_cacheSize.width(),
m_cacheSize.height());
const auto db = QSqlDatabase::database(m_uuid, false);
QSet<QString> seen;
for (const auto& file : result.files) {
seen.insert(file.info.absoluteFilePath());
}
for (auto it = m_byPath.begin(); it != m_byPath.end();) {
if (!seen.contains(it.key())) {
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare("DELETE FROM wallpapers WHERE path = ?");
query.addBindValue(it.key());
query.exec();
}
delete it.value();
it = m_byPath.erase(it);
} else {
++it;
}
}
for (const auto& file : result.files) {
const QFileInfo& info = file.info;
const QString path = info.absoluteFilePath();
const qint64 size = info.size();
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
const bool isVideo = hasVideoExtension(path);
watchDir(info.absolutePath());
const auto existing = m_byPath.constFind(path);
if (existing != m_byPath.constEnd()) {
if (existing.value()->size() == size) {
existing.value()->setHardware(file.hardware);
continue;
}
}
WallpaperEntry* entry = m_byPath.value(path);
if (!entry) {
entry =
new WallpaperEntry(path, m_dir.relativeFilePath(path), this);
m_byPath.insert(path, entry);
}
entry->setHardware(file.hardware);
bool reused = false;
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"SELECT cache, scheme_thumb, size, mtime FROM wallpapers"
" WHERE path = ?");
query.addBindValue(path);
query.exec();
if (query.next()) {
const auto cache = query.value(0).toByteArray();
const auto scheme = query.value(1).toByteArray();
const auto storedSize = query.value(2).toLongLong();
const auto storedMtime = query.value(3).toLongLong();
if (storedSize == size && storedMtime == mtime &&
!cache.isEmpty()) {
reused = true;
entry->setThumbnailPath(thumbnailUrl(path));
if (scheme.isEmpty()) {
queueMetadata(entry, isVideo, cache);
}
}
}
}
if (!reused) {
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"INSERT OR REPLACE INTO wallpapers"
" (path, kind, hardware, size, mtime, cache)"
" VALUES (?, ?, ?, ?, ?, NULL)");
query.addBindValue(path);
query.addBindValue(isVideo ? "video" : "image");
query.addBindValue(
isVideo ? (file.hardware ? 1 : 0) : QVariant());
query.addBindValue(size);
query.addBindValue(mtime);
if (!query.exec()) {
qWarning(dbLog)
<< "WallpaperDb: could not store the row for" << path
<< query.lastError();
}
}
queueMetadata(entry, isVideo);
}
}
rebuildEntries();
}
void WallpaperDb::queueMetadata(
WallpaperEntry* entry, bool isVideo, const QByteArray& existingCache) {
qCInfo(
dbLog,
"WallpaperDb: queued cache job for %s (%s%s)",
entry->path().toUtf8().constData(),
isVideo ? "video" : "image",
existingCache.isEmpty() ? "" : ", existing blob");
m_queue.append(MetadataJob{entry, entry->path(), isVideo, existingCache});
if (!m_queueTimer.isActive()) {
m_queueTimer.start();
}
}
void WallpaperDb::processQueue() {
if (m_jobRunning) {
return;
}
if (m_queue.isEmpty()) {
m_queueTimer.stop();
return;
}
if (m_cacheSize.isEmpty()) {
return;
}
const MetadataJob job = m_queue.takeFirst();
m_jobRunning = true;
qCInfo(
dbLog,
"WallpaperDb: caching %s (%s, %d jobs left in queue)",
job.path.toUtf8().constData(),
job.isVideo ? "video" : "image",
int(m_queue.size()));
runMetadataJob(job);
}
void WallpaperDb::runMetadataJob(const MetadataJob& job) {
struct CacheResult {
QString path;
QByteArray cache;
QByteArray scheme;
};
const QSize cacheSize = m_cacheSize;
QElapsedTimer clock;
clock.start();
auto* watcher = new QFutureWatcher<CacheResult>(this);
QObject::connect(
watcher,
&QFutureWatcher<CacheResult>::finished,
this,
[this, watcher, clock]() {
const auto result = watcher->result();
qCInfo(
dbLog,
"WallpaperDb: %s produced %d bytes of cache in %dms",
result.path.toUtf8().constData(),
int(result.cache.size()),
int(clock.elapsed()));
if (QSqlDatabase::contains(m_uuid)) {
const auto db = QSqlDatabase::database(m_uuid, false);
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?"
" WHERE path = ?");
query.addBindValue(result.cache);
query.addBindValue(result.scheme);
query.addBindValue(result.path);
if (!query.exec()) {
qWarning(dbLog)
<< "WallpaperDb: could not store the cache for"
<< result.path << query.lastError();
}
}
}
if (!result.cache.isEmpty()) {
if (auto* entry = m_byPath.value(result.path)) {
entry->setThumbnailPath(thumbnailUrl(result.path));
}
emit cacheStored(result.path);
}
m_jobRunning = false;
rebuildEntries();
watcher->deleteLater();
});
watcher->setFuture(QtConcurrent::run([job, cacheSize]() {
CacheResult result;
result.path = job.path;
QByteArray cache = job.existingCache;
if (cache.isEmpty()) {
cache = job.isVideo ? videoCacheBytes(job.path, cacheSize)
: imageCacheBytes(job.path, cacheSize);
}
result.cache = cache;
result.scheme = cache.isEmpty() ? QByteArray()
: schemeThumbBytes(cache, 128);
return result;
}));
}
void WallpaperDb::rebuildEntries() {
QList<WallpaperEntry*> next;
next.reserve(m_byPath.size());
for (auto* entry : std::as_const(m_byPath)) {
if (entryAllowed(entry)) {
next.append(entry);
}
}
std::sort(
next.begin(),
next.end(),
[](const WallpaperEntry* a, const WallpaperEntry* b) {
return a->relativePath() < b->relativePath();
});
if (next == m_entries) {
return;
}
m_entries = next;
emit entriesChanged();
}
bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
if (!entry->isVideo()) {
return true;
}
if (!m_requireHardwareCodec) {
return true;
}
return entry->hardware();
}
} // namespace ZShell::models