File indexing completed on 2024-04-28 15:35:50
0001 // SPDX-FileCopyrightText: 2015 Dan Leinir Turthra Jensen <admin@leinir.dk> 0002 // SPDX-License-Identifier: LGPL-2.1-only or LGPL-3.0-only or LicenseRef-KDE-Accepted-LGPL 0003 0004 #include "booklistmodel.h" 0005 0006 #include "bookdatabase.h" 0007 0008 #include <KFileMetaData/UserMetaData> 0009 0010 #include <QCoreApplication> 0011 #include <QDir> 0012 #include <QImage> 0013 #include <QMimeDatabase> 0014 #include <QStandardPaths> 0015 #include <QTimer> 0016 #include <QUrl> 0017 #include <QUuid> 0018 0019 #include "epubcontainer.h" 0020 0021 #include <arianna_debug.h> 0022 #include <qchar.h> 0023 #include <qloggingcategory.h> 0024 0025 class BookListModel::Private 0026 { 0027 public: 0028 Private() 0029 : contentModel(nullptr) 0030 , newlyAddedCategoryModel(nullptr) 0031 , authorCategoryModel(nullptr) 0032 , seriesCategoryModel(nullptr) 0033 , publisherCategoryModel(nullptr) 0034 , keywordCategoryModel(nullptr) 0035 , folderCategoryModel(nullptr) 0036 , cacheLoaded(false){}; 0037 0038 QList<BookEntry> entries; 0039 0040 ContentList *contentModel; 0041 CategoryEntriesModel *newlyAddedCategoryModel; 0042 CategoryEntriesModel *authorCategoryModel; 0043 CategoryEntriesModel *seriesCategoryModel; 0044 CategoryEntriesModel *publisherCategoryModel; 0045 CategoryEntriesModel *keywordCategoryModel; 0046 CategoryEntriesModel *folderCategoryModel; 0047 0048 bool cacheLoaded; 0049 0050 void initializeSubModels(BookListModel *q) 0051 { 0052 if (!newlyAddedCategoryModel) { 0053 newlyAddedCategoryModel = new CategoryEntriesModel(q); 0054 connect(q, &CategoryEntriesModel::entryDataUpdated, newlyAddedCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0055 connect(q, &CategoryEntriesModel::entryRemoved, newlyAddedCategoryModel, &CategoryEntriesModel::entryRemoved); 0056 Q_EMIT q->newlyAddedCategoryModelChanged(); 0057 } 0058 if (!authorCategoryModel) { 0059 authorCategoryModel = new CategoryEntriesModel(q); 0060 connect(q, &CategoryEntriesModel::entryDataUpdated, authorCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0061 connect(q, &CategoryEntriesModel::entryRemoved, authorCategoryModel, &CategoryEntriesModel::entryRemoved); 0062 Q_EMIT q->authorCategoryModelChanged(); 0063 } 0064 if (!seriesCategoryModel) { 0065 seriesCategoryModel = new CategoryEntriesModel(q); 0066 connect(q, &CategoryEntriesModel::entryDataUpdated, seriesCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0067 connect(q, &CategoryEntriesModel::entryRemoved, seriesCategoryModel, &CategoryEntriesModel::entryRemoved); 0068 Q_EMIT q->seriesCategoryModelChanged(); 0069 } 0070 if (!publisherCategoryModel) { 0071 publisherCategoryModel = new CategoryEntriesModel(q); 0072 connect(q, &CategoryEntriesModel::entryDataUpdated, publisherCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0073 connect(q, &CategoryEntriesModel::entryRemoved, publisherCategoryModel, &CategoryEntriesModel::entryRemoved); 0074 Q_EMIT q->publisherCategoryModelChanged(); 0075 } 0076 if (!keywordCategoryModel) { 0077 keywordCategoryModel = new CategoryEntriesModel(q); 0078 connect(q, &CategoryEntriesModel::entryDataUpdated, keywordCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0079 connect(q, &CategoryEntriesModel::entryRemoved, keywordCategoryModel, &CategoryEntriesModel::entryRemoved); 0080 Q_EMIT q->keywordCategoryModelChanged(); 0081 } 0082 if (!folderCategoryModel) { 0083 folderCategoryModel = new CategoryEntriesModel(q); 0084 connect(q, &CategoryEntriesModel::entryDataUpdated, folderCategoryModel, &CategoryEntriesModel::entryDataUpdated); 0085 connect(q, &CategoryEntriesModel::entryRemoved, folderCategoryModel, &CategoryEntriesModel::entryRemoved); 0086 q->folderCategoryModel(); 0087 } 0088 } 0089 0090 void addEntry(BookListModel *q, const BookEntry &entry) 0091 { 0092 entries.append(entry); 0093 q->append(entry); 0094 for (int i = 0; i < entry.author.size(); i++) { 0095 authorCategoryModel->addCategoryEntry(entry.author.at(i), entry); 0096 } 0097 for (int i = 0; i < entry.series.size(); i++) { 0098 seriesCategoryModel->addCategoryEntry(entry.series.at(i), entry, SeriesRole); 0099 } 0100 if (newlyAddedCategoryModel->indexOfFile(entry.filename) == -1) { 0101 newlyAddedCategoryModel->append(entry, CreatedRole); 0102 } 0103 publisherCategoryModel->addCategoryEntry(entry.publisher, entry); 0104 QUrl url(entry.filename.left(entry.filename.lastIndexOf(QLatin1Char('/')))); 0105 folderCategoryModel->addCategoryEntry(url.path().mid(1), entry); 0106 if (folderCategoryModel->indexOfFile(entry.filename) == -1) { 0107 folderCategoryModel->append(entry); 0108 } 0109 for (int i = 0; i < entry.genres.size(); i++) { 0110 keywordCategoryModel->addCategoryEntry(entry.genres.at(i), entry, GenreRole); 0111 } 0112 } 0113 0114 void loadCache(BookListModel *q) 0115 { 0116 QList<BookEntry> entries = BookDatabase::self().loadEntries(); 0117 if (entries.count() > 0) { 0118 initializeSubModels(q); 0119 } 0120 int i = 0; 0121 for (const BookEntry &entry : std::as_const(entries)) { 0122 /* 0123 * This might turn out a little slow, but we should avoid having entries 0124 * that do not exist. If we end up with slowdown issues when loading the 0125 * cache this would be a good place to start investigating. 0126 */ 0127 if (QFileInfo::exists(entry.filename)) { 0128 addEntry(q, entry); 0129 if (++i % 100 == 0) { 0130 Q_EMIT q->countChanged(); 0131 qApp->processEvents(); 0132 } 0133 } else { 0134 BookDatabase::self().removeEntry(entry); 0135 } 0136 } 0137 cacheLoaded = true; 0138 Q_EMIT q->cacheLoadedChanged(); 0139 } 0140 }; 0141 0142 QString saveCover(const QString &identifier, const QImage &image) 0143 { 0144 if (!image.isNull()) { 0145 const auto cacheLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); 0146 QString id = QUuid::createUuid().toString(); 0147 QString fileName = cacheLocation + QLatin1String("/covers/") + id + QLatin1String(".jpg"); 0148 QDir dir(cacheLocation); 0149 if (!dir.exists(QLatin1String("covers"))) { 0150 dir.mkdir(QLatin1String("covers")); 0151 } 0152 if (!image.save(fileName)) { 0153 qCWarning(ARIANNA_LOG) << "Error saving image" << fileName; 0154 } else { 0155 qCDebug(ARIANNA_LOG) << "saving cover to" << fileName; 0156 } 0157 return fileName; 0158 } else { 0159 qCDebug(ARIANNA_LOG) << "cover is empty"; 0160 // TODO generate generic cover 0161 return {}; 0162 } 0163 } 0164 0165 BookListModel::BookListModel(QObject *parent) 0166 : CategoryEntriesModel(parent) 0167 , d(std::make_unique<Private>()) 0168 { 0169 } 0170 0171 BookListModel::~BookListModel() = default; 0172 0173 void BookListModel::componentComplete() 0174 { 0175 QTimer::singleShot(0, this, [this]() { 0176 d->loadCache(this); 0177 }); 0178 } 0179 0180 bool BookListModel::cacheLoaded() const 0181 { 0182 return d->cacheLoaded; 0183 } 0184 0185 void BookListModel::setContentModel(ContentList *newModel) 0186 { 0187 if (d->contentModel) { 0188 d->contentModel->disconnect(this); 0189 } 0190 d->contentModel = newModel; 0191 if (d->contentModel) { 0192 connect(d->contentModel, &QAbstractItemModel::rowsInserted, this, &BookListModel::contentModelItemsInserted); 0193 } 0194 Q_EMIT contentModelChanged(); 0195 } 0196 0197 ContentList *BookListModel::contentModel() const 0198 { 0199 return d->contentModel; 0200 } 0201 0202 void BookListModel::contentModelItemsInserted(QModelIndex index, int first, int last) 0203 { 0204 d->initializeSubModels(this); 0205 int role = ContentList::FilePathRole; 0206 for (int i = first; i < last + 1; ++i) { 0207 QVariant filePath = d->contentModel->data(d->contentModel->index(first, 0, index), role); 0208 BookEntry entry; 0209 entry.filename = filePath.toUrl().toLocalFile(); 0210 QStringList splitName = entry.filename.split(QLatin1Char('/')); 0211 if (!splitName.isEmpty()) 0212 entry.filetitle = splitName.takeLast(); 0213 if (!splitName.isEmpty()) { 0214 entry.series = QStringList{}; 0215 entry.seriesNumbers = QStringList{QStringLiteral("0")}; 0216 entry.seriesVolumes = QStringList{QStringLiteral("0")}; 0217 } 0218 // just in case we end up without a title... using complete basename here, 0219 // as we would rather have "book one. part two" and the odd "book one - part two.tar" 0220 QFileInfo fileinfo(entry.filename); 0221 entry.title = fileinfo.completeBaseName(); 0222 0223 KFileMetaData::UserMetaData data(entry.filename); 0224 entry.rating = data.rating(); 0225 entry.comment = data.userComment(); 0226 entry.tags = data.tags(); 0227 0228 QVariantHash metadata = d->contentModel->data(d->contentModel->index(first, 0, index), Qt::UserRole + 2).toHash(); 0229 QVariantHash::const_iterator it = metadata.constBegin(); 0230 for (; it != metadata.constEnd(); it++) { 0231 if (it.key() == QLatin1String("author")) { 0232 entry.author = it.value().toStringList(); 0233 } else if (it.key() == QLatin1String("title")) { 0234 entry.title = it.value().toString().trimmed(); 0235 } else if (it.key() == QLatin1String("publisher")) { 0236 entry.publisher = it.value().toString().trimmed(); 0237 } else if (it.key() == QLatin1String("created")) { 0238 entry.created = it.value().toDateTime(); 0239 } else if (it.key() == QLatin1String("currentLocation")) { 0240 entry.currentLocation = it.value().toString(); 0241 } else if (it.key() == QLatin1String("currentProgress")) { 0242 entry.currentProgress = it.value().toInt(); 0243 } else if (it.key() == QLatin1String("comments")) { 0244 entry.comment = it.value().toString(); 0245 } else if (it.key() == QLatin1String("tags")) { 0246 entry.tags = it.value().toStringList(); 0247 } else if (it.key() == QLatin1String("rating")) { 0248 entry.rating = it.value().toInt(); 0249 } 0250 } 0251 QMimeDatabase db; 0252 QString mimetype = db.mimeTypeForFile(entry.filename).name(); 0253 if (mimetype == QStringLiteral("application/epub+zip")) { 0254 EPubContainer epub(nullptr); 0255 epub.openFile(entry.filename); 0256 const auto titles = epub.getMetadata(QStringLiteral("title")); 0257 if (!titles.isEmpty()) { 0258 entry.title = titles[0]; 0259 } 0260 entry.author = epub.getMetadata(QStringLiteral("creator")); 0261 entry.rights = epub.getMetadata(QStringLiteral("rights")).join(QStringLiteral(", ")); 0262 entry.source = epub.getMetadata(QStringLiteral("source")).join(QStringLiteral(", ")); 0263 entry.identifier = epub.getMetadata(QStringLiteral("identifier")).join(QStringLiteral(", ")); 0264 entry.language = epub.getMetadata(QStringLiteral("language")).join(QStringLiteral(", ")); 0265 entry.genres = epub.getMetadata(QStringLiteral("subject")); 0266 entry.publisher = epub.getMetadata(QStringLiteral("publisher")).join(QStringLiteral(", ")); 0267 0268 auto image = epub.getImage(epub.getMetadata(QStringLiteral("cover")).join(QChar())); 0269 entry.thumbnail = saveCover(epub.getMetadata(QStringLiteral("identifier")).join(QChar()), image); 0270 0271 const auto collections = epub.collections(); 0272 for (const auto &collection : collections) { 0273 entry.series.append(collection.name); 0274 entry.seriesVolumes.append(QString::number(collection.position)); 0275 } 0276 } 0277 0278 d->addEntry(this, entry); 0279 BookDatabase::self().addEntry(entry); 0280 } 0281 Q_EMIT countChanged(); 0282 } 0283 0284 CategoryEntriesModel *BookListModel::newlyAddedCategoryModel() const 0285 { 0286 return d->newlyAddedCategoryModel; 0287 } 0288 0289 CategoryEntriesModel *BookListModel::authorCategoryModel() const 0290 { 0291 return d->authorCategoryModel; 0292 } 0293 0294 CategoryEntriesModel *BookListModel::seriesCategoryModel() const 0295 { 0296 return d->seriesCategoryModel; 0297 } 0298 0299 CategoryEntriesModel *BookListModel::seriesModelForEntry(const QString &fileName) 0300 { 0301 for (const BookEntry &entry : std::as_const(d->entries)) { 0302 if (entry.filename == fileName) { 0303 return d->seriesCategoryModel->leafModelForEntry(entry); 0304 } 0305 } 0306 return nullptr; 0307 } 0308 0309 CategoryEntriesModel *BookListModel::publisherCategoryModel() const 0310 { 0311 return d->publisherCategoryModel; 0312 } 0313 0314 CategoryEntriesModel *BookListModel::keywordCategoryModel() const 0315 { 0316 return d->keywordCategoryModel; 0317 } 0318 0319 CategoryEntriesModel *BookListModel::folderCategoryModel() const 0320 { 0321 return d->folderCategoryModel; 0322 } 0323 0324 int BookListModel::count() const 0325 { 0326 return d->entries.count(); 0327 } 0328 0329 void BookListModel::setBookData(const QString &fileName, const QString &property, const QString &value) 0330 { 0331 for (BookEntry &entry : d->entries) { 0332 if (entry.filename == fileName) { 0333 if (property == QStringLiteral("currentLocation")) { 0334 entry.currentLocation = value; 0335 BookDatabase::self().updateEntry(entry.filename, property, {value}); 0336 } else if (property == QStringLiteral("currentProgress")) { 0337 entry.currentProgress = value.toInt(); 0338 BookDatabase::self().updateEntry(entry.filename, property, QVariant(value.toInt())); 0339 } else if (property == QStringLiteral("locations")) { 0340 entry.locations = value; 0341 BookDatabase::self().updateEntry(entry.filename, property, {value}); 0342 } else if (property == QStringLiteral("rating")) { 0343 entry.rating = value.toInt(); 0344 BookDatabase::self().updateEntry(entry.filename, property, QVariant(value.toInt())); 0345 } else if (property == QStringLiteral("tags")) { 0346 entry.tags = value.split(QLatin1Char(',')); 0347 BookDatabase::self().updateEntry(entry.filename, property, QVariant(value.split(QLatin1Char(',')))); 0348 } else if (property == QStringLiteral("comment")) { 0349 entry.comment = value; 0350 BookDatabase::self().updateEntry(entry.filename, property, QVariant(value)); 0351 } 0352 Q_EMIT entryDataUpdated(entry); 0353 break; 0354 } 0355 } 0356 } 0357 0358 void BookListModel::removeBook(const QString &fileName, bool deleteFile) 0359 { 0360 if (deleteFile) { 0361 // KIO::DeleteJob *job = KIO::del(QUrl::fromLocalFile(fileName), KIO::HideProgressInfo); 0362 // job->start(); 0363 } 0364 0365 for (const BookEntry &entry : std::as_const(d->entries)) { 0366 if (entry.filename == fileName) { 0367 Q_EMIT entryRemoved(entry); 0368 BookDatabase::self().removeEntry(entry); 0369 break; 0370 } 0371 } 0372 } 0373 0374 QStringList BookListModel::knownBookFiles() const 0375 { 0376 QStringList files; 0377 for (const auto &entry : std::as_const(d->entries)) { 0378 files.append(entry.filename); 0379 } 0380 return files; 0381 } 0382 0383 #include "moc_booklistmodel.cpp"