diff --git a/src/qtquick/BookDatabase.cpp b/src/qtquick/BookDatabase.cpp index 24d42b6..010c4b6 100644 --- a/src/qtquick/BookDatabase.cpp +++ b/src/qtquick/BookDatabase.cpp @@ -1,151 +1,165 @@ /* * Copyright (C) 2017 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "BookDatabase.h" #include "CategoryEntriesModel.h" #include #include #include #include #include #include class BookDatabase::Private { public: Private() { db = QSqlDatabase::addDatabase("QSQLITE"); QDir location{QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)}; if(!location.exists()) location.mkpath("."); dbfile = location.absoluteFilePath("library.sqlite"); db.setDatabaseName(dbfile); } QSqlDatabase db; QString dbfile; bool prepareDb() { if (!db.open()) { qDebug() << "Failed to open the book database file" << dbfile << db.lastError(); return false; } QStringList tables = db.tables(); if (tables.contains("books", Qt::CaseInsensitive)) return true; QSqlQuery q; if (!q.exec(QLatin1String("create table books(filename varchar primary key, filetitle varchar, title varchar, series varchar, author varchar, publisher varchar, created datetime, lastOpenedTime datetime, totalPages integer, currentPage integer, thumbnail varchar, description varchar, comment varchar, tags varchar, rating integer, seriesVolumes varchar, seriesNumbers varchar)"))) { qDebug() << "Database could not create the table books"; return false; } return true; } void closeDb() { db.close(); } }; BookDatabase::BookDatabase(QObject* parent) : QObject(parent) , d(new Private) { } BookDatabase::~BookDatabase() { delete d; } QList BookDatabase::loadEntries() { if(!d->prepareDb()) { return QList(); } QList entries; QSqlQuery allEntries("SELECT filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating, seriesNumbers, seriesVolumes FROM books"); while(allEntries.next()) { BookEntry* entry = new BookEntry(); entry->filename = allEntries.value(0).toString(); entry->filetitle = allEntries.value(1).toString(); entry->title = allEntries.value(2).toString(); entry->series = allEntries.value(3).toString().split(","); entry->author = allEntries.value(4).toString().split(","); entry->publisher = allEntries.value(5).toString(); entry->created = allEntries.value(6).toDateTime(); entry->lastOpenedTime = allEntries.value(7).toDateTime(); entry->totalPages = allEntries.value(8).toInt(); entry->currentPage = allEntries.value(9).toInt(); entry->thumbnail = allEntries.value(10).toString(); entry->description = allEntries.value(11).toString().split(","); entry->comment = allEntries.value(12).toString(); entry->tags = allEntries.value(13).toString().split(","); entry->rating = allEntries.value(14).toInt(); entry->seriesNumbers = allEntries.value(15).toString().split(","); entry->seriesVolumes = allEntries.value(16).toString().split(","); entries.append(entry); } d->closeDb(); return entries; } void BookDatabase::addEntry(BookEntry* entry) { if(!d->prepareDb()) { return; } qDebug() << "Adding newly discovered book to the database" << entry->filename; QSqlQuery newEntry; newEntry.prepare("INSERT INTO books (filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating, seriesNumbers, seriesVolumes) " "VALUES (:filename, :filetitle, :title, :series, :author, :publisher, :created, :lastOpenedTime, :totalPages, :currentPage, :thumbnail, :description, :comment, :tags, :rating, :seriesNumbers, :seriesVolumes)"); newEntry.bindValue(":filename", entry->filename); newEntry.bindValue(":filetitle", entry->filetitle); newEntry.bindValue(":title", entry->title); newEntry.bindValue(":series", entry->series.join(",")); newEntry.bindValue(":author", entry->author.join(",")); newEntry.bindValue(":publisher", entry->publisher); newEntry.bindValue(":publisher", entry->publisher); newEntry.bindValue(":created", entry->created); newEntry.bindValue(":lastOpenedTime", entry->lastOpenedTime); newEntry.bindValue(":totalPages", entry->totalPages); newEntry.bindValue(":currentPage", entry->currentPage); newEntry.bindValue(":thumbnail", entry->thumbnail); newEntry.bindValue(":description", entry->description.join(",")); newEntry.bindValue(":comment", entry->comment); newEntry.bindValue(":tags", entry->tags.join(",")); newEntry.bindValue(":rating", entry->rating); newEntry.bindValue(":seriesNumbers", entry->seriesNumbers.join(",")); newEntry.bindValue(":seriesVolumes", entry->seriesVolumes.join(",")); newEntry.exec(); d->closeDb(); } + +void BookDatabase::removeEntry(BookEntry* entry) +{ + if(!d->prepareDb()) { + return; + } + qDebug() << "Removing book from the database" << entry->filename; + + QSqlQuery removeEntry; + removeEntry.prepare("DELETE FROM books WHERE filename='"+entry->filename+"';"); + removeEntry.exec(); + + d->closeDb(); +} diff --git a/src/qtquick/BookDatabase.h b/src/qtquick/BookDatabase.h index 17cb91a..df4e791 100644 --- a/src/qtquick/BookDatabase.h +++ b/src/qtquick/BookDatabase.h @@ -1,57 +1,62 @@ /* * Copyright (C) 2017 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #ifndef BOOKDATABASE_H #define BOOKDATABASE_H #include struct BookEntry; /** * \brief A Class to hold a cache of known books to reduce the amount of time spent indexing. * * BookDatabase handles holding the conversion between SQL entry and * BookEntry structs. * * The BookEntry struct is defined in CategoryEntriesModel. */ class BookDatabase : public QObject { Q_OBJECT public: explicit BookDatabase(QObject* parent = nullptr); ~BookDatabase() override; /** * @return a list of all known books in the database. */ QList loadEntries(); /** * \brief Add a new book to the cache. * @param entry The entry to add. */ void addEntry(BookEntry* entry); + /** + * @brief remove an entry by filename from the cache. + * @param entry the entry to remove. + */ + void removeEntry(BookEntry* entry); private: class Private; Private* d; }; #endif//BOOKDATABASE_H diff --git a/src/qtquick/BookListModel.cpp b/src/qtquick/BookListModel.cpp index 9fa5a09..54bb15d 100644 --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -1,410 +1,420 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "BookListModel.h" #include "BookDatabase.h" #include "CategoryEntriesModel.h" #include "ArchiveBookModel.h" #include "AcbfAuthor.h" #include "AcbfSequence.h" #include "AcbfBookinfo.h" #include #include #include #include #include #include #include #include class BookListModel::Private { public: Private() : contentModel(nullptr) , titleCategoryModel(nullptr) , newlyAddedCategoryModel(nullptr) , authorCategoryModel(nullptr) , seriesCategoryModel(nullptr) , publisherCategoryModel(nullptr) , folderCategoryModel(nullptr) , cacheLoaded(false) { db = new BookDatabase(); }; ~Private() { qDeleteAll(entries); db->deleteLater(); } QList entries; QAbstractListModel* contentModel; CategoryEntriesModel* titleCategoryModel; CategoryEntriesModel* newlyAddedCategoryModel; CategoryEntriesModel* authorCategoryModel; CategoryEntriesModel* seriesCategoryModel; CategoryEntriesModel* publisherCategoryModel; CategoryEntriesModel* folderCategoryModel; BookDatabase* db; bool cacheLoaded; void initializeSubModels(BookListModel* q) { if(!titleCategoryModel) { titleCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), titleCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), titleCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->titleCategoryModelChanged(); } if(!newlyAddedCategoryModel) { newlyAddedCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->newlyAddedCategoryModelChanged(); } if(!authorCategoryModel) { authorCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), authorCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), authorCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->authorCategoryModelChanged(); } if(!seriesCategoryModel) { seriesCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), seriesCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), seriesCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->seriesCategoryModelChanged(); } if(!publisherCategoryModel) { publisherCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), publisherCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), publisherCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->publisherCategoryModelChanged(); } if(!folderCategoryModel) { folderCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), folderCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), folderCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->folderCategoryModel(); } } void addEntry(BookListModel* q, BookEntry* entry) { entries.append(entry); q->append(entry); titleCategoryModel->addCategoryEntry(entry->title.left(1).toUpper(), entry); for (int i=0; iauthor.size(); i++) { authorCategoryModel->addCategoryEntry(entry->author.at(i), entry); } for (int i=0; iseries.size(); i++) { seriesCategoryModel->addCategoryEntry(entry->series.at(i), entry, SeriesRole); } if (newlyAddedCategoryModel->indexOfFile(entry->filename) == -1) { newlyAddedCategoryModel->append(entry, CreatedRole); } publisherCategoryModel->addCategoryEntry(entry->publisher, entry); QUrl url(entry->filename.left(entry->filename.lastIndexOf("/"))); folderCategoryModel->addCategoryEntry(url.path().mid(1), entry); if (folderCategoryModel->indexOfFile(entry->filename) == -1) { folderCategoryModel->append(entry); } } void loadCache(BookListModel* q) { QList entries = db->loadEntries(); if(entries.count() > 0) { initializeSubModels(q); } int i = 0; foreach(BookEntry* entry, entries) { - addEntry(q, entry); - if(++i % 100 == 0) { - emit q->countChanged(); - qApp->processEvents(); + /* + * This might turn out a little slow, but we should avoid having entries + * that do not exist. If we end up with slowdown issues when loading the + * cache this would be a good place to start investigating. + */ + if (QFileInfo::exists(entry->filename)) { + addEntry(q, entry); + if(++i % 100 == 0) { + emit q->countChanged(); + qApp->processEvents(); + } + } else { + db->removeEntry(entry); } } cacheLoaded = true; emit q->cacheLoadedChanged(); } }; BookListModel::BookListModel(QObject* parent) : CategoryEntriesModel(parent) , d(new Private) { } BookListModel::~BookListModel() { delete d; } void BookListModel::componentComplete() { QTimer::singleShot(0, this, [this](){ d->loadCache(this); }); } bool BookListModel::cacheLoaded() const { return d->cacheLoaded; } void BookListModel::setContentModel(QObject* newModel) { if(d->contentModel) { d->contentModel->disconnect(this); } d->contentModel = qobject_cast(newModel); if(d->contentModel) { connect(d->contentModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(contentModelItemsInserted(QModelIndex,int, int))); } emit contentModelChanged(); } QObject * BookListModel::contentModel() const { return d->contentModel; } void BookListModel::contentModelItemsInserted(QModelIndex index, int first, int last) { d->initializeSubModels(this); int newRow = d->entries.count(); beginInsertRows(QModelIndex(), newRow, newRow + (last - first)); int role = d->contentModel->roleNames().key("filePath"); for(int i = first; i < last + 1; ++i) { QVariant filePath = d->contentModel->data(d->contentModel->index(first, 0, index), role); BookEntry* entry = new BookEntry(); entry->filename = filePath.toUrl().toLocalFile(); QStringList splitName = entry->filename.split("/"); if (!splitName.isEmpty()) entry->filetitle = splitName.takeLast(); if(!splitName.isEmpty()) { entry->series = QStringList(splitName.takeLast()); // hahahaheuristics (dumb assumptions about filesystems, go!) entry->seriesNumbers = QStringList("0"); entry->seriesVolumes = QStringList("0"); } // just in case we end up without a title... using complete basename here, // as we would rather have "book one. part two" and the odd "book one - part two.tar" QFileInfo fileinfo(entry->filename); entry->title = fileinfo.completeBaseName(); if(entry->filename.toLower().endsWith("cbr")) { entry->thumbnail = QString("image://comiccover/").append(entry->filename); } #ifdef USE_PERUSE_PDFTHUMBNAILER else if(entry->filename.toLower().endsWith("pdf")) { entry->thumbnail = QString("image://pdfcover/").append(entry->filename); } #endif else { entry->thumbnail = QString("image://preview/").append(entry->filename); } KFileMetaData::UserMetaData data(entry->filename); entry->rating = data.rating(); entry->comment = data.userComment(); entry->tags = data.tags(); QVariantHash metadata = d->contentModel->data(d->contentModel->index(first, 0, index), Qt::UserRole + 2).toHash(); QVariantHash::const_iterator it = metadata.constBegin(); for (; it != metadata.constEnd(); it++) { if(it.key() == QLatin1String("author")) { entry->author = it.value().toStringList(); } else if(it.key() == QLatin1String("title")) { entry->title = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("publisher")) { entry->publisher = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("created")) { entry->created = it.value().toDateTime(); } else if(it.key() == QLatin1String("currentPage")) { entry->currentPage = it.value().toInt(); } else if(it.key() == QLatin1String("totalPages")) { entry->totalPages = it.value().toInt(); } else if(it.key() == QLatin1String("comments")) { entry->comment = it.value().toString();} else if(it.key() == QLatin1Literal("tags")) { entry->tags = it.value().toStringList();} else if(it.key() == QLatin1String("rating")) { entry->rating = it.value().toInt();} } // ACBF information is always preferred for CBRs, so let's just use that if it's there QMimeDatabase db; QString mimetype = db.mimeTypeForFile(entry->filename).name(); if(mimetype == "application/x-cbz" || mimetype == "application/x-cbr" || mimetype == "application/vnd.comicbook+zip" || mimetype == "application/vnd.comicbook+rar") { ArchiveBookModel* bookModel = new ArchiveBookModel(this); bookModel->setFilename(entry->filename); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(bookModel->acbfData()); if(acbfDocument) { for(AdvancedComicBookFormat::Sequence* sequence : acbfDocument->metaData()->bookInfo()->sequence()) { if (!entry->series.contains(sequence->title())) { entry->series.append(sequence->title()); entry->seriesNumbers.append(QString::number(sequence->number())); entry->seriesVolumes.append(QString::number(sequence->volume())); } else { int series = entry->series.indexOf(sequence->title()); entry->seriesNumbers.replace(series, QString::number(sequence->number())); entry->seriesVolumes.replace(series, QString::number(sequence->volume())); } } for(AdvancedComicBookFormat::Author* author : acbfDocument->metaData()->bookInfo()->author()) { entry->author.append(author->displayName()); } entry->description = acbfDocument->metaData()->bookInfo()->annotation(""); } if (entry->author.isEmpty()) { entry->author.append(bookModel->author()); } entry->title = bookModel->title(); entry->publisher = bookModel->publisher(); entry->totalPages = bookModel->pageCount(); bookModel->deleteLater(); } d->addEntry(this, entry); d->db->addEntry(entry); } endInsertRows(); emit countChanged(); qApp->processEvents(); } QObject * BookListModel::titleCategoryModel() const { return d->titleCategoryModel; } QObject * BookListModel::newlyAddedCategoryModel() const { return d->newlyAddedCategoryModel; } QObject * BookListModel::authorCategoryModel() const { return d->authorCategoryModel; } QObject * BookListModel::seriesCategoryModel() const { return d->seriesCategoryModel; } QObject * BookListModel::seriesModelForEntry(QString fileName) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { return d->seriesCategoryModel->leafModelForEntry(entry); } } return nullptr; } QObject *BookListModel::publisherCategoryModel() const { return d->publisherCategoryModel; } QObject * BookListModel::folderCategoryModel() const { return d->folderCategoryModel; } int BookListModel::count() const { return d->entries.count(); } void BookListModel::setBookData(QString fileName, QString property, QString value) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { if(property == "totalPages") { entry->totalPages = value.toInt(); } else if(property == "currentPage") { entry->currentPage = value.toInt(); } else if(property == "rating") { entry->rating = value.toInt(); } else if(property == "tags") { entry->tags = value.split(","); } else if(property == "comment") { entry->comment = value; } emit entryDataUpdated(entry); break; } } } void BookListModel::removeBook(QString fileName, bool deleteFile) { if(deleteFile) { KIO::DeleteJob* job = KIO::del(QUrl::fromLocalFile(fileName), KIO::HideProgressInfo); job->start(); } Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { emit entryRemoved(entry); + d->db->removeEntry(entry); delete entry; break; } } } QStringList BookListModel::knownBookFiles() const { QStringList files; foreach(BookEntry* entry, d->entries) { files.append(entry->filename); } return files; }