File indexing completed on 2024-05-19 04:56:09
0001 /** 0002 * \file tagsearcher.cpp 0003 * Search for strings in tags. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 08 Feb 2014 0008 * 0009 * Copyright (C) 2014-2024 Urs Fleisch 0010 * 0011 * This file is part of Kid3. 0012 * 0013 * Kid3 is free software; you can redistribute it and/or modify 0014 * it under the terms of the GNU General Public License as published by 0015 * the Free Software Foundation; either version 2 of the License, or 0016 * (at your option) any later version. 0017 * 0018 * Kid3 is distributed in the hope that it will be useful, 0019 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0020 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0021 * GNU General Public License for more details. 0022 * 0023 * You should have received a copy of the GNU General Public License 0024 * along with this program. If not, see <http://www.gnu.org/licenses/>. 0025 */ 0026 0027 #include "tagsearcher.h" 0028 #include "trackdatamodel.h" 0029 #include "fileproxymodel.h" 0030 #include "bidirfileproxymodeliterator.h" 0031 0032 /** 0033 * Constructor. 0034 */ 0035 TagSearcher::Position::Position() 0036 : m_part(FileName), m_frameIndex(-1), m_matchedPos(-1), m_matchedLength(-1) 0037 { 0038 } 0039 0040 /** 0041 * Clear to invalid position. 0042 */ 0043 void TagSearcher::Position::clear() 0044 { 0045 m_fileIndex = QPersistentModelIndex(); 0046 m_frameName.clear(); 0047 m_frameIndex = -1; 0048 m_matchedPos = -1; 0049 m_matchedLength = -1; 0050 } 0051 0052 /** 0053 * Check if position is valid. 0054 * @return true if valid, false if not found. 0055 */ 0056 bool TagSearcher::Position::isValid() const 0057 { 0058 return m_fileIndex.isValid() && m_matchedPos != -1; 0059 } 0060 0061 0062 /** 0063 * Constructor. 0064 * @param parent parent object 0065 */ 0066 TagSearcher::TagSearcher(QObject* parent) : QObject(parent), 0067 m_fileProxyModel(nullptr), m_iterator(nullptr), m_aborted(false), m_started(false) 0068 { 0069 } 0070 0071 /** 0072 * Clear abort flag. 0073 */ 0074 void TagSearcher::clearAborted() 0075 { 0076 m_aborted = false; 0077 } 0078 0079 /** 0080 * Check if dialog was aborted. 0081 * @return true if aborted. 0082 */ 0083 bool TagSearcher::isAborted() const 0084 { 0085 return m_aborted; 0086 } 0087 0088 /** 0089 * Set model of files to be searched. 0090 * @param model file proxy model 0091 */ 0092 void TagSearcher::setModel(FileProxyModel* model) 0093 { 0094 if (m_iterator && m_fileProxyModel != model) { 0095 delete m_iterator; 0096 m_iterator = nullptr; 0097 } 0098 m_fileProxyModel = model; 0099 if (m_fileProxyModel && !m_iterator) { 0100 m_iterator = new BiDirFileProxyModelIterator(m_fileProxyModel, this); 0101 connect(m_iterator, &BiDirFileProxyModelIterator::nextReady, 0102 this, &TagSearcher::searchNextFile); 0103 } 0104 } 0105 0106 /** 0107 * Set root index of directory to search. 0108 * @param index root index of directory 0109 */ 0110 void TagSearcher::setRootIndex(const QPersistentModelIndex& index) 0111 { 0112 m_iterator->setRootIndex(index); 0113 } 0114 0115 /** 0116 * Set index of file to start search. 0117 * @param index index of file where search is started 0118 */ 0119 void TagSearcher::setStartIndex(const QPersistentModelIndex& index) 0120 { 0121 m_startIndex = index; 0122 } 0123 0124 /** 0125 * Set abort flag. 0126 */ 0127 void TagSearcher::abort() 0128 { 0129 m_aborted = true; 0130 m_started = false; 0131 if (m_iterator) { 0132 m_iterator->abort(); 0133 } 0134 } 0135 0136 /** 0137 * Find next occurrence of string. 0138 * @param params search parameters 0139 */ 0140 void TagSearcher::find(const Parameters ¶ms) 0141 { 0142 setParameters(params); 0143 findNext(1); 0144 } 0145 0146 /** 0147 * Find next occurrence of same string. 0148 */ 0149 void TagSearcher::findNext(int advanceChars) 0150 { 0151 m_aborted = false; 0152 if (m_iterator) { 0153 if (m_started) { 0154 continueSearch(advanceChars); 0155 } else { 0156 bool continueFromCurrentPosition = false; 0157 if (m_startIndex.isValid()) { 0158 continueFromCurrentPosition = m_currentPosition.isValid() && 0159 m_currentPosition.getFileIndex() == m_startIndex; 0160 m_iterator->setCurrentIndex(m_startIndex); 0161 m_startIndex = QPersistentModelIndex(); 0162 } 0163 m_started = true; 0164 if (continueFromCurrentPosition) { 0165 continueSearch(advanceChars); 0166 } else { 0167 m_iterator->start(); 0168 } 0169 } 0170 } 0171 } 0172 0173 /** 0174 * Search next file. 0175 * @param index index of file in file proxy model 0176 */ 0177 void TagSearcher::searchNextFile(const QPersistentModelIndex& index) 0178 { 0179 if (index.isValid()) { 0180 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) { 0181 emit progress(taggedFile->getFilename()); 0182 taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile); 0183 0184 Position pos; 0185 if (searchInFile(taggedFile, &pos, 1)) { 0186 pos.m_fileIndex = index; 0187 m_currentPosition = pos; 0188 if (m_iterator) { 0189 m_iterator->suspend(); 0190 } 0191 emit progress(getLocationString(taggedFile)); 0192 emit textFound(); 0193 } 0194 } 0195 } else { 0196 m_started = false; 0197 m_currentPosition.clear(); 0198 emit progress(tr("Search finished")); 0199 emit textFound(); 0200 } 0201 } 0202 0203 /** 0204 * Continue search in current file, if no other match is found, resume 0205 * file iteration. 0206 * @param advanceChars number of characters to advance from current position 0207 */ 0208 void TagSearcher::continueSearch(int advanceChars) 0209 { 0210 if (m_currentPosition.isValid()) { 0211 if (TaggedFile* taggedFile = 0212 FileProxyModel::getTaggedFileOfIndex(m_currentPosition.getFileIndex())) { 0213 if (searchInFile(taggedFile, &m_currentPosition, advanceChars)) { 0214 emit progress(getLocationString(taggedFile)); 0215 emit textFound(); 0216 return; 0217 } 0218 } 0219 } 0220 if (m_iterator) { 0221 m_iterator->resume(); 0222 } 0223 } 0224 0225 /** 0226 * Search for next occurrence in a file. 0227 * @param taggedFile tagged file 0228 * @param pos position of last match in @a taggedFile, will be updated 0229 * with new position 0230 * @param advanceChars number of characters to advance from current position 0231 * @return true if found. 0232 */ 0233 bool TagSearcher::searchInFile(TaggedFile* taggedFile, Position* pos, 0234 int advanceChars) const 0235 { 0236 if (pos->getPart() <= Position::FileName && 0237 ((m_params.getFlags() & AllFrames) || 0238 (m_params.getFrameMask() & (1ULL << TrackDataModel::FT_FileName)))) { 0239 int idx = 0; 0240 if (pos->getPart() == Position::FileName) { 0241 idx = pos->m_matchedPos + advanceChars; 0242 } 0243 if (int len = findInString(taggedFile->getFilename(), idx); len != -1) { 0244 pos->m_part = Position::FileName; 0245 pos->m_matchedPos = idx; 0246 pos->m_matchedLength = len; 0247 return true; 0248 } 0249 } 0250 FOR_ALL_TAGS(tagNr) { 0251 if (Position::Part part = Position::tagNumberToPart(tagNr); 0252 pos->getPart() <= part) { 0253 FrameCollection frames; 0254 taggedFile->getAllFrames(tagNr, frames); 0255 if (searchInFrames(frames, part, pos, advanceChars)) { 0256 return true; 0257 } 0258 } 0259 } 0260 return false; 0261 } 0262 0263 /** 0264 * Search for next occurrence in frames. 0265 * @param frames frames of tag 0266 * @param part tag 1 or tag 2 0267 * @param pos position of last match, will be updated with new position 0268 * @param advanceChars number of characters to advance from current position 0269 * @return true if found. 0270 */ 0271 bool TagSearcher::searchInFrames(const FrameCollection& frames, 0272 Position::Part part, Position* pos, 0273 int advanceChars) const 0274 { 0275 int idx = 0; 0276 int frameNr = 0; 0277 auto begin = frames.cbegin(); 0278 auto end = frames.cend(); 0279 if (pos->getPart() == part) { 0280 idx = pos->m_matchedPos + advanceChars; 0281 for (frameNr = 0; 0282 frameNr < pos->getFrameIndex() && begin != end; ++frameNr) { 0283 ++begin; 0284 } 0285 } 0286 int len = -1; 0287 QString frameName; 0288 for (auto it = begin; it != end; ++it, ++frameNr) { 0289 if ((m_params.getFlags() & AllFrames) || 0290 (m_params.getFrameMask() & (1ULL << it->getType()))) { 0291 len = findInString(it->getValue(), idx); 0292 if (len != -1) { 0293 frameName = it->getExtendedType().getTranslatedName(); 0294 break; 0295 } 0296 } 0297 idx = 0; 0298 } 0299 if (len != -1) { 0300 pos->m_part = part; 0301 pos->m_frameName = frameName; 0302 pos->m_frameIndex = frameNr; 0303 pos->m_matchedPos = idx; 0304 pos->m_matchedLength = len; 0305 return true; 0306 } 0307 return false; 0308 } 0309 0310 /** 0311 * Replace found text. 0312 * @param params search parameters 0313 */ 0314 void TagSearcher::replace(const TagSearcher::Parameters& params) 0315 { 0316 setParameters(params); 0317 replaceNext(); 0318 } 0319 0320 /** 0321 * Replace found text. 0322 */ 0323 void TagSearcher::replaceNext() 0324 { 0325 QString replaced; 0326 if (m_currentPosition.isValid()) { 0327 if (TaggedFile* taggedFile = 0328 FileProxyModel::getTaggedFileOfIndex(m_currentPosition.getFileIndex())) { 0329 if (m_currentPosition.getPart() == Position::FileName) { 0330 QString str = taggedFile->getFilename(); 0331 replaced = str.mid(m_currentPosition.getMatchedPos(), 0332 m_currentPosition.getMatchedLength()); 0333 replaceString(replaced); 0334 str.replace(m_currentPosition.getMatchedPos(), 0335 m_currentPosition.getMatchedLength(), replaced); 0336 taggedFile->setFilename(str); 0337 } else { 0338 FrameCollection frames; 0339 taggedFile->getAllFrames( 0340 Position::partToTagNumber(m_currentPosition.getPart()), frames); 0341 auto it = frames.begin(); 0342 auto end = frames.end(); 0343 for (int frameNr = 0; 0344 frameNr < m_currentPosition.getFrameIndex() && it != end; 0345 ++frameNr) { 0346 ++it; 0347 } 0348 if (it != end) { 0349 auto& frame = const_cast<Frame&>(*it); 0350 QString str = frame.getValue(); 0351 replaced = str.mid(m_currentPosition.getMatchedPos(), 0352 m_currentPosition.getMatchedLength()); 0353 replaceString(replaced); 0354 str.replace(m_currentPosition.getMatchedPos(), 0355 m_currentPosition.getMatchedLength(), replaced); 0356 frame.setValueIfChanged(str); 0357 taggedFile->setFrames( 0358 Position::partToTagNumber(m_currentPosition.getPart()), frames); 0359 } 0360 } 0361 } 0362 } 0363 if (!replaced.isNull()) { 0364 emit textReplaced(); 0365 findNext(replaced.length()); 0366 } else { 0367 findNext(1); 0368 } 0369 } 0370 0371 /** 0372 * Replace all occurrences. 0373 * @param params search parameters 0374 */ 0375 void TagSearcher::replaceAll(const TagSearcher::Parameters& params) 0376 { 0377 setParameters(params); 0378 disconnect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext); 0379 connect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext, 0380 Qt::QueuedConnection); 0381 replaceNext(); 0382 } 0383 0384 /** 0385 * If a text is found replace it and then search the next occurrence. 0386 */ 0387 void TagSearcher::replaceThenFindNext() 0388 { 0389 if (!m_aborted && m_currentPosition.isValid()) { 0390 replaceNext(); 0391 } else { 0392 disconnect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext); 0393 } 0394 } 0395 0396 /** 0397 * Search string for text. 0398 * @param str string to be searched 0399 * @param idx start index of search, will be updated with index of found text 0400 * @return length of match if found, else -1. 0401 */ 0402 int TagSearcher::findInString(const QString& str, int& idx) const 0403 { 0404 if (m_regExp.pattern().isEmpty()) { 0405 idx = str.indexOf(m_params.getSearchText(), idx, 0406 m_params.getFlags() & CaseSensitive 0407 ? Qt::CaseSensitive : Qt::CaseInsensitive); 0408 return idx != -1 ? m_params.getSearchText().length() : -1; 0409 } 0410 auto match = m_regExp.match(str, idx); 0411 idx = match.capturedStart(); 0412 return match.hasMatch() ? match.capturedLength() : -1; 0413 } 0414 0415 /** 0416 * Replace string. 0417 * @param str string which will be replaced 0418 */ 0419 void TagSearcher::replaceString(QString& str) const 0420 { 0421 if (m_regExp.pattern().isEmpty()) { 0422 str.replace(m_params.getSearchText(), m_params.getReplaceText(), 0423 m_params.getFlags() & CaseSensitive 0424 ? Qt::CaseSensitive : Qt::CaseInsensitive); 0425 } else { 0426 str.replace(m_regExp, m_params.getReplaceText()); 0427 } 0428 } 0429 0430 /** 0431 * Set and preprocess search parameters. 0432 * @param params search parameters 0433 */ 0434 void TagSearcher::setParameters(const Parameters& params) 0435 { 0436 m_params = params; 0437 SearchFlags flags = m_params.getFlags(); 0438 if (m_iterator) { 0439 m_iterator->setDirectionBackwards(flags & Backwards); 0440 } 0441 if (flags & RegExp) { 0442 m_regExp.setPattern(m_params.getSearchText()); 0443 m_regExp.setPatternOptions(flags & CaseSensitive 0444 ? QRegularExpression::NoPatternOption 0445 : QRegularExpression::CaseInsensitiveOption); 0446 } else { 0447 m_regExp.setPattern(QString()); 0448 m_regExp.setPatternOptions(QRegularExpression::NoPatternOption); 0449 } 0450 } 0451 0452 /** 0453 * Get a string describing where the text was found. 0454 * @param taggedFile tagged file 0455 * @return description of location. 0456 */ 0457 QString TagSearcher::getLocationString(const TaggedFile* taggedFile) const 0458 { 0459 QString location = taggedFile->getFilename(); 0460 location += QLatin1String(": "); 0461 if (m_currentPosition.getPart() == Position::FileName) { 0462 location += tr("Filename"); 0463 } else { 0464 location += tr("Tag %1").arg(Frame::tagNumberToString( 0465 Position::partToTagNumber(m_currentPosition.getPart()))); 0466 location += QLatin1String(": "); 0467 location += m_currentPosition.getFrameName(); 0468 } 0469 return location; 0470 } 0471 0472 /** 0473 * Get parameters as variant list. 0474 * @return variant list containing search text, replace text, flags, 0475 * frameMask. 0476 */ 0477 QVariantList TagSearcher::Parameters::toVariantList() const 0478 { 0479 QVariantList lst; 0480 lst << m_searchText << m_replaceText << static_cast<int>(m_flags) 0481 << m_frameMask; 0482 return lst; 0483 } 0484 0485 /** 0486 * Set parameters from variant list. 0487 * @param lst variant list containing search text, replace text, flags, 0488 * frameMask 0489 */ 0490 void TagSearcher::Parameters::fromVariantList(const QVariantList& lst) 0491 { 0492 if (lst.size() >= 4) { 0493 m_searchText = lst.at(0).toString(); 0494 m_replaceText = lst.at(1).toString(); 0495 m_flags = SearchFlags(lst.at(2).toInt()); 0496 m_frameMask = lst.at(3).toULongLong(); 0497 } 0498 }