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"