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 &params)
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 }