File indexing completed on 2025-01-26 04:14:59
0001 /* 0002 * Copyright (C) 2015 Dan Leinir Turthra Jensen <admin@leinir.dk> 0003 * 0004 * This library is free software; you can redistribute it and/or 0005 * modify it under the terms of the GNU Lesser General Public 0006 * License as published by the Free Software Foundation; either 0007 * version 2.1 of the License, or (at your option) version 3, or any 0008 * later version accepted by the membership of KDE e.V. (or its 0009 * successor approved by the membership of KDE e.V.), which shall 0010 * act as a proxy defined in Section 6 of version 3 of the license. 0011 * 0012 * This library is distributed in the hope that it will be useful, 0013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 0015 * Lesser General Public License for more details. 0016 * 0017 * You should have received a copy of the GNU Lesser General Public 0018 * License along with this library. If not, see <http://www.gnu.org/licenses/>. 0019 * 0020 */ 0021 0022 #include "BookListModel.h" 0023 0024 #include "BookDatabase.h" 0025 #include "CategoryEntriesModel.h" 0026 #include "ArchiveBookModel.h" 0027 0028 #include "AcbfAuthor.h" 0029 #include "AcbfSequence.h" 0030 #include "AcbfBookinfo.h" 0031 0032 #include <kio/deletejob.h> 0033 #include <KFileMetaData/UserMetaData> 0034 0035 #include <QCoreApplication> 0036 #include <QDir> 0037 #include <QMimeDatabase> 0038 #include <QTimer> 0039 #include <QUrl> 0040 0041 #include <qtquick_debug.h> 0042 0043 class BookListModel::Private { 0044 public: 0045 Private() 0046 : contentModel(nullptr) 0047 , titleCategoryModel(nullptr) 0048 , newlyAddedCategoryModel(nullptr) 0049 , authorCategoryModel(nullptr) 0050 , seriesCategoryModel(nullptr) 0051 , publisherCategoryModel(nullptr) 0052 , keywordCategoryModel(nullptr) 0053 , folderCategoryModel(nullptr) 0054 , cacheLoaded(false) 0055 { 0056 db = new BookDatabase(); 0057 }; 0058 ~Private() 0059 { 0060 qDeleteAll(entries); 0061 db->deleteLater(); 0062 } 0063 QList<BookEntry*> entries; 0064 0065 QAbstractListModel* contentModel; 0066 CategoryEntriesModel* titleCategoryModel; 0067 CategoryEntriesModel* newlyAddedCategoryModel; 0068 CategoryEntriesModel* authorCategoryModel; 0069 CategoryEntriesModel* seriesCategoryModel; 0070 CategoryEntriesModel* publisherCategoryModel; 0071 CategoryEntriesModel* keywordCategoryModel; 0072 CategoryEntriesModel* folderCategoryModel; 0073 0074 BookDatabase* db; 0075 bool cacheLoaded; 0076 0077 void initializeSubModels(BookListModel* q) { 0078 if(!titleCategoryModel) 0079 { 0080 titleCategoryModel = new CategoryEntriesModel(q); 0081 connect(q, &CategoryEntriesModel::entryDataUpdated, titleCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0082 connect(q, &CategoryEntriesModel::entryRemoved, titleCategoryModel, &CategoryEntriesModel::entryRemoved); 0083 emit q->titleCategoryModelChanged(); 0084 } 0085 if(!newlyAddedCategoryModel) 0086 { 0087 newlyAddedCategoryModel = new CategoryEntriesModel(q); 0088 connect(q, &CategoryEntriesModel::entryDataUpdated, newlyAddedCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0089 connect(q, &CategoryEntriesModel::entryRemoved, newlyAddedCategoryModel, &CategoryEntriesModel::entryRemoved); 0090 emit q->newlyAddedCategoryModelChanged(); 0091 } 0092 if(!authorCategoryModel) 0093 { 0094 authorCategoryModel = new CategoryEntriesModel(q); 0095 connect(q, &CategoryEntriesModel::entryDataUpdated, authorCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0096 connect(q, &CategoryEntriesModel::entryRemoved, authorCategoryModel, &CategoryEntriesModel::entryRemoved); 0097 emit q->authorCategoryModelChanged(); 0098 } 0099 if(!seriesCategoryModel) 0100 { 0101 seriesCategoryModel = new CategoryEntriesModel(q); 0102 connect(q, &CategoryEntriesModel::entryDataUpdated, seriesCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0103 connect(q, &CategoryEntriesModel::entryRemoved, seriesCategoryModel, &CategoryEntriesModel::entryRemoved); 0104 emit q->seriesCategoryModelChanged(); 0105 } 0106 if(!publisherCategoryModel) 0107 { 0108 publisherCategoryModel = new CategoryEntriesModel(q); 0109 connect(q, &CategoryEntriesModel::entryDataUpdated, publisherCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0110 connect(q, &CategoryEntriesModel::entryRemoved, publisherCategoryModel, &CategoryEntriesModel::entryRemoved); 0111 emit q->publisherCategoryModelChanged(); 0112 } 0113 if(!keywordCategoryModel) 0114 { 0115 keywordCategoryModel = new CategoryEntriesModel(q); 0116 connect(q, &CategoryEntriesModel::entryDataUpdated, keywordCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0117 connect(q, &CategoryEntriesModel::entryRemoved, keywordCategoryModel, &CategoryEntriesModel::entryRemoved); 0118 emit q->keywordCategoryModelChanged(); 0119 } 0120 if(!folderCategoryModel) 0121 { 0122 folderCategoryModel = new CategoryEntriesModel(q); 0123 connect(q, &CategoryEntriesModel::entryDataUpdated, folderCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0124 connect(q, &CategoryEntriesModel::entryRemoved, folderCategoryModel, &CategoryEntriesModel::entryRemoved); 0125 emit q->folderCategoryModel(); 0126 } 0127 } 0128 0129 void addEntry(BookListModel* q, BookEntry* entry) { 0130 entries.append(entry); 0131 q->append(entry); 0132 titleCategoryModel->addCategoryEntry(entry->title.left(1).toUpper(), entry); 0133 for (int i=0; i<entry->author.size(); i++) { 0134 authorCategoryModel->addCategoryEntry(entry->author.at(i), entry); 0135 } 0136 for (int i=0; i<entry->series.size(); i++) { 0137 seriesCategoryModel->addCategoryEntry(entry->series.at(i), entry, SeriesRole); 0138 } 0139 if (newlyAddedCategoryModel->indexOfFile(entry->filename) == -1) { 0140 newlyAddedCategoryModel->append(entry, CreatedRole); 0141 } 0142 publisherCategoryModel->addCategoryEntry(entry->publisher, entry); 0143 QUrl url(entry->filename.left(entry->filename.lastIndexOf("/"))); 0144 folderCategoryModel->addCategoryEntry(url.path().mid(1), entry); 0145 if (folderCategoryModel->indexOfFile(entry->filename) == -1) { 0146 folderCategoryModel->append(entry); 0147 } 0148 for (int i=0; i<entry->genres.size(); i++) { 0149 keywordCategoryModel->addCategoryEntry(QString("Genre/").append(entry->genres.at(i)), entry, GenreRole); 0150 } 0151 for (int i=0; i<entry->characters.size(); i++) { 0152 keywordCategoryModel->addCategoryEntry(QString("Characters/").append(entry->characters.at(i)), entry, GenreRole); 0153 } 0154 for (int i=0; i<entry->keywords.size(); i++) { 0155 keywordCategoryModel->addCategoryEntry(QString("Keywords/").append(entry->keywords.at(i)), entry, GenreRole); 0156 } 0157 0158 } 0159 0160 void loadCache(BookListModel* q) { 0161 QList<BookEntry*> entries = db->loadEntries(); 0162 if(entries.count() > 0) 0163 { 0164 initializeSubModels(q); 0165 } 0166 int i = 0; 0167 for(BookEntry* entry : entries) 0168 { 0169 /* 0170 * This might turn out a little slow, but we should avoid having entries 0171 * that do not exist. If we end up with slowdown issues when loading the 0172 * cache this would be a good place to start investigating. 0173 */ 0174 if (QFileInfo::exists(entry->filename)) { 0175 addEntry(q, entry); 0176 if(++i % 100 == 0) { 0177 emit q->countChanged(); 0178 qApp->processEvents(); 0179 } 0180 } else { 0181 db->removeEntry(entry); 0182 } 0183 } 0184 cacheLoaded = true; 0185 emit q->cacheLoadedChanged(); 0186 } 0187 }; 0188 0189 BookListModel::BookListModel(QObject* parent) 0190 : CategoryEntriesModel(parent) 0191 , d(new Private) 0192 { 0193 } 0194 0195 BookListModel::~BookListModel() 0196 { 0197 delete d; 0198 } 0199 0200 void BookListModel::componentComplete() 0201 { 0202 QTimer::singleShot(0, this, [this](){ d->loadCache(this); }); 0203 } 0204 0205 bool BookListModel::cacheLoaded() const 0206 { 0207 return d->cacheLoaded; 0208 } 0209 0210 void BookListModel::setContentModel(QObject* newModel) 0211 { 0212 if(d->contentModel) 0213 { 0214 d->contentModel->disconnect(this); 0215 } 0216 d->contentModel = qobject_cast<QAbstractListModel*>(newModel); 0217 if(d->contentModel) 0218 { 0219 connect(d->contentModel, &QAbstractItemModel::rowsInserted, this, &BookListModel::contentModelItemsInserted); 0220 } 0221 emit contentModelChanged(); 0222 } 0223 0224 QObject * BookListModel::contentModel() const 0225 { 0226 return d->contentModel; 0227 } 0228 0229 void BookListModel::contentModelItemsInserted(QModelIndex index, int first, int last) 0230 { 0231 d->initializeSubModels(this); 0232 int newRow = d->entries.count(); 0233 beginInsertRows(QModelIndex(), newRow, newRow + (last - first)); 0234 int role = d->contentModel->roleNames().key("filePath"); 0235 for(int i = first; i < last + 1; ++i) 0236 { 0237 QVariant filePath = d->contentModel->data(d->contentModel->index(first, 0, index), role); 0238 BookEntry* entry = new BookEntry(); 0239 entry->filename = filePath.toUrl().toLocalFile(); 0240 QStringList splitName = entry->filename.split("/"); 0241 if (!splitName.isEmpty()) 0242 entry->filetitle = splitName.takeLast(); 0243 if(!splitName.isEmpty()) { 0244 entry->series = QStringList(splitName.takeLast()); // hahahaheuristics (dumb assumptions about filesystems, go!) 0245 entry->seriesNumbers = QStringList("0"); 0246 entry->seriesVolumes = QStringList("0"); 0247 } 0248 // just in case we end up without a title... using complete basename here, 0249 // as we would rather have "book one. part two" and the odd "book one - part two.tar" 0250 QFileInfo fileinfo(entry->filename); 0251 entry->title = fileinfo.completeBaseName(); 0252 0253 if(entry->filename.toLower().endsWith("cbr") || entry->filename.toLower().endsWith("cbz")) { 0254 entry->thumbnail = QString("image://comiccover/").append(entry->filename); 0255 } 0256 #ifdef USE_PERUSE_PDFTHUMBNAILER 0257 else if(entry->filename.toLower().endsWith("pdf")) { 0258 entry->thumbnail = QString("image://pdfcover/").append(entry->filename); 0259 } 0260 #endif 0261 else { 0262 entry->thumbnail = QString("image://preview/").append(entry->filename); 0263 } 0264 0265 KFileMetaData::UserMetaData data(entry->filename); 0266 entry->rating = data.rating(); 0267 entry->comment = data.userComment(); 0268 entry->tags = data.tags(); 0269 0270 QVariantHash metadata = d->contentModel->data(d->contentModel->index(first, 0, index), Qt::UserRole + 2).toHash(); 0271 QVariantHash::const_iterator it = metadata.constBegin(); 0272 for (; it != metadata.constEnd(); it++) { 0273 if(it.key() == QLatin1String("author")) 0274 { entry->author = it.value().toStringList(); } 0275 else if(it.key() == QLatin1String("title")) 0276 { entry->title = it.value().toString().trimmed(); } 0277 else if(it.key() == QLatin1String("publisher")) 0278 { entry->publisher = it.value().toString().trimmed(); } 0279 else if(it.key() == QLatin1String("created")) 0280 { entry->created = it.value().toDateTime(); } 0281 else if(it.key() == QLatin1String("currentPage")) 0282 { entry->currentPage = it.value().toInt(); } 0283 else if(it.key() == QLatin1String("totalPages")) 0284 { entry->totalPages = it.value().toInt(); } 0285 else if(it.key() == QLatin1String("comments")) 0286 { entry->comment = it.value().toString();} 0287 else if(it.key() == QLatin1Literal("tags")) 0288 { entry->tags = it.value().toStringList();} 0289 else if(it.key() == QLatin1String("rating")) 0290 { entry->rating = it.value().toInt();} 0291 } 0292 // ACBF information is always preferred for CBRs, so let's just use that if it's there 0293 QMimeDatabase db; 0294 QString mimetype = db.mimeTypeForFile(entry->filename).name(); 0295 if(mimetype == "application/x-cbz" || mimetype == "application/x-cbr" || mimetype == "application/vnd.comicbook+zip" || mimetype == "application/vnd.comicbook+rar") { 0296 ArchiveBookModel* bookModel = new ArchiveBookModel(this); 0297 bookModel->setFilename(entry->filename); 0298 0299 AdvancedComicBookFormat::Document* acbfDocument = qobject_cast<AdvancedComicBookFormat::Document*>(bookModel->acbfData()); 0300 if(acbfDocument) { 0301 for(AdvancedComicBookFormat::Sequence* sequence : acbfDocument->metaData()->bookInfo()->sequence()) { 0302 if (!entry->series.contains(sequence->title())) { 0303 entry->series.append(sequence->title()); 0304 entry->seriesNumbers.append(QString::number(sequence->number())); 0305 entry->seriesVolumes.append(QString::number(sequence->volume())); 0306 } else { 0307 int series = entry->series.indexOf(sequence->title()); 0308 entry->seriesNumbers.replace(series, QString::number(sequence->number())); 0309 entry->seriesVolumes.replace(series, QString::number(sequence->volume())); 0310 } 0311 0312 } 0313 for(AdvancedComicBookFormat::Author* author : acbfDocument->metaData()->bookInfo()->author()) { 0314 entry->author.append(author->displayName()); 0315 } 0316 entry->description = acbfDocument->metaData()->bookInfo()->annotation(""); 0317 entry->genres = acbfDocument->metaData()->bookInfo()->genres(); 0318 entry->characters = acbfDocument->metaData()->bookInfo()->characters(); 0319 entry->keywords = acbfDocument->metaData()->bookInfo()->keywords(""); 0320 } 0321 0322 if (entry->author.isEmpty()) { 0323 entry->author.append(bookModel->author()); 0324 } 0325 entry->title = bookModel->title(); 0326 entry->publisher = bookModel->publisher(); 0327 entry->totalPages = bookModel->pageCount(); 0328 bookModel->deleteLater(); 0329 } 0330 0331 d->addEntry(this, entry); 0332 d->db->addEntry(entry); 0333 } 0334 endInsertRows(); 0335 emit countChanged(); 0336 qApp->processEvents(); 0337 } 0338 0339 QObject * BookListModel::titleCategoryModel() const 0340 { 0341 return d->titleCategoryModel; 0342 } 0343 0344 QObject * BookListModel::newlyAddedCategoryModel() const 0345 { 0346 return d->newlyAddedCategoryModel; 0347 } 0348 0349 QObject * BookListModel::authorCategoryModel() const 0350 { 0351 return d->authorCategoryModel; 0352 } 0353 0354 QObject * BookListModel::seriesCategoryModel() const 0355 { 0356 return d->seriesCategoryModel; 0357 } 0358 0359 QObject * BookListModel::seriesModelForEntry(QString fileName) 0360 { 0361 for(BookEntry* entry : d->entries) 0362 { 0363 if(entry->filename == fileName) 0364 { 0365 return d->seriesCategoryModel->leafModelForEntry(entry); 0366 } 0367 } 0368 return nullptr; 0369 } 0370 0371 QObject *BookListModel::publisherCategoryModel() const 0372 { 0373 return d->publisherCategoryModel; 0374 } 0375 0376 QObject *BookListModel::keywordCategoryModel() const 0377 { 0378 return d->keywordCategoryModel; 0379 } 0380 0381 QObject * BookListModel::folderCategoryModel() const 0382 { 0383 return d->folderCategoryModel; 0384 } 0385 0386 int BookListModel::count() const 0387 { 0388 return d->entries.count(); 0389 } 0390 0391 void BookListModel::setBookData(QString fileName, QString property, QString value) 0392 { 0393 for(BookEntry* entry : d->entries) 0394 { 0395 if(entry->filename == fileName) 0396 { 0397 if(property == "totalPages") 0398 { 0399 entry->totalPages = value.toInt(); 0400 d->db->updateEntry(entry->filename, property, QVariant(value.toInt())); 0401 } 0402 else if(property == "currentPage") 0403 { 0404 entry->currentPage = value.toInt(); 0405 d->db->updateEntry(entry->filename, property, QVariant(value.toInt())); 0406 } 0407 else if(property == "rating") 0408 { 0409 entry->rating = value.toInt(); 0410 d->db->updateEntry(entry->filename, property, QVariant(value.toInt())); 0411 } 0412 else if(property == "tags") 0413 { 0414 entry->tags = value.split(","); 0415 d->db->updateEntry(entry->filename, property, QVariant(value.split(","))); 0416 } 0417 else if(property == "comment") { 0418 entry->comment = value; 0419 d->db->updateEntry(entry->filename, property, QVariant(value)); 0420 } 0421 emit entryDataUpdated(entry); 0422 break; 0423 } 0424 } 0425 } 0426 0427 void BookListModel::removeBook(QString fileName, bool deleteFile) 0428 { 0429 if(deleteFile) { 0430 KIO::DeleteJob* job = KIO::del(QUrl::fromLocalFile(fileName), KIO::HideProgressInfo); 0431 job->start(); 0432 } 0433 0434 for(BookEntry* entry : d->entries) 0435 { 0436 if(entry->filename == fileName) 0437 { 0438 emit entryRemoved(entry); 0439 d->db->removeEntry(entry); 0440 delete entry; 0441 break; 0442 } 0443 } 0444 } 0445 0446 QStringList BookListModel::knownBookFiles() const 0447 { 0448 QStringList files; 0449 for(BookEntry* entry : d->entries) { 0450 files.append(entry->filename); 0451 } 0452 return files; 0453 }