Ver código fonte

Introduce sqlite based backend for the image model

Donald Carr 8 anos atrás
pai
commit
39d03c2f8f
4 arquivos alterados com 241 adições e 33 exclusões
  1. 3 1
      artriculate.pro
  2. 3 1
      qml/common/ArtImage.qml
  3. 233 31
      src/picturemodel.cpp
  4. 2 0
      src/picturemodel.h

+ 3 - 1
artriculate.pro

@@ -1,8 +1,10 @@
 TEMPLATE = app
 
-QT += qml quick dbus
+QT += qml quick dbus sql
 CONFIG += c++11
 
+DEFINES *= QT_USE_QSTRINGBUILDER
+
 SOURCES += src/main.cpp \
     src/picturemodel.cpp \
     src/helperfunctions.cpp

+ 3 - 1
qml/common/ArtImage.qml

@@ -54,11 +54,13 @@ Item {
     }
 
     Component.onCompleted: {
-        modelIndex = Math.floor(Math.random()*imageModel.count)
+        modelIndex = imageModel.requestIndex()
         if (globalSettings.effect !== "" && Effects.validate(globalSettings.effect)) {
             var component = Qt.createComponent("VisualEffect.qml");
             component.status !== Component.Ready && console.log('Component failed with:' + component.errorString())
             root.effect = component.createObject(root, { target: image, effect: globalSettings.effect })
         }
     }
+
+    Component.onDestruction: imageModel.retireIndex(modelIndex)
 }

+ 233 - 31
src/picturemodel.cpp

@@ -26,6 +26,26 @@
 #include <QImageReader>
 #include <QMimeDatabase>
 #include <QElapsedTimer>
+#include <QStandardPaths>
+
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlError>
+#include <QtSql/QSqlQuery>
+
+namespace {
+    QString stripDbHostileCharacters(QString path) {
+        return path.replace(QString("/"), QString(""));
+    }
+
+    inline int offsetHash(int hash) { return hash + 1; }
+}
+
+struct ArtPiece {
+    ArtPiece() : refCount(0) { /**/ }
+    QString path;
+    QSize size;
+    int refCount;
+};
 
 struct FSNode {
   FSNode(const QString& rname, const FSNode *pparent = nullptr);
@@ -63,27 +83,29 @@ class FSNodeTree : public QObject
 {
     Q_OBJECT
 public:
-    FSNodeTree(PictureModel *p);
+    FSNodeTree(const QString& path);
+    virtual ~FSNodeTree();
 
     void addModelNode(const FSNode* parentNode);
-    void setModelRoot(const QString& rootDir) { this->rootDir = rootDir; }
 
     int fileCount() const { return files.length(); }
     QVector<FSLeafNode*> files;
 public slots:
-    void populate();
+    void populate(bool useDatabaseBackend);
 signals:
     void countChanged();
 private:
+    QSqlError initDb();
+    QSqlError dumpTreeToDb();
+
     QStringList extensions;
     QString rootDir;
 };
 
-FSNodeTree::FSNodeTree(PictureModel *p)
-    : QObject(nullptr)
+FSNodeTree::FSNodeTree(const QString& path)
+    : QObject(nullptr),
+      rootDir(path)
 {
-    connect(this, SIGNAL(countChanged()), p, SIGNAL(countChanged()));
-
     QMimeDatabase mimeDatabase;
     foreach(const QByteArray &m, QImageReader::supportedMimeTypes()) {
         foreach(const QString &suffix, mimeDatabase.mimeTypeForName(m).suffixes())
@@ -95,6 +117,18 @@ FSNodeTree::FSNodeTree(PictureModel *p)
     }
 }
 
+FSNodeTree::~FSNodeTree()
+{
+    QSet<const FSNode*> nodes;
+    foreach(const FSNode *node, files) {
+        while(node) {
+            nodes << node;
+            node = node->parent;
+        }
+    }
+    qDeleteAll(nodes.toList());
+}
+
 void FSNodeTree::addModelNode(const FSNode* parentNode)
 {
     // TODO: Check for symlink recursion
@@ -137,16 +171,70 @@ void FSNodeTree::addModelNode(const FSNode* parentNode)
     }
 }
 
-void FSNodeTree::populate()
+void FSNodeTree::populate(bool useDatabaseBackend)
 {
     QElapsedTimer timer;
     timer.start();
     QDir currentDir(rootDir);
     if (!currentDir.exists()) {
-        qDebug() << "Being told to watch a non existent directory";
+        qDebug() << "Being told to watch a non existent directory:" << rootDir;
     }
     addModelNode(new FSNode(rootDir));
     qDebug() << "Completed building file tree after:" << timer.elapsed();
+
+    if (useDatabaseBackend) {
+        timer.restart();
+        QSqlError err = dumpTreeToDb();
+        qDebug() << "Completed database dump after:" << timer.elapsed();
+
+        if (err.type() != QSqlError::NoError) {
+            qDebug() << "Database dump of content tree failed with" << err.text();
+        } else {
+            qDebug() << "Successfully finished populating DB:" << rootDir;
+        }
+    }
+}
+
+QSqlError FSNodeTree::dumpTreeToDb()
+{
+    QSqlError err = initDb();
+    if (err.type() != QSqlError::NoError) {
+        return err;
+    }
+
+    QString insertQuery = QString("INSERT INTO %1 (path, width, height) VALUES ").arg(::stripDbHostileCharacters(rootDir));
+    QString insertQueryValues("(?, ?, ?),");
+
+    insertQuery.reserve(insertQuery.size() + insertQueryValues.size()*files.length());
+    for(int i = 0; i < files.length(); i++) {
+         insertQuery.append(insertQueryValues);
+    }
+
+    insertQuery = insertQuery.replace(insertQuery.length()-1, 1, ";");
+
+    QSqlQuery q;
+
+    if (!q.prepare(insertQuery))
+        return q.lastError();
+
+    foreach(const FSLeafNode *node, files) {
+        q.addBindValue(node->qualifyNode(node));
+        q.addBindValue(node->size.width());
+        q.addBindValue(node->size.height());
+    }
+
+    q.exec();
+
+    return q.lastError();
+}
+
+QSqlError FSNodeTree::initDb()
+{
+    QSqlQuery q;
+    if (!q.exec(QString("create table %1 (path varchar, width integer, height integer)").arg(::stripDbHostileCharacters(rootDir))))
+        return q.lastError();
+
+    return QSqlError();
 }
 
 class PictureModel::PictureModelPrivate {
@@ -155,36 +243,117 @@ public:
     ~PictureModelPrivate();
 
     FSNodeTree *fsTree;
+    bool useDatabaseBackend;
+
+    void cacheIndex(int index);
+    void retireCachedIndex(int index);
+    int itemCount();
+
+    QHash<int, ArtPiece*> artwork;
 private:
+    PictureModel *parent;
+    int collectionSize;
+    QString artPath;
+    void createFSTree(const QString &path);
     QThread scanningThread;
 };
 
 PictureModel::PictureModelPrivate::PictureModelPrivate(PictureModel* p)
+    : fsTree(nullptr),
+      parent(p)
 {
     QSettings settings;
-    QString artPath = settings.value("artPath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString();
+    useDatabaseBackend = settings.value("useDatabaseBackend", true).toBool();
+    settings.setValue("useDatabaseBackend", useDatabaseBackend);
 
+    artPath = settings.value("artPath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString();
     settings.setValue("artPath", artPath);
 
-    fsTree = new FSNodeTree(p);
-
-    fsTree->setModelRoot(artPath);
+    if (useDatabaseBackend) {
+        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
+        QFileInfo dbFile(QStandardPaths::standardLocations(QStandardPaths::DataLocation).first() + "/" + qApp->applicationName() + ".db");
+        QDir().mkpath(dbFile.absolutePath());
+        db.setDatabaseName(dbFile.absoluteFilePath());
+
+        if (db.open()) {
+            QStringList tables = db.tables();
+            if (tables.contains(::stripDbHostileCharacters(artPath), Qt::CaseInsensitive)) {
+                QString queryString = "SELECT COUNT(*) FROM " % ::stripDbHostileCharacters(artPath) % ";";
+                QSqlQuery query(queryString);
+                query.next();
+
+                collectionSize = query.value(0).toInt();
+                QMetaObject::invokeMethod(parent, "countChanged");
+            } else {
+                createFSTree(artPath);
+            }
+        } else {
+            qDebug() << "Failed to open the database:" << dbFile.absoluteFilePath();
+            qDebug() << "Error:" << db.lastError().text();
+            qApp->exit(-1);
+        }
+    } else {
+        createFSTree(artPath);
+    }
+};
 
+void PictureModel::PictureModelPrivate::createFSTree(const QString &path)
+{
+    fsTree = new FSNodeTree(path);
+    connect(fsTree, &FSNodeTree::countChanged, parent, &PictureModel::countChanged);
     fsTree->moveToThread(&scanningThread);
     scanningThread.start();
-
-    QMetaObject::invokeMethod(fsTree, "populate", Qt::QueuedConnection);
-};
+    QMetaObject::invokeMethod(fsTree, "populate", Qt::QueuedConnection, Q_ARG(bool, useDatabaseBackend));
+}
 
 PictureModel::PictureModelPrivate::~PictureModelPrivate()
 {
-    scanningThread.quit();
-    scanningThread.wait(5000);
+    if (fsTree) {
+        scanningThread.quit();
+        scanningThread.wait(5000);
+
+        delete fsTree;
+        fsTree = nullptr;
+    }
+}
 
-    delete fsTree;
-    fsTree = nullptr;
+int PictureModel::PictureModelPrivate::itemCount() {
+    return fsTree ? fsTree->fileCount() : collectionSize;
 };
 
+void PictureModel::PictureModelPrivate::cacheIndex(int index)
+{
+    int hashIndex = ::offsetHash(index);
+
+    if (artwork.contains(hashIndex)) {
+        artwork[hashIndex]->refCount++;
+        return;
+    }
+
+    QString queryString = "SELECT path, width, height FROM " % ::stripDbHostileCharacters(artPath) % " LIMIT 1 OFFSET " % QString::number(index) % ";";
+
+    QSqlQuery query(queryString);
+
+    query.next();
+
+    ArtPiece *art = new ArtPiece;
+    art->path = query.value(0).toString();
+    art->size = QSize(query.value(1).toInt(), query.value(2).toInt());
+    art->refCount++;
+
+    artwork[hashIndex] = art;
+}
+
+void PictureModel::PictureModelPrivate::retireCachedIndex(int index)
+{
+    int hashIndex = ::offsetHash(index);
+    artwork[hashIndex]->refCount--;
+    if (artwork[hashIndex]->refCount < 1) {
+        delete artwork[hashIndex];
+        artwork.remove(hashIndex);
+    }
+}
+
 PictureModel::PictureModel(QObject *parent)
     : QAbstractListModel(parent),
       d(new PictureModelPrivate(this)) { /**/ }
@@ -198,12 +367,14 @@ PictureModel::~PictureModel()
 int PictureModel::rowCount(const QModelIndex &parent) const
 {
     Q_UNUSED(parent)
-    return d->fsTree->fileCount();
+    return d->itemCount();
 }
 
 QVariant PictureModel::data(const QModelIndex &index, int role) const
 {
-    if (index.row() < 0 || index.row() >= d->fsTree->fileCount()) {
+    // What the fuck; Qt queries item 0 before we substantiate it
+    // I get to offset my hash by 1 or loss a piece of art
+    if (index.row() <= 0 || index.row() >= d->itemCount()) {
         switch (role) {
         case SizeRole:
             return QSize(1222,900);
@@ -215,20 +386,51 @@ QVariant PictureModel::data(const QModelIndex &index, int role) const
         }
     }
 
-
-    switch (role) {
-    case SizeRole:
-        return d->fsTree->files.at(index.row())->size;
-    case NameRole:
-        return d->fsTree->files.at(index.row())->name;
-    case PathRole:
-    default:
-        return QUrl::fromLocalFile(FSNode::qualifyNode(d->fsTree->files.at(index.row())));
+    if (d->fsTree) {
+        switch (role) {
+        case SizeRole:
+            return d->fsTree->files.at(index.row())->size;
+        case NameRole:
+            return d->fsTree->files.at(index.row())->name;
+        case PathRole:
+        default:
+            return QUrl::fromLocalFile(FSNode::qualifyNode(d->fsTree->files.at(index.row())));
+        }
+    } else {
+        int hashIndex = ::offsetHash(index.row());
+        switch (role) {
+        case SizeRole: {
+            return d->artwork[hashIndex]->size;
+        }
+        case NameRole:
+            return d->artwork[hashIndex]->path;
+        case PathRole:
+        default:
+            return QUrl::fromLocalFile(d->artwork[hashIndex]->path);
+        }
     }
 
     return QVariant();
 }
 
+int PictureModel::requestIndex()
+{
+    int index = d->itemCount() == 0 ? 0 : qrand() % d->itemCount();
+
+    if (!d->fsTree) {
+        d->cacheIndex(index);
+    }
+
+    return index;
+}
+
+void PictureModel::retireIndex(int index)
+{
+    if (!d->fsTree) {
+        d->retireCachedIndex(index);
+    }
+}
+
 QHash<int, QByteArray> PictureModel::roleNames() const
 {
     QHash<int, QByteArray> roles;

+ 2 - 0
src/picturemodel.h

@@ -41,6 +41,8 @@ public:
     int rowCount(const QModelIndex & parent = QModelIndex()) const;
     Q_INVOKABLE QVariant data(const int &row, int role = PathRole) const { return data(index(row, 0), role); }
     QVariant data(const QModelIndex & index, int role = PathRole) const;
+    Q_INVOKABLE int requestIndex();
+    Q_INVOKABLE void retireIndex(int index);
 signals:
     void countChanged();