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 }