picturemodel.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. /****************************************************************************
  2. ** Artriculate: Art comes tumbling down
  3. ** Copyright (C) 2016 Chaos Reins
  4. **
  5. ** This program is free software: you can redistribute it and/or modify
  6. ** it under the terms of the GNU General Public License as published by
  7. ** the Free Software Foundation, either version 3 of the License, or
  8. ** (at your option) any later version.
  9. **
  10. ** This program is distributed in the hope that it will be useful,
  11. ** but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ** GNU General Public License for more details.
  14. **
  15. ** You should have received a copy of the GNU General Public License
  16. ** along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. ****************************************************************************/
  18. #include "picturemodel.h"
  19. #include <QDir>
  20. #include <QDebug>
  21. #include <QCoreApplication>
  22. #include <QSettings>
  23. #include <QThread>
  24. #include <QImageReader>
  25. #include <QMimeDatabase>
  26. #include <QElapsedTimer>
  27. #include <QStandardPaths>
  28. #include <QtSql/QSqlDatabase>
  29. #include <QtSql/QSqlError>
  30. #include <QtSql/QSqlQuery>
  31. #include <QtSql/QSqlDriver>
  32. namespace {
  33. QSqlDatabase openDBConnection(const QString &connectionName) {
  34. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
  35. QFileInfo dbFile(QStandardPaths::standardLocations(QStandardPaths::DataLocation).first() + "/" + qApp->applicationName() + ".db");
  36. QDir().mkpath(dbFile.absolutePath());
  37. db.setDatabaseName(dbFile.absoluteFilePath());
  38. if (!db.open()) {
  39. qDebug() << "Failed to open the database:" << dbFile.absoluteFilePath();
  40. qDebug() << "Error:" << db.lastError().text();
  41. qApp->exit(-1);
  42. }
  43. return db;
  44. }
  45. QString stripDbHostileCharacters(QString path) {
  46. return path.replace(QString("/"), QString(""));
  47. }
  48. inline int offsetHash(int hash) { return hash + 1; }
  49. }
  50. struct ArtPiece {
  51. ArtPiece() : refCount(0) { /**/ }
  52. QString path;
  53. QSize size;
  54. int refCount;
  55. };
  56. struct FSNode {
  57. FSNode(const QString& rname, const FSNode *pparent = nullptr);
  58. static QString qualifyNode(const FSNode *node);
  59. const QString name;
  60. const FSNode *parent;
  61. };
  62. struct FSLeafNode : public FSNode {
  63. using FSNode::FSNode;
  64. QSize size;
  65. };
  66. FSNode::FSNode(const QString& rname, const FSNode *pparent)
  67. : name(rname),
  68. parent(pparent)
  69. {
  70. }
  71. QString FSNode::qualifyNode(const FSNode *node) {
  72. QString qualifiedPath;
  73. while(node->parent != nullptr) {
  74. qualifiedPath = "/" + node->name + qualifiedPath;
  75. node = node->parent;
  76. }
  77. qualifiedPath = node->name + qualifiedPath;
  78. return qualifiedPath;
  79. }
  80. class FSNodeTree : public QObject
  81. {
  82. Q_OBJECT
  83. public:
  84. FSNodeTree(const QString& path);
  85. virtual ~FSNodeTree();
  86. void addModelNode(const FSNode* parentNode);
  87. int fileCount() const { return files.length(); }
  88. QVector<FSLeafNode*> files;
  89. public slots:
  90. void populate(bool useDatabaseBackend);
  91. signals:
  92. void countChanged();
  93. private:
  94. void dumpTreeToDb();
  95. QStringList extensions;
  96. QString rootDir;
  97. };
  98. FSNodeTree::FSNodeTree(const QString& path)
  99. : QObject(nullptr),
  100. rootDir(path)
  101. {
  102. QMimeDatabase mimeDatabase;
  103. foreach(const QByteArray &m, QImageReader::supportedMimeTypes()) {
  104. foreach(const QString &suffix, mimeDatabase.mimeTypeForName(m).suffixes())
  105. extensions.append(suffix);
  106. }
  107. if (extensions.isEmpty()) {
  108. qFatal("Your Qt install has no image format support");
  109. }
  110. }
  111. FSNodeTree::~FSNodeTree()
  112. {
  113. QSet<const FSNode*> nodes;
  114. foreach(const FSNode *node, files) {
  115. while(node) {
  116. nodes << node;
  117. node = node->parent;
  118. }
  119. }
  120. qDeleteAll(nodes.toList());
  121. }
  122. void FSNodeTree::addModelNode(const FSNode* parentNode)
  123. {
  124. // TODO: Check for symlink recursion
  125. QDir parentDir(FSNode::qualifyNode(parentNode));
  126. foreach(const QString &currentDir, parentDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
  127. const FSNode *dir = new FSNode(currentDir, parentNode);
  128. addModelNode(dir);
  129. }
  130. foreach(const QString &currentFile, parentDir.entryList(QDir::Files)) {
  131. QString extension = currentFile.mid(currentFile.length() - 3);
  132. if (!extensions.contains(extension))
  133. continue;
  134. FSLeafNode *file = new FSLeafNode(currentFile, parentNode);
  135. const QString fullPath = FSNode::qualifyNode(file);
  136. QSize size = QImageReader(fullPath).size();
  137. bool rational = false;
  138. if (size.isValid()) {
  139. file->size = size;
  140. qreal ratio = qreal(size.width())/size.height();
  141. if ((ratio < 0.01) || (ratio > 100)) {
  142. qDebug() << "Image" << fullPath << "has excessive ratio" << ratio << "excluded";
  143. } else {
  144. rational = true;
  145. }
  146. } else {
  147. qDebug() << "Discarding" << fullPath << "due to invalid size";
  148. }
  149. if (rational) {
  150. files << file;
  151. emit countChanged();
  152. } else {
  153. delete file;
  154. }
  155. }
  156. }
  157. void FSNodeTree::populate(bool useDatabaseBackend)
  158. {
  159. QElapsedTimer timer;
  160. timer.start();
  161. QDir currentDir(rootDir);
  162. if (!currentDir.exists()) {
  163. qDebug() << "Being told to watch a non existent directory:" << rootDir;
  164. }
  165. addModelNode(new FSNode(rootDir));
  166. qDebug() << "Completed building file tree containing:" << files.length() << "images after:" << timer.elapsed() << "ms";
  167. if (useDatabaseBackend) {
  168. qDebug() << "No database found; dumping tree to db" << rootDir;
  169. timer.restart();
  170. dumpTreeToDb();
  171. qDebug() << "Completed database dump after:" << timer.elapsed() << "ms";
  172. }
  173. }
  174. void FSNodeTree::dumpTreeToDb()
  175. {
  176. QSqlDatabase db = openDBConnection("write");
  177. QSqlQuery q("", db);
  178. if (!q.exec(QString("create table %1 (path varchar, width integer, height integer)").arg(::stripDbHostileCharacters(rootDir)))) {
  179. qDebug() << "Failed to init DB with:" << q.lastError().text();
  180. return;
  181. }
  182. qDebug() << "Database supports transactions" << QSqlDatabase::database().driver()->hasFeature(QSqlDriver::Transactions);
  183. // Turns out SQLITE has a 999 variable limit by default
  184. // Arch shieleded me from this
  185. int varLimitPerWave = 999;
  186. int varCountPerItem = 3;
  187. int itemCountPerWave = varLimitPerWave/varCountPerItem;
  188. int waveCount = files.length()/itemCountPerWave;
  189. const int waveTail = files.length()%itemCountPerWave;
  190. if (waveTail > 0) {
  191. waveCount += 1;
  192. }
  193. qDebug() << "About to drop" << files.length() << "files to DB";
  194. qDebug() << "This will require" << waveCount << "separate DB transactions";
  195. for (int wave = 0; wave < waveCount; wave++)
  196. {
  197. int itemCount = itemCountPerWave;
  198. if ((waveTail > 0) && (wave == waveCount - 1)) {
  199. itemCount = waveTail;
  200. }
  201. QString insertQuery = QString("INSERT INTO %1 (path, width, height) VALUES ").arg(::stripDbHostileCharacters(rootDir));
  202. QString insertQueryValues("(?, ?, ?),");
  203. insertQuery.reserve(insertQuery.size() + insertQueryValues.size()*itemCount);
  204. for(int i = 0; i < itemCount; i++) {
  205. insertQuery.append(insertQueryValues);
  206. }
  207. insertQuery = insertQuery.replace(insertQuery.length()-1, 1, ";");
  208. db.transaction();
  209. QSqlQuery query("", db);
  210. if (!query.prepare(insertQuery)) {
  211. qDebug() << "Query preperation failed with" << query.lastError().text();
  212. return;
  213. }
  214. for(int i = wave*itemCountPerWave; i < (wave*itemCountPerWave + itemCount); i++) {
  215. const FSLeafNode *node = files.at(i);
  216. query.addBindValue(node->qualifyNode(node));
  217. query.addBindValue(node->size.width());
  218. query.addBindValue(node->size.height());
  219. }
  220. query.exec();
  221. if (db.commit()) {
  222. qDebug() << "SQL transaction succeeded";
  223. } else {
  224. qDebug() << "SQL transaction failed";
  225. }
  226. QSqlError err = query.lastError();
  227. if (err.type() != QSqlError::NoError) {
  228. qDebug() << "Database dump of content tree failed with" << err.text();
  229. } else {
  230. qDebug() << "Successfully finished adding wave" << wave << "to DB" << rootDir;
  231. }
  232. }
  233. }
  234. class PictureModel::PictureModelPrivate {
  235. public:
  236. PictureModelPrivate(PictureModel* p);
  237. ~PictureModelPrivate();
  238. FSNodeTree *fsTree;
  239. bool useDatabaseBackend;
  240. bool assumeLinearAccess = false;
  241. void cacheIndex(int index);
  242. void retireCachedIndex(int index);
  243. int itemCount();
  244. QHash<int, ArtPiece*> artwork;
  245. private:
  246. PictureModel *parent;
  247. int collectionSize;
  248. QString artPath;
  249. void createFSTree(const QString &path);
  250. QThread scanningThread;
  251. };
  252. PictureModel::PictureModelPrivate::PictureModelPrivate(PictureModel* p)
  253. : fsTree(nullptr),
  254. parent(p)
  255. {
  256. QSettings settings;
  257. useDatabaseBackend = settings.value("useDatabaseBackend", true).toBool();
  258. settings.setValue("useDatabaseBackend", useDatabaseBackend);
  259. artPath = settings.value("artPath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString();
  260. settings.setValue("artPath", artPath);
  261. if (useDatabaseBackend) {
  262. QSqlDatabase db = openDBConnection("read");
  263. QStringList tables = db.tables();
  264. if (tables.contains(::stripDbHostileCharacters(artPath), Qt::CaseInsensitive)) {
  265. QString queryString = "SELECT COUNT(*) FROM " % ::stripDbHostileCharacters(artPath) % ";";
  266. QSqlQuery query(queryString, db);
  267. query.next();
  268. collectionSize = query.value(0).toInt();
  269. QMetaObject::invokeMethod(parent, "countChanged");
  270. qDebug() << "Using existing database entry for" << artPath;
  271. } else {
  272. qDebug() << "No database found; creating file tree" << artPath;
  273. createFSTree(artPath);
  274. }
  275. } else {
  276. createFSTree(artPath);
  277. }
  278. };
  279. void PictureModel::PictureModelPrivate::createFSTree(const QString &path)
  280. {
  281. fsTree = new FSNodeTree(path);
  282. connect(fsTree, &FSNodeTree::countChanged, parent, &PictureModel::countChanged);
  283. fsTree->moveToThread(&scanningThread);
  284. scanningThread.start();
  285. QMetaObject::invokeMethod(fsTree, "populate", Qt::QueuedConnection, Q_ARG(bool, useDatabaseBackend));
  286. }
  287. PictureModel::PictureModelPrivate::~PictureModelPrivate()
  288. {
  289. if (fsTree) {
  290. scanningThread.quit();
  291. scanningThread.wait(5000);
  292. delete fsTree;
  293. fsTree = nullptr;
  294. }
  295. }
  296. int PictureModel::PictureModelPrivate::itemCount() {
  297. return fsTree ? fsTree->fileCount() : collectionSize;
  298. };
  299. void PictureModel::PictureModelPrivate::cacheIndex(int index)
  300. {
  301. int hashIndex = ::offsetHash(index);
  302. if (artwork.contains(hashIndex)) {
  303. artwork[hashIndex]->refCount++;
  304. return;
  305. }
  306. QString queryString = "SELECT path, width, height FROM " % ::stripDbHostileCharacters(artPath) % " LIMIT 1 OFFSET " % QString::number(index) % ";";
  307. QSqlDatabase db = QSqlDatabase::database("read", true);
  308. QSqlQuery query(queryString, db);
  309. query.next();
  310. ArtPiece *art = new ArtPiece;
  311. art->path = query.value(0).toString();
  312. art->size = QSize(query.value(1).toInt(), query.value(2).toInt());
  313. art->refCount++;
  314. artwork[hashIndex] = art;
  315. }
  316. void PictureModel::PictureModelPrivate::retireCachedIndex(int index)
  317. {
  318. int hashIndex = ::offsetHash(index);
  319. artwork[hashIndex]->refCount--;
  320. if (assumeLinearAccess || artwork[hashIndex]->refCount < 1) {
  321. delete artwork[hashIndex];
  322. artwork.remove(hashIndex);
  323. }
  324. }
  325. PictureModel::PictureModel(QObject *parent)
  326. : QAbstractListModel(parent),
  327. d(new PictureModelPrivate(this)) { /**/ }
  328. PictureModel::~PictureModel()
  329. {
  330. delete d;
  331. d = nullptr;
  332. }
  333. int PictureModel::rowCount(const QModelIndex &parent) const
  334. {
  335. Q_UNUSED(parent)
  336. return d->itemCount();
  337. }
  338. QVariant PictureModel::data(const QModelIndex &index, int role) const
  339. {
  340. if (d->assumeLinearAccess) {
  341. requestIndex(index.row());
  342. }
  343. // What the fuck; Qt queries item 0 before we substantiate it
  344. // I get to offset my hash by 1 or loss a piece of art
  345. if (index.row() <= 0 || index.row() >= d->itemCount()) {
  346. switch (role) {
  347. case SizeRole:
  348. return QSize(1222,900);
  349. case NameRole:
  350. return "Qt logo";
  351. case PathRole:
  352. default:
  353. return QString("qrc:///qt_logo_green_rgb.png");
  354. }
  355. }
  356. if (d->fsTree) {
  357. switch (role) {
  358. case SizeRole:
  359. return d->fsTree->files.at(index.row())->size;
  360. case NameRole:
  361. return d->fsTree->files.at(index.row())->name;
  362. case PathRole:
  363. default:
  364. return QUrl::fromLocalFile(FSNode::qualifyNode(d->fsTree->files.at(index.row())));
  365. }
  366. } else {
  367. int hashIndex = ::offsetHash(index.row());
  368. switch (role) {
  369. case SizeRole: {
  370. return d->artwork[hashIndex]->size;
  371. }
  372. case NameRole:
  373. return d->artwork[hashIndex]->path;
  374. case PathRole:
  375. default:
  376. return QUrl::fromLocalFile(d->artwork[hashIndex]->path);
  377. }
  378. }
  379. return QVariant();
  380. }
  381. int PictureModel::requestIndex(int index) const
  382. {
  383. if (index == -1) {
  384. index = d->itemCount() == 0 ? 0 : qrand() % d->itemCount();
  385. }
  386. if (!d->fsTree) {
  387. d->cacheIndex(index);
  388. }
  389. return index;
  390. }
  391. void PictureModel::retireIndex(int index) const
  392. {
  393. if (!d->fsTree) {
  394. d->retireCachedIndex(index);
  395. }
  396. }
  397. void PictureModel::assumeLinearAccess()
  398. {
  399. d->assumeLinearAccess = true;
  400. }
  401. QHash<int, QByteArray> PictureModel::roleNames() const
  402. {
  403. QHash<int, QByteArray> roles;
  404. roles[NameRole] = "name";
  405. roles[PathRole] = "path";
  406. roles[SizeRole] = "size";
  407. return roles;
  408. }
  409. #include "moc/picturemodel.moc"