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 ¤tTotal);
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