File indexing completed on 2024-04-28 05:08:20

0001 /***************************************************************************
0002     Copyright (C) 2005-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "entryupdater.h"
0026 #include "entry.h"
0027 #include "entrycomparison.h"
0028 #include "collection.h"
0029 #include "tellico_kernel.h"
0030 #include "progressmanager.h"
0031 #include "gui/statusbar.h"
0032 #include "document.h"
0033 #include "fetch/fetchresult.h"
0034 #include "entrymatchdialog.h"
0035 #include "tellico_debug.h"
0036 
0037 #include <KLocalizedString>
0038 
0039 #include <QTimer>
0040 #include <QApplication>
0041 
0042 namespace {
0043   static const int CHECK_COLLECTION_IMAGES_STEP_SIZE = 10;
0044 }
0045 
0046 using Tellico::EntryUpdater;
0047 
0048 // for each entry, we loop over all available fetchers
0049 // then we loop over all entries
0050 EntryUpdater::EntryUpdater(Tellico::Data::CollPtr coll_, Tellico::Data::EntryList entries_, QObject* parent_)
0051     : QObject(parent_)
0052     , m_coll(coll_)
0053     , m_entriesToUpdate(entries_)
0054     , m_cancelled(false) {
0055   // for now, we're assuming all entries are same collection type
0056   m_fetchers = Fetch::Manager::self()->createUpdateFetchers(m_coll->type());
0057   foreach(Fetch::Fetcher::Ptr fetcher, m_fetchers) {
0058     connect(fetcher.data(), &Fetch::Fetcher::signalResultFound,
0059             this, &EntryUpdater::slotResult);
0060     connect(fetcher.data(), &Fetch::Fetcher::signalDone,
0061             this, &EntryUpdater::slotDone);
0062   }
0063   init();
0064 }
0065 
0066 EntryUpdater::EntryUpdater(const QString& source_, Tellico::Data::CollPtr coll_, Tellico::Data::EntryList entries_, QObject* parent_)
0067     : QObject(parent_)
0068     , m_coll(coll_)
0069     , m_entriesToUpdate(entries_)
0070     , m_cancelled(false) {
0071   // for now, we're assuming all entries are same collection type
0072   Fetch::Fetcher::Ptr f = Fetch::Manager::self()->createUpdateFetcher(m_coll->type(), source_);
0073   if(f) {
0074     m_fetchers.append(f);
0075     connect(f.data(), &Fetch::Fetcher::signalResultFound,
0076             this, &EntryUpdater::slotResult);
0077     connect(f.data(), &Fetch::Fetcher::signalDone,
0078             this, &EntryUpdater::slotDone);
0079   }
0080   init();
0081 }
0082 
0083 EntryUpdater::~EntryUpdater() {
0084   foreach(const UpdateResult& res, m_results) {
0085     delete res.result;
0086   }
0087   m_results.clear();
0088 }
0089 
0090 void EntryUpdater::init() {
0091   m_fetchIndex = 0;
0092   m_origEntryCount = m_entriesToUpdate.count();
0093   QString label;
0094   if(m_entriesToUpdate.count() == 1) {
0095     label = i18n("Updating %1...", m_entriesToUpdate.front()->title());
0096   } else {
0097     label = i18n("Updating entries...");
0098   }
0099   Kernel::self()->beginCommandGroup(i18n("Update Entries"));
0100   ProgressItem& item = ProgressManager::self()->newProgressItem(this, label, true /*canCancel*/);
0101   item.setTotalSteps(m_fetchers.count() * m_origEntryCount);
0102   connect(&item, &Tellico::ProgressItem::signalCancelled,
0103           this, &Tellico::EntryUpdater::slotCancel);
0104 
0105   // done if no fetchers available
0106   if(m_fetchers.isEmpty()) {
0107     QTimer::singleShot(500, this, &EntryUpdater::slotCleanup);
0108   } else {
0109     slotStartNext(); // starts fetching
0110   }
0111 }
0112 
0113 void EntryUpdater::slotStartNext() {
0114   StatusBar::self()->setStatus(i18n("Updating <b>%1</b>...", m_entriesToUpdate.front()->title()));
0115   ProgressManager::self()->setProgress(this, m_fetchers.count() * (m_origEntryCount - m_entriesToUpdate.count()) + m_fetchIndex);
0116 
0117   Fetch::Fetcher::Ptr f = m_fetchers[m_fetchIndex];
0118   f->startUpdate(m_entriesToUpdate.front());
0119 }
0120 
0121 void EntryUpdater::slotDone() {
0122   if(m_cancelled) {
0123     QTimer::singleShot(500, this, &EntryUpdater::slotCleanup);
0124     return;
0125   }
0126 
0127   if(m_results.isEmpty()) {
0128     myLog() << "No search results found to update entry";
0129   } else {
0130     handleResults();
0131   }
0132 
0133   m_results.clear();
0134   ++m_fetchIndex;
0135   if(m_fetchIndex == m_fetchers.count()) {
0136     m_fetchIndex = 0;
0137     // we've gone through the loop for the first entry in the vector
0138     // pop it and move on
0139     m_entriesToUpdate.removeAll(m_entriesToUpdate.front());
0140     // if there are no more entries, and this is the last fetcher, time to delete
0141     if(m_entriesToUpdate.isEmpty()) {
0142       QTimer::singleShot(500, this, &EntryUpdater::slotCleanup);
0143       return;
0144     }
0145   }
0146   qApp->processEvents();
0147   // so the entry updater can clean up a bit
0148   QTimer::singleShot(500, this, &EntryUpdater::slotStartNext);
0149 }
0150 
0151 void EntryUpdater::slotResult(Tellico::Fetch::FetchResult* result_) {
0152   if(!result_ || m_cancelled) {
0153     return;
0154   }
0155   auto fetcher = m_fetchers[m_fetchIndex];
0156   if(!fetcher || !fetcher->isSearching()) {
0157     return;
0158   }
0159 
0160   Data::EntryPtr matchEntry = result_->fetchEntry();
0161   if(matchEntry && !m_entriesToUpdate.isEmpty()) {
0162     m_fetchedEntries.append(matchEntry);
0163     const int match = m_coll->sameEntry(m_entriesToUpdate.front(), matchEntry);
0164     m_results.append(UpdateResult(result_, match));
0165     myLog() << "Found match:" << matchEntry->title() << "- score =" << match;
0166     if(match >= EntryComparison::ENTRY_PERFECT_MATCH) {
0167       myLog() << "Score exceeds high confidence threshold, stopping search";
0168       fetcher->stop();
0169     }
0170   }
0171   qApp->processEvents();
0172 }
0173 
0174 void EntryUpdater::slotCancel() {
0175   m_cancelled = true;
0176   Fetch::Fetcher::Ptr f = m_fetchers[m_fetchIndex];
0177   if(f) {
0178     f->stop(); // ends up calling slotDone();
0179   } else {
0180     slotDone();
0181   }
0182 }
0183 
0184 void EntryUpdater::handleResults() {
0185   Data::EntryPtr entryToUpdate = m_entriesToUpdate.front();
0186   int bestScore = 0;
0187   ResultList matches;
0188   foreach(const UpdateResult& res, m_results) {
0189     Data::EntryPtr matchEntry = res.result->fetchEntry();
0190     if(!matchEntry) {
0191       continue;
0192     }
0193     const int match = res.matchScore;
0194     // if the match is GOOD but not PERFECT, keep all of them
0195     if(match >= EntryComparison::ENTRY_PERFECT_MATCH) {
0196       if(match > bestScore) {
0197         bestScore = match;
0198         matches.clear();
0199         matches.append(res);
0200       } else if(match == bestScore) {
0201         // multiple "perfect" matches
0202         matches.append(res);
0203       }
0204     } else if(match >= EntryComparison::ENTRY_GOOD_MATCH) {
0205       myLog() << "Found good match:" << matchEntry->title() << "- score=" << match;
0206       bestScore = qMax(bestScore, match);
0207       // keep all the results that don't exceed the perfect match
0208       matches.append(res);
0209     } else if(match > bestScore) {
0210       myLog() << "Found better match:" << matchEntry->title() << "- score=" << match;
0211       bestScore = match;
0212       matches.clear();
0213       matches.append(res);
0214     } else if(m_results.count() == 1 && bestScore == 0 && entryToUpdate->title().isEmpty()) {
0215       // special case for updates which may backfire, but let's go with it
0216       // if there is a single result AND the best match is zero AND title is empty
0217       // let's assume it's a case where an entry with a single url or link was updated
0218       myLog() << "Updating entry with 0 score and empty title:" << matchEntry->title();
0219       bestScore = EntryComparison::ENTRY_PERFECT_MATCH;
0220       matches.append(res);
0221     }
0222   }
0223   if(bestScore < EntryComparison::ENTRY_GOOD_MATCH) {
0224     if(bestScore > 0) {
0225       myLog() << "Best match is not good enough, not updating the entry";
0226     }
0227     return;
0228   }
0229   UpdateResult match;
0230   if(matches.count() == 1) {
0231     match = matches.front();
0232   } else if(matches.count() > 1) {
0233     myLog() << "Found" << matches.count() << "good results";
0234     match = askUser(matches);
0235   }
0236   // askUser() could come back with nil
0237   if(match.result) {
0238     myLog() << "Best match is good enough, updating the entry";
0239     mergeCurrent(match.result->fetchEntry(), match.result->fetcher()->updateOverwrite());
0240   }
0241 }
0242 
0243 Tellico::EntryUpdater::UpdateResult EntryUpdater::askUser(const ResultList& results) {
0244   EntryMatchDialog dlg(Kernel::self()->widget(), m_entriesToUpdate.front(),
0245                        m_fetchers[m_fetchIndex].data(), results);
0246 
0247   if(dlg.exec() != QDialog::Accepted) {
0248     return UpdateResult();
0249   }
0250   return dlg.updateResult();
0251 }
0252 
0253 void EntryUpdater::mergeCurrent(Tellico::Data::EntryPtr entry_, bool overWrite_) {
0254   if(!entry_) {
0255     return;
0256   }
0257 
0258   Data::EntryPtr currEntry = m_entriesToUpdate.front();
0259   m_matchedEntries.append(entry_);
0260   Kernel::self()->updateEntry(currEntry, entry_, overWrite_);
0261   if(m_entriesToUpdate.count() % CHECK_COLLECTION_IMAGES_STEP_SIZE == 1) {
0262     // I don't want to remove any images in the entries that are getting
0263     // updated since they'll reference them later and the command isn't
0264     // executed until the command history group is finished
0265     // so remove pointers to matched entries
0266     Data::EntryList nonUpdatedEntries = m_fetchedEntries;
0267     foreach(Data::EntryPtr match, m_matchedEntries) {
0268       nonUpdatedEntries.removeAll(match);
0269     }
0270     Data::Document::self()->removeImagesNotInCollection(nonUpdatedEntries, m_matchedEntries);
0271   }
0272 }
0273 
0274 void EntryUpdater::slotCleanup() {
0275   ProgressManager::self()->setDone(this);
0276   StatusBar::self()->clearStatus();
0277   Kernel::self()->endCommandGroup();
0278   deleteLater();
0279 }