1 /**
2  * \file tagsearcher.cpp
3  * Search for strings in tags.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 08 Feb 2014
8  *
9  * Copyright (C) 2014-2018  Urs Fleisch
10  *
11  * This file is part of Kid3.
12  *
13  * Kid3 is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * Kid3 is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "tagsearcher.h"
28 #include "trackdatamodel.h"
29 #include "fileproxymodel.h"
30 #include "bidirfileproxymodeliterator.h"
31 
32 /**
33  * Constructor.
34  */
Position()35 TagSearcher::Position::Position()
36   : m_part(FileName), m_frameIndex(-1), m_matchedPos(-1), m_matchedLength(-1)
37 {
38 }
39 
40 /**
41  * Clear to invalid position.
42  */
clear()43 void TagSearcher::Position::clear()
44 {
45   m_fileIndex = QPersistentModelIndex();
46   m_frameName.clear();
47   m_frameIndex = -1;
48   m_matchedPos = -1;
49   m_matchedLength = -1;
50 }
51 
52 /**
53  * Check if position is valid.
54  * @return true if valid, false if not found.
55  */
isValid() const56 bool TagSearcher::Position::isValid() const
57 {
58   return m_fileIndex.isValid() && m_matchedPos != -1;
59 }
60 
61 
62 /**
63  * Constructor.
64  * @param parent parent object
65  */
TagSearcher(QObject * parent)66 TagSearcher::TagSearcher(QObject* parent) : QObject(parent),
67   m_fileProxyModel(nullptr), m_iterator(nullptr), m_aborted(false), m_started(false)
68 {
69 }
70 
71 /**
72  * Clear abort flag.
73  */
clearAborted()74 void TagSearcher::clearAborted()
75 {
76   m_aborted = false;
77 }
78 
79 /**
80  * Check if dialog was aborted.
81  * @return true if aborted.
82  */
isAborted() const83 bool TagSearcher::isAborted() const
84 {
85   return m_aborted;
86 }
87 
88 /**
89  * Set model of files to be searched.
90  * @param model file proxy model
91  */
setModel(FileProxyModel * model)92 void TagSearcher::setModel(FileProxyModel* model)
93 {
94   if (m_iterator && m_fileProxyModel != model) {
95     delete m_iterator;
96     m_iterator = nullptr;
97   }
98   m_fileProxyModel = model;
99   if (m_fileProxyModel && !m_iterator) {
100     m_iterator = new BiDirFileProxyModelIterator(m_fileProxyModel, this);
101     connect(m_iterator, &BiDirFileProxyModelIterator::nextReady,
102             this, &TagSearcher::searchNextFile);
103   }
104 }
105 
106 /**
107  * Set root index of directory to search.
108  * @param index root index of directory
109  */
setRootIndex(const QPersistentModelIndex & index)110 void TagSearcher::setRootIndex(const QPersistentModelIndex& index)
111 {
112   m_iterator->setRootIndex(index);
113 }
114 
115 /**
116  * Set index of file to start search.
117  * @param index index of file where search is started
118  */
setStartIndex(const QPersistentModelIndex & index)119 void TagSearcher::setStartIndex(const QPersistentModelIndex& index)
120 {
121   m_startIndex = index;
122 }
123 
124 /**
125  * Set abort flag.
126  */
abort()127 void TagSearcher::abort()
128 {
129   m_aborted = true;
130   m_started = false;
131   if (m_iterator) {
132     m_iterator->abort();
133   }
134 }
135 
136 /**
137  * Find next occurrence of string.
138  * @param params search parameters
139  */
find(const Parameters & params)140 void TagSearcher::find(const Parameters &params)
141 {
142   setParameters(params);
143   findNext(1);
144 }
145 
146 /**
147  * Find next occurrence of same string.
148  */
findNext(int advanceChars)149 void TagSearcher::findNext(int advanceChars)
150 {
151   m_aborted = false;
152   if (m_iterator) {
153     if (m_started) {
154       continueSearch(advanceChars);
155     } else {
156       bool continueFromCurrentPosition = false;
157       if (m_startIndex.isValid()) {
158         continueFromCurrentPosition = m_currentPosition.isValid() &&
159             m_currentPosition.getFileIndex() == m_startIndex;
160         m_iterator->setCurrentIndex(m_startIndex);
161         m_startIndex = QPersistentModelIndex();
162       }
163       m_started = true;
164       if (continueFromCurrentPosition) {
165         continueSearch(advanceChars);
166       } else {
167         m_iterator->start();
168       }
169     }
170   }
171 }
172 
173 /**
174  * Search next file.
175  * @param index index of file in file proxy model
176  */
searchNextFile(const QPersistentModelIndex & index)177 void TagSearcher::searchNextFile(const QPersistentModelIndex& index)
178 {
179   if (index.isValid()) {
180     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
181       emit progress(taggedFile->getFilename());
182       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
183 
184       Position pos;
185       if (searchInFile(taggedFile, &pos, 1)) {
186         pos.m_fileIndex = index;
187         m_currentPosition = pos;
188         if (m_iterator) {
189           m_iterator->suspend();
190         }
191         emit progress(getLocationString(taggedFile));
192         emit textFound();
193       }
194     }
195   } else {
196     m_started = false;
197     m_currentPosition.clear();
198     emit progress(tr("Search finished"));
199     emit textFound();
200   }
201 }
202 
203 /**
204  * Continue search in current file, if no other match is found, resume
205  * file iteration.
206  * @param advanceChars number of characters to advance from current position
207  */
continueSearch(int advanceChars)208 void TagSearcher::continueSearch(int advanceChars)
209 {
210   if (m_currentPosition.isValid()) {
211     if (TaggedFile* taggedFile =
212        FileProxyModel::getTaggedFileOfIndex(m_currentPosition.getFileIndex())) {
213       if (searchInFile(taggedFile, &m_currentPosition, advanceChars)) {
214         emit progress(getLocationString(taggedFile));
215         emit textFound();
216         return;
217       }
218     }
219   }
220   if (m_iterator) {
221     m_iterator->resume();
222   }
223 }
224 
225 /**
226  * Search for next occurrence in a file.
227  * @param taggedFile tagged file
228  * @param pos position of last match in @a taggedFile, will be updated
229  * with new position
230  * @param advanceChars number of characters to advance from current position
231  * @return true if found.
232  */
searchInFile(TaggedFile * taggedFile,Position * pos,int advanceChars) const233 bool TagSearcher::searchInFile(TaggedFile* taggedFile, Position* pos,
234                                int advanceChars) const
235 {
236   if (pos->getPart() <= Position::FileName &&
237       ((m_params.getFlags() & AllFrames) ||
238        (m_params.getFrameMask() & (1ULL << TrackDataModel::FT_FileName)))) {
239     int idx = 0;
240     if (pos->getPart() == Position::FileName) {
241       idx = pos->m_matchedPos + advanceChars;
242     }
243     int len = findInString(taggedFile->getFilename(), idx);
244     if (len != -1) {
245       pos->m_part = Position::FileName;
246       pos->m_matchedPos = idx;
247       pos->m_matchedLength = len;
248       return true;
249     }
250   }
251   FOR_ALL_TAGS(tagNr) {
252     Position::Part part = Position::tagNumberToPart(tagNr);
253     if (pos->getPart() <= part) {
254       FrameCollection frames;
255       taggedFile->getAllFrames(tagNr, frames);
256       if (searchInFrames(frames, part, pos, advanceChars)) {
257         return true;
258       }
259     }
260   }
261   return false;
262 }
263 
264 /**
265  * Search for next occurrence in frames.
266  * @param frames frames of tag
267  * @param part tag 1 or tag 2
268  * @param pos position of last match, will be updated with new position
269  * @param advanceChars number of characters to advance from current position
270  * @return true if found.
271  */
searchInFrames(const FrameCollection & frames,Position::Part part,Position * pos,int advanceChars) const272 bool TagSearcher::searchInFrames(const FrameCollection& frames,
273                                  Position::Part part, Position* pos,
274                                  int advanceChars) const
275 {
276   int idx = 0;
277   int frameNr = 0;
278   auto begin = frames.cbegin();
279   auto end = frames.cend();
280   if (pos->getPart() == part) {
281     idx = pos->m_matchedPos + advanceChars;
282     for (frameNr = 0;
283          frameNr < pos->getFrameIndex() && begin != end; ++frameNr) {
284       ++begin;
285     }
286   }
287   int len = -1;
288   QString frameName;
289   for (auto it = begin; it != end; ++it, ++frameNr) {
290     if ((m_params.getFlags() & AllFrames) ||
291         (m_params.getFrameMask() & (1ULL << it->getType()))) {
292       len = findInString(it->getValue(), idx);
293       if (len != -1) {
294         frameName = it->getExtendedType().getTranslatedName();
295         break;
296       }
297     }
298     idx = 0;
299   }
300   if (len != -1) {
301     pos->m_part = part;
302     pos->m_frameName = frameName;
303     pos->m_frameIndex = frameNr;
304     pos->m_matchedPos = idx;
305     pos->m_matchedLength = len;
306     return true;
307   }
308   return false;
309 }
310 
311 /**
312  * Replace found text.
313  * @param params search parameters
314  */
replace(const TagSearcher::Parameters & params)315 void TagSearcher::replace(const TagSearcher::Parameters& params)
316 {
317   setParameters(params);
318   replaceNext();
319 }
320 
321 /**
322  * Replace found text.
323  */
replaceNext()324 void TagSearcher::replaceNext()
325 {
326   QString replaced;
327   if (m_currentPosition.isValid()) {
328     if (TaggedFile* taggedFile =
329         FileProxyModel::getTaggedFileOfIndex(m_currentPosition.getFileIndex())) {
330       if (m_currentPosition.getPart() == Position::FileName) {
331         QString str = taggedFile->getFilename();
332         replaced = str.mid(m_currentPosition.getMatchedPos(),
333                            m_currentPosition.getMatchedLength());
334         replaceString(replaced);
335         str.replace(m_currentPosition.getMatchedPos(),
336                     m_currentPosition.getMatchedLength(), replaced);
337         taggedFile->setFilename(str);
338       } else {
339         FrameCollection frames;
340         taggedFile->getAllFrames(
341               Position::partToTagNumber(m_currentPosition.getPart()), frames);
342         auto it = frames.begin();
343         auto end = frames.end();
344         for (int frameNr = 0;
345              frameNr < m_currentPosition.getFrameIndex() && it != end;
346              ++frameNr) {
347           ++it;
348         }
349         if (it != end) {
350           auto& frame = const_cast<Frame&>(*it);
351           QString str = frame.getValue();
352           replaced = str.mid(m_currentPosition.getMatchedPos(),
353                              m_currentPosition.getMatchedLength());
354           replaceString(replaced);
355           str.replace(m_currentPosition.getMatchedPos(),
356                       m_currentPosition.getMatchedLength(), replaced);
357           frame.setValueIfChanged(str);
358           taggedFile->setFrames(
359                 Position::partToTagNumber(m_currentPosition.getPart()), frames);
360         }
361       }
362     }
363   }
364   if (!replaced.isNull()) {
365     emit textReplaced();
366     findNext(replaced.length());
367   } else {
368     findNext(1);
369   }
370 }
371 
372 /**
373  * Replace all occurrences.
374  * @param params search parameters
375  */
replaceAll(const TagSearcher::Parameters & params)376 void TagSearcher::replaceAll(const TagSearcher::Parameters& params)
377 {
378   setParameters(params);
379   disconnect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext);
380   connect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext,
381           Qt::QueuedConnection);
382   replaceNext();
383 }
384 
385 /**
386  * If a text is found replace it and then search the next occurrence.
387  */
replaceThenFindNext()388 void TagSearcher::replaceThenFindNext()
389 {
390   if (!m_aborted && m_currentPosition.isValid()) {
391     replaceNext();
392   } else {
393     disconnect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext);
394   }
395 }
396 
397 /**
398  * Search string for text.
399  * @param str string to be searched
400  * @param idx start index of search, will be updated with index of found text
401  * @return length of match if found, else -1.
402  */
findInString(const QString & str,int & idx) const403 int TagSearcher::findInString(const QString& str, int& idx) const
404 {
405   if (m_regExp.pattern().isEmpty()) {
406     idx = str.indexOf(m_params.getSearchText(), idx,
407                       m_params.getFlags() & CaseSensitive
408                       ? Qt::CaseSensitive : Qt::CaseInsensitive);
409     return idx != -1 ? m_params.getSearchText().length() : -1;
410   } else {
411     auto match = m_regExp.match(str, idx);
412     idx = match.capturedStart();
413     return match.hasMatch() ? match.capturedLength() : -1;
414   }
415 }
416 
417 /**
418  * Replace string.
419  * @param str string which will be replaced
420  */
replaceString(QString & str) const421 void TagSearcher::replaceString(QString& str) const
422 {
423   if (m_regExp.pattern().isEmpty()) {
424     str.replace(m_params.getSearchText(), m_params.getReplaceText(),
425                 m_params.getFlags() & CaseSensitive
426                 ? Qt::CaseSensitive : Qt::CaseInsensitive);
427   } else {
428     str.replace(m_regExp, m_params.getReplaceText());
429   }
430 }
431 
432 /**
433  * Set and preprocess search parameters.
434  * @param params search parameters
435  */
setParameters(const Parameters & params)436 void TagSearcher::setParameters(const Parameters& params)
437 {
438   m_params = params;
439   SearchFlags flags = m_params.getFlags();
440   if (m_iterator) {
441     m_iterator->setDirectionBackwards(flags & Backwards);
442   }
443   if (flags & RegExp) {
444     m_regExp.setPattern(m_params.getSearchText());
445     m_regExp.setPatternOptions(flags & CaseSensitive
446                                ? QRegularExpression::NoPatternOption
447                                : QRegularExpression::CaseInsensitiveOption);
448   } else {
449     m_regExp.setPattern(QString());
450     m_regExp.setPatternOptions(QRegularExpression::NoPatternOption);
451   }
452 }
453 
454 /**
455  * Get a string describing where the text was found.
456  * @param taggedFile tagged file
457  * @return description of location.
458  */
getLocationString(TaggedFile * taggedFile) const459 QString TagSearcher::getLocationString(TaggedFile* taggedFile) const
460 {
461   QString location = taggedFile->getFilename();
462   location += QLatin1String(": ");
463   if (m_currentPosition.getPart() == Position::FileName) {
464     location += tr("Filename");
465   } else {
466     location += tr("Tag %1").arg(Frame::tagNumberToString(
467           Position::partToTagNumber(m_currentPosition.getPart())));
468     location += QLatin1String(": ");
469     location += m_currentPosition.getFrameName();
470   }
471   return location;
472 }
473 
474 /**
475  * Get parameters as variant list.
476  * @return variant list containing search text, replace text, flags,
477  * frameMask.
478  */
toVariantList() const479 QVariantList TagSearcher::Parameters::toVariantList() const
480 {
481   QVariantList lst;
482   lst << m_searchText << m_replaceText << static_cast<int>(m_flags)
483       << m_frameMask;
484   return lst;
485 }
486 
487 /**
488  * Set parameters from variant list.
489  * @param lst variant list containing search text, replace text, flags,
490  * frameMask
491  */
fromVariantList(const QVariantList & lst)492 void TagSearcher::Parameters::fromVariantList(const QVariantList& lst)
493 {
494   if (lst.size() >= 4) {
495     m_searchText = lst.at(0).toString();
496     m_replaceText = lst.at(1).toString();
497     m_flags = SearchFlags(lst.at(2).toInt());
498     m_frameMask = lst.at(3).toULongLong();
499   }
500 }
501