File indexing completed on 2024-04-28 16:31:58

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.first;
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 //  myDebug() << "starting update from" << f->source();
0119   f->startUpdate(m_entriesToUpdate.front());
0120 }
0121 
0122 void EntryUpdater::slotDone() {
0123   if(m_cancelled) {
0124     QTimer::singleShot(500, this, &EntryUpdater::slotCleanup);
0125     return;
0126   }
0127 
0128   if(!m_results.isEmpty()) {
0129     handleResults();
0130   }
0131 
0132   m_results.clear();
0133   ++m_fetchIndex;
0134 //  myDebug() << 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 //  myDebug() << "update result:" << result_->title << " [" << fetcher->source() << "]";
0161   m_results.append(UpdateResult(result_, fetcher->updateOverwrite()));
0162   Data::EntryPtr e = result_->fetchEntry();
0163   if(e && !m_entriesToUpdate.isEmpty()) {
0164     m_fetchedEntries.append(e);
0165     const int match = m_coll->sameEntry(m_entriesToUpdate.front(), e);
0166     if(match >= EntryComparison::ENTRY_PERFECT_MATCH) {
0167       fetcher->stop();
0168     }
0169   }
0170   qApp->processEvents();
0171 }
0172 
0173 void EntryUpdater::slotCancel() {
0174   m_cancelled = true;
0175   Fetch::Fetcher::Ptr f = m_fetchers[m_fetchIndex];
0176   if(f) {
0177     f->stop(); // ends up calling slotDone();
0178   } else {
0179     slotDone();
0180   }
0181 }
0182 
0183 void EntryUpdater::handleResults() {
0184   Data::EntryPtr entry = m_entriesToUpdate.front();
0185   int best = 0;
0186   ResultList matches;
0187   foreach(const UpdateResult& res, m_results) {
0188     Data::EntryPtr e = res.first->fetchEntry();
0189     if(!e) {
0190       continue;
0191     }
0192     m_fetchedEntries.append(e);
0193     int match = m_coll->sameEntry(entry, e);
0194     if(match) {
0195 //      myDebug() << e->title() << "matches by" << match;
0196     }
0197     // if the match is GOOD but not PERFECT, keep all of them
0198     if(match >= EntryComparison::ENTRY_PERFECT_MATCH) {
0199       if(match > best) {
0200         best = match;
0201         matches.clear();
0202         matches.append(res);
0203       } else if(match == best) {
0204         matches.append(res);
0205       }
0206     } else if(match >= EntryComparison::ENTRY_GOOD_MATCH) {
0207       best = qMax(best, match);
0208       // keep all the results that don't exceed the perfect match
0209       matches.append(res);
0210     } else if(match > best) {
0211       best = match;
0212       matches.clear();
0213       matches.append(res);
0214     } else if(m_results.count() == 1 && best == 0 && entry->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:" << e->title();
0219       best = EntryComparison::ENTRY_PERFECT_MATCH;
0220       matches.append(res);
0221     }
0222   }
0223   if(best < EntryComparison::ENTRY_GOOD_MATCH) {
0224     if(best > 0) {
0225       myDebug() << "no good match (score > 10), best match =" << best << "(" << matches.count() << "matches)";
0226     }
0227     return;
0228   }
0229 //  myDebug() << "best match = " << best << " (" << matches.count() << " matches)";
0230   UpdateResult match(nullptr, true);
0231   if(matches.count() == 1) {
0232     match = matches.front();
0233   } else if(matches.count() > 1) {
0234     match = askUser(matches);
0235   }
0236   // askUser() could come back with nil
0237   if(match.first) {
0238     mergeCurrent(match.first->fetchEntry(), match.second);
0239   }
0240 }
0241 
0242 Tellico::EntryUpdater::UpdateResult EntryUpdater::askUser(const ResultList& results) {
0243   EntryMatchDialog dlg(Kernel::self()->widget(), m_entriesToUpdate.front(),
0244                        m_fetchers[m_fetchIndex].data(), results);
0245 
0246   if(dlg.exec() != QDialog::Accepted) {
0247     return UpdateResult(nullptr, false);
0248   }
0249   return dlg.updateResult();
0250 }
0251 
0252 void EntryUpdater::mergeCurrent(Tellico::Data::EntryPtr entry_, bool overWrite_) {
0253   Data::EntryPtr currEntry = m_entriesToUpdate.front();
0254   if(entry_) {
0255     m_matchedEntries.append(entry_);
0256     Kernel::self()->updateEntry(currEntry, entry_, overWrite_);
0257     if(m_entriesToUpdate.count() % CHECK_COLLECTION_IMAGES_STEP_SIZE == 1) {
0258       // I don't want to remove any images in the entries that are getting
0259       // updated since they'll reference them later and the command isn't
0260       // executed until the command history group is finished
0261       // so remove pointers to matched entries
0262       Data::EntryList nonUpdatedEntries = m_fetchedEntries;
0263       foreach(Data::EntryPtr match, m_matchedEntries) {
0264         nonUpdatedEntries.removeAll(match);
0265       }
0266       Data::Document::self()->removeImagesNotInCollection(nonUpdatedEntries, m_matchedEntries);
0267     }
0268   }
0269 }
0270 
0271 void EntryUpdater::slotCleanup() {
0272   ProgressManager::self()->setDone(this);
0273   StatusBar::self()->clearStatus();
0274   Kernel::self()->endCommandGroup();
0275   deleteLater();
0276 }