#include "wallpaperdb.hpp" #include "wallpaperutil.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 WallpaperDb::entries() { return QQmlListProperty(this, &m_entries); } QString WallpaperDb::thumbnailUrl(const QString& path) { return QStringLiteral("image://wallpapers/") + 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(this); QObject::connect( watcher, &QFutureWatcher::finished, this, [this, watcher]() { applyScan(watcher->result()); watcher->deleteLater(); }); const QDir dir = m_dir; QHash> 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 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(this); QObject::connect( watcher, &QFutureWatcher::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 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