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 }