1 /**
2  * \file kid3application.cpp
3  * Kid3 application logic, independent of GUI.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 10 Jul 2011
8  *
9  * Copyright (C) 2011-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 "kid3application.h"
28 #if QT_VERSION >= 0x060000
29 #include <QStringConverter>
30 #else
31 #include <QTextCodec>
32 #endif
33 #include <QTextStream>
34 #include <QNetworkAccessManager>
35 #include <QTimer>
36 #include <QCoreApplication>
37 #include <QPluginLoader>
38 #include <QElapsedTimer>
39 #include <QUrl>
40 #ifdef Q_OS_MAC
41 #include <CoreFoundation/CFURL.h>
42 #endif
43 #ifdef Q_OS_ANDROID
44 #include <QStandardPaths>
45 #endif
46 #if defined Q_OS_LINUX && !defined Q_OS_ANDROID
47 #include <malloc.h>
48 #endif
49 #ifdef HAVE_QTDBUS
50 #include <QDBusConnection>
51 #include <unistd.h>
52 #include "scriptinterface.h"
53 #endif
54 #include "filesystemmodel.h"
55 #include "icoreplatformtools.h"
56 #include "fileproxymodeliterator.h"
57 #include "filefilter.h"
58 #include "modeliterator.h"
59 #include "trackdatamodel.h"
60 #include "timeeventmodel.h"
61 #include "frameobjectmodel.h"
62 #include "playlistmodel.h"
63 #include "imagedataprovider.h"
64 #include "pictureframe.h"
65 #include "textimporter.h"
66 #include "importparser.h"
67 #include "textexporter.h"
68 #include "serverimporter.h"
69 #include "saferename.h"
70 #include "configstore.h"
71 #include "formatconfig.h"
72 #include "tagconfig.h"
73 #include "fileconfig.h"
74 #include "importconfig.h"
75 #include "guiconfig.h"
76 #include "playlistconfig.h"
77 #include "isettings.h"
78 #include "playlistcreator.h"
79 #include "iframeeditor.h"
80 #include "batchimportprofile.h"
81 #include "batchimportconfig.h"
82 #include "iserverimporterfactory.h"
83 #include "iservertrackimporterfactory.h"
84 #include "itaggedfilefactory.h"
85 #include "iusercommandprocessor.h"
86 #ifdef Q_OS_ANDROID
87 #include "androidutils.h"
88 #endif
89 #include "importplugins.h"
90 
91 namespace {
92 
93 /**
94  * Get the file name of the plugin from the plugin name.
95  * @param pluginName name of the plugin
96  * @return file name.
97  */
pluginFileName(const QString & pluginName)98 QString pluginFileName(const QString& pluginName)
99 {
100   QString fileName = pluginName.toLower();
101 #ifdef Q_OS_WIN32
102 #ifdef Q_CC_MSVC
103   fileName += QLatin1String(".dll");
104 #else
105   fileName = QLatin1String("lib") + fileName + QLatin1String(".dll");
106 #endif
107 #elif defined Q_OS_MAC
108   fileName = QLatin1String("lib") + fileName + QLatin1String(".dylib");
109 #else
110   fileName = QLatin1String("lib") + fileName + QLatin1String(".so");
111 #endif
112   return fileName;
113 }
114 
115 /**
116  * Get text encoding from tag config as frame text encoding.
117  * @return frame text encoding.
118  */
frameTextEncodingFromConfig()119 Frame::TextEncoding frameTextEncodingFromConfig()
120 {
121   Frame::TextEncoding encoding;
122   switch (TagConfig::instance().textEncoding()) {
123   case TagConfig::TE_UTF16:
124     encoding = Frame::TE_UTF16;
125     break;
126   case TagConfig::TE_UTF8:
127     encoding = Frame::TE_UTF8;
128     break;
129   case TagConfig::TE_ISO8859_1:
130   default:
131     encoding = Frame::TE_ISO8859_1;
132   }
133   return encoding;
134 }
135 
136 /**
137  * Extract file path, field name and index from frame name.
138  *
139  * @param frameName frame name with additional information for file, field and
140  * index
141  * @param dataFileName the path to a data file is returned here if available,
142  * else null
143  * @param fieldName the field name is returned here if available, else null
144  * @param index the index is returned here if available, else 0
145  */
extractFileFieldIndex(QString & frameName,QString & dataFileName,QString & fieldName,int & index)146 void extractFileFieldIndex(
147     QString& frameName, QString& dataFileName, QString& fieldName, int& index)
148 {
149   dataFileName.clear();
150   fieldName.clear();
151   index = 0;
152   int colonIndex = frameName.indexOf(QLatin1Char(':'));
153   if (colonIndex != -1) {
154     dataFileName = frameName.mid(colonIndex + 1);
155     frameName.truncate(colonIndex);
156   }
157   int dotIndex = frameName.indexOf(QLatin1Char('.'));
158   if (dotIndex != -1) {
159     fieldName = frameName.mid(dotIndex + 1);
160     frameName.truncate(dotIndex);
161   }
162   int bracketIndex = frameName.indexOf(QLatin1Char('['));
163   if (bracketIndex != -1) {
164     const int closingBracketIndex =
165         frameName.indexOf(QLatin1Char(']'), bracketIndex + 1);
166     if (closingBracketIndex > bracketIndex) {
167       bool ok;
168 #if QT_VERSION >= 0x060000
169       index = frameName.mid(
170           bracketIndex + 1, closingBracketIndex - bracketIndex - 1).toInt(&ok);
171 #else
172       index = frameName.midRef(
173           bracketIndex + 1, closingBracketIndex - bracketIndex - 1).toInt(&ok);
174 #endif
175       if (ok) {
176         frameName.remove(bracketIndex, closingBracketIndex - bracketIndex + 1);
177       }
178     }
179   }
180 }
181 
182 }
183 
184 /** Fallback for path to search for plugins */
185 QString Kid3Application::s_pluginsPathFallback;
186 
187 /**
188  * Constructor.
189  * @param platformTools platform tools
190  * @param parent parent object
191  */
Kid3Application(ICorePlatformTools * platformTools,QObject * parent)192 Kid3Application::Kid3Application(ICorePlatformTools* platformTools,
193                                  QObject* parent) : QObject(parent),
194   m_platformTools(platformTools),
195   m_configStore(new ConfigStore(m_platformTools->applicationSettings())),
196   m_fileSystemModel(new FileSystemModel(this)),
197   m_fileProxyModel(new FileProxyModel(m_platformTools->iconProvider(), this)),
198   m_fileProxyModelIterator(new FileProxyModelIterator(m_fileProxyModel)),
199   m_dirProxyModel(new DirProxyModel(this)),
200   m_fileSelectionModel(new QItemSelectionModel(m_fileProxyModel, this)),
201   m_dirSelectionModel(new QItemSelectionModel(m_dirProxyModel, this)),
202   m_trackDataModel(new TrackDataModel(m_platformTools->iconProvider(), this)),
203   m_netMgr(new QNetworkAccessManager(this)),
204   m_downloadClient(new DownloadClient(m_netMgr)),
205   m_textExporter(new TextExporter(this)),
206   m_tagSearcher(new TagSearcher(this)),
207   m_dirRenamer(new DirRenamer(this)),
208   m_batchImporter(new BatchImporter(m_netMgr)),
209   m_player(nullptr),
210   m_expressionFileFilter(nullptr),
211   m_downloadImageDest(ImageForSelectedFiles),
212   m_fileFilter(nullptr), m_filterPassed(0), m_filterTotal(0),
213   m_batchImportProfile(nullptr), m_batchImportTagVersion(Frame::TagNone),
214   m_editFrameTaggedFile(nullptr), m_addFrameTaggedFile(nullptr),
215   m_frameEditor(nullptr), m_storedFrameEditor(nullptr),
216   m_imageProvider(nullptr),
217 #ifdef Q_OS_ANDROID
218   m_pendingIntentsChecked(false),
219 #endif
220 #ifdef HAVE_QTDBUS
221   m_dbusEnabled(false),
222 #endif
223   m_filtered(false), m_selectionOperationRunning(false)
224 {
225   const TagConfig& tagCfg = TagConfig::instance();
226   FOR_ALL_TAGS(tagNr) {
227     bool id3v1 = tagNr == Frame::Tag_Id3v1;
228     m_genreModel[tagNr] = new GenreModel(id3v1, this);
229     m_framesModel[tagNr] = new FrameTableModel(
230           id3v1, platformTools->iconProvider(), this);
231     if (!id3v1) {
232       m_framesModel[tagNr]->setFrameOrder(tagCfg.quickAccessFrameOrder());
233       connect(&tagCfg, &TagConfig::quickAccessFrameOrderChanged,
234               m_framesModel[tagNr], &FrameTableModel::setFrameOrder);
235     }
236     m_framesSelectionModel[tagNr] = new QItemSelectionModel(m_framesModel[tagNr], this);
237     m_framelist[tagNr] = new FrameList(tagNr, m_framesModel[tagNr],
238         m_framesSelectionModel[tagNr]);
239     connect(m_framelist[tagNr], &FrameList::frameEdited,
240             this, &Kid3Application::onFrameEdited);
241     connect(m_framelist[tagNr], &FrameList::frameAdded,
242             this, &Kid3Application::onTag2FrameAdded);
243     m_tagContext[tagNr] = new Kid3ApplicationTagContext(this, tagNr);
244   }
245   m_selection = new TaggedFileSelection(m_framesModel, this);
246   setObjectName(QLatin1String("Kid3Application"));
247   m_fileSystemModel->setReadOnly(false);
248   const FileConfig& fileCfg = FileConfig::instance();
249   m_fileSystemModel->setSortIgnoringPunctuation(fileCfg.sortIgnoringPunctuation());
250   m_fileProxyModel->setSourceModel(m_fileSystemModel);
251   m_dirProxyModel->setSourceModel(m_fileSystemModel);
252   connect(m_fileSelectionModel,
253           &QItemSelectionModel::selectionChanged,
254           this, &Kid3Application::fileSelected);
255   connect(m_fileSelectionModel,
256           &QItemSelectionModel::selectionChanged,
257           this, &Kid3Application::fileSelectionChanged);
258   connect(m_fileProxyModel, &FileProxyModel::modifiedChanged,
259           this, &Kid3Application::modifiedChanged);
260 
261   connect(m_selection, &TaggedFileSelection::singleFileChanged,
262           this, &Kid3Application::updateCoverArtImageId);
263   connect(m_selection, &TaggedFileSelection::fileNameModified,
264           this, &Kid3Application::selectedFilesUpdated);
265 
266   initPlugins();
267   m_batchImporter->setImporters(m_importers, m_trackDataModel);
268 
269 #ifdef Q_OS_ANDROID
270   new AndroidUtils(this);
271   // Make sure that configuration changes are saved for the Android app.
272   // Old style connect syntax is used to avoid a dependency to QGuiApplication.
273   connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)),
274           this, SLOT(onApplicationStateChanged(Qt::ApplicationState)));
275   QObject::connect(AndroidUtils::instance(), &AndroidUtils::filePathReceived,
276                    this, [this](const QString& path) {
277     dropLocalFiles({path}, false);
278   });
279 #endif
280 }
281 
282 /**
283  * Destructor.
284  */
~Kid3Application()285 Kid3Application::~Kid3Application()
286 {
287 #ifdef Q_OS_MAC
288   // If a song is played, then stopped and Kid3 is terminated, it will crash in
289   // the QMediaPlayer destructor (Dispatch queue: com.apple.main-thread,
290   // objc_msgSend() selector name: setRate). Avoid calling the destructor by
291   // setting the QMediaPlayer's parent to null. See also:
292   // https://qt-project.org/forums/viewthread/29651
293   if (m_player) {
294     m_player->setParent(0);
295   }
296 #endif
297 }
298 
299 /**
300  * Save config when suspended, check intents when activated.
301  * @param state application state
302  */
onApplicationStateChanged(Qt::ApplicationState state)303 void Kid3Application::onApplicationStateChanged(Qt::ApplicationState state)
304 {
305 #ifdef Q_OS_ANDROID
306   if (state == Qt::ApplicationSuspended) {
307     saveConfig();
308   } else if (state == Qt::ApplicationActive) {
309     // When the app becomes active for the first time,
310     // check if it was launched with an intent.
311     if (!m_pendingIntentsChecked) {
312       m_pendingIntentsChecked = true;
313       AndroidUtils::instance()->checkPendingIntents();
314     }
315   }
316 #else
317   Q_UNUSED(state)
318 #endif
319 }
320 
321 #ifdef HAVE_QTDBUS
322 /**
323  * Activate the D-Bus interface.
324  * This method shall be called only once at initialization.
325  */
activateDbusInterface()326 void Kid3Application::activateDbusInterface()
327 {
328   if (QDBusConnection::sessionBus().isConnected()) {
329     QString serviceName(QLatin1String("org.kde.kid3"));
330     QDBusConnection::sessionBus().registerService(serviceName);
331     // For the case of multiple Kid3 instances running, register also a service
332     // with the PID appended. On KDE such a service is already registered but
333     // the call to registerService() seems to succeed nevertheless.
334     serviceName += QLatin1Char('-');
335     serviceName += QString::number(::getpid());
336     QDBusConnection::sessionBus().registerService(serviceName);
337     new ScriptInterface(this);
338     if (QDBusConnection::sessionBus().registerObject(QLatin1String("/Kid3"), this)) {
339       m_dbusEnabled = true;
340     } else {
341       qWarning("Registering D-Bus object failed");
342     }
343   } else {
344     qWarning("Cannot connect to the D-BUS session bus.");
345   }
346 }
347 #endif
348 
349 /**
350  * Load and initialize plugins depending on configuration.
351  */
initPlugins()352 void Kid3Application::initPlugins()
353 {
354   // Load plugins, set information about plugins in configuration.
355   ImportConfig& importCfg = ImportConfig::instance();
356   TagConfig& tagCfg = TagConfig::instance();
357   importCfg.clearAvailablePlugins();
358   tagCfg.clearAvailablePlugins();
359   const auto plugins = loadPlugins();
360   for (QObject* plugin : plugins) {
361     checkPlugin(plugin);
362   }
363   // Order the meta data plugins as configured.
364   QStringList pluginOrder = tagCfg.pluginOrder();
365   if (!pluginOrder.isEmpty()) {
366     QList<ITaggedFileFactory*> orderedFactories;
367     for (int i = 0; i < pluginOrder.size(); ++i) {
368       orderedFactories.append(nullptr); // clazy:exclude=reserve-candidates
369     }
370     const auto factories = FileProxyModel::taggedFileFactories();
371     for (ITaggedFileFactory* factory : factories) {
372       int idx = pluginOrder.indexOf(factory->name());
373       if (idx >= 0) {
374         orderedFactories[idx] = factory;
375       } else {
376         orderedFactories.append(factory); // clazy:exclude=reserve-candidates
377       }
378     }
379     orderedFactories.removeAll(nullptr);
380     FileProxyModel::taggedFileFactories().swap(orderedFactories);
381   }
382 }
383 
384 /**
385  * Find directory containing plugins.
386  * @param pluginsDir the plugin directory is returned here
387  * @return true if found.
388  */
findPluginsDirectory(QDir & pluginsDir)389 bool Kid3Application::findPluginsDirectory(QDir& pluginsDir) {
390   // First check if we are running from the build directory to load the
391   // plugins from there.
392   pluginsDir.setPath(QCoreApplication::applicationDirPath());
393   QString dirName = pluginsDir.dirName();
394 #ifdef Q_OS_WIN
395   QString buildType;
396   if (dirName.compare(QLatin1String("debug"), Qt::CaseInsensitive) == 0 ||
397       dirName.compare(QLatin1String("release"), Qt::CaseInsensitive) == 0) {
398     buildType = dirName;
399     pluginsDir.cdUp();
400     dirName = pluginsDir.dirName();
401   }
402 #endif
403   bool pluginsDirFound = pluginsDir.cd(QLatin1String(
404       (dirName == QLatin1String("qt") || dirName == QLatin1String("kde") ||
405        dirName == QLatin1String("cli") || dirName == QLatin1String("qml"))
406       ? "../../plugins"
407       : dirName == QLatin1String("test")
408         ? "../plugins"
409         : CFG_PLUGINSDIR));
410 #ifdef Q_OS_MAC
411   if (!pluginsDirFound) {
412     pluginsDirFound = pluginsDir.cd(QLatin1String("../../../../../plugins"));
413   }
414 #endif
415 #ifdef Q_OS_WIN
416   if (pluginsDirFound && !buildType.isEmpty()) {
417     pluginsDir.cd(buildType);
418   }
419 #endif
420   return pluginsDirFound;
421 }
422 
423 /**
424  * Set fallback path for directory containing plugins.
425  * @param path path to be searched for plugins if they are not found at the
426  * standard location relative to the application directory
427  */
setPluginsPathFallback(const QString & path)428 void Kid3Application::setPluginsPathFallback(const QString& path)
429 {
430   s_pluginsPathFallback = path;
431 }
432 
433 /**
434  * Load plugins.
435  * @return list of plugin instances.
436  */
loadPlugins()437 QObjectList Kid3Application::loadPlugins()
438 {
439   QObjectList plugins = QPluginLoader::staticInstances();
440 
441   QDir pluginsDir;
442   bool pluginsDirFound = findPluginsDirectory(pluginsDir);
443   if (!pluginsDirFound && !s_pluginsPathFallback.isEmpty()) {
444     pluginsDir.setPath(s_pluginsPathFallback);
445     pluginsDirFound = true;
446   }
447   if (pluginsDirFound) {
448     ImportConfig& importCfg = ImportConfig::instance();
449     TagConfig& tagCfg = TagConfig::instance();
450 
451     // Construct a set of disabled plugin file names
452     QMap<QString, QString> disabledImportPluginFileNames;
453     const QStringList disabledPlugins = importCfg.disabledPlugins();
454     for (const QString& pluginName : disabledPlugins) {
455       disabledImportPluginFileNames.insert(pluginFileName(pluginName),
456                                            pluginName);
457     }
458     QMap<QString, QString> disabledTagPluginFileNames;
459     const QStringList disabledTagPlugins = tagCfg.disabledPlugins();
460     for (const QString& pluginName : disabledTagPlugins) {
461       disabledTagPluginFileNames.insert(pluginFileName(pluginName),
462                                         pluginName);
463     }
464 
465     QStringList availablePlugins = importCfg.availablePlugins();
466     QStringList availableTagPlugins = tagCfg.availablePlugins();
467     const auto fileNames = pluginsDir.entryList(QDir::Files);
468     for (const QString& fileName : fileNames) {
469       if (disabledImportPluginFileNames.contains(fileName)) {
470         availablePlugins.append(
471               disabledImportPluginFileNames.value(fileName));
472         continue;
473       }
474       if (disabledTagPluginFileNames.contains(fileName)) {
475         availableTagPlugins.append(
476               disabledTagPluginFileNames.value(fileName));
477         continue;
478       }
479       QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
480       QObject* plugin = loader.instance();
481       if (plugin) {
482         QString name(plugin->objectName());
483         if (disabledPlugins.contains(name)) {
484           availablePlugins.append(name);
485           loader.unload();
486         } else if (disabledTagPlugins.contains(name)) {
487           availableTagPlugins.append(name);
488           loader.unload();
489         } else {
490           plugins.append(plugin);
491         }
492       }
493     }
494     importCfg.setAvailablePlugins(availablePlugins);
495     tagCfg.setAvailablePlugins(availableTagPlugins);
496   }
497   return plugins;
498 }
499 
500 /**
501  * Check type of a loaded plugin and register it.
502  * @param plugin instance returned by plugin loader
503  */
checkPlugin(QObject * plugin)504 void Kid3Application::checkPlugin(QObject* plugin)
505 {
506   if (IServerImporterFactory* importerFactory =
507       qobject_cast<IServerImporterFactory*>(plugin)) {
508     ImportConfig& importCfg = ImportConfig::instance();
509     QStringList availablePlugins = importCfg.availablePlugins();
510     availablePlugins.append(plugin->objectName());
511     importCfg.setAvailablePlugins(availablePlugins);
512     if (!importCfg.disabledPlugins().contains(plugin->objectName())) {
513       const auto keys = importerFactory->serverImporterKeys();
514       for (const QString& key : keys) {
515         m_importers.append(importerFactory->createServerImporter(
516                              key, m_netMgr, m_trackDataModel));
517       }
518     }
519   }
520   if (IServerTrackImporterFactory* importerFactory =
521       qobject_cast<IServerTrackImporterFactory*>(plugin)) {
522     ImportConfig& importCfg = ImportConfig::instance();
523     QStringList availablePlugins = importCfg.availablePlugins();
524     availablePlugins.append(plugin->objectName());
525     importCfg.setAvailablePlugins(availablePlugins);
526     if (!importCfg.disabledPlugins().contains(plugin->objectName())) {
527       const auto keys = importerFactory->serverTrackImporterKeys();
528       for (const QString& key : keys) {
529         m_trackImporters.append(importerFactory->createServerTrackImporter(
530                              key, m_netMgr, m_trackDataModel));
531       }
532     }
533   }
534   if (ITaggedFileFactory* taggedFileFactory =
535       qobject_cast<ITaggedFileFactory*>(plugin)) {
536     TagConfig& tagCfg = TagConfig::instance();
537     QStringList availablePlugins = tagCfg.availablePlugins();
538     availablePlugins.append(plugin->objectName());
539     tagCfg.setAvailablePlugins(availablePlugins);
540     if (!tagCfg.disabledPlugins().contains(plugin->objectName())) {
541       int features = tagCfg.taggedFileFeatures();
542       const auto keys = taggedFileFactory->taggedFileKeys();
543       for (const QString& key : keys) {
544         taggedFileFactory->initialize(key);
545         features |= taggedFileFactory->taggedFileFeatures(key);
546       }
547       tagCfg.setTaggedFileFeatures(features);
548       FileProxyModel::taggedFileFactories().append(taggedFileFactory);
549     }
550   }
551   if (IUserCommandProcessor* userCommandProcessor =
552       qobject_cast<IUserCommandProcessor*>(plugin)) {
553     ImportConfig& importCfg = ImportConfig::instance();
554     QStringList availablePlugins = importCfg.availablePlugins();
555     availablePlugins.append(plugin->objectName());
556     importCfg.setAvailablePlugins(availablePlugins);
557     if (!importCfg.disabledPlugins().contains(plugin->objectName())) {
558       m_userCommandProcessors.append(userCommandProcessor);
559     }
560   }
561 }
562 
563 /**
564  * Get names of available server track importers.
565  * @return list of server track importer names.
566  */
getServerImporterNames() const567 QStringList Kid3Application::getServerImporterNames() const
568 {
569   QStringList names;
570   const auto importers = m_importers;
571   for (const ServerImporter* importer : importers) {
572     names.append(QString::fromLatin1(importer->name()));
573   }
574   return names;
575 }
576 
577 /**
578  * Get audio player.
579  * @return audio player.
580  */
getAudioPlayer()581 QObject* Kid3Application::getAudioPlayer()
582 {
583   if (!m_player) {
584 #ifdef HAVE_QTDBUS
585     m_player = m_platformTools->createAudioPlayer(this, m_dbusEnabled);
586 #else
587     m_player = m_platformTools->createAudioPlayer(this, false);
588 #endif
589   }
590 #ifdef HAVE_QTDBUS
591   if (m_dbusEnabled) {
592     activateMprisInterface();
593   }
594 #endif
595   return m_player;
596 }
597 
598 /**
599  * Delete audio player.
600  */
deleteAudioPlayer()601 void Kid3Application::deleteAudioPlayer() {
602   if (m_player) {
603     QMetaObject::invokeMethod(m_player, "stop");
604 #ifdef HAVE_QTDBUS
605     if (m_dbusEnabled) {
606       deactivateMprisInterface();
607     }
608 #endif
609     delete m_player;
610     m_player = nullptr;
611   }
612 }
613 
614 #ifdef HAVE_QTDBUS
615 /**
616  * Activate the MPRIS D-Bus Interface if not already active.
617  */
activateMprisInterface()618 void Kid3Application::activateMprisInterface()
619 {
620   if (!m_mprisServiceName.isEmpty() || !m_player)
621     return;
622 
623   if (QDBusConnection::sessionBus().isConnected()) {
624     m_mprisServiceName = QLatin1String("org.mpris.MediaPlayer2.kid3");
625     bool ok = QDBusConnection::sessionBus().registerService(m_mprisServiceName);
626     if (!ok) {
627       // If another instance of Kid3 is already running register a service
628       // with ".instancePID" appended, see
629       // https://specifications.freedesktop.org/mpris-spec/latest/
630       m_mprisServiceName += QLatin1String(".instance");
631       m_mprisServiceName += QString::number(::getpid());
632       ok = QDBusConnection::sessionBus().registerService(m_mprisServiceName);
633     }
634     if (ok) {
635       if (!QDBusConnection::sessionBus().registerObject(
636             QLatin1String("/org/mpris/MediaPlayer2"), m_player)) {
637         qWarning("Registering D-Bus MPRIS object failed");
638       }
639     } else {
640       m_mprisServiceName.clear();
641       qWarning("Registering D-Bus MPRIS service failed");
642     }
643   } else {
644     qWarning("Cannot connect to the D-BUS session bus.");
645   }
646 }
647 
648 /**
649  * Deactivate the MPRIS D-Bus Interface if it is active.
650  */
deactivateMprisInterface()651 void Kid3Application::deactivateMprisInterface()
652 {
653   if (m_mprisServiceName.isEmpty())
654     return;
655 
656   if (QDBusConnection::sessionBus().isConnected()) {
657     QDBusConnection::sessionBus().unregisterObject(
658           QLatin1String("/org/mpris/MediaPlayer2"));
659     if (QDBusConnection::sessionBus().unregisterService(m_mprisServiceName)) {
660       m_mprisServiceName.clear();
661     } else {
662       qWarning("Unregistering D-Bus MPRIS service failed");
663     }
664   } else {
665     qWarning("Cannot connect to the D-BUS session bus.");
666   }
667 }
668 #endif
669 
670 /**
671  * Get settings.
672  * @return settings.
673  */
getSettings() const674 ISettings* Kid3Application::getSettings() const
675 {
676   return m_platformTools->applicationSettings();
677 }
678 
679 /**
680  * Apply configuration changes.
681  */
applyChangedConfiguration()682 void Kid3Application::applyChangedConfiguration()
683 {
684   saveConfig();
685   const FileConfig& fileCfg = FileConfig::instance();
686   FOR_ALL_TAGS(tagNr) {
687     if (!TagConfig::instance().markTruncations()) {
688       m_framesModel[tagNr]->markRows(0);
689     }
690     if (!fileCfg.markChanges()) {
691       m_framesModel[tagNr]->markChangedFrames(0);
692     }
693     m_genreModel[tagNr]->init();
694   }
695   notifyConfigurationChange();
696   quint64 oldQuickAccessFrames = FrameCollection::getQuickAccessFrames();
697   if (TagConfig::instance().quickAccessFrames() != oldQuickAccessFrames) {
698     FrameCollection::setQuickAccessFrames(
699           TagConfig::instance().quickAccessFrames());
700     emit selectedFilesUpdated();
701   }
702 
703   QStringList nameFilters(m_platformTools->getNameFilterPatterns(
704                             fileCfg.nameFilter()).split(QLatin1Char(' ')));
705   m_fileProxyModel->setNameFilters(nameFilters);
706   m_fileProxyModel->setFolderFilters(fileCfg.includeFolders(),
707                                      fileCfg.excludeFolders());
708 
709   QDir::Filters oldFilter = m_fileSystemModel->filter();
710   QDir::Filters filter = oldFilter;
711   if (fileCfg.showHiddenFiles()) {
712     filter |= QDir::Hidden;
713   } else {
714     filter &= ~QDir::Hidden;
715   }
716   if (filter != oldFilter) {
717     m_fileSystemModel->setFilter(filter);
718   }
719 }
720 
721 /**
722  * Save settings to the configuration.
723  */
saveConfig()724 void Kid3Application::saveConfig()
725 {
726   if (FileConfig::instance().loadLastOpenedFile()) {
727     FileConfig::instance().setLastOpenedFile(
728         m_fileProxyModel->filePath(currentOrRootIndex()));
729   }
730   m_configStore->writeToConfig();
731   getSettings()->sync();
732 }
733 
734 /**
735  * Read settings from the configuration.
736  */
readConfig()737 void Kid3Application::readConfig()
738 {
739   if (FileConfig::instance().nameFilter().isEmpty()) {
740     setAllFilesFileFilter();
741   }
742   notifyConfigurationChange();
743   FrameCollection::setQuickAccessFrames(
744         TagConfig::instance().quickAccessFrames());
745 }
746 
747 /**
748  * Open directory.
749  * When finished directoryOpened() is emitted, also if false is returned.
750  *
751  * @param paths file or directory paths, if multiple paths are given, the
752  * common directory is opened and the files are selected
753  * @param fileCheck if true, only open directory if paths exist
754  *
755  * @return true if ok.
756  */
openDirectory(const QStringList & paths,bool fileCheck)757 bool Kid3Application::openDirectory(const QStringList& paths, bool fileCheck)
758 {
759 #ifdef Q_OS_ANDROID
760   const QStringList musicLocations =
761       QStandardPaths::standardLocations(QStandardPaths::MusicLocation).mid(0, 1);
762   const QStringList pathList = paths.isEmpty() ? musicLocations : paths;
763 #else
764   const QStringList pathList(paths);
765 #endif
766   bool ok = true;
767   QStringList filePaths;
768   QStringList dirComponents;
769   for (const QString& path : pathList) {
770     if (!path.isEmpty()) {
771       QFileInfo fileInfo(path);
772       if (fileCheck && !fileInfo.exists()) {
773         ok = false;
774         break;
775       }
776       QString dirPath;
777       if (!fileInfo.isDir()) {
778         dirPath = fileInfo.absolutePath();
779         if (fileInfo.isFile()) {
780           filePaths.append(fileInfo.absoluteFilePath());
781         }
782       } else {
783         dirPath = QDir(path).absolutePath();
784       }
785       QStringList dirPathComponents = dirPath.split(QDir::separator());
786       if (dirComponents.isEmpty()) {
787         dirComponents = dirPathComponents;
788       } else {
789         // Reduce dirPath to common prefix.
790         auto dirIt = dirComponents.begin();
791         auto dirPathIt = dirPathComponents.constBegin();
792         while (dirIt != dirComponents.end() &&
793                dirPathIt != dirPathComponents.constEnd() &&
794                *dirIt == *dirPathIt) {
795           ++dirIt;
796           ++dirPathIt;
797         }
798         dirComponents.erase(dirIt, dirComponents.end());
799       }
800     }
801   }
802   QString dir;
803   if (ok) {
804     dir = dirComponents.join(QDir::separator());
805     if (dir.isEmpty() && !filePaths.isEmpty()) {
806       dir = QDir::rootPath();
807     }
808     ok = !dir.isEmpty();
809   }
810   QModelIndex rootIndex;
811   QModelIndexList fileIndexes;
812   if (ok) {
813     const FileConfig& fileCfg = FileConfig::instance();
814     QStringList nameFilters(m_platformTools->getNameFilterPatterns(
815                               fileCfg.nameFilter()).split(QLatin1Char(' ')));
816     m_fileProxyModel->setNameFilters(nameFilters);
817     m_fileProxyModel->setFolderFilters(fileCfg.includeFolders(),
818                                        fileCfg.excludeFolders());
819     QDir::Filters filter = QDir::AllEntries | QDir::AllDirs;
820     if (fileCfg.showHiddenFiles()) {
821       filter |= QDir::Hidden;
822     }
823     m_fileSystemModel->setFilter(filter);
824     rootIndex = m_fileSystemModel->setRootPath(dir);
825     fileIndexes.reserve(filePaths.size());
826     const auto constFilePaths = filePaths;
827     for (const QString& filePath : constFilePaths) {
828       fileIndexes.append(m_fileSystemModel->index(filePath));
829     }
830     ok = rootIndex.isValid();
831   }
832   if (ok) {
833     setFiltered(false);
834     m_dirName = dir;
835     emit dirNameChanged(m_dirName);
836     QModelIndex oldRootIndex = m_fileProxyModelRootIndex;
837     m_fileProxyModelRootIndex = m_fileProxyModel->mapFromSource(rootIndex);
838     m_fileProxyModelFileIndexes.clear();
839     const auto constFileIndexes = fileIndexes;
840     for (const QModelIndex& fileIndex : constFileIndexes) {
841       m_fileProxyModelFileIndexes.append(
842             m_fileProxyModel->mapFromSource(fileIndex));
843     }
844     if (m_fileProxyModelRootIndex != oldRootIndex) {
845       connect(m_fileProxyModel, &FileProxyModel::sortingFinished,
846               this, &Kid3Application::onDirectoryLoaded);
847     } else {
848       QTimer::singleShot(0, this, &Kid3Application::onDirectoryOpened);
849     }
850   }
851   if (!ok) {
852     QTimer::singleShot(0, this, &Kid3Application::onDirectoryOpened);
853   }
854   return ok;
855 }
856 
857 /**
858  * Update selection and emit signals when directory is opened.
859  */
onDirectoryOpened()860 void Kid3Application::onDirectoryOpened()
861 {
862   QModelIndex fsRoot = m_fileProxyModel->mapToSource(m_fileProxyModelRootIndex);
863   m_dirProxyModelRootIndex = m_dirProxyModel->mapFromSource(fsRoot);
864 
865   emit fileRootIndexChanged(m_fileProxyModelRootIndex);
866   emit dirRootIndexChanged(m_dirProxyModelRootIndex);
867 
868   if (m_fileProxyModelRootIndex.isValid()) {
869     m_fileSelectionModel->clearSelection();
870     if (!m_fileProxyModelFileIndexes.isEmpty()) {
871       const auto fileIndexes = m_fileProxyModelFileIndexes;
872       for (const QPersistentModelIndex& fileIndex : fileIndexes) {
873         m_fileSelectionModel->select(fileIndex,
874             QItemSelectionModel::Select | QItemSelectionModel::Rows);
875       }
876 #if QT_VERSION >= 0x050600
877       m_fileSelectionModel->setCurrentIndex(m_fileProxyModelFileIndexes.constFirst(),
878                                             QItemSelectionModel::NoUpdate);
879 #else
880       m_fileSelectionModel->setCurrentIndex(m_fileProxyModelFileIndexes.first(),
881                                             QItemSelectionModel::NoUpdate);
882 #endif
883     } else {
884       m_fileSelectionModel->setCurrentIndex(m_fileProxyModelRootIndex,
885           QItemSelectionModel::Clear | QItemSelectionModel::Current |
886           QItemSelectionModel::Rows);
887     }
888   }
889 
890   emit directoryOpened();
891 
892   if (m_dirUpIndex.isValid()) {
893     m_dirSelectionModel->setCurrentIndex(m_dirUpIndex,
894         QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
895     m_dirUpIndex = QPersistentModelIndex();
896   }
897 }
898 
899 /**
900  * Called when the gatherer thread has finished to load the directory.
901  */
onDirectoryLoaded()902 void Kid3Application::onDirectoryLoaded()
903 {
904   disconnect(m_fileProxyModel, &FileProxyModel::sortingFinished,
905              this, &Kid3Application::onDirectoryLoaded);
906   onDirectoryOpened();
907 }
908 
909 /**
910  * Unload all tags.
911  * The tags of all files which are not modified or selected are freed to
912  * reclaim their memory.
913  */
unloadAllTags()914 void Kid3Application::unloadAllTags()
915 {
916   TaggedFileIterator it(m_fileProxyModelRootIndex);
917   while (it.hasNext()) {
918     TaggedFile* taggedFile = it.next();
919     if (taggedFile->isTagInformationRead() && !taggedFile->isChanged() &&
920         !m_fileSelectionModel->isSelected(taggedFile->getIndex())) {
921       taggedFile->clearTags(false);
922       taggedFile->closeFileHandle();
923     }
924   }
925 #if defined Q_OS_LINUX && !defined Q_OS_ANDROID
926   if (::malloc_trim(0)) {
927     qDebug("Memory released by malloc_trim()");
928   }
929 #endif
930 }
931 
932 /**
933  * Get directory path of opened directory.
934  * @return directory path.
935  */
getDirPath() const936 QString Kid3Application::getDirPath() const
937 {
938   return FileProxyModel::getPathIfIndexOfDir(m_fileProxyModelRootIndex);
939 }
940 
941 /**
942  * Get current index in file proxy model or root index if current index is
943  * invalid.
944  * @return current index, root index if not valid.
945  */
currentOrRootIndex() const946 QModelIndex Kid3Application::currentOrRootIndex() const
947 {
948   QModelIndex index(m_fileSelectionModel->currentIndex());
949   if (index.isValid())
950     return index;
951   else
952     return m_fileProxyModelRootIndex;
953 }
954 
955 /**
956  * Save all changed files.
957  * longRunningOperationProgress() is emitted while saving files.
958  *
959  * @return list of files with error, empty if ok.
960  */
saveDirectory()961 QStringList Kid3Application::saveDirectory()
962 {
963   QStringList errorFiles;
964   int numFiles = 0, totalFiles = 0;
965   // Get number of files to be saved to display correct progressbar
966   TaggedFileIterator countIt(m_fileProxyModelRootIndex);
967   while (countIt.hasNext()) {
968     if (countIt.next()->isChanged()) {
969       ++totalFiles;
970     }
971   }
972   QString operationName = tr("Saving folder...");
973   bool aborted = false;
974   emit longRunningOperationProgress(operationName, -1, totalFiles, &aborted);
975 
976   TaggedFileIterator it(m_fileProxyModelRootIndex);
977   while (it.hasNext()) {
978     TaggedFile* taggedFile = it.next();
979     QString fileName = taggedFile->getFilename();
980     if (taggedFile->isFilenameChanged() &&
981         Utils::replaceIllegalFileNameCharacters(fileName)) {
982       taggedFile->setFilename(fileName);
983     }
984     bool renamed = false;
985     if (taggedFile->isChanged() &&
986         !taggedFile->writeTags(false, &renamed,
987                                FileConfig::instance().preserveTime())) {
988       QDir dir(taggedFile->getDirname());
989       if (dir.exists(fileName) && taggedFile->isFilenameChanged()) {
990         // File is renamed to a file name which already exists.
991         // Try another file name ending with a number.
992         QString baseName = fileName;
993         QString ext;
994         int dotPos = baseName.lastIndexOf(QLatin1Char('.'));
995         if (dotPos != -1) {
996           ext = baseName.mid(dotPos);
997           baseName.truncate(dotPos);
998         }
999         baseName.append(QLatin1Char('('));
1000         ext.prepend(QLatin1Char(')'));
1001         bool ok = false;
1002         for (int nr = 1; nr < 100; ++nr) {
1003           QString newName = baseName + QString::number(nr) + ext;
1004           if (!dir.exists(newName)) {
1005             taggedFile->setFilename(newName);
1006             ok = taggedFile->writeTags(false, &renamed,
1007                                        FileConfig::instance().preserveTime());
1008             break;
1009           }
1010         }
1011         if (ok) {
1012           continue;
1013         } else {
1014           taggedFile->setFilename(fileName);
1015         }
1016       }
1017       QString errorMsg = taggedFile->getAbsFilename();
1018       errorFiles.push_back(errorMsg);
1019     }
1020     ++numFiles;
1021     emit longRunningOperationProgress(operationName, numFiles, totalFiles,
1022                                       &aborted);
1023     if (aborted) {
1024       break;
1025     }
1026   }
1027   if (totalFiles == 0) {
1028     // To signal that operation is finished.
1029     ++totalFiles;
1030   }
1031   emit longRunningOperationProgress(operationName, totalFiles, totalFiles,
1032                                     &aborted);
1033 
1034   return errorFiles;
1035 }
1036 
1037 /**
1038  * Update tags of selected files to contain contents of frame models.
1039  */
frameModelsToTags()1040 void Kid3Application::frameModelsToTags()
1041 {
1042   if (!m_currentSelection.isEmpty()) {
1043     FOR_ALL_TAGS(tagNr) {
1044       FrameCollection frames(m_framesModel[tagNr]->getEnabledFrames());
1045       for (auto it = m_currentSelection.constBegin();
1046            it != m_currentSelection.constEnd();
1047            ++it) {
1048         if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(*it)) {
1049           taggedFile->setFrames(tagNr, frames);
1050         }
1051       }
1052     }
1053   }
1054 }
1055 
1056 /**
1057  * Update frame models to contain contents of selected files.
1058  * The properties starting with "selection" will be set by this method.
1059  */
tagsToFrameModels()1060 void Kid3Application::tagsToFrameModels()
1061 {
1062   QList<QPersistentModelIndex> indexes;
1063   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
1064   indexes.reserve(selectedIndexes.size());
1065   for (const QModelIndex& index : selectedIndexes) {
1066     indexes.append(QPersistentModelIndex(index));
1067   }
1068 
1069   if (addTaggedFilesToSelection(indexes, true)) {
1070     m_currentSelection.swap(indexes);
1071   }
1072 }
1073 
1074 /**
1075  * Update frame models to contain contents of item selection.
1076  * The properties starting with "selection" will be set by this method.
1077  * @param selected item selection
1078  */
selectedTagsToFrameModels(const QItemSelection & selected)1079 void Kid3Application::selectedTagsToFrameModels(const QItemSelection& selected)
1080 {
1081   QList<QPersistentModelIndex> indexes;
1082   const auto selectedIndexes = selected.indexes();
1083   for (const QModelIndex& index : selectedIndexes) {
1084     if (index.column() == 0) {
1085       indexes.append(QPersistentModelIndex(index));
1086     }
1087   }
1088 
1089   if (addTaggedFilesToSelection(indexes, m_currentSelection.isEmpty())) {
1090     m_currentSelection.append(indexes);
1091   }
1092 }
1093 
1094 /**
1095  * Update frame models to contain contents of selected files.
1096  * @param indexes tagged file indexes
1097  * @param startSelection true if a new selection is started, false to add to
1098  * the existing selection
1099  * @return true if ok, false if selection operation is already running.
1100  */
addTaggedFilesToSelection(const QList<QPersistentModelIndex> & indexes,bool startSelection)1101 bool Kid3Application::addTaggedFilesToSelection(
1102     const QList<QPersistentModelIndex>& indexes, bool startSelection)
1103 {
1104   // It would crash if this is called while a long running selection operation
1105   // is in progress.
1106   if (m_selectionOperationRunning)
1107     return false;
1108 
1109   m_selectionOperationRunning = true;
1110 
1111   if (startSelection) {
1112     m_selection->beginAddTaggedFiles();
1113   }
1114 
1115   QElapsedTimer timer;
1116   timer.start();
1117   QString operationName = tr("Selection");
1118   int longRunningTotal = 0;
1119   int done = 0;
1120   bool aborted = false;
1121   for (auto it = indexes.constBegin(); it != indexes.constEnd(); ++it, ++done) {
1122     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(*it)) {
1123       m_selection->addTaggedFile(taggedFile);
1124       if (!longRunningTotal) {
1125         if (timer.elapsed() >= 3000) {
1126           longRunningTotal = indexes.size();
1127           emit longRunningOperationProgress(operationName, -1, longRunningTotal,
1128                                             &aborted);
1129         }
1130       } else {
1131         emit longRunningOperationProgress(operationName, done, longRunningTotal,
1132                                           &aborted);
1133         if (aborted) {
1134           break;
1135         }
1136       }
1137     }
1138   }
1139   if (longRunningTotal) {
1140     emit longRunningOperationProgress(operationName, longRunningTotal,
1141                                       longRunningTotal, &aborted);
1142   }
1143 
1144   m_selection->endAddTaggedFiles();
1145 
1146   if (TaggedFile* taggedFile = m_selection->singleFile()) {
1147     FOR_ALL_TAGS(tagNr) {
1148       m_framelist[tagNr]->setTaggedFile(taggedFile);
1149     }
1150   }
1151   m_selection->clearUnusedFrames();
1152   m_selectionOperationRunning = false;
1153   return true;
1154 }
1155 
1156 /**
1157  * Revert file modifications.
1158  * Acts on selected files or all files if no file is selected.
1159  */
revertFileModifications()1160 void Kid3Application::revertFileModifications()
1161 {
1162   SelectedTaggedFileIterator it(getRootIndex(),
1163                                 getFileSelectionModel(),
1164                                 true);
1165   while (it.hasNext()) {
1166     TaggedFile* taggedFile = it.next();
1167     taggedFile->readTags(true);
1168   }
1169   if (!it.hasNoSelection()) {
1170     emit selectedFilesUpdated();
1171   }
1172 }
1173 
1174 /**
1175  * Set filter state.
1176  *
1177  * @param val true if list is filtered
1178  */
setFiltered(bool val)1179 void Kid3Application::setFiltered(bool val)
1180 {
1181   if (m_filtered != val) {
1182     m_filtered = val;
1183     emit filteredChanged(m_filtered);
1184   }
1185 }
1186 
1187 /**
1188  * Import.
1189  *
1190  * @param tagMask tag mask
1191  * @param path    path of file, "clipboard" for import from clipboard
1192  * @param fmtIdx  index of format
1193  *
1194  * @return true if ok.
1195  */
importTags(Frame::TagVersion tagMask,const QString & path,int fmtIdx)1196 bool Kid3Application::importTags(Frame::TagVersion tagMask,
1197                                  const QString& path, int fmtIdx)
1198 {
1199   const ImportConfig& importCfg = ImportConfig::instance();
1200   filesToTrackDataModel(importCfg.importDest());
1201   QString text;
1202   if (path == QLatin1String("clipboard")) {
1203     text = m_platformTools->readFromClipboard();
1204   } else {
1205     QFile file(path);
1206     if (file.open(QIODevice::ReadOnly)) {
1207       text = QTextStream(&file).readAll();
1208       file.close();
1209     }
1210   }
1211   if (!text.isNull() &&
1212       fmtIdx < importCfg.importFormatHeaders().size()) {
1213     TextImporter(getTrackDataModel()).updateTrackData(
1214       text,
1215       importCfg.importFormatHeaders().at(fmtIdx),
1216       importCfg.importFormatTracks().at(fmtIdx));
1217     trackDataModelToFiles(tagMask);
1218     return true;
1219   }
1220   return false;
1221 }
1222 
1223 /**
1224  * Import from tags.
1225  *
1226  * @param tagMask tag mask
1227  * @param source format to get source text from tags
1228  * @param extraction regular expression with frame names and captures to
1229  * extract from source text
1230  */
importFromTags(Frame::TagVersion tagMask,const QString & source,const QString & extraction)1231 void Kid3Application::importFromTags(Frame::TagVersion tagMask,
1232                                      const QString& source,
1233                                      const QString& extraction)
1234 {
1235   ImportTrackDataVector trackDataVector;
1236   filesToTrackData(tagMask, trackDataVector);
1237   TextImporter::importFromTags(source, extraction, trackDataVector);
1238   getTrackDataModel()->setTrackData(trackDataVector);
1239   trackDataModelToFiles(tagMask);
1240 }
1241 
1242 /**
1243  * Import from tags on selected files.
1244  *
1245  * @param tagMask tag mask
1246  * @param source format to get source text from tags
1247  * @param extraction regular expression with frame names and captures to
1248  * extract from source text
1249  *
1250  * @return extracted values for "%{__return}(.+)", empty if not used.
1251  */
importFromTagsToSelection(Frame::TagVersion tagMask,const QString & source,const QString & extraction)1252 QStringList Kid3Application::importFromTagsToSelection(Frame::TagVersion tagMask,
1253                                                        const QString& source,
1254                                                        const QString& extraction)
1255 {
1256   emit fileSelectionUpdateRequested();
1257   SelectedTaggedFileIterator it(getRootIndex(),
1258                                 getFileSelectionModel(),
1259                                 true);
1260   ImportParser parser;
1261   parser.setFormat(extraction);
1262   while (it.hasNext()) {
1263     TaggedFile* taggedFile = it.next();
1264     taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
1265     ImportTrackData trackData(*taggedFile, tagMask);
1266     TextImporter::importFromTags(source, parser, trackData);
1267     taggedFile->setFrames(Frame::tagNumberFromMask(tagMask), trackData);
1268   }
1269   emit selectedFilesUpdated();
1270   return parser.getReturnValues();
1271 }
1272 
1273 /**
1274  * Export.
1275  *
1276  * @param tagVersion tag version
1277  * @param path   path of file, "clipboard" for export to clipboard
1278  * @param fmtIdx index of format
1279  *
1280  * @return true if ok.
1281  */
exportTags(Frame::TagVersion tagVersion,const QString & path,int fmtIdx)1282 bool Kid3Application::exportTags(Frame::TagVersion tagVersion,
1283                                  const QString& path, int fmtIdx)
1284 {
1285   ImportTrackDataVector trackDataVector;
1286   filesToTrackData(tagVersion, trackDataVector);
1287   m_textExporter->setTrackData(trackDataVector);
1288   m_textExporter->updateTextUsingConfig(fmtIdx);
1289   if (path == QLatin1String("clipboard")) {
1290     return m_platformTools->writeToClipboard(m_textExporter->getText());
1291   } else {
1292     return m_textExporter->exportToFile(path);
1293   }
1294 }
1295 
1296 /**
1297  * Write playlist according to playlist configuration.
1298  *
1299  * @param cfg playlist configuration to use
1300  *
1301  * @return true if ok.
1302  */
writePlaylist(const PlaylistConfig & cfg)1303 bool Kid3Application::writePlaylist(const PlaylistConfig& cfg)
1304 {
1305   PlaylistCreator plCtr(getDirPath(), cfg);
1306   QItemSelectionModel* selectModel = getFileSelectionModel();
1307   bool noSelection = !cfg.onlySelectedFiles() || !selectModel ||
1308                      !selectModel->hasSelection();
1309   bool ok = true;
1310   QModelIndex rootIndex;
1311 
1312   if (cfg.location() == PlaylistConfig::PL_CurrentDirectory) {
1313     // Get first child of parent of current index.
1314     rootIndex = currentOrRootIndex();
1315     if (rootIndex.model() && rootIndex.model()->rowCount(rootIndex) <= 0)
1316       rootIndex = rootIndex.parent();
1317     if (const QAbstractItemModel* model = rootIndex.model()) {
1318       for (int row = 0; row < model->rowCount(rootIndex); ++row) {
1319         QModelIndex index = model->index(row, 0, rootIndex);
1320         PlaylistCreator::Item plItem(index, plCtr);
1321         if (plItem.isFile() &&
1322             (noSelection || selectModel->isSelected(index))) {
1323           ok = plItem.add() && ok;
1324         }
1325       }
1326     }
1327   } else {
1328     QString selectedDirPrefix;
1329     rootIndex = getRootIndex();
1330     ModelIterator it(rootIndex);
1331     while (it.hasNext()) {
1332       QModelIndex index = it.next();
1333       PlaylistCreator::Item plItem(index, plCtr);
1334       bool inSelectedDir = false;
1335       if (plItem.isDir()) {
1336         if (!selectedDirPrefix.isEmpty()) {
1337           if (plItem.getDirName().startsWith(selectedDirPrefix)) {
1338             inSelectedDir = true;
1339           } else {
1340             selectedDirPrefix = QLatin1String("");
1341           }
1342         }
1343         if (inSelectedDir || noSelection || selectModel->isSelected(index)) {
1344           // if directory is selected, all its files are selected
1345           if (!inSelectedDir) {
1346             selectedDirPrefix = plItem.getDirName();
1347           }
1348         }
1349       } else if (plItem.isFile()) {
1350         QString dirName = plItem.getDirName();
1351         if (!selectedDirPrefix.isEmpty()) {
1352           if (dirName.startsWith(selectedDirPrefix)) {
1353             inSelectedDir = true;
1354           } else {
1355             selectedDirPrefix = QLatin1String("");
1356           }
1357         }
1358         if (inSelectedDir || noSelection || selectModel->isSelected(index)) {
1359           ok = plItem.add() && ok;
1360         }
1361       }
1362     }
1363   }
1364 
1365   ok = plCtr.write() && ok;
1366   return ok;
1367 }
1368 
1369 /**
1370  * Write empty playlist.
1371  * @param cfg playlist configuration to use
1372  * @param fileName file name for playlist
1373  * @return true if ok.
1374  */
writeEmptyPlaylist(const PlaylistConfig & cfg,const QString & fileName)1375 bool Kid3Application::writeEmptyPlaylist(const PlaylistConfig& cfg,
1376                                          const QString& fileName)
1377 {
1378   QString path = getDirPath();
1379   PlaylistCreator plCtr(path, cfg);
1380   if (!path.endsWith(QLatin1Char('/'))) {
1381     path += QLatin1Char('/');
1382   }
1383   path += fileName;
1384   QString ext = cfg.fileExtensionForFormat();
1385   if (!path.endsWith(ext)) {
1386     path += ext;
1387   }
1388   return plCtr.write(path, QList<QPersistentModelIndex>());
1389 }
1390 
1391 /**
1392  * Write playlist using current playlist configuration.
1393  *
1394  * @return true if ok.
1395  */
writePlaylist()1396 bool Kid3Application::writePlaylist()
1397 {
1398   return writePlaylist(PlaylistConfig::instance());
1399 }
1400 
1401 /**
1402  * Get items of a playlist.
1403  * @param path path to playlist file
1404  * @return list of absolute paths to playlist items.
1405  */
getPlaylistItems(const QString & path)1406 QStringList Kid3Application::getPlaylistItems(const QString& path)
1407 {
1408   return playlistModel(path)->pathsInPlaylist();
1409 }
1410 
1411 /**
1412  * Set items of a playlist.
1413  * @param path path to playlist file
1414  * @param items list of absolute paths to playlist items
1415  * @return true if ok, false if not all @a items were found and added or
1416  *         saving failed.
1417  */
setPlaylistItems(const QString & path,const QStringList & items)1418 bool Kid3Application::setPlaylistItems(const QString& path,
1419                                        const QStringList& items)
1420 {
1421   PlaylistModel* model = playlistModel(path);
1422   if (model->setPathsInPlaylist(items)) {
1423     return model->save();
1424   }
1425   return false;
1426 }
1427 
1428 /**
1429  * Get playlist model for a play list file.
1430  * @param path path to playlist file
1431  * @return playlist model.
1432  */
playlistModel(const QString & path)1433 PlaylistModel* Kid3Application::playlistModel(const QString& path)
1434 {
1435   // Create an absolute path with a value which does not depend on the file's
1436   // existence or whether the path given is relative or absolute.
1437   QString absPath;
1438   if (!path.isEmpty()) {
1439     QFileInfo fileInfo(path);
1440     absPath = fileInfo.absoluteDir().filePath(fileInfo.fileName());
1441   }
1442 
1443   PlaylistModel* model = m_playlistModels.value(absPath);
1444   if (!model) {
1445     model = new PlaylistModel(m_fileProxyModel, this);
1446     m_playlistModels.insert(absPath, model);
1447   }
1448   model->setPlaylistFile(absPath);
1449   return model;
1450 }
1451 
1452 /**
1453  * Check if any playlist model has unsaved modifications.
1454  * @return true if there is a modified playlist model.
1455  */
hasModifiedPlaylistModel() const1456 bool Kid3Application::hasModifiedPlaylistModel() const
1457 {
1458   for (auto it = m_playlistModels.constBegin();
1459        it != m_playlistModels.constEnd();
1460        ++it) {
1461     if ((*it)->isModified()) {
1462       return true;
1463     }
1464   }
1465   return false;
1466 }
1467 
1468 /**
1469  * Save all modified playlist models.
1470  */
saveModifiedPlaylistModels()1471 void Kid3Application::saveModifiedPlaylistModels()
1472 {
1473   for (auto it = m_playlistModels.begin(); it != m_playlistModels.end(); ++it) { // clazy:exclude=detaching-member
1474     if ((*it)->isModified()) {
1475       (*it)->save();
1476     }
1477   }
1478 }
1479 
1480 /**
1481  * Set track data with tagged files of directory.
1482  *
1483  * @param tagVersion tag version
1484  * @param trackDataList is filled with track data
1485  */
filesToTrackData(Frame::TagVersion tagVersion,ImportTrackDataVector & trackDataList)1486 void Kid3Application::filesToTrackData(Frame::TagVersion tagVersion,
1487                                        ImportTrackDataVector& trackDataList)
1488 {
1489   TaggedFileOfDirectoryIterator it(currentOrRootIndex());
1490   while (it.hasNext()) {
1491     TaggedFile* taggedFile = it.next();
1492     taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
1493     trackDataList.push_back(ImportTrackData(*taggedFile, tagVersion));
1494   }
1495 }
1496 
1497 /**
1498  * Set track data model with tagged files of directory.
1499  *
1500  * @param tagVersion tag version
1501  */
filesToTrackDataModel(Frame::TagVersion tagVersion)1502 void Kid3Application::filesToTrackDataModel(Frame::TagVersion tagVersion)
1503 {
1504   ImportTrackDataVector trackDataList;
1505   filesToTrackData(tagVersion, trackDataList);
1506   getTrackDataModel()->setTrackData(trackDataList);
1507 }
1508 
1509 /**
1510  * Set tagged files of directory from track data model.
1511  *
1512  * @param tagVersion tags to set
1513  */
trackDataModelToFiles(Frame::TagVersion tagVersion)1514 void Kid3Application::trackDataModelToFiles(Frame::TagVersion tagVersion)
1515 {
1516   ImportTrackDataVector trackDataList(getTrackDataModel()->getTrackData());
1517   auto it = trackDataList.begin();
1518   FrameFilter flt;
1519   Frame::TagNumber fltTagNr = Frame::tagNumberFromMask(tagVersion);
1520   if (fltTagNr < Frame::Tag_NumValues) {
1521     flt = frameModel(fltTagNr)->getEnabledFrameFilter(true);
1522   }
1523   TaggedFileOfDirectoryIterator tfit(currentOrRootIndex());
1524   while (tfit.hasNext()) {
1525     TaggedFile* taggedFile = tfit.next();
1526     taggedFile->readTags(false);
1527     if (it != trackDataList.end()) {
1528       it->removeDisabledFrames(flt);
1529       formatFramesIfEnabled(*it);
1530       FOR_TAGS_IN_MASK(tagNr, tagVersion) {
1531         if (tagNr == Frame::Tag_Id3v1) {
1532           taggedFile->setFrames(tagNr, *it, false);
1533         } else {
1534           FrameCollection oldFrames;
1535           taggedFile->getAllFrames(tagNr, oldFrames);
1536           it->markChangedFrames(oldFrames);
1537           taggedFile->setFrames(tagNr, *it, true);
1538         }
1539       }
1540       ++it;
1541     } else {
1542       break;
1543     }
1544   }
1545 
1546   if ((tagVersion & (1 << Frame::Tag_Picture)) &&
1547       flt.isEnabled(Frame::FT_Picture) &&
1548       !trackDataList.getCoverArtUrl().isEmpty()) {
1549     downloadImage(trackDataList.getCoverArtUrl(), ImageForImportTrackData);
1550   }
1551 
1552   if (getFileSelectionModel()->hasSelection()) {
1553     emit selectedFilesUpdated();
1554   }
1555 }
1556 
1557 /**
1558  * Download an image file.
1559  *
1560  * @param url  URL of image
1561  * @param dest specifies affected files
1562  */
downloadImage(const QUrl & url,DownloadImageDestination dest)1563 void Kid3Application::downloadImage(const QUrl& url, DownloadImageDestination dest)
1564 {
1565   QUrl imgurl(DownloadClient::getImageUrl(url));
1566   if (!imgurl.isEmpty()) {
1567     m_downloadImageDest = dest;
1568     m_downloadClient->startDownload(imgurl);
1569   }
1570 }
1571 
1572 /**
1573  * Download an image file.
1574  *
1575  * @param url URL of image
1576  * @param allFilesInDir true to add the image to all files in the directory
1577  */
downloadImage(const QString & url,bool allFilesInDir)1578 void Kid3Application::downloadImage(const QString& url, bool allFilesInDir)
1579 {
1580   QUrl imgurl(url);
1581   downloadImage(imgurl, allFilesInDir
1582                 ? ImageForAllFilesInDirectory : ImageForSelectedFiles);
1583 }
1584 
1585 /**
1586  * Perform a batch import for the selected directories.
1587  * @param profile batch import profile
1588  * @param tagVersion import destination tag versions
1589  */
batchImport(const BatchImportProfile & profile,Frame::TagVersion tagVersion)1590 void Kid3Application::batchImport(const BatchImportProfile& profile,
1591                                   Frame::TagVersion tagVersion)
1592 {
1593   m_batchImportProfile = &profile;
1594   m_batchImportTagVersion = tagVersion;
1595   m_batchImportAlbums.clear();
1596   m_batchImportTrackDataList.clear();
1597   m_lastProcessedDirName.clear();
1598   m_batchImporter->clearAborted();
1599   m_batchImporter->emitReportImportEvent(BatchImporter::ReadingDirectory,
1600                                          QString());
1601   // If no directories are selected, process files of the current directory.
1602   QList<QPersistentModelIndex> indexes;
1603   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
1604   for (const QModelIndex& index : selectedIndexes) {
1605     if (m_fileProxyModel->isDir(index)) {
1606       indexes.append(index);
1607     }
1608   }
1609   if (indexes.isEmpty()) {
1610     indexes.append(m_fileProxyModelRootIndex);
1611   }
1612 
1613   connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
1614           this, &Kid3Application::batchImportNextFile);
1615   m_fileProxyModelIterator->start(indexes);
1616 }
1617 
1618 /**
1619  * Perform a batch import for the selected directories.
1620  * @param profileName batch import profile name
1621  * @param tagVersion import destination tag versions
1622  * @return true if profile with @a profileName found.
1623  */
batchImport(const QString & profileName,Frame::TagVersion tagVersion)1624 bool Kid3Application::batchImport(const QString& profileName,
1625                                   Frame::TagVersion tagVersion)
1626 {
1627   if (!m_namedBatchImportProfile) {
1628     m_namedBatchImportProfile.reset(new BatchImportProfile);
1629   }
1630   if (BatchImportConfig::instance().getProfileByName(
1631         profileName, *m_namedBatchImportProfile)) {
1632     batchImport(*m_namedBatchImportProfile, tagVersion);
1633     return true;
1634   }
1635   return false;
1636 }
1637 
1638 /**
1639  * Apply single file to batch import.
1640  *
1641  * @param index index of file in file proxy model
1642  */
batchImportNextFile(const QPersistentModelIndex & index)1643 void Kid3Application::batchImportNextFile(const QPersistentModelIndex& index)
1644 {
1645   bool terminated = !index.isValid();
1646   if (!terminated) {
1647     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
1648       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
1649       if (taggedFile->getDirname() != m_lastProcessedDirName) {
1650         m_lastProcessedDirName = taggedFile->getDirname();
1651         if (!m_batchImportTrackDataList.isEmpty()) {
1652           m_batchImportAlbums.append(m_batchImportTrackDataList);
1653         }
1654         m_batchImportTrackDataList.clear();
1655         if (m_batchImporter->isAborted()) {
1656           terminated = true;
1657         }
1658       }
1659       m_batchImportTrackDataList.append(ImportTrackData(*taggedFile,
1660                                                       m_batchImportTagVersion));
1661     }
1662   }
1663   if (terminated) {
1664     m_fileProxyModelIterator->abort();
1665     disconnect(m_fileProxyModelIterator,
1666                &FileProxyModelIterator::nextReady,
1667                this, &Kid3Application::batchImportNextFile);
1668     if (!m_batchImporter->isAborted()) {
1669       if (!m_batchImportTrackDataList.isEmpty()) {
1670         m_batchImportAlbums.append(m_batchImportTrackDataList);
1671       }
1672       Frame::TagNumber fltTagNr =
1673           Frame::tagNumberFromMask(m_batchImportTagVersion);
1674       if (fltTagNr < Frame::Tag_NumValues) {
1675         m_batchImporter->setFrameFilter(
1676               frameModel(fltTagNr)->getEnabledFrameFilter(true));
1677       }
1678       m_batchImporter->start(m_batchImportAlbums, *m_batchImportProfile,
1679                              m_batchImportTagVersion);
1680     }
1681   }
1682 }
1683 
1684 /**
1685  * Format frames if format while editing is switched on.
1686  *
1687  * @param frames frames
1688  */
formatFramesIfEnabled(FrameCollection & frames) const1689 void Kid3Application::formatFramesIfEnabled(FrameCollection& frames) const
1690 {
1691   TagFormatConfig::instance().formatFramesIfEnabled(frames);
1692 }
1693 
1694 /**
1695  * Get name of selected file.
1696  *
1697  * @return absolute file name, ends with "/" if it is a directory.
1698  */
getFileNameOfSelectedFile()1699 QString Kid3Application::getFileNameOfSelectedFile()
1700 {
1701   QModelIndex index = getFileSelectionModel()->currentIndex();
1702   QString dirname = FileProxyModel::getPathIfIndexOfDir(index);
1703   if (!dirname.isNull()) {
1704     if (!dirname.endsWith(QLatin1Char('/'))) dirname += QLatin1Char('/');
1705     return dirname;
1706   } else if (TaggedFile* taggedFile =
1707              FileProxyModel::getTaggedFileOfIndex(index)) {
1708     return taggedFile->getAbsFilename();
1709   }
1710   return QLatin1String("");
1711 }
1712 
1713 /**
1714  * Set name of selected file.
1715  * Exactly one file has to be selected.
1716  *
1717  * @param name file name.
1718  */
setFileNameOfSelectedFile(const QString & name)1719 void Kid3Application::setFileNameOfSelectedFile(const QString& name)
1720 {
1721   if (TaggedFile* taggedFile = getSelectedFile()) {
1722     QFileInfo fi(name);
1723     taggedFile->setFilename(fi.fileName());
1724     emit selectedFilesUpdated();
1725   }
1726 }
1727 
1728 /**
1729  * Apply filename format.
1730  */
applyFilenameFormat()1731 void Kid3Application::applyFilenameFormat()
1732 {
1733   emit fileSelectionUpdateRequested();
1734   SelectedTaggedFileIterator it(getRootIndex(),
1735                                 getFileSelectionModel(),
1736                                 true);
1737   while (it.hasNext()) {
1738     TaggedFile* taggedFile = it.next();
1739     taggedFile->readTags(false);
1740     QString fn = taggedFile->getFilename();
1741     FilenameFormatConfig::instance().formatString(fn);
1742     taggedFile->setFilename(fn);
1743   }
1744   emit selectedFilesUpdated();
1745 }
1746 
1747 /**
1748  * Apply tag format.
1749  */
applyTagFormat()1750 void Kid3Application::applyTagFormat()
1751 {
1752   emit fileSelectionUpdateRequested();
1753   FrameCollection frames;
1754   FrameFilter flt[Frame::Tag_NumValues];
1755   FOR_ALL_TAGS(tagNr) {
1756     flt[tagNr] = frameModel(tagNr)->getEnabledFrameFilter(true);
1757   }
1758   SelectedTaggedFileIterator it(getRootIndex(),
1759                                 getFileSelectionModel(),
1760                                 true);
1761   while (it.hasNext()) {
1762     TaggedFile* taggedFile = it.next();
1763     taggedFile->readTags(false);
1764     FOR_ALL_TAGS(tagNr) {
1765       taggedFile->getAllFrames(tagNr, frames);
1766       frames.removeDisabledFrames(flt[tagNr]);
1767       TagFormatConfig::instance().formatFrames(frames);
1768       taggedFile->setFrames(tagNr, frames);
1769     }
1770   }
1771   emit selectedFilesUpdated();
1772 }
1773 
1774 /**
1775  * Apply text encoding.
1776  * Set the text encoding selected in the settings Tags/ID3v2/Text encoding
1777  * for all selected files which have an ID3v2 tag.
1778  */
applyTextEncoding()1779 void Kid3Application::applyTextEncoding()
1780 {
1781   emit fileSelectionUpdateRequested();
1782   Frame::TextEncoding encoding = frameTextEncodingFromConfig();
1783   FrameCollection frames;
1784   SelectedTaggedFileIterator it(getRootIndex(),
1785                                 getFileSelectionModel(),
1786                                 true);
1787   while (it.hasNext()) {
1788     TaggedFile* taggedFile = it.next();
1789     taggedFile->readTags(false);
1790     taggedFile->getAllFrames(Frame::Tag_Id3v2, frames);
1791     for (auto frameIt = frames.begin(); frameIt != frames.end(); ++frameIt) {
1792       auto& frame = const_cast<Frame&>(*frameIt);
1793       Frame::TextEncoding enc = encoding;
1794       if (taggedFile->getTagFormat(Frame::Tag_Id3v2) == QLatin1String("ID3v2.3.0")) {
1795         // TagLib sets the ID3v2.3.0 frame containing the date internally with
1796         // ISO-8859-1, so the encoding cannot be set for such frames.
1797         if (taggedFile->taggedFileKey() == QLatin1String("TaglibMetadata") &&
1798             frame.getType() == Frame::FT_Date &&
1799             enc != Frame::TE_ISO8859_1)
1800           continue;
1801         // Only ISO-8859-1 and UTF16 are allowed for ID3v2.3.0.
1802         if (enc != Frame::TE_ISO8859_1)
1803           enc = Frame::TE_UTF16;
1804       }
1805       Frame::FieldList& fields = frame.fieldList();
1806       for (auto fieldIt = fields.begin(); fieldIt != fields.end(); ++fieldIt) {
1807         if (fieldIt->m_id == Frame::ID_TextEnc &&
1808             fieldIt->m_value.toInt() != enc) {
1809           fieldIt->m_value = enc;
1810           frame.setValueChanged();
1811         }
1812       }
1813     }
1814     taggedFile->setFrames(Frame::Tag_Id3v2, frames);
1815   }
1816   emit selectedFilesUpdated();
1817 }
1818 
1819 /**
1820  * Copy tags into copy buffer.
1821  *
1822  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1823  */
copyTags(Frame::TagVersion tagMask)1824 void Kid3Application::copyTags(Frame::TagVersion tagMask)
1825 {
1826   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
1827   if (tagNr >= Frame::Tag_NumValues)
1828     return;
1829 
1830   emit fileSelectionUpdateRequested();
1831   m_copyTags = frameModel(tagNr)->frames().copyEnabledFrames(
1832     frameModel(tagNr)->getEnabledFrameFilter(true));
1833 }
1834 
1835 /**
1836  * Paste from copy buffer to tags.
1837  *
1838  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1839  */
pasteTags(Frame::TagVersion tagMask)1840 void Kid3Application::pasteTags(Frame::TagVersion tagMask)
1841 {
1842   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
1843   if (tagNr >= Frame::Tag_NumValues)
1844     return;
1845 
1846   emit fileSelectionUpdateRequested();
1847   FrameCollection frames(m_copyTags.copyEnabledFrames(
1848                          frameModel(tagNr)->getEnabledFrameFilter(true)));
1849   formatFramesIfEnabled(frames);
1850   SelectedTaggedFileIterator it(getRootIndex(),
1851                                 getFileSelectionModel(),
1852                                 false);
1853   while (it.hasNext()) {
1854     it.next()->setFrames(tagNr, frames, false);
1855   }
1856   emit selectedFilesUpdated();
1857 }
1858 
1859 /**
1860  * Set tag from other tag.
1861  *
1862  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1863  */
copyToOtherTag(Frame::TagVersion tagMask)1864 void Kid3Application::copyToOtherTag(Frame::TagVersion tagMask)
1865 {
1866   Frame::TagNumber dstTagNr = Frame::tagNumberFromMask(tagMask);
1867   if (dstTagNr >= Frame::Tag_NumValues)
1868     return;
1869 
1870   Frame::TagNumber srcTagNr = dstTagNr == Frame::Tag_2
1871       ? Frame::Tag_1 : Frame::Tag_2;
1872   copyTag(srcTagNr, dstTagNr);
1873 }
1874 
1875 /**
1876  * Copy from a tag to another tag.
1877  * @param srcTagNr source tag number
1878  * @param dstTagNr destination tag number
1879  */
copyTag(Frame::TagNumber srcTagNr,Frame::TagNumber dstTagNr)1880 void Kid3Application::copyTag(Frame::TagNumber srcTagNr, Frame::TagNumber dstTagNr)
1881 {
1882   emit fileSelectionUpdateRequested();
1883   FrameCollection frames;
1884   FrameFilter flt(frameModel(dstTagNr)->getEnabledFrameFilter(true));
1885   SelectedTaggedFileIterator it(getRootIndex(),
1886                                 getFileSelectionModel(),
1887                                 false);
1888   while (it.hasNext()) {
1889     TaggedFile* taggedFile = it.next();
1890     taggedFile->getAllFrames(srcTagNr, frames);
1891     frames.removeDisabledFrames(flt);
1892     frames.setIndexesInvalid();
1893     formatFramesIfEnabled(frames);
1894     taggedFile->setFrames(dstTagNr, frames, false);
1895   }
1896   emit selectedFilesUpdated();
1897 }
1898 
1899 /**
1900  * Remove tags in selected files.
1901  *
1902  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1903  */
removeTags(Frame::TagVersion tagMask)1904 void Kid3Application::removeTags(Frame::TagVersion tagMask)
1905 {
1906   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
1907   if (tagNr >= Frame::Tag_NumValues)
1908     return;
1909 
1910   emit fileSelectionUpdateRequested();
1911   FrameFilter flt(frameModel(tagNr)->getEnabledFrameFilter(true));
1912   SelectedTaggedFileIterator it(getRootIndex(),
1913                                 getFileSelectionModel(),
1914                                 false);
1915   while (it.hasNext()) {
1916     it.next()->deleteFrames(tagNr, flt);
1917   }
1918   emit selectedFilesUpdated();
1919 }
1920 
1921 /**
1922  * Set tags according to filename.
1923  *
1924  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1925  */
getTagsFromFilename(Frame::TagVersion tagMask)1926 void Kid3Application::getTagsFromFilename(Frame::TagVersion tagMask)
1927 {
1928   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
1929   if (tagNr >= Frame::Tag_NumValues)
1930     return;
1931 
1932   emit fileSelectionUpdateRequested();
1933   FrameCollection frames;
1934   QItemSelectionModel* selectModel = getFileSelectionModel();
1935   SelectedTaggedFileIterator it(getRootIndex(),
1936                                 selectModel,
1937                                 false);
1938   FrameFilter flt(frameModel(tagNr)->getEnabledFrameFilter(true));
1939   while (it.hasNext()) {
1940     TaggedFile* taggedFile = it.next();
1941     taggedFile->getAllFrames(tagNr, frames);
1942     taggedFile->getTagsFromFilename(
1943           frames, FileConfig::instance().fromFilenameFormat());
1944     frames.removeDisabledFrames(flt);
1945     formatFramesIfEnabled(frames);
1946     taggedFile->setFrames(tagNr, frames);
1947   }
1948   emit selectedFilesUpdated();
1949 }
1950 
1951 /**
1952  * Set filename according to tags.
1953  * If a single file is selected the tags in the GUI controls
1954  * are used, else the tags in the multiple selected files.
1955  *
1956  * @param tagVersion tag version
1957  */
getFilenameFromTags(Frame::TagVersion tagVersion)1958 void Kid3Application::getFilenameFromTags(Frame::TagVersion tagVersion)
1959 {
1960   emit fileSelectionUpdateRequested();
1961   QItemSelectionModel* selectModel = getFileSelectionModel();
1962   SelectedTaggedFileIterator it(getRootIndex(),
1963                                 selectModel,
1964                                 false);
1965   while (it.hasNext()) {
1966     TaggedFile* taggedFile = it.next();
1967     TrackData trackData(*taggedFile, tagVersion);
1968     if (!trackData.isEmptyOrInactive()) {
1969       taggedFile->setFilenameFormattedIfEnabled(
1970         trackData.formatFilenameFromTags(FileConfig::instance().toFilenameFormat()));
1971     }
1972   }
1973   emit selectedFilesUpdated();
1974 }
1975 
1976 /**
1977  * Get the selected file.
1978  *
1979  * @return the selected file,
1980  *         0 if not exactly one file is selected
1981  */
getSelectedFile()1982 TaggedFile* Kid3Application::getSelectedFile()
1983 {
1984   QModelIndexList selItems(
1985       m_fileSelectionModel->selectedRows());
1986   if (selItems.size() != 1)
1987     return nullptr;
1988 
1989   return FileProxyModel::getTaggedFileOfIndex(selItems.first());
1990 }
1991 
1992 /**
1993  * Update the stored current selection with the list of all selected items.
1994  */
updateCurrentSelection()1995 void Kid3Application::updateCurrentSelection()
1996 {
1997   m_currentSelection.clear();
1998   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
1999   for (const QModelIndex& index : selectedIndexes) {
2000     m_currentSelection.append(QPersistentModelIndex(index));
2001   }
2002 }
2003 
2004 /**
2005  * Edit selected frame.
2006  * @param tagNr tag number
2007  */
editFrame(Frame::TagNumber tagNr)2008 void Kid3Application::editFrame(Frame::TagNumber tagNr)
2009 {
2010   FrameList* framelist = m_framelist[tagNr];
2011   emit fileSelectionUpdateRequested();
2012   m_editFrameTaggedFile = getSelectedFile();
2013   if (const Frame* selectedFrame = frameModel(tagNr)->getFrameOfIndex(
2014         getFramesSelectionModel(tagNr)->currentIndex())) {
2015     if (m_editFrameTaggedFile) {
2016       framelist->setTaggedFile(m_editFrameTaggedFile);
2017       framelist->setFrame(*selectedFrame);
2018       if (selectedFrame->getIndex() != -1) {
2019         framelist->editFrame();
2020       } else {
2021         // Edit a frame which does not exist, switch to add mode.
2022         m_addFrameTaggedFile = m_editFrameTaggedFile;
2023         m_editFrameTaggedFile = nullptr;
2024         framelist->addAndEditFrame();
2025       }
2026     } else {
2027       // multiple files selected
2028       // Get the first selected file by using a temporary iterator.
2029       TaggedFile* firstFile = SelectedTaggedFileIterator(
2030             getRootIndex(), getFileSelectionModel(), false).peekNext();
2031       if (firstFile) {
2032         framelist->setTaggedFile(firstFile);
2033         m_editFrameName = framelist->getSelectedName();
2034         if (!m_editFrameName.isEmpty()) {
2035           framelist->setFrame(*selectedFrame);
2036           framelist->addFrameFieldList();
2037           framelist->editFrame();
2038         }
2039       }
2040     }
2041   }
2042 }
2043 
2044 /**
2045  * Called when a frame is edited.
2046  * @param frame edited frame, 0 if canceled
2047  */
onFrameEdited(const Frame * frame)2048 void Kid3Application::onFrameEdited(const Frame* frame)
2049 {
2050   auto framelist = qobject_cast<FrameList*>(sender());
2051   if (!framelist || !frame)
2052     return;
2053 
2054   Frame::TagNumber tagNr = framelist->tagNumber();
2055   if (m_editFrameTaggedFile) {
2056     emit frameModified(m_editFrameTaggedFile, tagNr);
2057   } else {
2058     framelist->setFrame(*frame);
2059 
2060     // Start a new iteration because the file selection model can be
2061     // changed by editFrameOfTaggedFile(), e.g. when a file is exported
2062     // from a picture frame.
2063     SelectedTaggedFileIterator tfit(getRootIndex(),
2064                                     getFileSelectionModel(),
2065                                     false);
2066     while (tfit.hasNext()) {
2067       TaggedFile* currentFile = tfit.next();
2068       FrameCollection frames;
2069       currentFile->getAllFrames(tagNr, frames);
2070       for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
2071         if (it->getName() == m_editFrameName) {
2072           currentFile->deleteFrame(tagNr, *it);
2073           break;
2074         }
2075       }
2076       framelist->setTaggedFile(currentFile);
2077       framelist->pasteFrame();
2078     }
2079     emit selectedFilesUpdated();
2080     framelist->selectByName(frame->getName());
2081   }
2082 }
2083 
2084 /**
2085  * Delete selected frame.
2086  * @param tagNr tag number
2087  * @param frameName name of frame to delete, empty to delete selected frame
2088  * @param index 0 for first frame with @a frameName, 1 for second, etc.
2089  */
deleteFrame(Frame::TagNumber tagNr,const QString & frameName,int index)2090 void Kid3Application::deleteFrame(Frame::TagNumber tagNr,
2091                                   const QString& frameName, int index)
2092 {
2093   FrameList* framelist = m_framelist[tagNr];
2094   emit fileSelectionUpdateRequested();
2095   TaggedFile* taggedFile = getSelectedFile();
2096   if (taggedFile && frameName.isEmpty()) {
2097     // delete selected frame from single file
2098     if (!framelist->deleteFrame()) {
2099       // frame not found
2100       return;
2101     }
2102     emit frameModified(taggedFile, tagNr);
2103   } else {
2104     // multiple files selected or frame name specified
2105     bool firstFile = true;
2106     QString name;
2107     SelectedTaggedFileIterator tfit(getRootIndex(),
2108                                     getFileSelectionModel(),
2109                                     false);
2110     while (tfit.hasNext()) {
2111       TaggedFile* currentFile = tfit.next();
2112       if (firstFile) {
2113         firstFile = false;
2114         taggedFile = currentFile;
2115         framelist->setTaggedFile(taggedFile);
2116         name = frameName.isEmpty() ? framelist->getSelectedName() : frameName;
2117       }
2118       FrameCollection frames;
2119       currentFile->getAllFrames(tagNr, frames);
2120       int currentIndex = 0;
2121       for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
2122         if (it->getName() == name) {
2123           if (currentIndex == index) {
2124             currentFile->deleteFrame(tagNr, *it);
2125             break;
2126           } else {
2127             ++currentIndex;
2128           }
2129         }
2130       }
2131     }
2132     framelist->saveCursor();
2133     emit selectedFilesUpdated();
2134     framelist->restoreCursor();
2135   }
2136 }
2137 
2138 /**
2139  * Select a frame type and add such a frame to frame list.
2140  * @param tagNr tag number
2141  * @param frame frame to add, if 0 the user has to select and edit the frame
2142  * @param edit if true and a frame is set, the user can edit the frame before
2143  * it is added
2144  */
addFrame(Frame::TagNumber tagNr,const Frame * frame,bool edit)2145 void Kid3Application::addFrame(Frame::TagNumber tagNr,
2146                                const Frame* frame, bool edit)
2147 {
2148   if (tagNr >= Frame::Tag_NumValues)
2149     return;
2150 
2151   FrameList* framelist = m_framelist[tagNr];
2152   emit fileSelectionUpdateRequested();
2153   TaggedFile* currentFile = nullptr;
2154   m_addFrameTaggedFile = getSelectedFile();
2155   if (m_addFrameTaggedFile) {
2156     currentFile = m_addFrameTaggedFile;
2157   } else {
2158     // multiple files selected
2159     SelectedTaggedFileIterator tfit(getRootIndex(),
2160                                     getFileSelectionModel(),
2161                                     false);
2162     if (tfit.hasNext()) {
2163       currentFile = tfit.next();
2164       framelist->setTaggedFile(currentFile);
2165     }
2166   }
2167 
2168   if (currentFile) {
2169     if (edit) {
2170       if (frame) {
2171         framelist->setFrame(*frame);
2172         framelist->addAndEditFrame();
2173       } else {
2174         framelist->selectAddAndEditFrame();
2175       }
2176     } else {
2177       framelist->setFrame(*frame);
2178       onFrameAdded(framelist->pasteFrame() ? &framelist->getFrame()
2179                                            : nullptr, tagNr);
2180     }
2181   }
2182 }
2183 
2184 /**
2185  * Called when a frame is added.
2186  * @param frame edited frame, 0 if canceled
2187  * @param tagNr tag number used if slot is not invoked by framelist signal
2188  */
onFrameAdded(const Frame * frame,Frame::TagNumber tagNr)2189 void Kid3Application::onFrameAdded(const Frame* frame, Frame::TagNumber tagNr)
2190 {
2191   if (!frame)
2192     return;
2193 
2194   auto framelist = qobject_cast<FrameList*>(sender());
2195   if (!framelist) {
2196     framelist = m_framelist[tagNr];
2197   }
2198   if (m_addFrameTaggedFile) {
2199     emit frameModified(m_addFrameTaggedFile, tagNr);
2200     if (framelist->isPictureFrame()) {
2201       // update preview picture
2202       emit selectedFilesUpdated();
2203     }
2204   } else {
2205     // multiple files selected
2206     bool firstFile = true;
2207     int frameId = -1;
2208     framelist->setFrame(*frame);
2209 
2210     SelectedTaggedFileIterator tfit(getRootIndex(),
2211                                     getFileSelectionModel(),
2212                                     false);
2213     while (tfit.hasNext()) {
2214       TaggedFile* currentFile = tfit.next();
2215       if (firstFile) {
2216         firstFile = false;
2217         m_addFrameTaggedFile = currentFile;
2218         framelist->setTaggedFile(currentFile);
2219         frameId = framelist->getSelectedId();
2220       } else {
2221         framelist->setTaggedFile(currentFile);
2222         framelist->pasteFrame();
2223       }
2224     }
2225     framelist->setTaggedFile(m_addFrameTaggedFile);
2226     if (frameId != -1) {
2227       framelist->setSelectedId(frameId);
2228     }
2229     emit selectedFilesUpdated();
2230     framelist->selectByName(frame->getName());
2231   }
2232 }
2233 
2234 /**
2235  * Called by framelist when a frame is added.
2236  * Same as onFrameAdded() with default argument, provided for functor-based
2237  * connections.
2238  * @param frame added frame, 0 if canceled
2239  */
onTag2FrameAdded(const Frame * frame)2240 void Kid3Application::onTag2FrameAdded(const Frame* frame)
2241 {
2242   onFrameAdded(frame, Frame::Tag_2);
2243 }
2244 
2245 /**
2246  * Select a frame type and add such a frame to the frame list.
2247  * @param tagNr tag number
2248  */
selectAndAddFrame(Frame::TagNumber tagNr)2249 void Kid3Application::selectAndAddFrame(Frame::TagNumber tagNr)
2250 {
2251   addFrame(tagNr, nullptr, true);
2252 }
2253 
2254 /**
2255  * Edit a picture frame if one exists or add a new one.
2256  */
editOrAddPicture()2257 void Kid3Application::editOrAddPicture()
2258 {
2259   if (m_framelist[Frame::Tag_Picture]->selectByName(QLatin1String("Picture"))) {
2260     editFrame(Frame::Tag_Picture);
2261   } else {
2262     PictureFrame frame;
2263     PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
2264     addFrame(Frame::Tag_Picture, &frame, true);
2265   }
2266 }
2267 
2268 /**
2269  * Open directory or add pictures on drop.
2270  *
2271  * @param paths paths of directories or files in directory
2272  * @param isInternal true if this is an internal drop
2273  */
dropLocalFiles(const QStringList & paths,bool isInternal)2274 void Kid3Application::dropLocalFiles(const QStringList& paths, bool isInternal)
2275 {
2276   QStringList filePaths;
2277   QStringList picturePaths;
2278   for (QString txt : paths) {
2279     int lfPos = txt.indexOf(QLatin1Char('\n'));
2280     if (lfPos > 0 && lfPos < static_cast<int>(txt.length()) - 1) {
2281       txt.truncate(lfPos + 1);
2282     }
2283     QString dir = txt.trimmed();
2284     if (!dir.isEmpty()) {
2285       if (dir.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) ||
2286           dir.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive) ||
2287           dir.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)) {
2288         picturePaths.append(dir); // clazy:exclude=reserve-candidates
2289       } else {
2290         filePaths.append(dir); // clazy:exclude=reserve-candidates
2291       }
2292     }
2293   }
2294   if (!filePaths.isEmpty() && !isInternal) {
2295     resetFileFilterIfNotMatching(filePaths);
2296     emit fileSelectionUpdateRequested();
2297     emit confirmedOpenDirectoryRequested(filePaths);
2298   } else if (!picturePaths.isEmpty()) {
2299     const auto constPicturePaths = picturePaths;
2300     for (const QString& picturePath : constPicturePaths) {
2301       PictureFrame frame;
2302       if (PictureFrame::setDataFromFile(frame, picturePath)) {
2303         QString fileName(picturePath);
2304         int slashPos = fileName.lastIndexOf(QLatin1Char('/'));
2305         if (slashPos != -1) {
2306           fileName = fileName.mid(slashPos + 1);
2307         }
2308         PictureFrame::setMimeTypeFromFileName(frame, fileName);
2309         PictureFrame::setDescription(frame, fileName);
2310         PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
2311         addFrame(Frame::Tag_Picture, &frame);
2312         emit selectedFilesUpdated();
2313       }
2314     }
2315   }
2316 }
2317 
2318 /**
2319  * Open directory or add pictures on drop.
2320  *
2321  * @param paths paths of directories or files in directory
2322  */
openDrop(const QStringList & paths)2323 void Kid3Application::openDrop(const QStringList& paths)
2324 {
2325   dropLocalFiles(paths, false);
2326 }
2327 
2328 /**
2329  * Handle drop of URLs.
2330  *
2331  * @param urlList picture, tagged file and folder URLs to handle (if local)
2332  * @param isInternal true if this is an internal drop
2333  */
dropUrls(const QList<QUrl> & urlList,bool isInternal)2334 void Kid3Application::dropUrls(const QList<QUrl>& urlList, bool isInternal)
2335 {
2336   QList<QUrl> urls(urlList);
2337 #ifdef Q_OS_MAC
2338   // workaround for https://bugreports.qt-project.org/browse/QTBUG-40449
2339   for (auto it = urls.begin(); it != urls.end(); ++it) {
2340     if (it->host().isEmpty() &&
2341         it->path().startsWith(QLatin1String("/.file/id="))) {
2342       *it = QUrl::fromCFURL(CFURLCreateFilePathURL(NULL, it->toCFURL(), NULL));
2343     }
2344   }
2345 #endif
2346   if (urls.isEmpty())
2347     return;
2348   if (urls.first().isLocalFile()) {
2349     QStringList localFiles;
2350     for (auto it = urls.constBegin(); it != urls.constEnd(); ++it) {
2351       localFiles.append(it->toLocalFile());
2352     }
2353     dropLocalFiles(localFiles, isInternal);
2354   } else {
2355     dropUrl(urls.first());
2356   }
2357 }
2358 
2359 /**
2360  * Handle drop of URLs.
2361  *
2362  * @param urlList picture, tagged file and folder URLs to handle (if local)
2363  */
openDropUrls(const QList<QUrl> & urlList)2364 void Kid3Application::openDropUrls(const QList<QUrl>& urlList)
2365 {
2366   dropUrls(urlList, false);
2367 }
2368 
2369 /**
2370  * Add picture on drop.
2371  *
2372  * @param frame dropped picture frame
2373  */
dropImage(Frame * frame)2374 void Kid3Application::dropImage(Frame* frame)
2375 {
2376   PictureFrame::setTextEncoding(*frame, frameTextEncodingFromConfig());
2377   addFrame(Frame::Tag_Picture, frame);
2378   emit selectedFilesUpdated();
2379 }
2380 
2381 /**
2382  * Handle URL on drop.
2383  *
2384  * @param url dropped URL.
2385  */
dropUrl(const QUrl & url)2386 void Kid3Application::dropUrl(const QUrl& url)
2387 {
2388   downloadImage(url, Kid3Application::ImageForSelectedFiles);
2389 }
2390 
2391 /**
2392  * Add a downloaded image.
2393  *
2394  * @param data     HTTP response of download
2395  * @param mimeType MIME type of data
2396  * @param url      URL of downloaded data
2397  */
imageDownloaded(const QByteArray & data,const QString & mimeType,const QString & url)2398 void Kid3Application::imageDownloaded(const QByteArray& data,
2399                               const QString& mimeType, const QString& url)
2400 {
2401   // An empty mime type is accepted to allow downloads via FTP.
2402   if (mimeType.startsWith(QLatin1String("image")) ||
2403       mimeType.isEmpty()) {
2404     PictureFrame frame(data, url, PictureFrame::PT_CoverFront, mimeType,
2405                        frameTextEncodingFromConfig());
2406     if (getDownloadImageDestination() == ImageForAllFilesInDirectory) {
2407       TaggedFileOfDirectoryIterator it(currentOrRootIndex());
2408       while (it.hasNext()) {
2409         TaggedFile* taggedFile = it.next();
2410         taggedFile->readTags(false);
2411         taggedFile->addFrame(Frame::Tag_Picture, frame);
2412       }
2413     } else if (getDownloadImageDestination() == ImageForImportTrackData) {
2414       const ImportTrackDataVector& trackDataVector(
2415             getTrackDataModel()->trackData());
2416       for (auto it = trackDataVector.constBegin();
2417            it != trackDataVector.constEnd();
2418            ++it) {
2419         TaggedFile* taggedFile;
2420         if (it->isEnabled() && (taggedFile = it->getTaggedFile()) != nullptr) {
2421           taggedFile->readTags(false);
2422           taggedFile->addFrame(Frame::Tag_Picture, frame);
2423         }
2424       }
2425     } else {
2426       addFrame(Frame::Tag_Picture, &frame);
2427     }
2428     emit selectedFilesUpdated();
2429   }
2430 }
2431 
2432 /**
2433  * Set the first file as the current file.
2434  *
2435  * @param select true to select the file
2436  * @param onlyTaggedFiles only consider tagged files
2437  *
2438  * @return true if a file exists.
2439  */
firstFile(bool select,bool onlyTaggedFiles)2440 bool Kid3Application::firstFile(bool select, bool onlyTaggedFiles)
2441 {
2442   m_fileSelectionModel->setCurrentIndex(getRootIndex(),
2443                                         QItemSelectionModel::NoUpdate);
2444   return nextFile(select, onlyTaggedFiles);
2445 }
2446 
2447 /**
2448  * Set the next file as the current file.
2449  *
2450  * @param select true to select the file
2451  * @param onlyTaggedFiles only consider tagged files
2452  *
2453  * @return true if a next file exists.
2454  */
nextFile(bool select,bool onlyTaggedFiles)2455 bool Kid3Application::nextFile(bool select, bool onlyTaggedFiles)
2456 {
2457   QModelIndex next(m_fileSelectionModel->currentIndex()), current;
2458   do {
2459     current = next;
2460     next = QModelIndex();
2461     if (m_fileProxyModel->rowCount(current) > 0) {
2462       // to first child
2463       next = m_fileProxyModel->index(0, 0, current);
2464     } else {
2465       QModelIndex parent = current;
2466       while (!next.isValid() && parent.isValid()) {
2467         // to next sibling or next sibling of parent
2468         int row = parent.row();
2469         if (parent == getRootIndex() || !parent.isValid()) {
2470           // do not move beyond root index
2471           return false;
2472         }
2473         parent = parent.parent();
2474         if (row + 1 < m_fileProxyModel->rowCount(parent)) {
2475           // to next sibling
2476           next = m_fileProxyModel->index(row + 1, 0, parent);
2477         }
2478       }
2479     }
2480   } while (onlyTaggedFiles && !FileProxyModel::getTaggedFileOfIndex(next));
2481   if (!next.isValid())
2482     return false;
2483   m_fileSelectionModel->setCurrentIndex(next,
2484     select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows
2485            : QItemSelectionModel::Current);
2486   return true;
2487 }
2488 
2489 /**
2490  * Set the previous file as the current file.
2491  *
2492  * @param select true to select the file
2493  * @param onlyTaggedFiles only consider tagged files
2494  *
2495  * @return true if a previous file exists.
2496  */
previousFile(bool select,bool onlyTaggedFiles)2497 bool Kid3Application::previousFile(bool select, bool onlyTaggedFiles)
2498 {
2499   QModelIndex previous(m_fileSelectionModel->currentIndex()), current;
2500   do {
2501     current = previous;
2502     previous = QModelIndex();
2503     int row = current.row() - 1;
2504     if (row >= 0) {
2505       // to last leafnode of previous sibling
2506       previous = current.sibling(row, 0);
2507       row = m_fileProxyModel->rowCount(previous) - 1;
2508       while (row >= 0) {
2509         previous = m_fileProxyModel->index(row, 0, previous);
2510         row = m_fileProxyModel->rowCount(previous) - 1;
2511       }
2512     } else {
2513       // to parent
2514       previous = current.parent();
2515     }
2516     if (previous == getRootIndex() || !previous.isValid()) {
2517       return false;
2518     }
2519   } while (onlyTaggedFiles && !FileProxyModel::getTaggedFileOfIndex(previous));
2520   if (!previous.isValid())
2521     return false;
2522   m_fileSelectionModel->setCurrentIndex(previous,
2523     select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows
2524            : QItemSelectionModel::Current);
2525   return true;
2526 }
2527 
2528 /**
2529  * Select or deselect the current file.
2530  *
2531  * @param select true to select the file, false to deselect it
2532  *
2533  * @return true if a current file exists.
2534  */
selectCurrentFile(bool select)2535 bool Kid3Application::selectCurrentFile(bool select)
2536 {
2537   QModelIndex currentIdx(m_fileSelectionModel->currentIndex());
2538   if (!currentIdx.isValid() || currentIdx == getRootIndex())
2539     return false;
2540 
2541   m_fileSelectionModel->setCurrentIndex(currentIdx,
2542     (select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect) |
2543     QItemSelectionModel::Rows);
2544   return true;
2545 }
2546 
2547 /**
2548  * Select all files.
2549  */
selectAllFiles()2550 void Kid3Application::selectAllFiles()
2551 {
2552   QItemSelection selection;
2553   ModelIterator it(m_fileProxyModelRootIndex);
2554   while (it.hasNext()) {
2555     selection.append(QItemSelectionRange(it.next()));
2556   }
2557   m_fileSelectionModel->select(selection,
2558       QItemSelectionModel::Select | QItemSelectionModel::Rows);
2559 }
2560 
2561 /**
2562  * Deselect all files.
2563  */
deselectAllFiles()2564 void Kid3Application::deselectAllFiles()
2565 {
2566   m_fileSelectionModel->clearSelection();
2567 }
2568 
2569 /**
2570  * Select all files in the current directory.
2571  */
selectAllInDirectory()2572 void Kid3Application::selectAllInDirectory()
2573 {
2574   QModelIndex parent = m_fileSelectionModel->currentIndex();
2575   if (parent.isValid()) {
2576     if (!m_fileProxyModel->hasChildren(parent)) {
2577       parent = parent.parent();
2578     }
2579     QItemSelection selection;
2580     for (int row = 0; row < m_fileProxyModel->rowCount(parent); ++row) {
2581       QModelIndex index = m_fileProxyModel->index(row, 0, parent);
2582       if (!m_fileProxyModel->hasChildren(index)) {
2583         selection.append(QItemSelectionRange(index)); // clazy:exclude=reserve-candidates
2584       }
2585     }
2586     m_fileSelectionModel->select(selection,
2587                      QItemSelectionModel::Select | QItemSelectionModel::Rows);
2588   }
2589 }
2590 
2591 /**
2592  * Invert current selection.
2593  */
invertSelection()2594 void Kid3Application::invertSelection()
2595 {
2596   QModelIndexList todo;
2597   todo.append(m_fileProxyModelRootIndex);
2598   while (!todo.isEmpty()) {
2599     QModelIndex parent = todo.takeFirst();
2600     QModelIndex first, last;
2601     for (int row = 0, numRows = m_fileProxyModel->rowCount(parent);
2602          row < numRows;
2603          ++row) {
2604       QModelIndex idx = m_fileProxyModel->index(row, 0, parent);
2605       if (row == 0) {
2606         first = idx;
2607       } else if (row == numRows - 1) {
2608         last = idx;
2609       }
2610       if (m_fileProxyModel->hasChildren(idx)) {
2611         todo.append(idx);
2612       }
2613     }
2614     m_fileSelectionModel->select(
2615           QItemSelection(first, last),
2616           QItemSelectionModel::Toggle | QItemSelectionModel::Rows);
2617   }
2618 }
2619 
2620 /**
2621  * Set a specific file as the current file.
2622  *
2623  * @param filePath path to file
2624  * @param select true to select the file
2625  *
2626  * @return true if file exists.
2627  */
selectFile(const QString & filePath,bool select)2628 bool Kid3Application::selectFile(const QString& filePath, bool select)
2629 {
2630   QModelIndex index = m_fileProxyModel->index(filePath);
2631   if (!index.isValid())
2632     return false;
2633 
2634   m_fileSelectionModel->setCurrentIndex(index,
2635     select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows
2636            : QItemSelectionModel::Current);
2637   return true;
2638 }
2639 
2640 /**
2641  * Get paths to all selected files.
2642  * @param onlyTaggedFiles only consider tagged files
2643  * @return list of absolute file paths.
2644  */
getSelectedFilePaths(bool onlyTaggedFiles) const2645 QStringList Kid3Application::getSelectedFilePaths(bool onlyTaggedFiles) const
2646 {
2647   QStringList files;
2648   const QModelIndexList selItems = m_fileSelectionModel->selectedRows();
2649   if (onlyTaggedFiles) {
2650     for (const QModelIndex& index : selItems) {
2651       if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index))
2652       {
2653         files.append(taggedFile->getAbsFilename());
2654       }
2655     }
2656   } else {
2657     files.reserve(selItems.size());
2658     for (const QModelIndex& index : selItems) {
2659       files.append(m_fileProxyModel->filePath(index));
2660     }
2661   }
2662   return files;
2663 }
2664 
2665 /**
2666  * Fetch entries of directory if not already fetched.
2667  * This works like FileList::expand(), but without expanding tree view
2668  * items and independent of the GUI. The processing is done in the background
2669  * by FileSystemModel, so the fetched items are not immediately available
2670  * after calling this method.
2671  *
2672  * @param index index of directory item
2673  */
fetchDirectory(const QModelIndex & index)2674 void Kid3Application::fetchDirectory(const QModelIndex& index)
2675 {
2676   if (index.isValid() && m_fileProxyModel->canFetchMore(index)) {
2677     m_fileProxyModel->fetchMore(index);
2678   }
2679 }
2680 
2681 /**
2682  * Fetch entries of directory and toggle expanded state if GUI available.
2683  * @param index index of directory item
2684  */
expandDirectory(const QModelIndex & index)2685 void Kid3Application::expandDirectory(const QModelIndex& index)
2686 {
2687   fetchDirectory(index);
2688   emit toggleExpandedRequested(index);
2689 }
2690 
2691 /**
2692  * Expand the whole file list if GUI available.
2693  * expandFileListFinished() is emitted when finished.
2694  */
requestExpandFileList()2695 void Kid3Application::requestExpandFileList()
2696 {
2697   emit expandFileListRequested();
2698 }
2699 
2700 /**
2701  * Called when operation for requestExpandFileList() is finished.
2702  */
notifyExpandFileListFinished()2703 void Kid3Application::notifyExpandFileListFinished()
2704 {
2705   emit expandFileListFinished();
2706 }
2707 
2708 /**
2709  * Process change of selection.
2710  * The GUI is signaled to update the current selection and the controls.
2711  * @param selected selected items
2712  * @param deselected deselected items
2713  */
fileSelected(const QItemSelection & selected,const QItemSelection & deselected)2714 void Kid3Application::fileSelected(const QItemSelection& selected,
2715                                    const QItemSelection& deselected)
2716 {
2717   emit fileSelectionUpdateRequested();
2718   emit selectedFilesChanged(selected, deselected);
2719 }
2720 
2721 /**
2722  * Search in tags for a given text.
2723  * @param params search parameters
2724  */
findText(const TagSearcher::Parameters & params)2725 void Kid3Application::findText(const TagSearcher::Parameters& params)
2726 {
2727   m_tagSearcher->setModel(m_fileProxyModel);
2728   m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex);
2729   m_tagSearcher->find(params);
2730 }
2731 
2732 /**
2733  * Replace found text.
2734  * @param params search parameters
2735  */
replaceText(const TagSearcher::Parameters & params)2736 void Kid3Application::replaceText(const TagSearcher::Parameters& params)
2737 {
2738   m_tagSearcher->setModel(m_fileProxyModel);
2739   m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex);
2740   m_tagSearcher->replace(params);
2741 }
2742 
2743 /**
2744  * Replace all occurrences.
2745  * @param params search parameters
2746  */
replaceAll(const TagSearcher::Parameters & params)2747 void Kid3Application::replaceAll(const TagSearcher::Parameters& params)
2748 {
2749   m_tagSearcher->setModel(m_fileProxyModel);
2750   m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex);
2751   m_tagSearcher->replaceAll(params);
2752 }
2753 
2754 /**
2755  * Schedule actions to rename a directory.
2756  * When finished renameActionsScheduled() is emitted.
2757  */
scheduleRenameActions()2758 void Kid3Application::scheduleRenameActions()
2759 {
2760   m_dirRenamer->clearActions();
2761   m_dirRenamer->clearAborted();
2762   // If directories are selected, rename them, else process files of the
2763   // current directory.
2764   QList<QPersistentModelIndex> indexes;
2765   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
2766   for (const QModelIndex& index : selectedIndexes) {
2767     if (m_fileProxyModel->isDir(index)) {
2768       indexes.append(index);
2769     }
2770   }
2771   if (indexes.isEmpty()) {
2772     indexes.append(m_fileProxyModelRootIndex);
2773   }
2774 
2775   connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
2776           this, &Kid3Application::scheduleNextRenameAction);
2777   m_fileProxyModelIterator->start(indexes);
2778 }
2779 
2780 /**
2781  * Schedule rename action for a file.
2782  *
2783  * @param index index of file in file proxy model
2784  */
scheduleNextRenameAction(const QPersistentModelIndex & index)2785 void Kid3Application::scheduleNextRenameAction(const QPersistentModelIndex& index)
2786 {
2787   bool terminated = !index.isValid();
2788   if (!terminated) {
2789     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
2790       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
2791       m_dirRenamer->scheduleAction(taggedFile);
2792       if (m_dirRenamer->isAborted()) {
2793         terminated = true;
2794       }
2795     }
2796   }
2797   if (terminated) {
2798     m_fileProxyModelIterator->abort();
2799     disconnect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
2800                this, &Kid3Application::scheduleNextRenameAction);
2801     m_dirRenamer->endScheduleActions();
2802     emit renameActionsScheduled();
2803   }
2804 }
2805 
2806 /**
2807  * Open directory after resetting the file system model.
2808  * When finished directoryOpened() is emitted, also if false is returned.
2809  *
2810  * @param paths file or directory paths, if multiple paths are given, the
2811  * common directory is opened and the files are selected, if empty, the
2812  * currently open directory is reopened
2813  *
2814  * @return true if ok.
2815  */
openDirectoryAfterReset(const QStringList & paths)2816 bool Kid3Application::openDirectoryAfterReset(const QStringList& paths)
2817 {
2818   // Clear the selection.
2819   m_selection->beginAddTaggedFiles();
2820   m_selection->endAddTaggedFiles();
2821   QStringList dirs(paths);
2822   if (dirs.isEmpty()) {
2823     dirs.append(m_fileSystemModel->rootPath());
2824   }
2825   m_fileSystemModel->clear();
2826   return openDirectory(dirs);
2827 }
2828 
2829 /**
2830  * Apply file filter after the file system model has been reset.
2831  */
applyFilterAfterReset()2832 void Kid3Application::applyFilterAfterReset()
2833 {
2834   disconnect(this, &Kid3Application::directoryOpened,
2835              this, &Kid3Application::applyFilterAfterReset);
2836   proceedApplyingFilter();
2837 }
2838 
2839 /**
2840  * Apply a file filter.
2841  *
2842  * @param fileFilter filter to apply.
2843  */
applyFilter(FileFilter & fileFilter)2844 void Kid3Application::applyFilter(FileFilter& fileFilter)
2845 {
2846   m_fileFilter = &fileFilter;
2847   /*
2848    * When a lot of files are filtered out,
2849    * QSortFilterProxyModel::invalidateFilter() is extremely slow (probably
2850    * depending on the source model). In this case, I measured
2851    * 3s for 3000 files, 8s for 5000 files, 54s for 10000 files, and too long
2852    * to wait for more files. If such a case is detected, the file system model
2853    * is recreated in order to avoid calling invalidateFilter().
2854    */
2855   if (m_filterTotal - m_filterPassed > 4000) {
2856     connect(this, &Kid3Application::directoryOpened,
2857             this, &Kid3Application::applyFilterAfterReset);
2858     openDirectoryAfterReset();
2859   } else {
2860     m_fileProxyModel->disableFilteringOutIndexes();
2861     proceedApplyingFilter();
2862   }
2863 }
2864 
2865 /**
2866  * Second stage for applyFilter().
2867  */
proceedApplyingFilter()2868 void Kid3Application::proceedApplyingFilter()
2869 {
2870   const bool justClearingFilter =
2871       m_fileFilter->isEmptyFilterExpression() && isFiltered();
2872   setFiltered(false);
2873   m_fileFilter->clearAborted();
2874   m_filterPassed = 0;
2875   m_filterTotal = 0;
2876   emit fileFiltered(FileFilter::Started, QString(),
2877                     m_filterPassed, m_filterTotal);
2878 
2879   m_lastProcessedDirName.clear();
2880   if (!justClearingFilter) {
2881     connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
2882             this, &Kid3Application::filterNextFile);
2883     m_fileProxyModelIterator->start(m_fileProxyModelRootIndex);
2884   } else {
2885     emit fileFiltered(FileFilter::Finished, QString(),
2886                       m_filterPassed, m_filterTotal);
2887   }
2888 }
2889 
2890 /**
2891  * Apply single file to file filter.
2892  *
2893  * @param index index of file in file proxy model
2894  */
filterNextFile(const QPersistentModelIndex & index)2895 void Kid3Application::filterNextFile(const QPersistentModelIndex& index)
2896 {
2897   if (!m_fileFilter)
2898     return;
2899 
2900   bool terminated = !index.isValid();
2901   if (!terminated) {
2902     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
2903       bool tagInfoRead = taggedFile->isTagInformationRead();
2904       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
2905       if (taggedFile->getDirname() != m_lastProcessedDirName) {
2906         m_lastProcessedDirName = taggedFile->getDirname();
2907         emit fileFiltered(FileFilter::Directory, m_lastProcessedDirName,
2908                           m_filterPassed, m_filterTotal);
2909       }
2910       bool ok;
2911       bool pass = m_fileFilter->filter(*taggedFile, &ok);
2912       if (ok) {
2913         ++m_filterTotal;
2914         if (pass) {
2915           ++m_filterPassed;
2916         }
2917         emit fileFiltered(
2918               pass ? FileFilter::FilePassed : FileFilter::FileFilteredOut,
2919               taggedFile->getFilename(), m_filterPassed, m_filterTotal);
2920         if (!pass)
2921           m_fileProxyModel->filterOutIndex(taggedFile->getIndex());
2922       } else {
2923         emit fileFiltered(FileFilter::ParseError, QString(),
2924                           m_filterPassed, m_filterTotal);
2925         terminated = true;
2926       }
2927 
2928       // Free resources if tag was not read before filtering
2929       if (!pass && !tagInfoRead) {
2930         taggedFile->clearTags(false);
2931       }
2932 
2933       if (m_fileFilter->isAborted()) {
2934         terminated = true;
2935         emit fileFiltered(FileFilter::Aborted, QString(),
2936                           m_filterPassed, m_filterTotal);
2937       }
2938     }
2939   }
2940   if (terminated) {
2941     if (!m_fileFilter->isAborted()) {
2942       emit fileFiltered(FileFilter::Finished, QString(),
2943                         m_filterPassed, m_filterTotal);
2944     }
2945 
2946     m_fileProxyModelIterator->abort();
2947     m_fileProxyModel->applyFilteringOutIndexes();
2948     setFiltered(!m_fileFilter->isEmptyFilterExpression());
2949 
2950     disconnect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
2951                this, &Kid3Application::filterNextFile);
2952   }
2953 }
2954 
2955 /**
2956  * Apply a file filter.
2957  *
2958  * @param expression filter expression
2959  */
applyFilter(const QString & expression)2960 void Kid3Application::applyFilter(const QString& expression)
2961 {
2962   if (!m_expressionFileFilter) {
2963     m_expressionFileFilter = new FileFilter(this);
2964   }
2965   m_expressionFileFilter->clearAborted();
2966   m_expressionFileFilter->setFilterExpression(expression);
2967   m_expressionFileFilter->initParser();
2968   applyFilter(*m_expressionFileFilter);
2969 }
2970 
2971 /**
2972  * Abort expression file filter.
2973  */
abortFilter()2974 void Kid3Application::abortFilter()
2975 {
2976   if (m_expressionFileFilter) {
2977     m_expressionFileFilter->abort();
2978   }
2979 }
2980 
2981 /**
2982  * Perform rename actions and change application directory afterwards if it
2983  * was renamed.
2984  *
2985  * @return error messages, null string if no error occurred.
2986  */
performRenameActions()2987 QString Kid3Application::performRenameActions()
2988 {
2989   QString errorMsg;
2990   m_dirRenamer->setDirName(getDirName());
2991   m_dirRenamer->performActions(&errorMsg);
2992   if (m_dirRenamer->getDirName() != getDirName()) {
2993     openDirectory({m_dirRenamer->getDirName()});
2994   }
2995   return errorMsg;
2996 }
2997 
2998 /**
2999  * Reset the file system model and then try to perform the rename actions.
3000  * On Windows, renaming directories fails when they have a subdirectory which
3001  * is open in the file system model. This method can be used to retry in such
3002  * a situation.
3003  */
tryRenameActionsAfterReset()3004 void Kid3Application::tryRenameActionsAfterReset()
3005 {
3006   connect(this, &Kid3Application::directoryOpened,
3007           this, &Kid3Application::performRenameActionsAfterReset);
3008   openDirectoryAfterReset();
3009 }
3010 
3011 /**
3012  * Perform rename actions after the file system model has been reset.
3013  */
performRenameActionsAfterReset()3014 void Kid3Application::performRenameActionsAfterReset()
3015 {
3016   disconnect(this, &Kid3Application::directoryOpened,
3017              this, &Kid3Application::performRenameActionsAfterReset);
3018   performRenameActions();
3019 }
3020 
3021 /**
3022  * Reset the file system model and then try to rename a file.
3023  * On Windows, renaming directories fails when they have a subdirectory which
3024  * is open in the file system model. This method can be used to retry in such
3025  * a situation.
3026  *
3027  * @param oldName old file name
3028  * @param newName new file name
3029  */
tryRenameAfterReset(const QString & oldName,const QString & newName)3030 void Kid3Application::tryRenameAfterReset(const QString& oldName,
3031                                           const QString& newName)
3032 {
3033   m_renameAfterResetOldName = oldName;
3034   m_renameAfterResetNewName = newName;
3035   connect(this, &Kid3Application::directoryOpened,
3036           this, &Kid3Application::renameAfterReset);
3037   openDirectoryAfterReset();
3038 }
3039 
3040 /**
3041  * Rename after the file system model has been reset.
3042  */
renameAfterReset()3043 void Kid3Application::renameAfterReset()
3044 {
3045   disconnect(this, &Kid3Application::directoryOpened, this, &Kid3Application::renameAfterReset);
3046   if (!m_renameAfterResetOldName.isEmpty() &&
3047       !m_renameAfterResetNewName.isEmpty()) {
3048     Utils::safeRename(m_renameAfterResetOldName, m_renameAfterResetNewName);
3049     m_renameAfterResetOldName.clear();
3050     m_renameAfterResetNewName.clear();
3051   }
3052 }
3053 
3054 /**
3055  * Set the directory name from the tags.
3056  * The directory must not have modified files.
3057  * renameActionsScheduled() is emitted when the rename actions have been
3058  * scheduled. Then performRenameActions() has to be called to effectively
3059  * rename the directory.
3060  *
3061  * @param tagMask tag mask
3062  * @param format  directory name format
3063  * @param create  true to create, false to rename
3064  *
3065  * @return true if ok.
3066  */
renameDirectory(Frame::TagVersion tagMask,const QString & format,bool create)3067 bool Kid3Application::renameDirectory(Frame::TagVersion tagMask,
3068                                      const QString& format, bool create)
3069 {
3070   TaggedFile* taggedFile =
3071     TaggedFileOfDirectoryIterator::first(currentOrRootIndex());
3072   if (!isModified() && taggedFile) {
3073     m_dirRenamer->setTagVersion(tagMask);
3074     m_dirRenamer->setFormat(format);
3075     m_dirRenamer->setAction(create);
3076     scheduleRenameActions();
3077     return true;
3078   }
3079   return false;
3080 }
3081 
3082 /**
3083  * Check modification state.
3084  *
3085  * @return true if a file is modified.
3086  */
isModified() const3087 bool Kid3Application::isModified() const
3088 {
3089   return m_fileProxyModel->isModified();
3090 }
3091 
3092 /**
3093  * Number tracks in selected files of directory.
3094  *
3095  * @param nr start number
3096  * @param total total number of tracks, used if >0
3097  * @param tagVersion determines on which tags the numbers are set
3098  * @param options options for numbering operation
3099  */
numberTracks(int nr,int total,Frame::TagVersion tagVersion,NumberTrackOptions options)3100 void Kid3Application::numberTracks(int nr, int total,
3101                                    Frame::TagVersion tagVersion,
3102                                    NumberTrackOptions options)
3103 {
3104   QString lastDirName;
3105   bool totalEnabled = TagConfig::instance().enableTotalNumberOfTracks();
3106   bool directoryMode = true;
3107   int startNr = nr;
3108   emit fileSelectionUpdateRequested();
3109   int numDigits = TagConfig::instance().trackNumberDigits();
3110   if (numDigits < 1 || numDigits > 5)
3111     numDigits = 1;
3112 
3113   // If directories are selected, number their files, else process the selected
3114   // files of the current directory.
3115   AbstractTaggedFileIterator* it =
3116       new TaggedFileOfSelectedDirectoriesIterator(getFileSelectionModel());
3117   if (!it->hasNext()) {
3118     delete it;
3119     it = new SelectedTaggedFileOfDirectoryIterator(
3120                currentOrRootIndex(),
3121                getFileSelectionModel(),
3122                true);
3123     directoryMode = false;
3124   }
3125   while (it->hasNext()) {
3126     TaggedFile* taggedFile = it->next();
3127     taggedFile->readTags(false);
3128     if (options & NumberTracksResetCounterForEachDirectory) {
3129       QString dirName = taggedFile->getDirname();
3130       if (lastDirName != dirName) {
3131         nr = startNr;
3132         if (totalEnabled && directoryMode) {
3133           total = taggedFile->getTotalNumberOfTracksInDir();
3134         }
3135         lastDirName = dirName;
3136       }
3137     }
3138     FOR_TAGS_IN_MASK(tagNr, tagVersion) {
3139       if (tagNr == Frame::Tag_Id3v1) {
3140         if (options & NumberTracksEnabled) {
3141           QString value;
3142           value.setNum(nr);
3143           Frame frame;
3144           if (taggedFile->getFrame(tagNr, Frame::FT_Track, frame)) {
3145             frame.setValueIfChanged(value);
3146             if (frame.isValueChanged()) {
3147               taggedFile->setFrame(tagNr, frame);
3148             }
3149           } else {
3150             frame.setValue(value);
3151             frame.setExtendedType(Frame::ExtendedType(Frame::FT_Track));
3152             taggedFile->setFrame(tagNr, frame);
3153           }
3154         }
3155       } else {
3156         // For tag 2 the frame is written, so that we have control over the
3157         // format and the total number of tracks, and it is possible to change
3158         // the format even if the numbers stay the same.
3159         FrameCollection frames;
3160         taggedFile->getAllFrames(tagNr, frames);
3161         Frame frame(Frame::FT_Track, QLatin1String(""), QLatin1String(""), -1);
3162         auto frameIt = frames.find(frame);
3163         QString value;
3164         if (options & NumberTracksEnabled) {
3165           if (total > 0) {
3166             value = QString(QLatin1String("%1/%2"))
3167                 .arg(nr, numDigits, 10, QLatin1Char('0'))
3168                 .arg(total, numDigits, 10, QLatin1Char('0'));
3169           } else {
3170             value = QString(QLatin1String("%1"))
3171                 .arg(nr, numDigits, 10, QLatin1Char('0'));
3172           }
3173           if (frameIt != frames.end()) {
3174             frame = *frameIt;
3175             frame.setValueIfChanged(value);
3176             if (frame.isValueChanged()) {
3177               taggedFile->setFrame(tagNr, frame);
3178             }
3179           } else {
3180             frame.setValue(value);
3181             frame.setExtendedType(Frame::ExtendedType(Frame::FT_Track));
3182             taggedFile->setFrame(tagNr, frame);
3183           }
3184         } else {
3185           // If track numbering is not enabled, just reformat the current value.
3186           if (frameIt != frames.end()) {
3187             frame = *frameIt;
3188             int currentTotal;
3189             int currentNr = TaggedFile::splitNumberAndTotal(frame.getValue(),
3190                                                             &currentTotal);
3191             // Set the total if enabled.
3192             if (totalEnabled && total > 0) {
3193               currentTotal = total;
3194             }
3195             if (currentTotal > 0) {
3196               value = QString(QLatin1String("%1/%2"))
3197                   .arg(currentNr, numDigits, 10, QLatin1Char('0'))
3198                   .arg(currentTotal, numDigits, 10, QLatin1Char('0'));
3199             } else {
3200               value = QString(QLatin1String("%1"))
3201                   .arg(currentNr, numDigits, 10, QLatin1Char('0'));
3202             }
3203             frame.setValueIfChanged(value);
3204             if (frame.isValueChanged()) {
3205               taggedFile->setFrame(tagNr, frame);
3206             }
3207           }
3208         }
3209       }
3210     }
3211     ++nr;
3212   }
3213   emit selectedFilesUpdated();
3214   delete it;
3215 }
3216 
3217 /**
3218  * Play audio file.
3219  */
playAudio()3220 void Kid3Application::playAudio()
3221 {
3222   QObject* player = getAudioPlayer();
3223   if (!player)
3224     return;
3225 
3226   QStringList files;
3227   int fileNr = 0;
3228   QModelIndexList selectedRows = m_fileSelectionModel->selectedRows();
3229   if (selectedRows.size() > 1) {
3230     // play only the selected files if more than one is selected
3231     SelectedTaggedFileIterator it(m_fileProxyModelRootIndex,
3232                                   m_fileSelectionModel,
3233                                   false);
3234     while (it.hasNext()) {
3235       files.append(it.next()->getAbsFilename());
3236     }
3237   } else {
3238     if (selectedRows.size() == 1) {
3239       // If a playlist file is selected, play the files in the playlist.
3240       QModelIndex index = selectedRows.first();
3241       index = index.sibling(index.row(), 0);
3242       QString path = m_fileProxyModel->filePath(index);
3243       bool isPlaylist = false;
3244       PlaylistConfig::formatFromFileExtension(path, &isPlaylist);
3245       if (isPlaylist) {
3246         files = playlistModel(path)->pathsInPlaylist();
3247       }
3248     }
3249     if (files.isEmpty()) {
3250       // play all files if none or only one is selected
3251       int idx = 0;
3252       ModelIterator it(m_fileProxyModelRootIndex);
3253       while (it.hasNext()) {
3254         QModelIndex index = it.next();
3255         if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
3256           files.append(taggedFile->getAbsFilename());
3257           if (m_fileSelectionModel->isSelected(index)) {
3258             fileNr = idx;
3259           }
3260           ++idx;
3261         }
3262       }
3263     }
3264   }
3265   emit aboutToPlayAudio();
3266   QMetaObject::invokeMethod(player, "setFiles",
3267                             Q_ARG(QStringList, files), Q_ARG(int, fileNr));
3268 }
3269 
3270 /**
3271  * Show play tool bar.
3272  */
showAudioPlayer()3273 void Kid3Application::showAudioPlayer()
3274 {
3275   emit aboutToPlayAudio();
3276 }
3277 
3278 /**
3279  * Get number of tracks in current directory.
3280  *
3281  * @return number of tracks, 0 if not found.
3282  */
getTotalNumberOfTracksInDir()3283 int Kid3Application::getTotalNumberOfTracksInDir()
3284 {
3285   if (TaggedFile* taggedFile = TaggedFileOfDirectoryIterator::first(
3286       currentOrRootIndex())) {
3287     return taggedFile->getTotalNumberOfTracksInDir();
3288   }
3289   return 0;
3290 }
3291 
3292 /**
3293  * Create a filter string for the file dialog.
3294  * The filter string contains entries for all supported types.
3295  *
3296  * @return filter string.
3297  */
createFilterString() const3298 QString Kid3Application::createFilterString() const
3299 {
3300   return m_platformTools->fileDialogNameFilter(
3301         FileProxyModel::createNameFilters());
3302 }
3303 
3304 /**
3305  * Remove the file filter if necessary to open the files.
3306  * @param filePaths paths to files or directories
3307  */
resetFileFilterIfNotMatching(const QStringList & filePaths)3308 void Kid3Application::resetFileFilterIfNotMatching(const QStringList& filePaths)
3309 {
3310   QStringList nameFilters(m_platformTools->getNameFilterPatterns(
3311               FileConfig::instance().nameFilter()).split(QLatin1Char(' ')));
3312   if (!nameFilters.isEmpty() && nameFilters.first() != QLatin1String("*")) {
3313     for (const QString& filePath : filePaths) {
3314       QFileInfo fi(filePath);
3315       if (!QDir::match(nameFilters, fi.fileName()) && !fi.isDir()) {
3316         setAllFilesFileFilter();
3317         break;
3318       }
3319     }
3320   }
3321 }
3322 
3323 /**
3324  * Set file name filter for "All Files (*)".
3325  */
setAllFilesFileFilter()3326 void Kid3Application::setAllFilesFileFilter() {
3327   FileConfig::instance().setNameFilter(
3328         m_platformTools->fileDialogNameFilter(
3329           QList<QPair<QString, QString> >()
3330           << qMakePair(tr("All Files"), QString(QLatin1Char('*')))));
3331 }
3332 
3333 /**
3334  * Notify the tagged file factories about the changed configuration.
3335  */
notifyConfigurationChange()3336 void Kid3Application::notifyConfigurationChange()
3337 {
3338   const auto factories = FileProxyModel::taggedFileFactories();
3339   for (ITaggedFileFactory* factory : factories) {
3340     const auto keys = factory->taggedFileKeys();
3341     for (const QString& key : keys) {
3342       factory->notifyConfigurationChange(key);
3343     }
3344   }
3345 }
3346 
3347 /**
3348  * Convert ID3v2.3 to ID3v2.4 tags.
3349  */
convertToId3v24()3350 void Kid3Application::convertToId3v24()
3351 {
3352   emit fileSelectionUpdateRequested();
3353   SelectedTaggedFileIterator it(getRootIndex(),
3354                                 getFileSelectionModel(),
3355                                 false);
3356   while (it.hasNext()) {
3357     TaggedFile* taggedFile = it.next();
3358     taggedFile->readTags(false);
3359     if (taggedFile->hasTag(Frame::Tag_Id3v2) && !taggedFile->isChanged()) {
3360       QString tagFmt = taggedFile->getTagFormat(Frame::Tag_Id3v2);
3361       if (tagFmt.length() >= 7 && tagFmt.startsWith(QLatin1String("ID3v2.")) &&
3362           tagFmt[6] < QLatin1Char('4')) {
3363         if ((taggedFile->taggedFileFeatures() &
3364              (TaggedFile::TF_ID3v23 | TaggedFile::TF_ID3v24)) ==
3365               TaggedFile::TF_ID3v23) {
3366           FrameCollection frames;
3367           taggedFile->getAllFrames(Frame::Tag_Id3v2, frames);
3368           FrameFilter flt;
3369           flt.enableAll();
3370           taggedFile->deleteFrames(Frame::Tag_Id3v2, flt);
3371 
3372           // The file has to be reread to write ID3v2.4 tags
3373           taggedFile = FileProxyModel::readWithId3V24(taggedFile);
3374 
3375           // Restore the frames
3376           FrameFilter frameFlt;
3377           frameFlt.enableAll();
3378           taggedFile->setFrames(Frame::Tag_Id3v2,
3379                                 frames.copyEnabledFrames(frameFlt), false);
3380         }
3381 
3382         // Write the file with ID3v2.4 tags
3383         bool renamed;
3384         int storedFeatures = taggedFile->activeTaggedFileFeatures();
3385         taggedFile->setActiveTaggedFileFeatures(TaggedFile::TF_ID3v24);
3386         taggedFile->writeTags(true, &renamed,
3387                               FileConfig::instance().preserveTime());
3388         taggedFile->setActiveTaggedFileFeatures(storedFeatures);
3389         taggedFile->readTags(true);
3390       }
3391     }
3392   }
3393   emit selectedFilesUpdated();
3394 }
3395 
3396 /**
3397  * Convert ID3v2.4 to ID3v2.3 tags.
3398  */
convertToId3v23()3399 void Kid3Application::convertToId3v23()
3400 {
3401   emit fileSelectionUpdateRequested();
3402   SelectedTaggedFileIterator it(getRootIndex(),
3403                                 getFileSelectionModel(),
3404                                 false);
3405   while (it.hasNext()) {
3406     TaggedFile* taggedFile = it.next();
3407     taggedFile->readTags(false);
3408     if (taggedFile->hasTag(Frame::Tag_Id3v2) && !taggedFile->isChanged()) {
3409       QString tagFmt = taggedFile->getTagFormat(Frame::Tag_Id3v2);
3410       QString ext = taggedFile->getFileExtension();
3411       if (tagFmt.length() >= 7 && tagFmt.startsWith(QLatin1String("ID3v2.")) &&
3412           tagFmt[6] > QLatin1Char('3') &&
3413           (ext == QLatin1String(".mp3") || ext == QLatin1String(".mp2") ||
3414            ext == QLatin1String(".aac") || ext == QLatin1String(".wav") ||
3415            ext == QLatin1String(".dsf"))) {
3416         if (!(taggedFile->taggedFileFeatures() & TaggedFile::TF_ID3v23)) {
3417           FrameCollection frames;
3418           taggedFile->getAllFrames(Frame::Tag_Id3v2, frames);
3419           FrameFilter flt;
3420           flt.enableAll();
3421           taggedFile->deleteFrames(Frame::Tag_Id3v2, flt);
3422 
3423           // The file has to be reread to write ID3v2.3 tags
3424           taggedFile = FileProxyModel::readWithId3V23(taggedFile);
3425 
3426           // Restore the frames
3427           FrameFilter frameFlt;
3428           frameFlt.enableAll();
3429           taggedFile->setFrames(Frame::Tag_Id3v2,
3430                                 frames.copyEnabledFrames(frameFlt), false);
3431         }
3432 
3433         // Write the file with ID3v2.3 tags
3434         bool renamed;
3435         int storedFeatures = taggedFile->activeTaggedFileFeatures();
3436         taggedFile->setActiveTaggedFileFeatures(TaggedFile::TF_ID3v23);
3437         taggedFile->writeTags(true, &renamed,
3438                               FileConfig::instance().preserveTime());
3439         taggedFile->setActiveTaggedFileFeatures(storedFeatures);
3440         taggedFile->readTags(true);
3441       }
3442     }
3443   }
3444   emit selectedFilesUpdated();
3445 }
3446 
3447 /**
3448  * Get value of frame.
3449  * To get binary data like a picture, the name of a file to write can be
3450  * added after the @a name, e.g. "Picture:/path/to/file".
3451  *
3452  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
3453  * @param name    name of frame (e.g. "Artist")
3454  */
getFrame(Frame::TagVersion tagMask,const QString & name) const3455 QString Kid3Application::getFrame(Frame::TagVersion tagMask,
3456                                   const QString& name) const
3457 {
3458   QString frameName(name);
3459   QString dataFileName, fieldName;
3460   int index = 0;
3461   Frame::ExtendedType explicitType;
3462   if (frameName.startsWith(QLatin1Char('!'))) {
3463     frameName.remove(0, 1);
3464     explicitType = Frame::ExtendedType(Frame::FT_Other, frameName);
3465   }
3466   extractFileFieldIndex(frameName, dataFileName, fieldName, index);
3467   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
3468   if (tagNr >= Frame::Tag_NumValues)
3469     return QString();
3470 
3471   FrameTableModel* ft = m_framesModel[tagNr];
3472   const FrameCollection& frames = ft->frames();
3473   auto it = explicitType.getType() == Frame::FT_UnknownFrame
3474       ? frames.findByName(frameName, index)
3475       : frames.findByExtendedType(explicitType, index);
3476   if (it != frames.cend()) {
3477     if (!dataFileName.isEmpty()) {
3478       bool isSylt = it->getInternalName().startsWith(QLatin1String("SYLT"));
3479       if (isSylt ||
3480           it->getInternalName().startsWith(QLatin1String("ETCO"))) {
3481         QFile file(dataFileName);
3482         if (file.open(QIODevice::WriteOnly)) {
3483           TimeEventModel timeEventModel;
3484           if (isSylt) {
3485             timeEventModel.setType(TimeEventModel::SynchronizedLyrics);
3486             timeEventModel.fromSyltFrame(it->getFieldList());
3487           } else {
3488             timeEventModel.setType(TimeEventModel::EventTimingCodes);
3489             timeEventModel.fromEtcoFrame(it->getFieldList());
3490           }
3491           QTextStream stream(&file);
3492           QString codecName = FileConfig::instance().textEncoding();
3493           if (codecName != QLatin1String("System")) {
3494 #if QT_VERSION >= 0x060000
3495             if (auto encoding = QStringConverter::encodingForName(codecName.toLatin1())) {
3496               stream.setEncoding(encoding.value());
3497             }
3498 #else
3499             stream.setCodec(codecName.toLatin1());
3500 #endif
3501           }
3502           timeEventModel.toLrcFile(stream, frames.getTitle(),
3503                                    frames.getArtist(), frames.getAlbum());
3504           file.close();
3505         }
3506       } else {
3507         PictureFrame::writeDataToFile(*it, dataFileName);
3508       }
3509     }
3510     if (!fieldName.isEmpty()) {
3511       if (fieldName == QLatin1String("selected")) {
3512         const int frameIndex = it->getIndex();
3513         const int row = frameIndex >= 0
3514             ? ft->getRowWithFrameIndex(frameIndex)
3515             : std::distance(frames.cbegin(), it);
3516         if (row != -1) {
3517           return QLatin1String(ft->index(row, FrameTableModel::CI_Enable)
3518                                .data(Qt::CheckStateRole).toInt() == Qt::Checked
3519                                ? "1" : "0");
3520         }
3521         return QString();
3522       } else {
3523         return Frame::getField(*it, fieldName).toString();
3524       }
3525     }
3526     return it->getValue();
3527   } else {
3528     return QString();
3529   }
3530 }
3531 
3532 /**
3533  * Get names and values of all frames.
3534  *
3535  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
3536  *
3537  * @return map containing frame values.
3538  */
getAllFrames(Frame::TagVersion tagMask) const3539 QVariantMap Kid3Application::getAllFrames(Frame::TagVersion tagMask) const
3540 {
3541   QVariantMap map;
3542   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
3543   if (tagNr >= Frame::Tag_NumValues)
3544     return QVariantMap();
3545 
3546   FrameTableModel* ft = m_framesModel[tagNr];
3547   const FrameCollection& frames = ft->frames();
3548   for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
3549     QString name(it->getName());
3550     int nlPos = name.indexOf(QLatin1Char('\n'));
3551     if (nlPos > 0) {
3552       // probably "TXXX - User defined text information\nDescription" or
3553       // "WXXX - User defined URL link\nDescription"
3554       name = name.mid(nlPos + 1);
3555 #if QT_VERSION >= 0x060000
3556     } else if (name.mid(4, 3) == QLatin1String(" - ")) {
3557 #else
3558     } else if (name.midRef(4, 3) == QLatin1String(" - ")) {
3559 #endif
3560       // probably "ID3-ID - Description"
3561       name = name.left(4);
3562     }
3563     map.insert(name, it->getValue());
3564   }
3565   return map;
3566 }
3567 
3568 /**
3569  * Set value of frame.
3570  * For tag 2 (@a tagMask 2), if no frame with @a name exists, a new frame
3571  * is added, if @a value is empty, the frame is deleted.
3572  * To add binary data like a picture, a file can be added after the
3573  * @a name, e.g. "Picture:/path/to/file".
3574  *
3575  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
3576  * @param name    name of frame (e.g. "Artist")
3577  * @param value   value of frame
3578  *
3579  * @return true if ok.
3580  */
setFrame(Frame::TagVersion tagMask,const QString & name,const QString & value)3581 bool Kid3Application::setFrame(Frame::TagVersion tagMask,
3582                                const QString& name, const QString& value)
3583 {
3584   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
3585   if (tagNr >= Frame::Tag_NumValues)
3586     return false;
3587 
3588   FrameTableModel* ft = m_framesModel[tagNr];
3589   if (name == QLatin1String("*.selected")) {
3590     ft->setAllCheckStates(!value.isEmpty() && value != QLatin1String("0")
3591                                            && value != QLatin1String("false"));
3592     return true;
3593   }
3594 
3595   QString frameName(name);
3596   QString dataFileName, fieldName;
3597   int index = 0;
3598   Frame::ExtendedType explicitType;
3599   if (frameName.startsWith(QLatin1Char('!'))) {
3600     frameName.remove(0, 1);
3601     explicitType = Frame::ExtendedType(Frame::FT_Other, frameName);
3602   }
3603   extractFileFieldIndex(frameName, dataFileName, fieldName, index);
3604   FrameCollection frames(ft->frames());
3605   auto it = explicitType.getType() == Frame::FT_UnknownFrame
3606       ? frames.findByName(frameName, index)
3607       : frames.findByExtendedType(explicitType, index);
3608   if (it != frames.end()) {
3609     QString frmName(it->getName());
3610     bool isPicture, isGeob, isSylt = false;
3611     if (!dataFileName.isEmpty() &&
3612         (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0 &&
3613         ((isPicture = (it->getType() == Frame::FT_Picture)) ||
3614          (isGeob = frmName.startsWith(QLatin1String("GEOB"))) ||
3615          (isSylt = frmName.startsWith(QLatin1String("SYLT"))) ||
3616          frmName.startsWith(QLatin1String("ETCO")))) {
3617       if (isPicture) {
3618         deleteFrame(tagNr, frmName, index);
3619         PictureFrame frame;
3620         PictureFrame::setDescription(frame, value);
3621         PictureFrame::setDataFromFile(frame, dataFileName);
3622         PictureFrame::setMimeTypeFromFileName(frame, dataFileName);
3623         PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
3624         addFrame(tagNr, &frame);
3625       } else if (isGeob) {
3626         Frame frame(*it);
3627         deleteFrame(tagNr, frmName, index);
3628         Frame::setField(frame, Frame::ID_MimeType,
3629                         PictureFrame::getMimeTypeForFile(dataFileName));
3630         Frame::setField(frame, Frame::ID_Filename,
3631                         QFileInfo(dataFileName).fileName());
3632         Frame::setField(frame, Frame::ID_Description, value);
3633         PictureFrame::setDataFromFile(frame, dataFileName);
3634         addFrame(tagNr, &frame);
3635       } else {
3636         QFile file(dataFileName);
3637         if (file.open(QIODevice::ReadOnly)) {
3638           QTextStream stream(&file);
3639           Frame frame(*it);
3640           Frame::setField(frame, Frame::ID_Description, value);
3641           deleteFrame(tagNr, frmName, index);
3642           TimeEventModel timeEventModel;
3643           if (isSylt) {
3644             timeEventModel.setType(TimeEventModel::SynchronizedLyrics);
3645             timeEventModel.fromLrcFile(stream);
3646             timeEventModel.toSyltFrame(frame.fieldList());
3647           } else {
3648             timeEventModel.setType(TimeEventModel::EventTimingCodes);
3649             timeEventModel.fromLrcFile(stream);
3650             timeEventModel.toEtcoFrame(frame.fieldList());
3651           }
3652           file.close();
3653           addFrame(tagNr, &frame);
3654         }
3655       }
3656     } else if (value.isEmpty() && fieldName.isEmpty() &&
3657                (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0) {
3658       deleteFrame(tagNr, frmName, index);
3659     } else {
3660       auto& frame = const_cast<Frame&>(*it);
3661       if (fieldName.isEmpty()) {
3662         frame.setValueIfChanged(value);
3663       } else {
3664         if (fieldName == QLatin1String("selected")) {
3665           const int frameIndex = frame.getIndex();
3666           const int row = frameIndex >= 0
3667               ? ft->getRowWithFrameIndex(frameIndex)
3668               : std::distance(frames.cbegin(), it);
3669           if (row != -1) {
3670             ft->setData(ft->index(row, FrameTableModel::CI_Enable),
3671                         !value.isEmpty() && value != QLatin1String("0")
3672                                          && value != QLatin1String("false")
3673                         ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
3674             return true;
3675           }
3676         } else {
3677           TaggedFile* taggedFile = getSelectedFile();
3678           if (taggedFile && Frame::setField(frame, fieldName, value)) {
3679             taggedFile->setFrame(tagNr, frame);
3680           }
3681         }
3682       }
3683       ft->transferFrames(frames);
3684       ft->selectChangedFrames();
3685       emit fileSelectionUpdateRequested();
3686       emit selectedFilesUpdated();
3687     }
3688     return true;
3689   } else if (tagMask & (Frame::TagV2 | Frame::TagV3)) {
3690     Frame frame(explicitType.getType() == Frame::FT_UnknownFrame
3691                 ? Frame::ExtendedType(frameName) : explicitType, value, -1);
3692     QString frmName(frame.getInternalName());
3693     bool isPicture, isGeob, isSylt = false;
3694     if (!dataFileName.isEmpty() &&
3695         ((isPicture = (frame.getType() == Frame::FT_Picture)) ||
3696          (isGeob = frmName.startsWith(QLatin1String("GEOB"))) ||
3697          (isSylt = frmName.startsWith(QLatin1String("SYLT"))) ||
3698          frmName.startsWith(QLatin1String("ETCO")))) {
3699       if (isPicture) {
3700         PictureFrame::setFields(frame);
3701         PictureFrame::setDescription(frame, value);
3702         PictureFrame::setDataFromFile(frame, dataFileName);
3703         PictureFrame::setMimeTypeFromFileName(frame, dataFileName);
3704         PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
3705       } else if (isGeob) {
3706         PictureFrame::setGeobFields(
3707               frame, Frame::TE_ISO8859_1,
3708               PictureFrame::getMimeTypeForFile(dataFileName),
3709               QFileInfo(dataFileName).fileName(), value);
3710         PictureFrame::setDataFromFile(frame, dataFileName);
3711       } else {
3712         QFile file(dataFileName);
3713         if (file.open(QIODevice::ReadOnly)) {
3714           Frame::Field field;
3715           Frame::FieldList& fields = frame.fieldList();
3716           fields.clear();
3717           field.m_id = Frame::ID_Description;
3718           field.m_value = value;
3719           fields.append(field);
3720           field.m_id = Frame::ID_Data;
3721 #if QT_VERSION >= 0x060000
3722           field.m_value = QVariant(QMetaType(QMetaType::QVariantList));
3723 #else
3724           field.m_value = QVariant(QVariant::List);
3725 #endif
3726           fields.append(field);
3727           QTextStream stream(&file);
3728           TimeEventModel timeEventModel;
3729           if (isSylt) {
3730             timeEventModel.setType(TimeEventModel::SynchronizedLyrics);
3731             timeEventModel.fromLrcFile(stream);
3732             timeEventModel.toSyltFrame(frame.fieldList());
3733           } else {
3734             timeEventModel.setType(TimeEventModel::EventTimingCodes);
3735             timeEventModel.fromLrcFile(stream);
3736             timeEventModel.toEtcoFrame(frame.fieldList());
3737           }
3738           file.close();
3739         }
3740       }
3741     } else if (value.isEmpty()) {
3742       // Do not add an empty frame
3743       return false;
3744     }
3745     if (!fieldName.isEmpty()) {
3746       if (TaggedFile* taggedFile = getSelectedFile()) {
3747         frame.setValue(QString());
3748         taggedFile->addFieldList(tagNr, frame);
3749         if (!Frame::setField(frame, fieldName, value)) {
3750           return false;
3751         }
3752       }
3753     }
3754     addFrame(tagNr, &frame);
3755     return true;
3756   }
3757   return false;
3758 }
3759 
3760 /**
3761  * Get data from picture frame.
3762  * @return picture data, empty if not found.
3763  */
getPictureData() const3764 QByteArray Kid3Application::getPictureData() const
3765 {
3766   QByteArray data;
3767   const FrameCollection& frames = m_framesModel[Frame::Tag_Picture]->frames();
3768   auto it = frames.findByExtendedType(
3769         Frame::ExtendedType(Frame::FT_Picture));
3770   if (it != frames.cend()) {
3771     PictureFrame::getData(*it, data);
3772   }
3773   return data;
3774 }
3775 
3776 /**
3777  * Set data in picture frame.
3778  * @param data picture data
3779  */
setPictureData(const QByteArray & data)3780 void Kid3Application::setPictureData(const QByteArray& data)
3781 {
3782   const FrameCollection& frames = m_framesModel[Frame::Tag_Picture]->frames();
3783   auto it = frames.findByExtendedType(
3784         Frame::ExtendedType(Frame::FT_Picture));
3785   PictureFrame frame;
3786   if (it != frames.cend()) {
3787     frame = PictureFrame(*it);
3788     deleteFrame(Frame::Tag_Picture, QLatin1String("Picture"));
3789   }
3790   if (!data.isEmpty()) {
3791     PictureFrame::setData(frame, data);
3792     PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
3793     addFrame(Frame::Tag_Picture, &frame);
3794   }
3795 }
3796 
3797 /**
3798  * Close the file handle of a tagged file.
3799  * @param filePath path to file
3800  */
closeFileHandle(const QString & filePath)3801 void Kid3Application::closeFileHandle(const QString& filePath)
3802 {
3803  QModelIndex index = m_fileProxyModel->index(filePath);
3804  if (index.isValid()) {
3805    if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
3806      taggedFile->closeFileHandle();
3807    }
3808  }
3809 }
3810 
3811 /**
3812  * Set a frame editor object to act as the frame editor.
3813  * @param frameEditor frame editor object, null to disable
3814  */
setFrameEditor(FrameEditorObject * frameEditor)3815 void Kid3Application::setFrameEditor(FrameEditorObject* frameEditor)
3816 {
3817   if (m_frameEditor != frameEditor) {
3818     IFrameEditor* editor;
3819     bool storeCurrentEditor = false;
3820     if (frameEditor) {
3821       if (!m_frameEditor) {
3822         storeCurrentEditor = true;
3823       }
3824       editor = frameEditor;
3825     } else {
3826       editor = m_storedFrameEditor;
3827     }
3828     FOR_ALL_TAGS(tagNr) {
3829       if (tagNr != Frame::Tag_Id3v1) {
3830         FrameList* framelist = m_framelist[tagNr];
3831         if (storeCurrentEditor) {
3832           m_storedFrameEditor = framelist->frameEditor();
3833           storeCurrentEditor = false;
3834         }
3835         framelist->setFrameEditor(editor);
3836       }
3837     }
3838     m_frameEditor = frameEditor;
3839     emit frameEditorChanged();
3840   }
3841 }
3842 
3843 /**
3844  * Remove frame editor.
3845  * Has to be called in the destructor of the frame editor to avoid a dangling
3846  * pointer to a deleted object.
3847  * @param frameEditor frame editor
3848  */
removeFrameEditor(IFrameEditor * frameEditor)3849 void Kid3Application::removeFrameEditor(IFrameEditor* frameEditor)
3850 {
3851   if (m_storedFrameEditor == frameEditor) {
3852     m_storedFrameEditor = nullptr;
3853   }
3854   if (m_frameEditor == frameEditor) {
3855     setFrameEditor(nullptr);
3856   }
3857 }
3858 
3859 /**
3860  * Get the numbers of the selected rows in a list suitable for scripting.
3861  * @return list with row numbers.
3862  */
getFileSelectionRows()3863 QVariantList Kid3Application::getFileSelectionRows()
3864 {
3865   QVariantList rows;
3866   const auto indexes = m_fileSelectionModel->selectedRows();
3867   rows.reserve(indexes.size());
3868   for (const QModelIndex& index : indexes) {
3869     rows.append(index.row());
3870   }
3871   return rows;
3872 }
3873 
3874 /**
3875  * Set the file selection from a list of model indexes.
3876  * @param indexes list of model indexes suitable for scripting
3877  */
setFileSelectionIndexes(const QVariantList & indexes)3878 void Kid3Application::setFileSelectionIndexes(const QVariantList& indexes)
3879 {
3880   QItemSelection selection;
3881   QModelIndex firstIndex;
3882   for (const QVariant& var : indexes) {
3883     QModelIndex index = var.toModelIndex();
3884     if (!firstIndex.isValid()) {
3885       firstIndex = index;
3886     }
3887     selection.select(index, index);
3888   }
3889   disconnect(m_fileSelectionModel,
3890              &QItemSelectionModel::selectionChanged,
3891              this, &Kid3Application::fileSelectionChanged);
3892   m_fileSelectionModel->select(selection,
3893                    QItemSelectionModel::Clear | QItemSelectionModel::Select |
3894                    QItemSelectionModel::Rows);
3895   if (firstIndex.isValid()) {
3896     m_fileSelectionModel->setCurrentIndex(firstIndex,
3897         QItemSelectionModel::Select | QItemSelectionModel::Rows);
3898   }
3899   connect(m_fileSelectionModel,
3900           &QItemSelectionModel::selectionChanged,
3901           this, &Kid3Application::fileSelectionChanged);
3902 }
3903 
3904 /**
3905  * Set the image provider.
3906  * @param imageProvider image provider
3907  */
setImageProvider(ImageDataProvider * imageProvider)3908 void Kid3Application::setImageProvider(ImageDataProvider* imageProvider) {
3909   m_imageProvider = imageProvider;
3910 }
3911 
3912 /**
3913  * If an image provider is used, update its picture and change the
3914  * coverArtImageId property if the picture of the selection changed.
3915  * This can be used to change a QML image.
3916  */
updateCoverArtImageId()3917 void Kid3Application::updateCoverArtImageId()
3918 {
3919   // Only perform expensive picture operations if the signal is used
3920   // (when using a QML image provider).
3921   if (m_imageProvider &&
3922       receivers(SIGNAL(coverArtImageIdChanged(QString))) > 0) {
3923     setCoverArtImageData(m_selection->getPicture());
3924   }
3925 }
3926 
3927 /**
3928  * Set picture data for image provider.
3929  * @param picture picture data
3930  */
setCoverArtImageData(const QByteArray & picture)3931 void Kid3Application::setCoverArtImageData(const QByteArray& picture)
3932 {
3933   if (picture != m_imageProvider->getImageData()) {
3934     m_imageProvider->setImageData(picture);
3935     setNextCoverArtImageId();
3936     emit coverArtImageIdChanged(m_coverArtImageId);
3937   }
3938 }
3939 
3940 /**
3941  * Set the coverArtImageId property to a new value.
3942  * This can be used to trigger an update of QML images.
3943  */
setNextCoverArtImageId()3944 void Kid3Application::setNextCoverArtImageId() {
3945   static quint32 nr = 0;
3946   m_coverArtImageId = QString(QLatin1String("image://kid3/data/%1"))
3947       .arg(nr++, 8, 16, QLatin1Char('0'));
3948 }
3949 
3950 /**
3951  * Open a file select dialog to get a file name.
3952  * For script support, is only supported when a GUI is available.
3953  * @param caption dialog caption
3954  * @param dir working directory
3955  * @param filter file type filter
3956  * @param saveFile true to open a save file dialog
3957  * @return selected file, empty if canceled.
3958  */
selectFileName(const QString & caption,const QString & dir,const QString & filter,bool saveFile)3959 QString Kid3Application::selectFileName(const QString& caption, const QString& dir,
3960                                         const QString& filter, bool saveFile)
3961 {
3962   return saveFile
3963       ? m_platformTools->getSaveFileName(nullptr, caption, dir, filter, nullptr)
3964       : m_platformTools->getOpenFileName(nullptr, caption, dir, filter, nullptr);
3965 }
3966 
3967 /**
3968  * Open a file select dialog to get a directory name.
3969  * For script support, is only supported when a GUI is available.
3970  * @param caption dialog caption
3971  * @param dir working directory
3972  * @return selected directory, empty if canceled.
3973  */
selectDirName(const QString & caption,const QString & dir)3974 QString Kid3Application::selectDirName(const QString& caption,
3975                                        const QString& dir)
3976 {
3977   return m_platformTools->getExistingDirectory(nullptr, caption, dir);
3978 }
3979