File indexing completed on 2024-05-19 04:56:06
0001 /** 0002 * \file kid3application.cpp 0003 * Kid3 application logic, independent of GUI. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 10 Jul 2011 0008 * 0009 * Copyright (C) 2011-2024 Urs Fleisch 0010 * 0011 * This file is part of Kid3. 0012 * 0013 * Kid3 is free software; you can redistribute it and/or modify 0014 * it under the terms of the GNU General Public License as published by 0015 * the Free Software Foundation; either version 2 of the License, or 0016 * (at your option) any later version. 0017 * 0018 * Kid3 is distributed in the hope that it will be useful, 0019 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0020 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0021 * GNU General Public License for more details. 0022 * 0023 * You should have received a copy of the GNU General Public License 0024 * along with this program. If not, see <http://www.gnu.org/licenses/>. 0025 */ 0026 0027 #include "kid3application.h" 0028 #include <cerrno> 0029 #include <cstring> 0030 #if QT_VERSION >= 0x060000 0031 #include <QStringConverter> 0032 #else 0033 #include <QTextCodec> 0034 #endif 0035 #include <QTextStream> 0036 #include <QNetworkAccessManager> 0037 #include <QTimer> 0038 #include <QCoreApplication> 0039 #include <QPluginLoader> 0040 #include <QElapsedTimer> 0041 #include <QUrl> 0042 #ifdef Q_OS_MAC 0043 #include <CoreFoundation/CFURL.h> 0044 #endif 0045 #ifdef Q_OS_ANDROID 0046 #include <QStandardPaths> 0047 #endif 0048 #if defined Q_OS_LINUX && !defined Q_OS_ANDROID 0049 #include <malloc.h> 0050 #endif 0051 #ifdef HAVE_QTDBUS 0052 #include <QDBusConnection> 0053 #include <unistd.h> 0054 #include "scriptinterface.h" 0055 #endif 0056 #include "icoreplatformtools.h" 0057 #include "fileproxymodeliterator.h" 0058 #include "filefilter.h" 0059 #include "modeliterator.h" 0060 #include "trackdatamodel.h" 0061 #include "timeeventmodel.h" 0062 #include "frameobjectmodel.h" 0063 #include "playlistmodel.h" 0064 #include "imagedataprovider.h" 0065 #include "pictureframe.h" 0066 #include "textimporter.h" 0067 #include "importparser.h" 0068 #include "textexporter.h" 0069 #include "serverimporter.h" 0070 #include "saferename.h" 0071 #include "configstore.h" 0072 #include "formatconfig.h" 0073 #include "tagconfig.h" 0074 #include "fileconfig.h" 0075 #include "importconfig.h" 0076 #include "guiconfig.h" 0077 #include "playlistconfig.h" 0078 #include "isettings.h" 0079 #include "playlistcreator.h" 0080 #include "iframeeditor.h" 0081 #include "batchimportprofile.h" 0082 #include "batchimportconfig.h" 0083 #include "iserverimporterfactory.h" 0084 #include "iservertrackimporterfactory.h" 0085 #include "itaggedfilefactory.h" 0086 #include "iusercommandprocessor.h" 0087 #ifdef Q_OS_ANDROID 0088 #include "androidutils.h" 0089 #endif 0090 #include "importplugins.h" 0091 0092 namespace { 0093 0094 /** 0095 * Get the file name of the plugin from the plugin name. 0096 * @param pluginName name of the plugin 0097 * @return file name. 0098 */ 0099 QString pluginFileName(const QString& pluginName) 0100 { 0101 QString fileName = pluginName.toLower(); 0102 #ifdef Q_OS_WIN32 0103 #ifdef Q_CC_MSVC 0104 fileName += QLatin1String(".dll"); 0105 #else 0106 fileName = QLatin1String("lib") + fileName + QLatin1String(".dll"); 0107 #endif 0108 #elif defined Q_OS_MAC 0109 fileName = QLatin1String("lib") + fileName + QLatin1String(".dylib"); 0110 #else 0111 fileName = QLatin1String("lib") + fileName + QLatin1String(".so"); 0112 #endif 0113 return fileName; 0114 } 0115 0116 /** 0117 * Get text encoding from tag config as frame text encoding. 0118 * @return frame text encoding. 0119 */ 0120 Frame::TextEncoding frameTextEncodingFromConfig() 0121 { 0122 Frame::TextEncoding encoding; 0123 switch (TagConfig::instance().textEncoding()) { 0124 case TagConfig::TE_UTF16: 0125 encoding = Frame::TE_UTF16; 0126 break; 0127 case TagConfig::TE_UTF8: 0128 encoding = Frame::TE_UTF8; 0129 break; 0130 case TagConfig::TE_ISO8859_1: 0131 default: 0132 encoding = Frame::TE_ISO8859_1; 0133 } 0134 return encoding; 0135 } 0136 0137 /** 0138 * Extract file path, field name and index from frame name. 0139 * 0140 * @param frameName frame name with additional information for file, field and 0141 * index 0142 * @param dataFileName the path to a data file is returned here if available, 0143 * else null 0144 * @param fieldName the field name is returned here if available, else null 0145 * @param index the index is returned here if available, else 0 0146 */ 0147 void extractFileFieldIndex( 0148 QString& frameName, QString& dataFileName, QString& fieldName, int& index) 0149 { 0150 dataFileName.clear(); 0151 fieldName.clear(); 0152 index = 0; 0153 if (int colonIndex = frameName.indexOf(QLatin1Char(':')); colonIndex != -1) { 0154 dataFileName = frameName.mid(colonIndex + 1); 0155 frameName.truncate(colonIndex); 0156 } 0157 if (int dotIndex = frameName.indexOf(QLatin1Char('.')); dotIndex != -1) { 0158 fieldName = frameName.mid(dotIndex + 1); 0159 frameName.truncate(dotIndex); 0160 } 0161 if (int bracketIndex = frameName.indexOf(QLatin1Char('[')); bracketIndex != -1) { 0162 if (const int closingBracketIndex = 0163 frameName.indexOf(QLatin1Char(']'), bracketIndex + 1); 0164 closingBracketIndex > bracketIndex) { 0165 bool ok; 0166 #if QT_VERSION >= 0x060000 0167 index = frameName.mid( 0168 bracketIndex + 1, closingBracketIndex - bracketIndex - 1).toInt(&ok); 0169 #else 0170 index = frameName.midRef( 0171 bracketIndex + 1, closingBracketIndex - bracketIndex - 1).toInt(&ok); 0172 #endif 0173 if (ok) { 0174 frameName.remove(bracketIndex, closingBracketIndex - bracketIndex + 1); 0175 } 0176 } 0177 } 0178 } 0179 0180 /** 0181 * Get the internal rating frame name with optional field. 0182 * @param frame frame containing rating 0183 * @param taggedFile optional taggedFile to be used if @a frame does not have 0184 * a useful internal name 0185 * @param tagNr used together with @a taggedFile to guess rating name 0186 * @return internal name, "POPM.Email-Value" if POPM with Email value. 0187 */ 0188 QString ratingTypeName(const Frame& frame, 0189 const TaggedFile* taggedFile = nullptr, 0190 Frame::TagNumber tagNr = Frame::Tag_2) 0191 { 0192 QString name = frame.getInternalName(); 0193 if (name.startsWith(QLatin1String("POPM"))) { 0194 name.truncate(4); 0195 QVariant emailVar = frame.getFieldValue(Frame::ID_Email); 0196 if (QString emailValue; 0197 emailVar.isValid() && 0198 !(emailValue = emailVar.toString()).isEmpty()) { 0199 name += QLatin1Char('.'); 0200 name += emailValue; 0201 } 0202 } else if (taggedFile && 0203 name != QLatin1String("RATING") && 0204 name != QLatin1String("rate") && 0205 name != QLatin1String("IRTD") && 0206 name != QLatin1String("WM/SharedUserRating")) { 0207 QString tagFormat = taggedFile->getTagFormat(tagNr); 0208 if (tagFormat.isEmpty()) { 0209 if (QString ext = taggedFile->getFileExtension().toLower(); 0210 ext == QLatin1String(".mp3") || ext == QLatin1String(".mp2") || 0211 ext == QLatin1String(".aac") || ext == QLatin1String(".tta") || 0212 ext == QLatin1String(".dsf") || ext == QLatin1String(".dff")) { 0213 tagFormat = QLatin1String("ID3v2.3.0"); 0214 } else if (ext == QLatin1String(".ogg") || 0215 ext == QLatin1String(".flac") || 0216 ext == QLatin1String(".opus")) { 0217 tagFormat = QLatin1String("Vorbis"); 0218 } else if (ext == QLatin1String(".m4a")) { 0219 tagFormat = QLatin1String("MP4"); 0220 } else if (ext == QLatin1String(".wav") || 0221 ext == QLatin1String(".aiff")) { 0222 tagFormat = tagNr == Frame::Tag_3 ? QLatin1String("RIFF INFO") 0223 : QLatin1String("ID3v2.3.0"); 0224 } else if (ext == QLatin1String(".wma")) { 0225 tagFormat = QLatin1String("ASF"); 0226 } 0227 } 0228 if (tagFormat.startsWith(QLatin1String("ID3v2"))) { 0229 name = QLatin1String("POPM"); 0230 } else if (tagFormat == QLatin1String("Vorbis")) { 0231 name = QLatin1String("RATING"); 0232 } else if (tagFormat == QLatin1String("MP4")) { 0233 name = QLatin1String("rate"); 0234 } else if (tagFormat == QLatin1String("RIFF INFO")) { 0235 name = QLatin1String("IRTD"); 0236 } else if (tagFormat == QLatin1String("ASF")) { 0237 name = QLatin1String("WM/SharedUserRating"); 0238 } 0239 } 0240 return name; 0241 } 0242 0243 } 0244 0245 /** Fallback for path to search for plugins */ 0246 QString Kid3Application::s_pluginsPathFallback; 0247 0248 /** 0249 * Constructor. 0250 * @param platformTools platform tools 0251 * @param parent parent object 0252 */ 0253 Kid3Application::Kid3Application(ICorePlatformTools* platformTools, 0254 QObject* parent) : QObject(parent), 0255 m_platformTools(platformTools), 0256 m_configStore(new ConfigStore(m_platformTools->applicationSettings())), 0257 m_fileSystemModel(new TaggedFileSystemModel(m_platformTools->iconProvider(), this)), 0258 m_fileProxyModel(new FileProxyModel(this)), 0259 m_fileProxyModelIterator(new FileProxyModelIterator(m_fileProxyModel)), 0260 m_dirProxyModel(new DirProxyModel(this)), 0261 m_fileSelectionModel(new QItemSelectionModel(m_fileProxyModel, this)), 0262 m_dirSelectionModel(new QItemSelectionModel(m_dirProxyModel, this)), 0263 m_trackDataModel(new TrackDataModel(m_platformTools->iconProvider(), this)), 0264 m_netMgr(new QNetworkAccessManager(this)), 0265 m_downloadClient(new DownloadClient(m_netMgr)), 0266 m_textExporter(new TextExporter(this)), 0267 m_tagSearcher(new TagSearcher(this)), 0268 m_dirRenamer(new DirRenamer(this)), 0269 m_batchImporter(new BatchImporter(m_netMgr)), 0270 m_player(nullptr), 0271 m_expressionFileFilter(nullptr), 0272 m_downloadImageDest(ImageForSelectedFiles), 0273 m_fileFilter(nullptr), m_filterPassed(0), m_filterTotal(0), 0274 m_batchImportProfile(nullptr), m_batchImportTagVersion(Frame::TagNone), 0275 m_editFrameTaggedFile(nullptr), m_addFrameTaggedFile(nullptr), 0276 m_frameEditor(nullptr), m_storedFrameEditor(nullptr), 0277 m_imageProvider(nullptr), 0278 #ifdef Q_OS_ANDROID 0279 m_pendingIntentsChecked(false), 0280 #endif 0281 #ifdef HAVE_QTDBUS 0282 m_dbusEnabled(false), 0283 #endif 0284 m_filtered(false), m_selectionOperationRunning(false) 0285 { 0286 const TagConfig& tagCfg = TagConfig::instance(); 0287 FOR_ALL_TAGS(tagNr) { 0288 bool id3v1 = tagNr == Frame::Tag_Id3v1; 0289 m_genreModel[tagNr] = new GenreModel(id3v1, this); 0290 m_framesModel[tagNr] = new FrameTableModel( 0291 id3v1, platformTools->iconProvider(), this); 0292 if (!id3v1) { 0293 m_framesModel[tagNr]->setFrameOrder(tagCfg.quickAccessFrameOrder()); 0294 connect(&tagCfg, &TagConfig::quickAccessFrameOrderChanged, 0295 m_framesModel[tagNr], &FrameTableModel::setFrameOrder); 0296 } 0297 m_framesSelectionModel[tagNr] = new QItemSelectionModel(m_framesModel[tagNr], this); 0298 m_framelist[tagNr] = new FrameList(tagNr, m_framesModel[tagNr], 0299 m_framesSelectionModel[tagNr]); 0300 connect(m_framelist[tagNr], &FrameList::frameEdited, 0301 this, &Kid3Application::onFrameEdited); 0302 connect(m_framelist[tagNr], &FrameList::frameAdded, 0303 this, &Kid3Application::onTag2FrameAdded); 0304 m_tagContext[tagNr] = new Kid3ApplicationTagContext(this, tagNr); 0305 } 0306 m_selection = new TaggedFileSelection(m_framesModel, this); 0307 setObjectName(QLatin1String("Kid3Application")); 0308 m_fileSystemModel->setReadOnly(false); 0309 const FileConfig& fileCfg = FileConfig::instance(); 0310 m_fileSystemModel->setSortIgnoringPunctuation(fileCfg.sortIgnoringPunctuation()); 0311 m_fileProxyModel->setSourceModel(m_fileSystemModel); 0312 m_dirProxyModel->setSourceModel(m_fileSystemModel); 0313 connect(m_fileSelectionModel, 0314 &QItemSelectionModel::selectionChanged, 0315 this, &Kid3Application::fileSelected); 0316 connect(m_fileSelectionModel, 0317 &QItemSelectionModel::selectionChanged, 0318 this, &Kid3Application::fileSelectionChanged); 0319 connect(m_fileProxyModel, &FileProxyModel::modifiedChanged, 0320 this, &Kid3Application::modifiedChanged); 0321 0322 connect(m_selection, &TaggedFileSelection::singleFileChanged, 0323 this, &Kid3Application::updateCoverArtImageId); 0324 connect(m_selection, &TaggedFileSelection::fileNameModified, 0325 this, &Kid3Application::selectedFilesUpdated); 0326 0327 initPlugins(); 0328 m_batchImporter->setImporters(m_importers, m_trackDataModel); 0329 0330 #ifdef Q_OS_ANDROID 0331 new AndroidUtils(this); 0332 // Make sure that configuration changes are saved for the Android app. 0333 // Old style connect syntax is used to avoid a dependency to QGuiApplication. 0334 connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), 0335 this, SLOT(onApplicationStateChanged(Qt::ApplicationState))); 0336 QObject::connect(AndroidUtils::instance(), &AndroidUtils::filePathReceived, 0337 this, [this](const QString& path) { 0338 dropLocalFiles({path}, false); 0339 }); 0340 #endif 0341 } 0342 0343 /** 0344 * Destructor. 0345 */ 0346 Kid3Application::~Kid3Application() 0347 { 0348 #ifdef Q_OS_MAC 0349 // If a song is played, then stopped and Kid3 is terminated, it will crash in 0350 // the QMediaPlayer destructor (Dispatch queue: com.apple.main-thread, 0351 // objc_msgSend() selector name: setRate). Avoid calling the destructor by 0352 // setting the QMediaPlayer's parent to null. See also: 0353 // https://qt-project.org/forums/viewthread/29651 0354 if (m_player) { 0355 m_player->setParent(0); 0356 } 0357 #endif 0358 } 0359 0360 /** 0361 * Save config when suspended, check intents when activated. 0362 * @param state application state 0363 */ 0364 void Kid3Application::onApplicationStateChanged(Qt::ApplicationState state) 0365 { 0366 #ifdef Q_OS_ANDROID 0367 if (state == Qt::ApplicationSuspended) { 0368 saveConfig(); 0369 } else if (state == Qt::ApplicationActive) { 0370 // When the app becomes active for the first time, 0371 // check if it was launched with an intent. 0372 if (!m_pendingIntentsChecked) { 0373 m_pendingIntentsChecked = true; 0374 AndroidUtils::instance()->checkPendingIntents(); 0375 } 0376 } 0377 #else 0378 Q_UNUSED(state) 0379 #endif 0380 } 0381 0382 #ifdef HAVE_QTDBUS 0383 /** 0384 * Activate the D-Bus interface. 0385 * This method shall be called only once at initialization. 0386 */ 0387 void Kid3Application::activateDbusInterface() 0388 { 0389 if (QDBusConnection::sessionBus().isConnected()) { 0390 QString serviceName(QLatin1String("org.kde.kid3")); 0391 QDBusConnection::sessionBus().registerService(serviceName); 0392 // For the case of multiple Kid3 instances running, register also a service 0393 // with the PID appended. On KDE such a service is already registered but 0394 // the call to registerService() seems to succeed nevertheless. 0395 serviceName += QLatin1Char('-'); 0396 serviceName += QString::number(::getpid()); 0397 QDBusConnection::sessionBus().registerService(serviceName); 0398 new ScriptInterface(this); 0399 if (QDBusConnection::sessionBus().registerObject(QLatin1String("/Kid3"), this)) { 0400 m_dbusEnabled = true; 0401 } else { 0402 qWarning("Registering D-Bus object failed"); 0403 } 0404 } else { 0405 qWarning("Cannot connect to the D-BUS session bus."); 0406 } 0407 } 0408 #endif 0409 0410 /** 0411 * Load and initialize plugins depending on configuration. 0412 */ 0413 void Kid3Application::initPlugins() 0414 { 0415 // Load plugins, set information about plugins in configuration. 0416 ImportConfig& importCfg = ImportConfig::instance(); 0417 TagConfig& tagCfg = TagConfig::instance(); 0418 importCfg.clearAvailablePlugins(); 0419 tagCfg.clearAvailablePlugins(); 0420 const auto plugins = loadPlugins(); 0421 for (QObject* plugin : plugins) { 0422 checkPlugin(plugin); 0423 } 0424 // Order the meta data plugins as configured. 0425 if (QStringList pluginOrder = tagCfg.pluginOrder(); !pluginOrder.isEmpty()) { 0426 QList<ITaggedFileFactory*> orderedFactories; 0427 for (int i = 0; i < pluginOrder.size(); ++i) { 0428 orderedFactories.append(nullptr); // clazy:exclude=reserve-candidates 0429 } 0430 const auto factories = FileProxyModel::taggedFileFactories(); 0431 for (ITaggedFileFactory* factory : factories) { 0432 if (int idx = pluginOrder.indexOf(factory->name()); idx >= 0) { 0433 orderedFactories[idx] = factory; 0434 } else { 0435 orderedFactories.append(factory); // clazy:exclude=reserve-candidates 0436 } 0437 } 0438 orderedFactories.removeAll(nullptr); 0439 FileProxyModel::taggedFileFactories().swap(orderedFactories); 0440 } 0441 } 0442 0443 /** 0444 * Find directory containing plugins. 0445 * @param pluginsDir the plugin directory is returned here 0446 * @return true if found. 0447 */ 0448 bool Kid3Application::findPluginsDirectory(QDir& pluginsDir) { 0449 // First check if we are running from the build directory to load the 0450 // plugins from there. 0451 pluginsDir.setPath(QCoreApplication::applicationDirPath()); 0452 QString dirName = pluginsDir.dirName(); 0453 #ifdef Q_OS_WIN 0454 QString buildType; 0455 if (dirName.compare(QLatin1String("debug"), Qt::CaseInsensitive) == 0 || 0456 dirName.compare(QLatin1String("release"), Qt::CaseInsensitive) == 0) { 0457 buildType = dirName; 0458 pluginsDir.cdUp(); 0459 dirName = pluginsDir.dirName(); 0460 } 0461 #endif 0462 bool pluginsDirFound = pluginsDir.cd(QLatin1String( 0463 dirName == QLatin1String("qt") || dirName == QLatin1String("kde") || 0464 dirName == QLatin1String("cli") || dirName == QLatin1String("qml") 0465 ? "../../plugins" 0466 : dirName == QLatin1String("test") 0467 ? "../plugins" 0468 : CFG_PLUGINSDIR)); 0469 #ifdef Q_OS_MAC 0470 if (!pluginsDirFound) { 0471 pluginsDirFound = pluginsDir.cd(QLatin1String("../../../../../plugins")); 0472 } 0473 #endif 0474 #ifdef Q_OS_WIN 0475 if (pluginsDirFound && !buildType.isEmpty()) { 0476 pluginsDir.cd(buildType); 0477 } 0478 #endif 0479 return pluginsDirFound; 0480 } 0481 0482 /** 0483 * Set fallback path for directory containing plugins. 0484 * @param path path to be searched for plugins if they are not found at the 0485 * standard location relative to the application directory 0486 */ 0487 void Kid3Application::setPluginsPathFallback(const QString& path) 0488 { 0489 s_pluginsPathFallback = path; 0490 } 0491 0492 /** 0493 * Load plugins. 0494 * @return list of plugin instances. 0495 */ 0496 QObjectList Kid3Application::loadPlugins() 0497 { 0498 QObjectList plugins = QPluginLoader::staticInstances(); 0499 0500 QDir pluginsDir; 0501 bool pluginsDirFound = findPluginsDirectory(pluginsDir); 0502 if (!pluginsDirFound && !s_pluginsPathFallback.isEmpty()) { 0503 pluginsDir.setPath(s_pluginsPathFallback); 0504 pluginsDirFound = true; 0505 } 0506 if (pluginsDirFound) { 0507 ImportConfig& importCfg = ImportConfig::instance(); 0508 TagConfig& tagCfg = TagConfig::instance(); 0509 0510 // Construct a set of disabled plugin file names 0511 QMap<QString, QString> disabledImportPluginFileNames; 0512 const QStringList disabledPlugins = importCfg.disabledPlugins(); 0513 for (const QString& pluginName : disabledPlugins) { 0514 disabledImportPluginFileNames.insert(pluginFileName(pluginName), 0515 pluginName); 0516 } 0517 QMap<QString, QString> disabledTagPluginFileNames; 0518 const QStringList disabledTagPlugins = tagCfg.disabledPlugins(); 0519 for (const QString& pluginName : disabledTagPlugins) { 0520 disabledTagPluginFileNames.insert(pluginFileName(pluginName), 0521 pluginName); 0522 } 0523 0524 QStringList availablePlugins = importCfg.availablePlugins(); 0525 QStringList availableTagPlugins = tagCfg.availablePlugins(); 0526 const auto fileNames = pluginsDir.entryList(QDir::Files); 0527 for (const QString& fileName : fileNames) { 0528 if (disabledImportPluginFileNames.contains(fileName)) { 0529 availablePlugins.append( 0530 disabledImportPluginFileNames.value(fileName)); 0531 continue; 0532 } 0533 if (disabledTagPluginFileNames.contains(fileName)) { 0534 availableTagPlugins.append( 0535 disabledTagPluginFileNames.value(fileName)); 0536 continue; 0537 } 0538 QPluginLoader loader(pluginsDir.absoluteFilePath(fileName)); 0539 if (QObject* plugin = loader.instance()) { 0540 if (QString name(plugin->objectName()); disabledPlugins.contains(name)) { 0541 availablePlugins.append(name); 0542 loader.unload(); 0543 } else if (disabledTagPlugins.contains(name)) { 0544 availableTagPlugins.append(name); 0545 loader.unload(); 0546 } else { 0547 plugins.append(plugin); 0548 } 0549 } 0550 } 0551 importCfg.setAvailablePlugins(availablePlugins); 0552 tagCfg.setAvailablePlugins(availableTagPlugins); 0553 } 0554 return plugins; 0555 } 0556 0557 /** 0558 * Check type of a loaded plugin and register it. 0559 * @param plugin instance returned by plugin loader 0560 */ 0561 void Kid3Application::checkPlugin(QObject* plugin) 0562 { 0563 if (auto importerFactory = 0564 qobject_cast<IServerImporterFactory*>(plugin)) { 0565 ImportConfig& importCfg = ImportConfig::instance(); 0566 QStringList availablePlugins = importCfg.availablePlugins(); 0567 availablePlugins.append(plugin->objectName()); 0568 importCfg.setAvailablePlugins(availablePlugins); 0569 if (!importCfg.disabledPlugins().contains(plugin->objectName())) { 0570 const auto keys = importerFactory->serverImporterKeys(); 0571 for (const QString& key : keys) { 0572 m_importers.append(importerFactory->createServerImporter( 0573 key, m_netMgr, m_trackDataModel)); 0574 } 0575 } 0576 } 0577 if (auto importerFactory = 0578 qobject_cast<IServerTrackImporterFactory*>(plugin)) { 0579 ImportConfig& importCfg = ImportConfig::instance(); 0580 QStringList availablePlugins = importCfg.availablePlugins(); 0581 availablePlugins.append(plugin->objectName()); 0582 importCfg.setAvailablePlugins(availablePlugins); 0583 if (!importCfg.disabledPlugins().contains(plugin->objectName())) { 0584 const auto keys = importerFactory->serverTrackImporterKeys(); 0585 for (const QString& key : keys) { 0586 m_trackImporters.append(importerFactory->createServerTrackImporter( 0587 key, m_netMgr, m_trackDataModel)); 0588 } 0589 } 0590 } 0591 if (auto taggedFileFactory = 0592 qobject_cast<ITaggedFileFactory*>(plugin)) { 0593 TagConfig& tagCfg = TagConfig::instance(); 0594 QStringList availablePlugins = tagCfg.availablePlugins(); 0595 availablePlugins.append(plugin->objectName()); 0596 tagCfg.setAvailablePlugins(availablePlugins); 0597 if (!tagCfg.disabledPlugins().contains(plugin->objectName())) { 0598 int features = tagCfg.taggedFileFeatures(); 0599 const auto keys = taggedFileFactory->taggedFileKeys(); 0600 for (const QString& key : keys) { 0601 taggedFileFactory->initialize(key); 0602 features |= taggedFileFactory->taggedFileFeatures(key); 0603 } 0604 tagCfg.setTaggedFileFeatures(features); 0605 FileProxyModel::taggedFileFactories().append(taggedFileFactory); 0606 } 0607 } 0608 if (auto userCommandProcessor = 0609 qobject_cast<IUserCommandProcessor*>(plugin)) { 0610 ImportConfig& importCfg = ImportConfig::instance(); 0611 QStringList availablePlugins = importCfg.availablePlugins(); 0612 availablePlugins.append(plugin->objectName()); 0613 importCfg.setAvailablePlugins(availablePlugins); 0614 if (!importCfg.disabledPlugins().contains(plugin->objectName())) { 0615 m_userCommandProcessors.append(userCommandProcessor); 0616 } 0617 } 0618 } 0619 0620 /** 0621 * Get names of available server track importers. 0622 * @return list of server track importer names. 0623 */ 0624 QStringList Kid3Application::getServerImporterNames() const 0625 { 0626 QStringList names; 0627 const auto importers = m_importers; 0628 for (const ServerImporter* importer : importers) { 0629 names.append(QString::fromLatin1(importer->name())); 0630 } 0631 return names; 0632 } 0633 0634 /** 0635 * Get audio player. 0636 * @return audio player. 0637 */ 0638 QObject* Kid3Application::getAudioPlayer() 0639 { 0640 if (!m_player) { 0641 #ifdef HAVE_QTDBUS 0642 m_player = m_platformTools->createAudioPlayer(this, m_dbusEnabled); 0643 #else 0644 m_player = m_platformTools->createAudioPlayer(this, false); 0645 #endif 0646 } 0647 #ifdef HAVE_QTDBUS 0648 if (m_dbusEnabled) { 0649 activateMprisInterface(); 0650 } 0651 #endif 0652 return m_player; 0653 } 0654 0655 #ifdef HAVE_QTDBUS 0656 /** 0657 * Activate the MPRIS D-Bus Interface if not already active. 0658 */ 0659 void Kid3Application::activateMprisInterface() 0660 { 0661 if (!m_mprisServiceName.isEmpty() || !m_player) 0662 return; 0663 0664 if (QDBusConnection::sessionBus().isConnected()) { 0665 m_mprisServiceName = QLatin1String("org.mpris.MediaPlayer2.kid3"); 0666 bool ok = QDBusConnection::sessionBus().registerService(m_mprisServiceName); 0667 if (!ok) { 0668 // If another instance of Kid3 is already running register a service 0669 // with ".instancePID" appended, see 0670 // https://specifications.freedesktop.org/mpris-spec/latest/ 0671 m_mprisServiceName += QLatin1String(".instance"); 0672 m_mprisServiceName += QString::number(::getpid()); 0673 ok = QDBusConnection::sessionBus().registerService(m_mprisServiceName); 0674 } 0675 if (ok) { 0676 if (!QDBusConnection::sessionBus().registerObject( 0677 QLatin1String("/org/mpris/MediaPlayer2"), m_player)) { 0678 qWarning("Registering D-Bus MPRIS object failed"); 0679 } 0680 } else { 0681 m_mprisServiceName.clear(); 0682 qWarning("Registering D-Bus MPRIS service failed"); 0683 } 0684 } else { 0685 qWarning("Cannot connect to the D-BUS session bus."); 0686 } 0687 } 0688 0689 /** 0690 * Deactivate the MPRIS D-Bus Interface if it is active. 0691 */ 0692 void Kid3Application::deactivateMprisInterface() 0693 { 0694 if (m_mprisServiceName.isEmpty()) 0695 return; 0696 0697 if (QDBusConnection::sessionBus().isConnected()) { 0698 QDBusConnection::sessionBus().unregisterObject( 0699 QLatin1String("/org/mpris/MediaPlayer2")); 0700 if (QDBusConnection::sessionBus().unregisterService(m_mprisServiceName)) { 0701 m_mprisServiceName.clear(); 0702 } else { 0703 qWarning("Unregistering D-Bus MPRIS service failed"); 0704 } 0705 } else { 0706 qWarning("Cannot connect to the D-BUS session bus."); 0707 } 0708 } 0709 #endif 0710 0711 /** 0712 * Get settings. 0713 * @return settings. 0714 */ 0715 ISettings* Kid3Application::getSettings() const 0716 { 0717 return m_platformTools->applicationSettings(); 0718 } 0719 0720 /** 0721 * Apply configuration changes. 0722 */ 0723 void Kid3Application::applyChangedConfiguration() 0724 { 0725 saveConfig(); 0726 const FileConfig& fileCfg = FileConfig::instance(); 0727 FOR_ALL_TAGS(tagNr) { 0728 if (!TagConfig::instance().markTruncations()) { 0729 m_framesModel[tagNr]->markRows(0); 0730 } 0731 if (!fileCfg.markChanges()) { 0732 m_framesModel[tagNr]->markChangedFrames(QList<Frame::ExtendedType>()); 0733 } 0734 m_genreModel[tagNr]->init(); 0735 } 0736 notifyConfigurationChange(); 0737 0738 const TagConfig& tagCfg = TagConfig::instance(); 0739 if (quint64 oldQuickAccessFrames = FrameCollection::getQuickAccessFrames(); 0740 tagCfg.quickAccessFrames() != oldQuickAccessFrames) { 0741 FrameCollection::setQuickAccessFrames(tagCfg.quickAccessFrames()); 0742 emit selectedFilesUpdated(); 0743 } 0744 if (Frame::setNamesForCustomFrames(tagCfg.customFrames())) { 0745 emit selectedFilesUpdated(); 0746 } 0747 0748 QStringList nameFilters(m_platformTools->getNameFilterPatterns( 0749 fileCfg.nameFilter()).split(QLatin1Char(' '))); 0750 m_fileProxyModel->setNameFilters(nameFilters); 0751 m_fileProxyModel->setFolderFilters(fileCfg.includeFolders(), 0752 fileCfg.excludeFolders()); 0753 0754 QDir::Filters oldFilter = m_fileSystemModel->filter(); 0755 QDir::Filters filter = oldFilter; 0756 if (fileCfg.showHiddenFiles()) { 0757 filter |= QDir::Hidden; 0758 } else { 0759 filter &= ~QDir::Hidden; 0760 } 0761 if (filter != oldFilter) { 0762 m_fileSystemModel->setFilter(filter); 0763 } 0764 } 0765 0766 /** 0767 * Save settings to the configuration. 0768 */ 0769 void Kid3Application::saveConfig() 0770 { 0771 if (FileConfig::instance().loadLastOpenedFile()) { 0772 FileConfig::instance().setLastOpenedFile( 0773 m_fileProxyModel->filePath(currentOrRootIndex())); 0774 } 0775 m_configStore->writeToConfig(); 0776 getSettings()->sync(); 0777 } 0778 0779 /** 0780 * Read settings from the configuration. 0781 */ 0782 void Kid3Application::readConfig() 0783 { 0784 if (FileConfig::instance().nameFilter().isEmpty()) { 0785 setAllFilesFileFilter(); 0786 } 0787 notifyConfigurationChange(); 0788 0789 const TagConfig& tagCfg = TagConfig::instance(); 0790 FrameCollection::setQuickAccessFrames(tagCfg.quickAccessFrames()); 0791 Frame::setNamesForCustomFrames(tagCfg.customFrames()); 0792 } 0793 0794 /** 0795 * Open directory. 0796 * When finished directoryOpened() is emitted, also if false is returned. 0797 * 0798 * @param paths file or directory paths, if multiple paths are given, the 0799 * common directory is opened and the files are selected 0800 * @param fileCheck if true, only open directory if paths exist 0801 * 0802 * @return true if ok. 0803 */ 0804 bool Kid3Application::openDirectory(const QStringList& paths, bool fileCheck) 0805 { 0806 #ifdef Q_OS_ANDROID 0807 const QStringList musicLocations = 0808 QStandardPaths::standardLocations(QStandardPaths::MusicLocation).mid(0, 1); 0809 const QStringList pathList = paths.isEmpty() ? musicLocations : paths; 0810 #else 0811 const QStringList pathList(paths); 0812 #endif 0813 bool ok = true; 0814 QStringList filePaths; 0815 QStringList dirComponents; 0816 for (QString path : pathList) { 0817 if (!path.isEmpty()) { 0818 QFileInfo fileInfo(path); 0819 if (path.startsWith(QLatin1Char(':')) && !fileInfo.exists()) { 0820 // QFileInfo assumes that paths starting with a colon are absolute 0821 // and denote a QResource. Since no such file was found, try again 0822 // with a relative path to a file starting with a colon. 0823 path = QLatin1String("./") + path; 0824 fileInfo.setFile(path); 0825 } 0826 if (fileCheck && !fileInfo.exists()) { 0827 ok = false; 0828 break; 0829 } 0830 QString dirPath; 0831 if (!fileInfo.isDir()) { 0832 dirPath = fileInfo.absolutePath(); 0833 if (fileInfo.isFile()) { 0834 filePaths.append(fileInfo.absoluteFilePath()); 0835 } 0836 } else { 0837 dirPath = QDir(path).absolutePath(); 0838 } 0839 // absolutePath() returns paths with '/' separators also on Windows 0840 // where QDir::separator() is '\'. 0841 QStringList dirPathComponents = dirPath.split(QLatin1Char('/')); 0842 if (dirComponents.isEmpty()) { 0843 dirComponents = dirPathComponents; 0844 } else { 0845 // Reduce dirPath to common prefix. 0846 auto dirIt = dirComponents.begin(); 0847 auto dirPathIt = dirPathComponents.constBegin(); 0848 while (dirIt != dirComponents.end() && 0849 dirPathIt != dirPathComponents.constEnd() && 0850 *dirIt == *dirPathIt) { 0851 ++dirIt; 0852 ++dirPathIt; 0853 } 0854 dirComponents.erase(dirIt, dirComponents.end()); 0855 } 0856 } 0857 } 0858 QString dir; 0859 if (ok) { 0860 dir = dirComponents.join(QDir::separator()); 0861 if (dir.isEmpty() && !filePaths.isEmpty()) { 0862 dir = QDir::rootPath(); 0863 } 0864 ok = !dir.isEmpty(); 0865 } 0866 QModelIndex rootIndex; 0867 QModelIndexList fileIndexes; 0868 if (ok) { 0869 const FileConfig& fileCfg = FileConfig::instance(); 0870 QStringList nameFilters(m_platformTools->getNameFilterPatterns( 0871 fileCfg.nameFilter()).split(QLatin1Char(' '))); 0872 m_fileProxyModel->setNameFilters(nameFilters); 0873 m_fileProxyModel->setFolderFilters(fileCfg.includeFolders(), 0874 fileCfg.excludeFolders()); 0875 QDir::Filters filter = QDir::AllEntries | QDir::AllDirs; 0876 if (fileCfg.showHiddenFiles()) { 0877 filter |= QDir::Hidden; 0878 } 0879 m_fileSystemModel->setFilter(filter); 0880 rootIndex = m_fileSystemModel->setRootPath(dir); 0881 fileIndexes.reserve(filePaths.size()); 0882 const auto constFilePaths = filePaths; 0883 for (const QString& filePath : constFilePaths) { 0884 fileIndexes.append(m_fileSystemModel->index(filePath)); 0885 } 0886 ok = rootIndex.isValid(); 0887 } 0888 if (ok) { 0889 setFiltered(false); 0890 m_dirName = dir; 0891 emit dirNameChanged(m_dirName); 0892 QModelIndex oldRootIndex = m_fileProxyModelRootIndex; 0893 m_fileProxyModelRootIndex = m_fileProxyModel->mapFromSource(rootIndex); 0894 m_fileProxyModelFileIndexes.clear(); 0895 const auto constFileIndexes = fileIndexes; 0896 for (const QModelIndex& fileIndex : constFileIndexes) { 0897 m_fileProxyModelFileIndexes.append( 0898 m_fileProxyModel->mapFromSource(fileIndex)); 0899 } 0900 if (m_fileProxyModelRootIndex != oldRootIndex) { 0901 connect(m_fileProxyModel, &FileProxyModel::sortingFinished, 0902 this, &Kid3Application::onDirectoryLoaded); 0903 } else { 0904 QTimer::singleShot(0, this, &Kid3Application::onDirectoryOpened); 0905 } 0906 } 0907 if (!ok) { 0908 QTimer::singleShot(0, this, &Kid3Application::onDirectoryOpened); 0909 } 0910 return ok; 0911 } 0912 0913 /** 0914 * Update selection and emit signals when directory is opened. 0915 */ 0916 void Kid3Application::onDirectoryOpened() 0917 { 0918 QModelIndex fsRoot = m_fileProxyModel->mapToSource(m_fileProxyModelRootIndex); 0919 m_dirProxyModelRootIndex = m_dirProxyModel->mapFromSource(fsRoot); 0920 0921 emit fileRootIndexChanged(m_fileProxyModelRootIndex); 0922 emit dirRootIndexChanged(m_dirProxyModelRootIndex); 0923 0924 if (m_fileProxyModelRootIndex.isValid()) { 0925 m_fileSelectionModel->clearSelection(); 0926 if (!m_fileProxyModelFileIndexes.isEmpty()) { 0927 const auto fileIndexes = m_fileProxyModelFileIndexes; 0928 for (const QPersistentModelIndex& fileIndex : fileIndexes) { 0929 m_fileSelectionModel->select(fileIndex, 0930 QItemSelectionModel::Select | QItemSelectionModel::Rows); 0931 } 0932 #if QT_VERSION >= 0x050600 0933 m_fileSelectionModel->setCurrentIndex(m_fileProxyModelFileIndexes.constFirst(), 0934 QItemSelectionModel::NoUpdate); 0935 #else 0936 m_fileSelectionModel->setCurrentIndex(m_fileProxyModelFileIndexes.first(), 0937 QItemSelectionModel::NoUpdate); 0938 #endif 0939 } else { 0940 m_fileSelectionModel->setCurrentIndex(m_fileProxyModelRootIndex, 0941 QItemSelectionModel::Clear | QItemSelectionModel::Current | 0942 QItemSelectionModel::Rows); 0943 } 0944 } 0945 0946 emit directoryOpened(); 0947 0948 if (m_dirUpIndex.isValid()) { 0949 m_dirSelectionModel->setCurrentIndex(m_dirUpIndex, 0950 QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); 0951 m_dirUpIndex = QPersistentModelIndex(); 0952 } 0953 } 0954 0955 /** 0956 * Called when the gatherer thread has finished to load the directory. 0957 */ 0958 void Kid3Application::onDirectoryLoaded() 0959 { 0960 disconnect(m_fileProxyModel, &FileProxyModel::sortingFinished, 0961 this, &Kid3Application::onDirectoryLoaded); 0962 onDirectoryOpened(); 0963 } 0964 0965 /** 0966 * Unload all tags. 0967 * The tags of all files which are not modified or selected are freed to 0968 * reclaim their memory. 0969 */ 0970 void Kid3Application::unloadAllTags() 0971 { 0972 TaggedFileIterator it(m_fileProxyModelRootIndex); 0973 while (it.hasNext()) { 0974 if (TaggedFile* taggedFile = it.next(); 0975 taggedFile->isTagInformationRead() && !taggedFile->isChanged() && 0976 !m_fileSelectionModel->isSelected( 0977 m_fileProxyModel->mapFromSource(taggedFile->getIndex()))) { 0978 taggedFile->clearTags(false); 0979 taggedFile->closeFileHandle(); 0980 } 0981 } 0982 #if defined Q_OS_LINUX && defined __GLIBC__ 0983 if (::malloc_trim(0)) { 0984 qDebug("Memory released by malloc_trim()"); 0985 } 0986 #endif 0987 } 0988 0989 /** 0990 * Get directory path of opened directory. 0991 * @return directory path. 0992 */ 0993 QString Kid3Application::getDirPath() const 0994 { 0995 return FileProxyModel::getPathIfIndexOfDir(m_fileProxyModelRootIndex); 0996 } 0997 0998 /** 0999 * Get current index in file proxy model or root index if current index is 1000 * invalid. 1001 * @return current index, root index if not valid. 1002 */ 1003 QModelIndex Kid3Application::currentOrRootIndex() const 1004 { 1005 if (QModelIndex index(m_fileSelectionModel->currentIndex()); index.isValid()) 1006 return index; 1007 return m_fileProxyModelRootIndex; 1008 } 1009 1010 /** 1011 * Save all changed files. 1012 * longRunningOperationProgress() is emitted while saving files. 1013 * 1014 * @param errorDescriptions if not NULL, a list with error descriptions 1015 * corresponding to the errored files in the returned file list 1016 * is returned here. Null strings are used where no error description 1017 * is available. 1018 * 1019 * @return list of files with error, empty if ok. 1020 */ 1021 QStringList Kid3Application::saveDirectory(QStringList* errorDescriptions) 1022 { 1023 QStringList errorFiles; 1024 int numFiles = 0, totalFiles = 0; 1025 // Get number of files to be saved to display correct progressbar 1026 TaggedFileIterator countIt(m_fileProxyModelRootIndex); 1027 while (countIt.hasNext()) { 1028 if (countIt.next()->isChanged()) { 1029 ++totalFiles; 1030 } 1031 } 1032 QString operationName = tr("Saving folder..."); 1033 bool aborted = false; 1034 emit longRunningOperationProgress(operationName, -1, totalFiles, &aborted); 1035 1036 if (errorDescriptions) { 1037 errorDescriptions->clear(); 1038 } 1039 TaggedFileIterator it(m_fileProxyModelRootIndex); 1040 while (it.hasNext()) { 1041 TaggedFile* taggedFile = it.next(); 1042 QString fileName = taggedFile->getFilename(); 1043 if (taggedFile->isFilenameChanged() && 1044 Utils::replaceIllegalFileNameCharacters(fileName)) { 1045 taggedFile->setFilename(fileName); 1046 } 1047 bool renamed = false; 1048 if (errorDescriptions) { 1049 errno = 0; 1050 } 1051 if (taggedFile->isChanged() && 1052 !taggedFile->writeTags(false, &renamed, 1053 FileConfig::instance().preserveTime())) { 1054 if (QDir dir(taggedFile->getDirname()); 1055 dir.exists(fileName) && taggedFile->isFilenameChanged()) { 1056 // File is renamed to a file name which already exists. 1057 // Try another file name ending with a number. 1058 QString baseName = fileName; 1059 QString ext; 1060 if (int dotPos = baseName.lastIndexOf(QLatin1Char('.')); dotPos != -1) { 1061 ext = baseName.mid(dotPos); 1062 baseName.truncate(dotPos); 1063 } 1064 baseName.append(QLatin1Char('(')); 1065 ext.prepend(QLatin1Char(')')); 1066 bool ok = false; 1067 for (int nr = 1; nr < 100; ++nr) { 1068 if (QString newName = baseName + QString::number(nr) + ext; 1069 !dir.exists(newName)) { 1070 taggedFile->setFilename(newName); 1071 ok = taggedFile->writeTags(false, &renamed, 1072 FileConfig::instance().preserveTime()); 1073 break; 1074 } 1075 } 1076 if (ok) { 1077 continue; 1078 } 1079 taggedFile->setFilename(fileName); 1080 } 1081 QString errorMsg = taggedFile->getAbsFilename(); 1082 errorFiles.push_back(errorMsg); 1083 if (errorDescriptions) { 1084 QString errorDescription; 1085 if (const int errnum = errno) { 1086 if (const char* errdesc = ::strerror(errnum)) { 1087 errorDescription = QString::fromUtf8(errdesc); 1088 } 1089 } 1090 errorDescriptions->append(errorDescription); 1091 } 1092 } 1093 ++numFiles; 1094 emit longRunningOperationProgress(operationName, numFiles, totalFiles, 1095 &aborted); 1096 if (aborted) { 1097 break; 1098 } 1099 } 1100 if (totalFiles == 0) { 1101 // To signal that operation is finished. 1102 ++totalFiles; 1103 } 1104 emit longRunningOperationProgress(operationName, totalFiles, totalFiles, 1105 &aborted); 1106 1107 return errorFiles; 1108 } 1109 1110 /** 1111 * Save all changed files. 1112 * longRunningOperationProgress() is emitted while saving files. 1113 * 1114 * @return list of files with error, empty if ok. 1115 */ 1116 QStringList Kid3Application::saveDirectory() 1117 { 1118 return saveDirectory(nullptr); 1119 } 1120 1121 /** 1122 * Merge entries of two string lists. 1123 * 1124 * Combine two string lists to a resulting list with all strings from 1125 * @a leftStrs having the corresponding string from @a rightStrs appended 1126 * if available. A @a separator can be given to join the two parts. 1127 * The @a rightStrs can contain fewer elements than @a leftStrs, the 1128 * resulting string will then be only the element from @a leftStrs. 1129 * This function can be used to add details to an error message, e.g. 1130 * mergeStringLists(errorMsgs, errorDescriptions, ": "). 1131 * 1132 * @param leftStrs strings for left part 1133 * @param rightStrs strings for right part 1134 * @param separator separator between left and right parts 1135 * @return string list with combined left and right parts. 1136 */ 1137 QStringList Kid3Application::mergeStringLists( 1138 const QStringList& leftStrs, const QStringList& rightStrs, 1139 const QString& separator) 1140 { 1141 QStringList result; 1142 result.reserve(leftStrs.size()); 1143 int i = 0; 1144 for (QString leftStr : leftStrs) { 1145 if (i < rightStrs.size()) { 1146 if (const QString& rightStr = rightStrs.at(i); !rightStr.isEmpty()) { 1147 leftStr += separator; 1148 leftStr += rightStr; 1149 } 1150 } 1151 result.append(leftStr); 1152 ++i; 1153 } 1154 return result; 1155 } 1156 1157 /** 1158 * Update tags of selected files to contain contents of frame models. 1159 */ 1160 void Kid3Application::frameModelsToTags() 1161 { 1162 if (!m_currentSelection.isEmpty()) { 1163 FOR_ALL_TAGS(tagNr) { 1164 FrameCollection frames(m_framesModel[tagNr]->getEnabledFrames()); 1165 for (auto it = m_currentSelection.constBegin(); 1166 it != m_currentSelection.constEnd(); 1167 ++it) { 1168 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(*it)) { 1169 taggedFile->setFrames(tagNr, frames); 1170 } 1171 } 1172 } 1173 } 1174 } 1175 1176 /** 1177 * Update frame models to contain contents of selected files. 1178 * The properties starting with "selection" will be set by this method. 1179 */ 1180 void Kid3Application::tagsToFrameModels() 1181 { 1182 QList<QPersistentModelIndex> indexes; 1183 const auto selectedIndexes = m_fileSelectionModel->selectedRows(); 1184 indexes.reserve(selectedIndexes.size()); 1185 for (const QModelIndex& index : selectedIndexes) { 1186 indexes.append(QPersistentModelIndex(index)); 1187 } 1188 1189 if (addTaggedFilesToSelection(indexes, true)) { 1190 m_currentSelection.swap(indexes); 1191 } 1192 } 1193 1194 /** 1195 * Update frame models to contain contents of item selection. 1196 * The properties starting with "selection" will be set by this method. 1197 * @param selected item selection 1198 */ 1199 void Kid3Application::selectedTagsToFrameModels(const QItemSelection& selected) 1200 { 1201 QList<QPersistentModelIndex> indexes; 1202 const auto selectedIndexes = selected.indexes(); 1203 for (const QModelIndex& index : selectedIndexes) { 1204 if (index.column() == 0) { 1205 indexes.append(QPersistentModelIndex(index)); 1206 } 1207 } 1208 1209 if (addTaggedFilesToSelection(indexes, m_currentSelection.isEmpty())) { 1210 m_currentSelection.append(indexes); 1211 } 1212 } 1213 1214 /** 1215 * Update frame models to contain contents of selected files. 1216 * @param indexes tagged file indexes 1217 * @param startSelection true if a new selection is started, false to add to 1218 * the existing selection 1219 * @return true if ok, false if selection operation is already running. 1220 */ 1221 bool Kid3Application::addTaggedFilesToSelection( 1222 const QList<QPersistentModelIndex>& indexes, bool startSelection) 1223 { 1224 // It would crash if this is called while a long running selection operation 1225 // is in progress. 1226 if (m_selectionOperationRunning) 1227 return false; 1228 1229 m_selectionOperationRunning = true; 1230 1231 if (startSelection) { 1232 m_selection->beginAddTaggedFiles(); 1233 } 1234 1235 QElapsedTimer timer; 1236 timer.start(); 1237 QString operationName = tr("Selection"); 1238 int longRunningTotal = 0; 1239 int done = 0; 1240 bool aborted = false; 1241 for (auto it = indexes.constBegin(); it != indexes.constEnd(); ++it, ++done) { 1242 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(*it)) { 1243 m_selection->addTaggedFile(taggedFile); 1244 if (!longRunningTotal) { 1245 if (timer.elapsed() >= 3000) { 1246 longRunningTotal = indexes.size(); 1247 emit longRunningOperationProgress(operationName, -1, longRunningTotal, 1248 &aborted); 1249 } 1250 } else { 1251 emit longRunningOperationProgress(operationName, done, longRunningTotal, 1252 &aborted); 1253 if (aborted) { 1254 break; 1255 } 1256 } 1257 } 1258 } 1259 if (longRunningTotal) { 1260 emit longRunningOperationProgress(operationName, longRunningTotal, 1261 longRunningTotal, &aborted); 1262 } 1263 1264 m_selection->endAddTaggedFiles(); 1265 1266 if (TaggedFile* taggedFile = m_selection->singleFile()) { 1267 FOR_ALL_TAGS(tagNr) { 1268 m_framelist[tagNr]->setTaggedFile(taggedFile); 1269 } 1270 } 1271 m_selection->clearUnusedFrames(); 1272 m_selectionOperationRunning = false; 1273 return true; 1274 } 1275 1276 /** 1277 * Revert file modifications. 1278 * Acts on selected files or all files if no file is selected. 1279 */ 1280 void Kid3Application::revertFileModifications() 1281 { 1282 SelectedTaggedFileIterator it(getRootIndex(), 1283 getFileSelectionModel(), 1284 true); 1285 while (it.hasNext()) { 1286 TaggedFile* taggedFile = it.next(); 1287 taggedFile->readTags(true); 1288 } 1289 if (!it.hasNoSelection()) { 1290 emit selectedFilesUpdated(); 1291 } 1292 } 1293 1294 /** 1295 * Set filter state. 1296 * 1297 * @param val true if list is filtered 1298 */ 1299 void Kid3Application::setFiltered(bool val) 1300 { 1301 if (m_filtered != val) { 1302 m_filtered = val; 1303 emit filteredChanged(m_filtered); 1304 } 1305 } 1306 1307 /** 1308 * Import. 1309 * 1310 * @param tagMask tag mask 1311 * @param path path of file, "clipboard" for import from clipboard 1312 * @param fmtIdx index of format 1313 * 1314 * @return true if ok. 1315 */ 1316 bool Kid3Application::importTags(Frame::TagVersion tagMask, 1317 const QString& path, int fmtIdx) 1318 { 1319 const ImportConfig& importCfg = ImportConfig::instance(); 1320 filesToTrackDataModel(importCfg.importDest()); 1321 QString text; 1322 if (path == QLatin1String("clipboard")) { 1323 text = m_platformTools->readFromClipboard(); 1324 } else { 1325 QFile file(path); 1326 if (file.open(QIODevice::ReadOnly)) { 1327 text = QTextStream(&file).readAll(); 1328 file.close(); 1329 } 1330 } 1331 if (!text.isNull() && 1332 fmtIdx < importCfg.importFormatHeaders().size()) { 1333 TextImporter(getTrackDataModel()).updateTrackData( 1334 text, 1335 importCfg.importFormatHeaders().at(fmtIdx), 1336 importCfg.importFormatTracks().at(fmtIdx)); 1337 trackDataModelToFiles(tagMask); 1338 return true; 1339 } 1340 return false; 1341 } 1342 1343 /** 1344 * Import from tags. 1345 * 1346 * @param tagMask tag mask 1347 * @param source format to get source text from tags 1348 * @param extraction regular expression with frame names and captures to 1349 * extract from source text 1350 */ 1351 void Kid3Application::importFromTags(Frame::TagVersion tagMask, 1352 const QString& source, 1353 const QString& extraction) 1354 { 1355 ImportTrackDataVector trackDataVector; 1356 filesToTrackData(tagMask, trackDataVector); 1357 TextImporter::importFromTags(source, extraction, trackDataVector); 1358 getTrackDataModel()->setTrackData(trackDataVector); 1359 trackDataModelToFiles(tagMask); 1360 } 1361 1362 /** 1363 * Import from tags on selected files. 1364 * 1365 * @param tagMask tag mask 1366 * @param source format to get source text from tags 1367 * @param extraction regular expression with frame names and captures to 1368 * extract from source text 1369 * 1370 * @return extracted values for "%{__return}(.+)", empty if not used. 1371 */ 1372 QStringList Kid3Application::importFromTagsToSelection(Frame::TagVersion tagMask, 1373 const QString& source, 1374 const QString& extraction) 1375 { 1376 emit fileSelectionUpdateRequested(); 1377 SelectedTaggedFileIterator it(getRootIndex(), 1378 getFileSelectionModel(), 1379 true); 1380 ImportParser parser; 1381 parser.setFormat(extraction); 1382 while (it.hasNext()) { 1383 TaggedFile* taggedFile = it.next(); 1384 taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile); 1385 ImportTrackData trackData(*taggedFile, tagMask); 1386 TextImporter::importFromTags(source, parser, trackData); 1387 taggedFile->setFrames(Frame::tagNumberFromMask(tagMask), trackData); 1388 } 1389 emit selectedFilesUpdated(); 1390 return parser.getReturnValues(); 1391 } 1392 1393 /** 1394 * Export. 1395 * 1396 * @param tagVersion tag version 1397 * @param path path of file, "clipboard" for export to clipboard 1398 * @param fmtIdx index of format 1399 * 1400 * @return true if ok. 1401 */ 1402 bool Kid3Application::exportTags(Frame::TagVersion tagVersion, 1403 const QString& path, int fmtIdx) 1404 { 1405 ImportTrackDataVector trackDataVector; 1406 filesToTrackData(tagVersion, trackDataVector); 1407 m_textExporter->setTrackData(trackDataVector); 1408 m_textExporter->updateTextUsingConfig(fmtIdx); 1409 if (path == QLatin1String("clipboard")) { 1410 return m_platformTools->writeToClipboard(m_textExporter->getText()); 1411 } 1412 return m_textExporter->exportToFile(path); 1413 } 1414 1415 /** 1416 * Write playlist according to playlist configuration. 1417 * 1418 * @param cfg playlist configuration to use 1419 * 1420 * @return true if ok. 1421 */ 1422 bool Kid3Application::writePlaylist(const PlaylistConfig& cfg) 1423 { 1424 PlaylistCreator plCtr(getDirPath(), cfg); 1425 QItemSelectionModel* selectModel = getFileSelectionModel(); 1426 bool noSelection = !cfg.onlySelectedFiles() || !selectModel || 1427 !selectModel->hasSelection(); 1428 bool ok = true; 1429 QModelIndex rootIndex; 1430 1431 if (cfg.location() == PlaylistConfig::PL_CurrentDirectory) { 1432 // Get first child of parent of current index. 1433 rootIndex = currentOrRootIndex(); 1434 if (rootIndex.model() && rootIndex.model()->rowCount(rootIndex) <= 0) 1435 rootIndex = rootIndex.parent(); 1436 if (const QAbstractItemModel* model = rootIndex.model()) { 1437 for (int row = 0; row < model->rowCount(rootIndex); ++row) { 1438 QModelIndex index = model->index(row, 0, rootIndex); 1439 if (PlaylistCreator::Item plItem(index, plCtr); 1440 plItem.isFile() && 1441 (noSelection || selectModel->isSelected(index))) { 1442 ok = plItem.add() && ok; 1443 } 1444 } 1445 } 1446 } else { 1447 QString selectedDirPrefix; 1448 rootIndex = getRootIndex(); 1449 ModelIterator it(rootIndex); 1450 while (it.hasNext()) { 1451 QModelIndex index = it.next(); 1452 PlaylistCreator::Item plItem(index, plCtr); 1453 bool inSelectedDir = false; 1454 if (plItem.isDir()) { 1455 if (!selectedDirPrefix.isEmpty()) { 1456 if (plItem.getDirName().startsWith(selectedDirPrefix)) { 1457 inSelectedDir = true; 1458 } else { 1459 selectedDirPrefix = QLatin1String(""); 1460 } 1461 } 1462 if (inSelectedDir || noSelection || selectModel->isSelected(index)) { 1463 // if directory is selected, all its files are selected 1464 if (!inSelectedDir) { 1465 selectedDirPrefix = plItem.getDirName(); 1466 } 1467 } 1468 } else if (plItem.isFile()) { 1469 QString dirName = plItem.getDirName(); 1470 if (!selectedDirPrefix.isEmpty()) { 1471 if (dirName.startsWith(selectedDirPrefix)) { 1472 inSelectedDir = true; 1473 } else { 1474 selectedDirPrefix = QLatin1String(""); 1475 } 1476 } 1477 if (inSelectedDir || noSelection || selectModel->isSelected(index)) { 1478 ok = plItem.add() && ok; 1479 } 1480 } 1481 } 1482 } 1483 1484 ok = plCtr.write() && ok; 1485 return ok; 1486 } 1487 1488 /** 1489 * Write empty playlist. 1490 * @param cfg playlist configuration to use 1491 * @param fileName file name for playlist 1492 * @return true if ok. 1493 */ 1494 bool Kid3Application::writeEmptyPlaylist(const PlaylistConfig& cfg, 1495 const QString& fileName) 1496 { 1497 QString path = getDirPath(); 1498 PlaylistCreator plCtr(path, cfg); 1499 if (!path.endsWith(QLatin1Char('/'))) { 1500 path += QLatin1Char('/'); 1501 } 1502 path += fileName; 1503 if (QString ext = cfg.fileExtensionForFormat(); !path.endsWith(ext)) { 1504 path += ext; 1505 } 1506 return plCtr.write(path, QList<QPersistentModelIndex>()); 1507 } 1508 1509 /** 1510 * Write playlist using current playlist configuration. 1511 * 1512 * @return true if ok. 1513 */ 1514 bool Kid3Application::writePlaylist() 1515 { 1516 return writePlaylist(PlaylistConfig::instance()); 1517 } 1518 1519 /** 1520 * Get items of a playlist. 1521 * @param path path to playlist file 1522 * @return list of absolute paths to playlist items. 1523 */ 1524 QStringList Kid3Application::getPlaylistItems(const QString& path) 1525 { 1526 return playlistModel(path)->pathsInPlaylist(); 1527 } 1528 1529 /** 1530 * Set items of a playlist. 1531 * @param path path to playlist file 1532 * @param items list of absolute paths to playlist items 1533 * @return true if ok, false if not all @a items were found and added or 1534 * saving failed. 1535 */ 1536 bool Kid3Application::setPlaylistItems(const QString& path, 1537 const QStringList& items) 1538 { 1539 if (PlaylistModel* model = playlistModel(path); 1540 model->setPathsInPlaylist(items)) { 1541 return model->save(); 1542 } 1543 return false; 1544 } 1545 1546 /** 1547 * Get playlist model for a play list file. 1548 * @param path path to playlist file 1549 * @return playlist model. 1550 */ 1551 PlaylistModel* Kid3Application::playlistModel(const QString& path) 1552 { 1553 // Create an absolute path with a value which does not depend on the file's 1554 // existence or whether the path given is relative or absolute. 1555 QString absPath; 1556 if (!path.isEmpty()) { 1557 QFileInfo fileInfo(path); 1558 absPath = fileInfo.absoluteDir().filePath(fileInfo.fileName()); 1559 } 1560 1561 PlaylistModel* model = m_playlistModels.value(absPath); 1562 if (!model) { 1563 model = new PlaylistModel(m_fileProxyModel, this); 1564 m_playlistModels.insert(absPath, model); 1565 } 1566 model->setPlaylistFile(absPath); 1567 return model; 1568 } 1569 1570 /** 1571 * Check if any playlist model has unsaved modifications. 1572 * @return true if there is a modified playlist model. 1573 */ 1574 bool Kid3Application::hasModifiedPlaylistModel() const 1575 { 1576 for (auto it = m_playlistModels.constBegin(); 1577 it != m_playlistModels.constEnd(); 1578 ++it) { 1579 if ((*it)->isModified()) { 1580 return true; 1581 } 1582 } 1583 return false; 1584 } 1585 1586 /** 1587 * Save all modified playlist models. 1588 */ 1589 void Kid3Application::saveModifiedPlaylistModels() 1590 { 1591 for (auto it = m_playlistModels.begin(); it != m_playlistModels.end(); ++it) { // clazy:exclude=detaching-member 1592 if ((*it)->isModified()) { 1593 (*it)->save(); 1594 } 1595 } 1596 } 1597 1598 /** 1599 * Set track data with tagged files of directory. 1600 * 1601 * @param tagVersion tag version 1602 * @param trackDataList is filled with track data 1603 */ 1604 void Kid3Application::filesToTrackData(Frame::TagVersion tagVersion, 1605 ImportTrackDataVector& trackDataList) 1606 { 1607 TaggedFileOfDirectoryIterator it(currentOrRootIndex()); 1608 while (it.hasNext()) { 1609 TaggedFile* taggedFile = it.next(); 1610 taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile); 1611 trackDataList.push_back(ImportTrackData(*taggedFile, tagVersion)); 1612 } 1613 } 1614 1615 /** 1616 * Set track data model with tagged files of directory. 1617 * 1618 * @param tagVersion tag version 1619 */ 1620 void Kid3Application::filesToTrackDataModel(Frame::TagVersion tagVersion) 1621 { 1622 ImportTrackDataVector trackDataList; 1623 filesToTrackData(tagVersion, trackDataList); 1624 getTrackDataModel()->setTrackData(trackDataList); 1625 } 1626 1627 /** 1628 * Set tagged files of directory from track data model. 1629 * 1630 * @param tagVersion tags to set 1631 */ 1632 void Kid3Application::trackDataModelToFiles(Frame::TagVersion tagVersion) 1633 { 1634 ImportTrackDataVector trackDataList(getTrackDataModel()->getTrackData()); 1635 auto it = trackDataList.begin(); 1636 FrameFilter flt; 1637 if (Frame::TagNumber fltTagNr = Frame::tagNumberFromMask(tagVersion); 1638 fltTagNr < Frame::Tag_NumValues) { 1639 flt = frameModel(fltTagNr)->getEnabledFrameFilter(true); 1640 } 1641 TaggedFileOfDirectoryIterator tfit(currentOrRootIndex()); 1642 while (tfit.hasNext()) { 1643 TaggedFile* taggedFile = tfit.next(); 1644 taggedFile->readTags(false); 1645 if (it != trackDataList.end()) { 1646 it->removeDisabledFrames(flt); 1647 formatFramesIfEnabled(*it); 1648 FOR_TAGS_IN_MASK(tagNr, tagVersion) { 1649 if (tagNr == Frame::Tag_Id3v1) { 1650 taggedFile->setFrames(tagNr, *it, false); 1651 } else { 1652 FrameCollection oldFrames; 1653 taggedFile->getAllFrames(tagNr, oldFrames); 1654 it->markChangedFrames(oldFrames); 1655 taggedFile->setFrames(tagNr, *it, true); 1656 } 1657 } 1658 ++it; 1659 } else { 1660 break; 1661 } 1662 } 1663 1664 if ((tagVersion & (1 << Frame::Tag_Picture)) && 1665 flt.isEnabled(Frame::FT_Picture) && 1666 !trackDataList.getCoverArtUrl().isEmpty()) { 1667 downloadImage(trackDataList.getCoverArtUrl(), ImageForImportTrackData); 1668 } 1669 1670 if (getFileSelectionModel()->hasSelection()) { 1671 emit selectedFilesUpdated(); 1672 } 1673 } 1674 1675 /** 1676 * Download an image file. 1677 * 1678 * @param url URL of image 1679 * @param dest specifies affected files 1680 */ 1681 void Kid3Application::downloadImage(const QUrl& url, DownloadImageDestination dest) 1682 { 1683 if (QUrl imgurl(DownloadClient::getImageUrl(url)); !imgurl.isEmpty()) { 1684 m_downloadImageDest = dest; 1685 m_downloadClient->startDownload(imgurl); 1686 } 1687 } 1688 1689 /** 1690 * Download an image file. 1691 * 1692 * @param url URL of image 1693 * @param allFilesInDir true to add the image to all files in the directory 1694 */ 1695 void Kid3Application::downloadImage(const QString& url, bool allFilesInDir) 1696 { 1697 QUrl imgurl(url); 1698 downloadImage(imgurl, allFilesInDir 1699 ? ImageForAllFilesInDirectory : ImageForSelectedFiles); 1700 } 1701 1702 /** 1703 * Perform a batch import for the selected directories. 1704 * @param profile batch import profile 1705 * @param tagVersion import destination tag versions 1706 */ 1707 void Kid3Application::batchImport(const BatchImportProfile& profile, 1708 Frame::TagVersion tagVersion) 1709 { 1710 m_batchImportProfile = &profile; 1711 m_batchImportTagVersion = tagVersion; 1712 m_batchImportAlbums.clear(); 1713 m_batchImportTrackDataList.clear(); 1714 m_lastProcessedDirName.clear(); 1715 m_batchImporter->clearAborted(); 1716 m_batchImporter->emitReportImportEvent(BatchImporter::ReadingDirectory, 1717 QString()); 1718 // If no directories are selected, process files of the current directory. 1719 QList<QPersistentModelIndex> indexes; 1720 const auto selectedIndexes = m_fileSelectionModel->selectedRows(); 1721 for (const QModelIndex& index : selectedIndexes) { 1722 if (m_fileProxyModel->isDir(index)) { 1723 indexes.append(index); 1724 } 1725 } 1726 if (indexes.isEmpty()) { 1727 indexes.append(m_fileProxyModelRootIndex); 1728 } 1729 1730 connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady, 1731 this, &Kid3Application::batchImportNextFile); 1732 m_fileProxyModelIterator->start(indexes); 1733 } 1734 1735 /** 1736 * Perform a batch import for the selected directories. 1737 * @param profileName batch import profile name 1738 * @param tagVersion import destination tag versions 1739 * @return true if profile with @a profileName found. 1740 */ 1741 bool Kid3Application::batchImport(const QString& profileName, 1742 Frame::TagVersion tagVersion) 1743 { 1744 if (!m_namedBatchImportProfile) { 1745 m_namedBatchImportProfile.reset(new BatchImportProfile); 1746 } 1747 if (BatchImportConfig::instance().getProfileByName( 1748 profileName, *m_namedBatchImportProfile)) { 1749 batchImport(*m_namedBatchImportProfile, tagVersion); 1750 return true; 1751 } 1752 return false; 1753 } 1754 1755 /** 1756 * Apply single file to batch import. 1757 * 1758 * @param index index of file in file proxy model 1759 */ 1760 void Kid3Application::batchImportNextFile(const QPersistentModelIndex& index) 1761 { 1762 bool terminated = !index.isValid(); 1763 if (!terminated) { 1764 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) { 1765 taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile); 1766 if (taggedFile->getDirname() != m_lastProcessedDirName) { 1767 m_lastProcessedDirName = taggedFile->getDirname(); 1768 if (!m_batchImportTrackDataList.isEmpty()) { 1769 m_batchImportAlbums.append(m_batchImportTrackDataList); 1770 } 1771 m_batchImportTrackDataList.clear(); 1772 if (m_batchImporter->isAborted()) { 1773 terminated = true; 1774 } 1775 } 1776 m_batchImportTrackDataList.append(ImportTrackData(*taggedFile, 1777 m_batchImportTagVersion)); 1778 } 1779 } 1780 if (terminated) { 1781 m_fileProxyModelIterator->abort(); 1782 disconnect(m_fileProxyModelIterator, 1783 &FileProxyModelIterator::nextReady, 1784 this, &Kid3Application::batchImportNextFile); 1785 if (!m_batchImporter->isAborted()) { 1786 if (!m_batchImportTrackDataList.isEmpty()) { 1787 m_batchImportAlbums.append(m_batchImportTrackDataList); 1788 } 1789 if (Frame::TagNumber fltTagNr = 1790 Frame::tagNumberFromMask(m_batchImportTagVersion); 1791 fltTagNr < Frame::Tag_NumValues) { 1792 m_batchImporter->setFrameFilter( 1793 frameModel(fltTagNr)->getEnabledFrameFilter(true)); 1794 } 1795 m_batchImporter->start(m_batchImportAlbums, *m_batchImportProfile, 1796 m_batchImportTagVersion); 1797 } 1798 } 1799 } 1800 1801 /** 1802 * Format frames if format while editing is switched on. 1803 * 1804 * @param frames frames 1805 */ 1806 void Kid3Application::formatFramesIfEnabled(FrameCollection& frames) const 1807 { 1808 TagFormatConfig::instance().formatFramesIfEnabled(frames); 1809 } 1810 1811 /** 1812 * Get name of selected file. 1813 * 1814 * @return absolute file name, ends with "/" if it is a directory. 1815 */ 1816 QString Kid3Application::getFileNameOfSelectedFile() 1817 { 1818 QModelIndex index = getFileSelectionModel()->currentIndex(); 1819 if (QString dirname = FileProxyModel::getPathIfIndexOfDir(index); 1820 !dirname.isNull()) { 1821 if (!dirname.endsWith(QLatin1Char('/'))) dirname += QLatin1Char('/'); 1822 return dirname; 1823 } 1824 if (TaggedFile* taggedFile = 1825 FileProxyModel::getTaggedFileOfIndex(index)) { 1826 return taggedFile->getAbsFilename(); 1827 } 1828 return QLatin1String(""); 1829 } 1830 1831 /** 1832 * Set name of selected file. 1833 * Exactly one file has to be selected. 1834 * 1835 * @param name file name. 1836 */ 1837 void Kid3Application::setFileNameOfSelectedFile(const QString& name) 1838 { 1839 if (TaggedFile* taggedFile = getSelectedFile()) { 1840 QFileInfo fi(name); 1841 taggedFile->setFilename(fi.fileName()); 1842 emit selectedFilesUpdated(); 1843 } 1844 } 1845 1846 /** 1847 * Apply filename format. 1848 */ 1849 void Kid3Application::applyFilenameFormat() 1850 { 1851 emit fileSelectionUpdateRequested(); 1852 SelectedTaggedFileIterator it(getRootIndex(), 1853 getFileSelectionModel(), 1854 true); 1855 while (it.hasNext()) { 1856 TaggedFile* taggedFile = it.next(); 1857 taggedFile->readTags(false); 1858 QString fn = taggedFile->getFilename(); 1859 FilenameFormatConfig::instance().formatString(fn); 1860 taggedFile->setFilename(fn); 1861 } 1862 emit selectedFilesUpdated(); 1863 } 1864 1865 /** 1866 * Apply tag format. 1867 */ 1868 void Kid3Application::applyTagFormat() 1869 { 1870 emit fileSelectionUpdateRequested(); 1871 FrameCollection frames; 1872 FrameFilter flt[Frame::Tag_NumValues]; 1873 FOR_ALL_TAGS(tagNr) { 1874 flt[tagNr] = frameModel(tagNr)->getEnabledFrameFilter(true); 1875 } 1876 SelectedTaggedFileIterator it(getRootIndex(), 1877 getFileSelectionModel(), 1878 true); 1879 while (it.hasNext()) { 1880 TaggedFile* taggedFile = it.next(); 1881 taggedFile->readTags(false); 1882 FOR_ALL_TAGS(tagNr) { 1883 taggedFile->getAllFrames(tagNr, frames); 1884 frames.removeDisabledFrames(flt[tagNr]); 1885 TagFormatConfig::instance().formatFrames(frames); 1886 taggedFile->setFrames(tagNr, frames); 1887 } 1888 } 1889 emit selectedFilesUpdated(); 1890 } 1891 1892 /** 1893 * Apply text encoding. 1894 * Set the text encoding selected in the settings Tags/ID3v2/Text encoding 1895 * for all selected files which have an ID3v2 tag. 1896 */ 1897 void Kid3Application::applyTextEncoding() 1898 { 1899 emit fileSelectionUpdateRequested(); 1900 Frame::TextEncoding encoding = frameTextEncodingFromConfig(); 1901 FrameCollection frames; 1902 SelectedTaggedFileIterator it(getRootIndex(), 1903 getFileSelectionModel(), 1904 true); 1905 while (it.hasNext()) { 1906 TaggedFile* taggedFile = it.next(); 1907 taggedFile->readTags(false); 1908 taggedFile->getAllFrames(Frame::Tag_Id3v2, frames); 1909 for (auto frameIt = frames.begin(); frameIt != frames.end(); ++frameIt) { 1910 auto& frame = const_cast<Frame&>(*frameIt); 1911 Frame::TextEncoding enc = encoding; 1912 if (taggedFile->getTagFormat(Frame::Tag_Id3v2) == QLatin1String("ID3v2.3.0")) { 1913 // TagLib sets the ID3v2.3.0 frame containing the date internally with 1914 // ISO-8859-1, so the encoding cannot be set for such frames. 1915 if (taggedFile->taggedFileKey() == QLatin1String("TaglibMetadata") && 1916 frame.getType() == Frame::FT_Date && 1917 enc != Frame::TE_ISO8859_1) 1918 continue; 1919 // Only ISO-8859-1 and UTF16 are allowed for ID3v2.3.0. 1920 if (enc != Frame::TE_ISO8859_1) 1921 enc = Frame::TE_UTF16; 1922 } 1923 Frame::FieldList& fields = frame.fieldList(); 1924 for (auto fieldIt = fields.begin(); fieldIt != fields.end(); ++fieldIt) { 1925 if (fieldIt->m_id == Frame::ID_TextEnc && 1926 fieldIt->m_value.toInt() != enc) { 1927 fieldIt->m_value = enc; 1928 frame.setValueChanged(); 1929 } 1930 } 1931 } 1932 taggedFile->setFrames(Frame::Tag_Id3v2, frames); 1933 } 1934 emit selectedFilesUpdated(); 1935 } 1936 1937 /** 1938 * Copy tags into copy buffer. 1939 * 1940 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 1941 */ 1942 void Kid3Application::copyTags(Frame::TagVersion tagMask) 1943 { 1944 Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask); 1945 if (tagNr >= Frame::Tag_NumValues) 1946 return; 1947 1948 emit fileSelectionUpdateRequested(); 1949 m_copyTags = frameModel(tagNr)->frames().copyEnabledFrames( 1950 frameModel(tagNr)->getEnabledFrameFilter(true)); 1951 } 1952 1953 /** 1954 * Paste from copy buffer to tags. 1955 * 1956 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 1957 */ 1958 void Kid3Application::pasteTags(Frame::TagVersion tagMask) 1959 { 1960 Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask); 1961 if (tagNr >= Frame::Tag_NumValues) 1962 return; 1963 1964 emit fileSelectionUpdateRequested(); 1965 FrameCollection frames(m_copyTags.copyEnabledFrames( 1966 frameModel(tagNr)->getEnabledFrameFilter(true))); 1967 formatFramesIfEnabled(frames); 1968 SelectedTaggedFileIterator it(getRootIndex(), 1969 getFileSelectionModel(), 1970 false); 1971 while (it.hasNext()) { 1972 it.next()->setFrames(tagNr, frames, false); 1973 } 1974 emit selectedFilesUpdated(); 1975 } 1976 1977 /** 1978 * Set tag from other tag. 1979 * 1980 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 1981 */ 1982 void Kid3Application::copyToOtherTag(Frame::TagVersion tagMask) 1983 { 1984 Frame::TagNumber dstTagNr = Frame::tagNumberFromMask(tagMask); 1985 if (dstTagNr >= Frame::Tag_NumValues) 1986 return; 1987 1988 Frame::TagNumber srcTagNr = dstTagNr == Frame::Tag_2 1989 ? Frame::Tag_1 : Frame::Tag_2; 1990 copyTag(srcTagNr, dstTagNr); 1991 } 1992 1993 /** 1994 * Copy from a tag to another tag. 1995 * @param srcTagNr source tag number 1996 * @param dstTagNr destination tag number 1997 */ 1998 void Kid3Application::copyTag(Frame::TagNumber srcTagNr, Frame::TagNumber dstTagNr) 1999 { 2000 emit fileSelectionUpdateRequested(); 2001 FrameCollection frames; 2002 FrameFilter flt(frameModel(dstTagNr)->getEnabledFrameFilter(true)); 2003 SelectedTaggedFileIterator it(getRootIndex(), 2004 getFileSelectionModel(), 2005 false); 2006 while (it.hasNext()) { 2007 TaggedFile* taggedFile = it.next(); 2008 taggedFile->getAllFrames(srcTagNr, frames); 2009 frames.removeDisabledFrames(flt); 2010 frames.setIndexesInvalid(); 2011 formatFramesIfEnabled(frames); 2012 taggedFile->setFrames(dstTagNr, frames, false); 2013 } 2014 emit selectedFilesUpdated(); 2015 } 2016 2017 /** 2018 * Remove tags in selected files. 2019 * 2020 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 2021 */ 2022 void Kid3Application::removeTags(Frame::TagVersion tagMask) 2023 { 2024 Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask); 2025 if (tagNr >= Frame::Tag_NumValues) 2026 return; 2027 2028 emit fileSelectionUpdateRequested(); 2029 FrameFilter flt(frameModel(tagNr)->getEnabledFrameFilter(true)); 2030 SelectedTaggedFileIterator it(getRootIndex(), 2031 getFileSelectionModel(), 2032 false); 2033 while (it.hasNext()) { 2034 it.next()->deleteFrames(tagNr, flt); 2035 } 2036 emit selectedFilesUpdated(); 2037 } 2038 2039 /** 2040 * Set tags according to filename. 2041 * 2042 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 2043 */ 2044 void Kid3Application::getTagsFromFilename(Frame::TagVersion tagMask) 2045 { 2046 Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask); 2047 if (tagNr >= Frame::Tag_NumValues) 2048 return; 2049 2050 emit fileSelectionUpdateRequested(); 2051 FrameCollection frames; 2052 QItemSelectionModel* selectModel = getFileSelectionModel(); 2053 SelectedTaggedFileIterator it(getRootIndex(), 2054 selectModel, 2055 false); 2056 FrameFilter flt(frameModel(tagNr)->getEnabledFrameFilter(true)); 2057 while (it.hasNext()) { 2058 TaggedFile* taggedFile = it.next(); 2059 taggedFile->getAllFrames(tagNr, frames); 2060 taggedFile->getTagsFromFilename( 2061 frames, FileConfig::instance().fromFilenameFormat()); 2062 frames.removeDisabledFrames(flt); 2063 formatFramesIfEnabled(frames); 2064 taggedFile->setFrames(tagNr, frames); 2065 } 2066 emit selectedFilesUpdated(); 2067 } 2068 2069 /** 2070 * Set filename according to tags. 2071 * If a single file is selected the tags in the GUI controls 2072 * are used, else the tags in the multiple selected files. 2073 * 2074 * @param tagVersion tag version 2075 */ 2076 void Kid3Application::getFilenameFromTags(Frame::TagVersion tagVersion) 2077 { 2078 emit fileSelectionUpdateRequested(); 2079 QItemSelectionModel* selectModel = getFileSelectionModel(); 2080 SelectedTaggedFileIterator it(getRootIndex(), 2081 selectModel, 2082 false); 2083 while (it.hasNext()) { 2084 TaggedFile* taggedFile = it.next(); 2085 if (TrackData trackData(*taggedFile, tagVersion); 2086 !trackData.isEmptyOrInactive()) { 2087 taggedFile->setFilenameFormattedIfEnabled( 2088 trackData.formatFilenameFromTags(FileConfig::instance().toFilenameFormat())); 2089 } 2090 } 2091 emit selectedFilesUpdated(); 2092 } 2093 2094 /** 2095 * Get the selected file. 2096 * 2097 * @return the selected file, 2098 * 0 if not exactly one file is selected 2099 */ 2100 TaggedFile* Kid3Application::getSelectedFile() 2101 { 2102 QModelIndexList selItems( 2103 m_fileSelectionModel->selectedRows()); 2104 if (selItems.size() != 1) 2105 return nullptr; 2106 2107 return FileProxyModel::getTaggedFileOfIndex(selItems.first()); 2108 } 2109 2110 /** 2111 * Update the stored current selection with the list of all selected items. 2112 */ 2113 void Kid3Application::updateCurrentSelection() 2114 { 2115 m_currentSelection.clear(); 2116 const auto selectedIndexes = m_fileSelectionModel->selectedRows(); 2117 for (const QModelIndex& index : selectedIndexes) { 2118 m_currentSelection.append(QPersistentModelIndex(index)); 2119 } 2120 } 2121 2122 /** 2123 * Edit selected frame. 2124 * @param tagNr tag number 2125 */ 2126 void Kid3Application::editFrame(Frame::TagNumber tagNr) 2127 { 2128 FrameList* framelist = m_framelist[tagNr]; 2129 emit fileSelectionUpdateRequested(); 2130 m_editFrameTaggedFile = getSelectedFile(); 2131 if (const Frame* selectedFrame = frameModel(tagNr)->getFrameOfIndex( 2132 getFramesSelectionModel(tagNr)->currentIndex())) { 2133 if (m_editFrameTaggedFile) { 2134 framelist->setTaggedFile(m_editFrameTaggedFile); 2135 framelist->setFrame(*selectedFrame); 2136 if (selectedFrame->getIndex() != -1) { 2137 framelist->editFrame(); 2138 } else { 2139 // Edit a frame which does not exist, switch to add mode. 2140 m_addFrameTaggedFile = m_editFrameTaggedFile; 2141 m_editFrameTaggedFile = nullptr; 2142 framelist->addAndEditFrame(); 2143 } 2144 } else { 2145 // multiple files selected 2146 // Get the first selected file by using a temporary iterator. 2147 if (TaggedFile* firstFile = SelectedTaggedFileIterator( 2148 getRootIndex(), getFileSelectionModel(), false).peekNext()) { 2149 framelist->setTaggedFile(firstFile); 2150 m_editFrameName = framelist->getSelectedName(); 2151 if (!m_editFrameName.isEmpty()) { 2152 framelist->setFrame(*selectedFrame); 2153 framelist->addFrameFieldList(); 2154 framelist->editFrame(); 2155 } 2156 } 2157 } 2158 } 2159 } 2160 2161 /** 2162 * Called when a frame is edited. 2163 * @param frame edited frame, 0 if canceled 2164 */ 2165 void Kid3Application::onFrameEdited(const Frame* frame) 2166 { 2167 auto framelist = qobject_cast<FrameList*>(sender()); 2168 if (!framelist || !frame) 2169 return; 2170 2171 Frame::TagNumber tagNr = framelist->tagNumber(); 2172 if (m_editFrameTaggedFile) { 2173 emit frameModified(m_editFrameTaggedFile, tagNr); 2174 } else { 2175 framelist->setFrame(*frame); 2176 2177 // Start a new iteration because the file selection model can be 2178 // changed by editFrameOfTaggedFile(), e.g. when a file is exported 2179 // from a picture frame. 2180 SelectedTaggedFileIterator tfit(getRootIndex(), 2181 getFileSelectionModel(), 2182 false); 2183 while (tfit.hasNext()) { 2184 TaggedFile* currentFile = tfit.next(); 2185 FrameCollection frames; 2186 currentFile->getAllFrames(tagNr, frames); 2187 for (auto it = frames.cbegin(); it != frames.cend(); ++it) { 2188 if (it->getName() == m_editFrameName) { 2189 currentFile->deleteFrame(tagNr, *it); 2190 break; 2191 } 2192 } 2193 framelist->setTaggedFile(currentFile); 2194 framelist->pasteFrame(); 2195 } 2196 emit selectedFilesUpdated(); 2197 framelist->selectByName(frame->getName()); 2198 } 2199 } 2200 2201 /** 2202 * Delete selected frame. 2203 * @param tagNr tag number 2204 * @param frameName name of frame to delete, empty to delete selected frame 2205 * @param index 0 for first frame with @a frameName, 1 for second, etc. 2206 */ 2207 void Kid3Application::deleteFrame(Frame::TagNumber tagNr, 2208 const QString& frameName, int index) 2209 { 2210 FrameList* framelist = m_framelist[tagNr]; 2211 emit fileSelectionUpdateRequested(); 2212 if (TaggedFile* taggedFile = getSelectedFile(); 2213 taggedFile && frameName.isEmpty()) { 2214 // delete selected frame from single file 2215 if (!framelist->deleteFrame()) { 2216 // frame not found 2217 return; 2218 } 2219 emit frameModified(taggedFile, tagNr); 2220 } else { 2221 // multiple files selected or frame name specified 2222 bool firstFile = true; 2223 QString name; 2224 SelectedTaggedFileIterator tfit(getRootIndex(), 2225 getFileSelectionModel(), 2226 false); 2227 while (tfit.hasNext()) { 2228 TaggedFile* currentFile = tfit.next(); 2229 if (firstFile) { 2230 firstFile = false; 2231 taggedFile = currentFile; 2232 framelist->setTaggedFile(taggedFile); 2233 name = frameName.isEmpty() ? framelist->getSelectedName() : frameName; 2234 } 2235 FrameCollection frames; 2236 currentFile->getAllFrames(tagNr, frames); 2237 int currentIndex = 0; 2238 for (auto it = frames.cbegin(); it != frames.cend(); ++it) { 2239 if (it->getName() == name) { 2240 if (currentIndex == index) { 2241 currentFile->deleteFrame(tagNr, *it); 2242 break; 2243 } 2244 ++currentIndex; 2245 } 2246 } 2247 } 2248 framelist->saveCursor(); 2249 emit selectedFilesUpdated(); 2250 framelist->restoreCursor(); 2251 } 2252 } 2253 2254 /** 2255 * Select a frame type and add such a frame to frame list. 2256 * @param tagNr tag number 2257 * @param frame frame to add, if 0 the user has to select and edit the frame 2258 * @param edit if true and a frame is set, the user can edit the frame before 2259 * it is added 2260 */ 2261 void Kid3Application::addFrame(Frame::TagNumber tagNr, 2262 const Frame* frame, bool edit) 2263 { 2264 if (tagNr >= Frame::Tag_NumValues) 2265 return; 2266 2267 FrameList* framelist = m_framelist[tagNr]; 2268 emit fileSelectionUpdateRequested(); 2269 TaggedFile* currentFile = nullptr; 2270 m_addFrameTaggedFile = getSelectedFile(); 2271 if (m_addFrameTaggedFile) { 2272 currentFile = m_addFrameTaggedFile; 2273 } else { 2274 // multiple files selected 2275 if (SelectedTaggedFileIterator tfit( 2276 getRootIndex(), getFileSelectionModel(), false); 2277 tfit.hasNext()) { 2278 currentFile = tfit.next(); 2279 framelist->setTaggedFile(currentFile); 2280 } 2281 } 2282 2283 if (currentFile) { 2284 if (edit) { 2285 if (frame) { 2286 framelist->setFrame(*frame); 2287 framelist->addAndEditFrame(); 2288 } else { 2289 framelist->selectAddAndEditFrame(); 2290 } 2291 } else { 2292 framelist->setFrame(*frame); 2293 onFrameAdded(framelist->pasteFrame() ? &framelist->getFrame() 2294 : nullptr, tagNr); 2295 } 2296 } 2297 } 2298 2299 /** 2300 * Called when a frame is added. 2301 * @param frame edited frame, 0 if canceled 2302 * @param tagNr tag number used if slot is not invoked by framelist signal 2303 */ 2304 void Kid3Application::onFrameAdded(const Frame* frame, Frame::TagNumber tagNr) 2305 { 2306 if (!frame) 2307 return; 2308 2309 auto framelist = qobject_cast<FrameList*>(sender()); 2310 if (!framelist) { 2311 framelist = m_framelist[tagNr]; 2312 } 2313 if (m_addFrameTaggedFile) { 2314 emit frameModified(m_addFrameTaggedFile, tagNr); 2315 if (framelist->isPictureFrame()) { 2316 // update preview picture 2317 emit selectedFilesUpdated(); 2318 } 2319 } else { 2320 // multiple files selected 2321 bool firstFile = true; 2322 int frameId = -1; 2323 framelist->setFrame(*frame); 2324 2325 SelectedTaggedFileIterator tfit(getRootIndex(), 2326 getFileSelectionModel(), 2327 false); 2328 while (tfit.hasNext()) { 2329 TaggedFile* currentFile = tfit.next(); 2330 if (firstFile) { 2331 firstFile = false; 2332 m_addFrameTaggedFile = currentFile; 2333 framelist->setTaggedFile(currentFile); 2334 frameId = framelist->getSelectedId(); 2335 } else { 2336 framelist->setTaggedFile(currentFile); 2337 framelist->pasteFrame(); 2338 } 2339 } 2340 framelist->setTaggedFile(m_addFrameTaggedFile); 2341 if (frameId != -1) { 2342 framelist->setSelectedId(frameId); 2343 } 2344 emit selectedFilesUpdated(); 2345 framelist->selectByName(frame->getName()); 2346 } 2347 } 2348 2349 /** 2350 * Called by framelist when a frame is added. 2351 * Same as onFrameAdded() with default argument, provided for functor-based 2352 * connections. 2353 * @param frame added frame, 0 if canceled 2354 */ 2355 void Kid3Application::onTag2FrameAdded(const Frame* frame) 2356 { 2357 onFrameAdded(frame, Frame::Tag_2); 2358 } 2359 2360 /** 2361 * Select a frame type and add such a frame to the frame list. 2362 * @param tagNr tag number 2363 */ 2364 void Kid3Application::selectAndAddFrame(Frame::TagNumber tagNr) 2365 { 2366 addFrame(tagNr, nullptr, true); 2367 } 2368 2369 /** 2370 * Edit a picture frame if one exists or add a new one. 2371 */ 2372 void Kid3Application::editOrAddPicture() 2373 { 2374 if (m_framelist[Frame::Tag_Picture]->selectByName(QLatin1String("Picture"))) { 2375 editFrame(Frame::Tag_Picture); 2376 } else { 2377 PictureFrame frame; 2378 PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig()); 2379 addFrame(Frame::Tag_Picture, &frame, true); 2380 } 2381 } 2382 2383 /** 2384 * Open directory or add pictures on drop. 2385 * 2386 * @param paths paths of directories or files in directory 2387 * @param isInternal true if this is an internal drop 2388 */ 2389 void Kid3Application::dropLocalFiles(const QStringList& paths, bool isInternal) 2390 { 2391 QStringList filePaths; 2392 QStringList picturePaths; 2393 for (QString txt : paths) { 2394 if (int lfPos = txt.indexOf(QLatin1Char('\n')); 2395 lfPos > 0 && lfPos < txt.length() - 1) { 2396 txt.truncate(lfPos + 1); 2397 } 2398 if (QString dir = txt.trimmed(); !dir.isEmpty()) { 2399 if (dir.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || 2400 dir.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive) || 2401 dir.endsWith(QLatin1String(".webp"), Qt::CaseInsensitive) || 2402 dir.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)) { 2403 picturePaths.append(dir); // clazy:exclude=reserve-candidates 2404 } else { 2405 filePaths.append(dir); // clazy:exclude=reserve-candidates 2406 } 2407 } 2408 } 2409 if (!filePaths.isEmpty() && !isInternal) { 2410 resetFileFilterIfNotMatching(filePaths); 2411 emit fileSelectionUpdateRequested(); 2412 emit confirmedOpenDirectoryRequested(filePaths); 2413 } else if (!picturePaths.isEmpty()) { 2414 const auto constPicturePaths = picturePaths; 2415 for (const QString& picturePath : constPicturePaths) { 2416 PictureFrame frame; 2417 if (PictureFrame::setDataFromFile(frame, picturePath)) { 2418 QString fileName(picturePath); 2419 if (int slashPos = fileName.lastIndexOf(QLatin1Char('/')); slashPos != -1) { 2420 fileName = fileName.mid(slashPos + 1); 2421 } 2422 PictureFrame::setMimeTypeFromFileName(frame, fileName); 2423 PictureFrame::setDescription(frame, fileName); 2424 PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig()); 2425 addFrame(Frame::Tag_Picture, &frame); 2426 emit selectedFilesUpdated(); 2427 } 2428 } 2429 } 2430 } 2431 2432 /** 2433 * Open directory or add pictures on drop. 2434 * 2435 * @param paths paths of directories or files in directory 2436 */ 2437 void Kid3Application::openDrop(const QStringList& paths) 2438 { 2439 dropLocalFiles(paths, false); 2440 } 2441 2442 /** 2443 * Handle drop of URLs. 2444 * 2445 * @param urlList picture, tagged file and folder URLs to handle (if local) 2446 * @param isInternal true if this is an internal drop 2447 */ 2448 void Kid3Application::dropUrls(const QList<QUrl>& urlList, bool isInternal) 2449 { 2450 QList urls(urlList); 2451 #ifdef Q_OS_MAC 2452 // workaround for https://bugreports.qt-project.org/browse/QTBUG-40449 2453 for (auto it = urls.begin(); it != urls.end(); ++it) { 2454 if (it->host().isEmpty() && 2455 it->path().startsWith(QLatin1String("/.file/id="))) { 2456 *it = QUrl::fromCFURL(CFURLCreateFilePathURL(NULL, it->toCFURL(), NULL)); 2457 } 2458 } 2459 #endif 2460 if (urls.isEmpty()) 2461 return; 2462 if (urls.first().isLocalFile()) { 2463 QStringList localFiles; 2464 for (auto it = urls.constBegin(); it != urls.constEnd(); ++it) { 2465 localFiles.append(it->toLocalFile()); 2466 } 2467 dropLocalFiles(localFiles, isInternal); 2468 } else { 2469 dropUrl(urls.first()); 2470 } 2471 } 2472 2473 /** 2474 * Handle drop of URLs. 2475 * 2476 * @param urlList picture, tagged file and folder URLs to handle (if local) 2477 */ 2478 void Kid3Application::openDropUrls(const QList<QUrl>& urlList) 2479 { 2480 dropUrls(urlList, false); 2481 } 2482 2483 /** 2484 * Add picture on drop. 2485 * 2486 * @param frame dropped picture frame 2487 */ 2488 void Kid3Application::dropImage(Frame* frame) 2489 { 2490 PictureFrame::setTextEncoding(*frame, frameTextEncodingFromConfig()); 2491 addFrame(Frame::Tag_Picture, frame); 2492 emit selectedFilesUpdated(); 2493 } 2494 2495 /** 2496 * Handle URL on drop. 2497 * 2498 * @param url dropped URL. 2499 */ 2500 void Kid3Application::dropUrl(const QUrl& url) 2501 { 2502 downloadImage(url, Kid3Application::ImageForSelectedFiles); 2503 } 2504 2505 /** 2506 * Add a downloaded image. 2507 * 2508 * @param data HTTP response of download 2509 * @param mimeType MIME type of data 2510 * @param url URL of downloaded data 2511 */ 2512 void Kid3Application::imageDownloaded(const QByteArray& data, 2513 const QString& mimeType, const QString& url) 2514 { 2515 // An empty mime type is accepted to allow downloads via FTP. 2516 if (mimeType.startsWith(QLatin1String("image")) || 2517 mimeType.isEmpty()) { 2518 PictureFrame frame(data, url, PictureFrame::PT_CoverFront, mimeType, 2519 frameTextEncodingFromConfig()); 2520 if (getDownloadImageDestination() == ImageForAllFilesInDirectory) { 2521 TaggedFileOfDirectoryIterator it(currentOrRootIndex()); 2522 while (it.hasNext()) { 2523 TaggedFile* taggedFile = it.next(); 2524 taggedFile->readTags(false); 2525 taggedFile->addFrame(Frame::Tag_Picture, frame); 2526 } 2527 } else if (getDownloadImageDestination() == ImageForImportTrackData) { 2528 const ImportTrackDataVector& trackDataVector( 2529 getTrackDataModel()->trackData()); 2530 for (auto it = trackDataVector.constBegin(); 2531 it != trackDataVector.constEnd(); 2532 ++it) { 2533 if (TaggedFile* taggedFile; 2534 it->isEnabled() && (taggedFile = it->getTaggedFile()) != nullptr) { 2535 taggedFile->readTags(false); 2536 taggedFile->addFrame(Frame::Tag_Picture, frame); 2537 } 2538 } 2539 } else { 2540 addFrame(Frame::Tag_Picture, &frame); 2541 } 2542 emit selectedFilesUpdated(); 2543 } 2544 } 2545 2546 /** 2547 * Set the first file as the current file. 2548 * 2549 * @param select true to select the file 2550 * @param onlyTaggedFiles only consider tagged files 2551 * 2552 * @return true if a file exists. 2553 */ 2554 bool Kid3Application::firstFile(bool select, bool onlyTaggedFiles) 2555 { 2556 m_fileSelectionModel->setCurrentIndex(getRootIndex(), 2557 QItemSelectionModel::NoUpdate); 2558 return nextFile(select, onlyTaggedFiles); 2559 } 2560 2561 /** 2562 * Set the next file as the current file. 2563 * 2564 * @param select true to select the file 2565 * @param onlyTaggedFiles only consider tagged files 2566 * 2567 * @return true if a next file exists. 2568 */ 2569 bool Kid3Application::nextFile(bool select, bool onlyTaggedFiles) 2570 { 2571 QModelIndex next(m_fileSelectionModel->currentIndex()); 2572 do { 2573 QModelIndex current = next; 2574 next = QModelIndex(); 2575 if (m_fileProxyModel->rowCount(current) > 0) { 2576 // to first child 2577 next = m_fileProxyModel->index(0, 0, current); 2578 } else { 2579 QModelIndex parent = current; 2580 while (!next.isValid() && parent.isValid()) { 2581 // to next sibling or next sibling of parent 2582 int row = parent.row(); 2583 if (parent == getRootIndex() || !parent.isValid()) { 2584 // do not move beyond root index 2585 return false; 2586 } 2587 parent = parent.parent(); 2588 if (row + 1 < m_fileProxyModel->rowCount(parent)) { 2589 // to next sibling 2590 next = m_fileProxyModel->index(row + 1, 0, parent); 2591 } 2592 } 2593 } 2594 } while (onlyTaggedFiles && !FileProxyModel::getTaggedFileOfIndex(next)); 2595 if (!next.isValid()) 2596 return false; 2597 m_fileSelectionModel->setCurrentIndex(next, 2598 select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows 2599 : QItemSelectionModel::Current); 2600 return true; 2601 } 2602 2603 /** 2604 * Set the previous file as the current file. 2605 * 2606 * @param select true to select the file 2607 * @param onlyTaggedFiles only consider tagged files 2608 * 2609 * @return true if a previous file exists. 2610 */ 2611 bool Kid3Application::previousFile(bool select, bool onlyTaggedFiles) 2612 { 2613 QModelIndex previous(m_fileSelectionModel->currentIndex()); 2614 do { 2615 QModelIndex current = previous; 2616 previous = QModelIndex(); 2617 if (int row = current.row() - 1; row >= 0) { 2618 // to last leafnode of previous sibling 2619 previous = current.sibling(row, 0); 2620 row = m_fileProxyModel->rowCount(previous) - 1; 2621 while (row >= 0) { 2622 previous = m_fileProxyModel->index(row, 0, previous); 2623 row = m_fileProxyModel->rowCount(previous) - 1; 2624 } 2625 } else { 2626 // to parent 2627 previous = current.parent(); 2628 } 2629 if (previous == getRootIndex() || !previous.isValid()) { 2630 return false; 2631 } 2632 } while (onlyTaggedFiles && !FileProxyModel::getTaggedFileOfIndex(previous)); 2633 if (!previous.isValid()) 2634 return false; 2635 m_fileSelectionModel->setCurrentIndex(previous, 2636 select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows 2637 : QItemSelectionModel::Current); 2638 return true; 2639 } 2640 2641 /** 2642 * Select or deselect the current file. 2643 * 2644 * @param select true to select the file, false to deselect it 2645 * 2646 * @return true if a current file exists. 2647 */ 2648 bool Kid3Application::selectCurrentFile(bool select) 2649 { 2650 QModelIndex currentIdx(m_fileSelectionModel->currentIndex()); 2651 if (!currentIdx.isValid() || currentIdx == getRootIndex()) 2652 return false; 2653 2654 m_fileSelectionModel->setCurrentIndex(currentIdx, 2655 (select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect) | 2656 QItemSelectionModel::Rows); 2657 return true; 2658 } 2659 2660 /** 2661 * Select all files. 2662 */ 2663 void Kid3Application::selectAllFiles() 2664 { 2665 QItemSelection selection; 2666 ModelIterator it(m_fileProxyModelRootIndex); 2667 while (it.hasNext()) { 2668 selection.append(QItemSelectionRange(it.next())); 2669 } 2670 m_fileSelectionModel->select(selection, 2671 QItemSelectionModel::Select | QItemSelectionModel::Rows); 2672 } 2673 2674 /** 2675 * Deselect all files. 2676 */ 2677 void Kid3Application::deselectAllFiles() 2678 { 2679 m_fileSelectionModel->clearSelection(); 2680 } 2681 2682 /** 2683 * Select all files in the current directory. 2684 */ 2685 void Kid3Application::selectAllInDirectory() 2686 { 2687 if (QModelIndex parent = m_fileSelectionModel->currentIndex(); 2688 parent.isValid()) { 2689 if (!m_fileProxyModel->hasChildren(parent)) { 2690 parent = parent.parent(); 2691 } 2692 QItemSelection selection; 2693 for (int row = 0; row < m_fileProxyModel->rowCount(parent); ++row) { 2694 if (QModelIndex index = m_fileProxyModel->index(row, 0, parent); 2695 !m_fileProxyModel->hasChildren(index)) { 2696 selection.append(QItemSelectionRange(index)); // clazy:exclude=reserve-candidates 2697 } 2698 } 2699 m_fileSelectionModel->select(selection, 2700 QItemSelectionModel::Select | QItemSelectionModel::Rows); 2701 } 2702 } 2703 2704 /** 2705 * Invert current selection. 2706 */ 2707 void Kid3Application::invertSelection() 2708 { 2709 QModelIndexList todo; 2710 todo.append(m_fileProxyModelRootIndex); 2711 while (!todo.isEmpty()) { 2712 QModelIndex parent = todo.takeFirst(); 2713 QModelIndex first, last; 2714 for (int row = 0, numRows = m_fileProxyModel->rowCount(parent); 2715 row < numRows; 2716 ++row) { 2717 QModelIndex idx = m_fileProxyModel->index(row, 0, parent); 2718 if (row == 0) { 2719 first = idx; 2720 } else if (row == numRows - 1) { 2721 last = idx; 2722 } 2723 if (m_fileProxyModel->hasChildren(idx)) { 2724 todo.append(idx); 2725 } 2726 } 2727 m_fileSelectionModel->select( 2728 QItemSelection(first, last), 2729 QItemSelectionModel::Toggle | QItemSelectionModel::Rows); 2730 } 2731 } 2732 2733 /** 2734 * Set a specific file as the current file. 2735 * 2736 * @param filePath path to file 2737 * @param select true to select the file 2738 * 2739 * @return true if file exists. 2740 */ 2741 bool Kid3Application::selectFile(const QString& filePath, bool select) 2742 { 2743 QModelIndex index = m_fileProxyModel->index(filePath); 2744 if (!index.isValid()) 2745 return false; 2746 2747 m_fileSelectionModel->setCurrentIndex(index, 2748 select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows 2749 : QItemSelectionModel::Current); 2750 return true; 2751 } 2752 2753 /** 2754 * Get paths to all selected files. 2755 * @param onlyTaggedFiles only consider tagged files 2756 * @return list of absolute file paths. 2757 */ 2758 QStringList Kid3Application::getSelectedFilePaths(bool onlyTaggedFiles) const 2759 { 2760 QStringList files; 2761 const QModelIndexList selItems = m_fileSelectionModel->selectedRows(); 2762 if (onlyTaggedFiles) { 2763 for (const QModelIndex& index : selItems) { 2764 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) 2765 { 2766 files.append(taggedFile->getAbsFilename()); 2767 } 2768 } 2769 } else { 2770 files.reserve(selItems.size()); 2771 for (const QModelIndex& index : selItems) { 2772 files.append(m_fileProxyModel->filePath(index)); 2773 } 2774 } 2775 return files; 2776 } 2777 2778 /** 2779 * Fetch entries of directory if not already fetched. 2780 * This works like FileList::expand(), but without expanding tree view 2781 * items and independent of the GUI. The processing is done in the background 2782 * by FileSystemModel, so the fetched items are not immediately available 2783 * after calling this method. 2784 * 2785 * @param index index of directory item 2786 */ 2787 void Kid3Application::fetchDirectory(const QModelIndex& index) 2788 { 2789 if (index.isValid() && m_fileProxyModel->canFetchMore(index)) { 2790 m_fileProxyModel->fetchMore(index); 2791 } 2792 } 2793 2794 /** 2795 * Fetch entries of directory and toggle expanded state if GUI available. 2796 * @param index index of directory item 2797 */ 2798 void Kid3Application::expandDirectory(const QModelIndex& index) 2799 { 2800 fetchDirectory(index); 2801 emit toggleExpandedRequested(index); 2802 } 2803 2804 /** 2805 * Expand the whole file list if GUI available. 2806 * expandFileListFinished() is emitted when finished. 2807 */ 2808 void Kid3Application::requestExpandFileList() 2809 { 2810 emit expandFileListRequested(); 2811 } 2812 2813 /** 2814 * Called when operation for requestExpandFileList() is finished. 2815 */ 2816 void Kid3Application::notifyExpandFileListFinished() 2817 { 2818 emit expandFileListFinished(); 2819 } 2820 2821 /** 2822 * Process change of selection. 2823 * The GUI is signaled to update the current selection and the controls. 2824 * @param selected selected items 2825 * @param deselected deselected items 2826 */ 2827 void Kid3Application::fileSelected(const QItemSelection& selected, 2828 const QItemSelection& deselected) 2829 { 2830 emit fileSelectionUpdateRequested(); 2831 emit selectedFilesChanged(selected, deselected); 2832 } 2833 2834 /** 2835 * Search in tags for a given text. 2836 * @param params search parameters 2837 */ 2838 void Kid3Application::findText(const TagSearcher::Parameters& params) 2839 { 2840 m_tagSearcher->setModel(m_fileProxyModel); 2841 m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex); 2842 m_tagSearcher->find(params); 2843 } 2844 2845 /** 2846 * Replace found text. 2847 * @param params search parameters 2848 */ 2849 void Kid3Application::replaceText(const TagSearcher::Parameters& params) 2850 { 2851 m_tagSearcher->setModel(m_fileProxyModel); 2852 m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex); 2853 m_tagSearcher->replace(params); 2854 } 2855 2856 /** 2857 * Replace all occurrences. 2858 * @param params search parameters 2859 */ 2860 void Kid3Application::replaceAll(const TagSearcher::Parameters& params) 2861 { 2862 m_tagSearcher->setModel(m_fileProxyModel); 2863 m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex); 2864 m_tagSearcher->replaceAll(params); 2865 } 2866 2867 /** 2868 * Schedule actions to rename a directory. 2869 * When finished renameActionsScheduled() is emitted. 2870 */ 2871 void Kid3Application::scheduleRenameActions() 2872 { 2873 m_dirRenamer->clearActions(); 2874 m_dirRenamer->clearAborted(); 2875 // If directories are selected, rename them, else process files of the 2876 // current directory. 2877 QList<QPersistentModelIndex> indexes; 2878 const auto selectedIndexes = m_fileSelectionModel->selectedRows(); 2879 for (const QModelIndex& index : selectedIndexes) { 2880 if (m_fileProxyModel->isDir(index)) { 2881 indexes.append(index); 2882 } 2883 } 2884 if (indexes.isEmpty()) { 2885 indexes.append(m_fileProxyModelRootIndex); 2886 } 2887 2888 connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady, 2889 this, &Kid3Application::scheduleNextRenameAction); 2890 m_fileProxyModelIterator->start(indexes); 2891 } 2892 2893 /** 2894 * Schedule rename action for a file. 2895 * 2896 * @param index index of file in file proxy model 2897 */ 2898 void Kid3Application::scheduleNextRenameAction(const QPersistentModelIndex& index) 2899 { 2900 bool terminated = !index.isValid(); 2901 if (!terminated) { 2902 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) { 2903 taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile); 2904 m_dirRenamer->scheduleAction(taggedFile); 2905 if (m_dirRenamer->isAborted()) { 2906 terminated = true; 2907 } 2908 } 2909 } 2910 if (terminated) { 2911 m_fileProxyModelIterator->abort(); 2912 disconnect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady, 2913 this, &Kid3Application::scheduleNextRenameAction); 2914 m_dirRenamer->endScheduleActions(); 2915 emit renameActionsScheduled(); 2916 } 2917 } 2918 2919 /** 2920 * Open directory after resetting the file system model. 2921 * When finished directoryOpened() is emitted, also if false is returned. 2922 * 2923 * @param paths file or directory paths, if multiple paths are given, the 2924 * common directory is opened and the files are selected, if empty, the 2925 * currently open directory is reopened 2926 * 2927 * @return true if ok. 2928 */ 2929 bool Kid3Application::openDirectoryAfterReset(const QStringList& paths) 2930 { 2931 // Clear the selection. 2932 m_selection->beginAddTaggedFiles(); 2933 m_selection->endAddTaggedFiles(); 2934 QStringList dirs(paths); 2935 if (dirs.isEmpty()) { 2936 dirs.append(m_fileSystemModel->rootPath()); 2937 } 2938 m_fileSystemModel->clear(); 2939 return openDirectory(dirs); 2940 } 2941 2942 /** 2943 * Apply file filter after the file system model has been reset. 2944 */ 2945 void Kid3Application::applyFilterAfterReset() 2946 { 2947 disconnect(this, &Kid3Application::directoryOpened, 2948 this, &Kid3Application::applyFilterAfterReset); 2949 proceedApplyingFilter(); 2950 } 2951 2952 /** 2953 * Apply a file filter. 2954 * 2955 * @param fileFilter filter to apply. 2956 */ 2957 void Kid3Application::applyFilter(FileFilter& fileFilter) 2958 { 2959 m_fileFilter = &fileFilter; 2960 /* 2961 * When a lot of files are filtered out, 2962 * QSortFilterProxyModel::invalidateFilter() is extremely slow (probably 2963 * depending on the source model). In this case, I measured 2964 * 3s for 3000 files, 8s for 5000 files, 54s for 10000 files, and too long 2965 * to wait for more files. If such a case is detected, the file system model 2966 * is recreated in order to avoid calling invalidateFilter(). 2967 */ 2968 if (m_filterTotal - m_filterPassed > 4000) { 2969 connect(this, &Kid3Application::directoryOpened, 2970 this, &Kid3Application::applyFilterAfterReset); 2971 openDirectoryAfterReset(); 2972 } else { 2973 m_fileProxyModel->disableFilteringOutIndexes(); 2974 proceedApplyingFilter(); 2975 } 2976 } 2977 2978 /** 2979 * Second stage for applyFilter(). 2980 */ 2981 void Kid3Application::proceedApplyingFilter() 2982 { 2983 const bool justClearingFilter = 2984 m_fileFilter->isEmptyFilterExpression() && isFiltered(); 2985 setFiltered(false); 2986 m_fileFilter->clearAborted(); 2987 m_filterPassed = 0; 2988 m_filterTotal = 0; 2989 emit fileFiltered(FileFilter::Started, QString(), 2990 m_filterPassed, m_filterTotal); 2991 2992 m_lastProcessedDirName.clear(); 2993 if (!justClearingFilter) { 2994 connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady, 2995 this, &Kid3Application::filterNextFile); 2996 m_fileProxyModelIterator->start(m_fileProxyModelRootIndex); 2997 } else { 2998 emit fileFiltered(FileFilter::Finished, QString(), 2999 m_filterPassed, m_filterTotal); 3000 } 3001 } 3002 3003 /** 3004 * Apply single file to file filter. 3005 * 3006 * @param index index of file in file proxy model 3007 */ 3008 void Kid3Application::filterNextFile(const QPersistentModelIndex& index) 3009 { 3010 if (!m_fileFilter) 3011 return; 3012 3013 bool terminated = !index.isValid(); 3014 if (!terminated) { 3015 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) { 3016 bool tagInfoRead = taggedFile->isTagInformationRead(); 3017 taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile); 3018 if (taggedFile->getDirname() != m_lastProcessedDirName) { 3019 m_lastProcessedDirName = taggedFile->getDirname(); 3020 emit fileFiltered(FileFilter::Directory, m_lastProcessedDirName, 3021 m_filterPassed, m_filterTotal); 3022 } 3023 bool ok; 3024 bool pass = m_fileFilter->filter(*taggedFile, &ok); 3025 if (ok) { 3026 ++m_filterTotal; 3027 if (pass) { 3028 ++m_filterPassed; 3029 } 3030 emit fileFiltered( 3031 pass ? FileFilter::FilePassed : FileFilter::FileFilteredOut, 3032 taggedFile->getFilename(), m_filterPassed, m_filterTotal); 3033 if (!pass) 3034 m_fileProxyModel->filterOutIndex(taggedFile->getIndex()); 3035 } else { 3036 emit fileFiltered(FileFilter::ParseError, QString(), 3037 m_filterPassed, m_filterTotal); 3038 terminated = true; 3039 } 3040 3041 // Free resources if tag was not read before filtering 3042 if (!pass && !tagInfoRead) { 3043 taggedFile->clearTags(false); 3044 } 3045 3046 if (m_fileFilter->isAborted()) { 3047 terminated = true; 3048 emit fileFiltered(FileFilter::Aborted, QString(), 3049 m_filterPassed, m_filterTotal); 3050 } 3051 } 3052 } 3053 if (terminated) { 3054 if (!m_fileFilter->isAborted()) { 3055 emit fileFiltered(FileFilter::Finished, QString(), 3056 m_filterPassed, m_filterTotal); 3057 } 3058 3059 m_fileProxyModelIterator->abort(); 3060 m_fileProxyModel->applyFilteringOutIndexes(); 3061 setFiltered(!m_fileFilter->isEmptyFilterExpression()); 3062 3063 disconnect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady, 3064 this, &Kid3Application::filterNextFile); 3065 } 3066 } 3067 3068 /** 3069 * Apply a file filter. 3070 * 3071 * @param expression filter expression 3072 */ 3073 void Kid3Application::applyFilter(const QString& expression) 3074 { 3075 if (!m_expressionFileFilter) { 3076 m_expressionFileFilter = new FileFilter(this); 3077 } 3078 m_expressionFileFilter->clearAborted(); 3079 m_expressionFileFilter->setFilterExpression(expression); 3080 m_expressionFileFilter->initParser(); 3081 applyFilter(*m_expressionFileFilter); 3082 } 3083 3084 /** 3085 * Abort expression file filter. 3086 */ 3087 void Kid3Application::abortFilter() 3088 { 3089 if (m_expressionFileFilter) { 3090 m_expressionFileFilter->abort(); 3091 } 3092 } 3093 3094 /** 3095 * Perform rename actions and change application directory afterwards if it 3096 * was renamed. 3097 * 3098 * @return error messages, null string if no error occurred. 3099 */ 3100 QString Kid3Application::performRenameActions() 3101 { 3102 QString errorMsg; 3103 m_dirRenamer->setDirName(getDirName()); 3104 m_dirRenamer->performActions(&errorMsg); 3105 if (m_dirRenamer->getDirName() != getDirName()) { 3106 openDirectory({m_dirRenamer->getDirName()}); 3107 } 3108 return errorMsg; 3109 } 3110 3111 /** 3112 * Reset the file system model and then try to perform the rename actions. 3113 * On Windows, renaming directories fails when they have a subdirectory which 3114 * is open in the file system model. This method can be used to retry in such 3115 * a situation. 3116 */ 3117 void Kid3Application::tryRenameActionsAfterReset() 3118 { 3119 connect(this, &Kid3Application::directoryOpened, 3120 this, &Kid3Application::performRenameActionsAfterReset); 3121 openDirectoryAfterReset(); 3122 } 3123 3124 /** 3125 * Perform rename actions after the file system model has been reset. 3126 */ 3127 void Kid3Application::performRenameActionsAfterReset() 3128 { 3129 disconnect(this, &Kid3Application::directoryOpened, 3130 this, &Kid3Application::performRenameActionsAfterReset); 3131 performRenameActions(); 3132 } 3133 3134 /** 3135 * Reset the file system model and then try to rename a file. 3136 * On Windows, renaming directories fails when they have a subdirectory which 3137 * is open in the file system model. This method can be used to retry in such 3138 * a situation. 3139 * 3140 * @param oldName old file name 3141 * @param newName new file name 3142 */ 3143 void Kid3Application::tryRenameAfterReset(const QString& oldName, 3144 const QString& newName) 3145 { 3146 m_renameAfterResetOldName = oldName; 3147 m_renameAfterResetNewName = newName; 3148 connect(this, &Kid3Application::directoryOpened, 3149 this, &Kid3Application::renameAfterReset); 3150 openDirectoryAfterReset(); 3151 } 3152 3153 /** 3154 * Rename after the file system model has been reset. 3155 */ 3156 void Kid3Application::renameAfterReset() 3157 { 3158 disconnect(this, &Kid3Application::directoryOpened, this, &Kid3Application::renameAfterReset); 3159 if (!m_renameAfterResetOldName.isEmpty() && 3160 !m_renameAfterResetNewName.isEmpty()) { 3161 Utils::safeRename(m_renameAfterResetOldName, m_renameAfterResetNewName); 3162 m_renameAfterResetOldName.clear(); 3163 m_renameAfterResetNewName.clear(); 3164 } 3165 } 3166 3167 /** 3168 * Set the directory name from the tags. 3169 * The directory must not have modified files. 3170 * renameActionsScheduled() is emitted when the rename actions have been 3171 * scheduled. Then performRenameActions() has to be called to effectively 3172 * rename the directory. 3173 * 3174 * @param tagMask tag mask 3175 * @param format directory name format 3176 * @param create true to create, false to rename 3177 * 3178 * @return true if ok. 3179 */ 3180 bool Kid3Application::renameDirectory(Frame::TagVersion tagMask, 3181 const QString& format, bool create) 3182 { 3183 if (TaggedFile* taggedFile = 3184 TaggedFileOfDirectoryIterator::first(currentOrRootIndex()); 3185 !isModified() && taggedFile) { 3186 m_dirRenamer->setTagVersion(tagMask); 3187 m_dirRenamer->setFormat(format); 3188 m_dirRenamer->setAction(create); 3189 scheduleRenameActions(); 3190 return true; 3191 } 3192 return false; 3193 } 3194 3195 /** 3196 * Check modification state. 3197 * 3198 * @return true if a file is modified. 3199 */ 3200 bool Kid3Application::isModified() const 3201 { 3202 return m_fileProxyModel->isModified(); 3203 } 3204 3205 /** 3206 * Number tracks in selected files of directory. 3207 * 3208 * @param nr start number 3209 * @param total total number of tracks, used if >0 3210 * @param tagVersion determines on which tags the numbers are set 3211 * @param options options for numbering operation 3212 */ 3213 void Kid3Application::numberTracks(int nr, int total, 3214 Frame::TagVersion tagVersion, 3215 NumberTrackOptions options) 3216 { 3217 QString lastDirName; 3218 bool totalEnabled = TagConfig::instance().enableTotalNumberOfTracks(); 3219 bool directoryMode = true; 3220 int startNr = nr; 3221 emit fileSelectionUpdateRequested(); 3222 int numDigits = TagConfig::instance().trackNumberDigits(); 3223 if (numDigits < 1 || numDigits > 5) 3224 numDigits = 1; 3225 3226 // If directories are selected, number their files, else process the selected 3227 // files of the current directory. 3228 AbstractTaggedFileIterator* it = 3229 new TaggedFileOfSelectedDirectoriesIterator(getFileSelectionModel()); 3230 if (!it->hasNext()) { 3231 delete it; 3232 it = new SelectedTaggedFileOfDirectoryIterator( 3233 currentOrRootIndex(), 3234 getFileSelectionModel(), 3235 true); 3236 directoryMode = false; 3237 } 3238 while (it->hasNext()) { 3239 TaggedFile* taggedFile = it->next(); 3240 taggedFile->readTags(false); 3241 if (options & NumberTracksResetCounterForEachDirectory) { 3242 if (QString dirName = taggedFile->getDirname(); lastDirName != dirName) { 3243 nr = startNr; 3244 if (totalEnabled && directoryMode) { 3245 total = taggedFile->getTotalNumberOfTracksInDir(); 3246 } 3247 lastDirName = dirName; 3248 } 3249 } 3250 FOR_TAGS_IN_MASK(tagNr, tagVersion) { 3251 if (tagNr == Frame::Tag_Id3v1) { 3252 if (options & NumberTracksEnabled) { 3253 QString value; 3254 value.setNum(nr); 3255 if (Frame frame; 3256 taggedFile->getFrame(tagNr, Frame::FT_Track, frame)) { 3257 frame.setValueIfChanged(value); 3258 if (frame.isValueChanged()) { 3259 taggedFile->setFrame(tagNr, frame); 3260 } 3261 } else { 3262 frame.setValue(value); 3263 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Track)); 3264 taggedFile->setFrame(tagNr, frame); 3265 } 3266 } 3267 } else { 3268 // For tag 2 the frame is written, so that we have control over the 3269 // format and the total number of tracks, and it is possible to change 3270 // the format even if the numbers stay the same. 3271 FrameCollection frames; 3272 taggedFile->getAllFrames(tagNr, frames); 3273 Frame frame(Frame::FT_Track, QLatin1String(""), QLatin1String(""), -1); 3274 auto frameIt = frames.find(frame); 3275 QString value; 3276 if (options & NumberTracksEnabled) { 3277 if (total > 0) { 3278 value = QString(QLatin1String("%1/%2")) 3279 .arg(nr, numDigits, 10, QLatin1Char('0')) 3280 .arg(total, numDigits, 10, QLatin1Char('0')); 3281 } else { 3282 value = QString(QLatin1String("%1")) 3283 .arg(nr, numDigits, 10, QLatin1Char('0')); 3284 } 3285 if (frameIt != frames.end()) { 3286 frame = *frameIt; 3287 frame.setValueIfChanged(value); 3288 if (frame.isValueChanged()) { 3289 taggedFile->setFrame(tagNr, frame); 3290 } 3291 } else { 3292 frame.setValue(value); 3293 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Track)); 3294 taggedFile->setFrame(tagNr, frame); 3295 } 3296 } else { 3297 // If track numbering is not enabled, just reformat the current value. 3298 if (frameIt != frames.end()) { 3299 frame = *frameIt; 3300 int currentTotal; 3301 int currentNr = TaggedFile::splitNumberAndTotal(frame.getValue(), 3302 ¤tTotal); 3303 // Set the total if enabled. 3304 if (totalEnabled && total > 0) { 3305 currentTotal = total; 3306 } 3307 if (currentTotal > 0) { 3308 value = QString(QLatin1String("%1/%2")) 3309 .arg(currentNr, numDigits, 10, QLatin1Char('0')) 3310 .arg(currentTotal, numDigits, 10, QLatin1Char('0')); 3311 } else { 3312 value = QString(QLatin1String("%1")) 3313 .arg(currentNr, numDigits, 10, QLatin1Char('0')); 3314 } 3315 frame.setValueIfChanged(value); 3316 if (frame.isValueChanged()) { 3317 taggedFile->setFrame(tagNr, frame); 3318 } 3319 } 3320 } 3321 } 3322 } 3323 ++nr; 3324 } 3325 emit selectedFilesUpdated(); 3326 delete it; 3327 } 3328 3329 /** 3330 * Play audio file. 3331 */ 3332 void Kid3Application::playAudio() 3333 { 3334 QObject* player = getAudioPlayer(); 3335 if (!player) 3336 return; 3337 3338 QStringList files; 3339 int fileNr = 0; 3340 if (QModelIndexList selectedRows = m_fileSelectionModel->selectedRows(); 3341 selectedRows.size() > 1) { 3342 // play only the selected files if more than one is selected 3343 SelectedTaggedFileIterator it(m_fileProxyModelRootIndex, 3344 m_fileSelectionModel, 3345 false); 3346 while (it.hasNext()) { 3347 files.append(it.next()->getAbsFilename()); 3348 } 3349 } else { 3350 if (selectedRows.size() == 1) { 3351 // If a playlist file is selected, play the files in the playlist. 3352 QModelIndex index = selectedRows.first(); 3353 index = index.sibling(index.row(), 0); 3354 QString path = m_fileProxyModel->filePath(index); 3355 bool isPlaylist = false; 3356 PlaylistConfig::formatFromFileExtension(path, &isPlaylist); 3357 if (isPlaylist) { 3358 files = playlistModel(path)->pathsInPlaylist(); 3359 } 3360 } 3361 if (files.isEmpty()) { 3362 // play all files if none or only one is selected 3363 int idx = 0; 3364 ModelIterator it(m_fileProxyModelRootIndex); 3365 while (it.hasNext()) { 3366 QModelIndex index = it.next(); 3367 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) { 3368 files.append(taggedFile->getAbsFilename()); 3369 if (m_fileSelectionModel->isSelected(index)) { 3370 fileNr = idx; 3371 } 3372 ++idx; 3373 } 3374 } 3375 } 3376 } 3377 emit aboutToPlayAudio(); 3378 QMetaObject::invokeMethod(player, "setFiles", 3379 Q_ARG(QStringList, files), Q_ARG(int, fileNr)); 3380 } 3381 3382 /** 3383 * Show play tool bar. 3384 */ 3385 void Kid3Application::showAudioPlayer() 3386 { 3387 emit aboutToPlayAudio(); 3388 } 3389 3390 /** 3391 * Get number of tracks in current directory. 3392 * 3393 * @return number of tracks, 0 if not found. 3394 */ 3395 int Kid3Application::getTotalNumberOfTracksInDir() const 3396 { 3397 if (TaggedFile* taggedFile = TaggedFileOfDirectoryIterator::first( 3398 currentOrRootIndex())) { 3399 return taggedFile->getTotalNumberOfTracksInDir(); 3400 } 3401 return 0; 3402 } 3403 3404 /** 3405 * Create a filter string for the file dialog. 3406 * The filter string contains entries for all supported types. 3407 * 3408 * @return filter string. 3409 */ 3410 QString Kid3Application::createFilterString() const 3411 { 3412 return m_platformTools->fileDialogNameFilter( 3413 FileProxyModel::createNameFilters()); 3414 } 3415 3416 /** 3417 * Remove the file filter if necessary to open the files. 3418 * @param filePaths paths to files or directories 3419 */ 3420 void Kid3Application::resetFileFilterIfNotMatching(const QStringList& filePaths) 3421 { 3422 if (QStringList nameFilters(m_platformTools->getNameFilterPatterns( 3423 FileConfig::instance().nameFilter()).split(QLatin1Char(' '))); 3424 !nameFilters.isEmpty() && nameFilters.first() != QLatin1String("*")) { 3425 for (const QString& filePath : filePaths) { 3426 if (QFileInfo fi(filePath); 3427 !QDir::match(nameFilters, fi.fileName()) && !fi.isDir()) { 3428 setAllFilesFileFilter(); 3429 break; 3430 } 3431 } 3432 } 3433 } 3434 3435 /** 3436 * Set file name filter for "All Files (*)". 3437 */ 3438 void Kid3Application::setAllFilesFileFilter() { 3439 FileConfig::instance().setNameFilter( 3440 m_platformTools->fileDialogNameFilter( 3441 QList<QPair<QString, QString> >() 3442 << qMakePair(tr("All Files"), QString(QLatin1Char('*'))))); 3443 } 3444 3445 /** 3446 * Notify the tagged file factories about the changed configuration. 3447 */ 3448 void Kid3Application::notifyConfigurationChange() 3449 { 3450 const auto factories = FileProxyModel::taggedFileFactories(); 3451 for (ITaggedFileFactory* factory : factories) { 3452 const auto keys = factory->taggedFileKeys(); 3453 for (const QString& key : keys) { 3454 factory->notifyConfigurationChange(key); 3455 } 3456 } 3457 } 3458 3459 /** 3460 * Convert ID3v2.3 to ID3v2.4 tags. 3461 */ 3462 void Kid3Application::convertToId3v24() 3463 { 3464 emit fileSelectionUpdateRequested(); 3465 SelectedTaggedFileIterator it(getRootIndex(), 3466 getFileSelectionModel(), 3467 false); 3468 while (it.hasNext()) { 3469 TaggedFile* taggedFile = it.next(); 3470 taggedFile->readTags(false); 3471 if (taggedFile->hasTag(Frame::Tag_Id3v2) && !taggedFile->isChanged()) { 3472 if (QString tagFmt = taggedFile->getTagFormat(Frame::Tag_Id3v2); 3473 tagFmt.length() >= 7 && tagFmt.startsWith(QLatin1String("ID3v2.")) && 3474 tagFmt[6] < QLatin1Char('4')) { 3475 if ((taggedFile->taggedFileFeatures() & 3476 (TaggedFile::TF_ID3v23 | TaggedFile::TF_ID3v24)) == 3477 TaggedFile::TF_ID3v23) { 3478 FrameCollection frames; 3479 taggedFile->getAllFrames(Frame::Tag_Id3v2, frames); 3480 FrameFilter flt; 3481 flt.enableAll(); 3482 taggedFile->deleteFrames(Frame::Tag_Id3v2, flt); 3483 3484 // The file has to be reread to write ID3v2.4 tags 3485 taggedFile = FileProxyModel::readWithId3V24(taggedFile); 3486 3487 // Restore the frames 3488 FrameFilter frameFlt; 3489 frameFlt.enableAll(); 3490 taggedFile->setFrames(Frame::Tag_Id3v2, 3491 frames.copyEnabledFrames(frameFlt), false); 3492 } 3493 3494 // Write the file with ID3v2.4 tags 3495 bool renamed; 3496 int storedFeatures = taggedFile->activeTaggedFileFeatures(); 3497 taggedFile->setActiveTaggedFileFeatures(TaggedFile::TF_ID3v24); 3498 taggedFile->writeTags(true, &renamed, 3499 FileConfig::instance().preserveTime()); 3500 taggedFile->setActiveTaggedFileFeatures(storedFeatures); 3501 taggedFile->readTags(true); 3502 } 3503 } 3504 } 3505 emit selectedFilesUpdated(); 3506 } 3507 3508 /** 3509 * Convert ID3v2.4 to ID3v2.3 tags. 3510 */ 3511 void Kid3Application::convertToId3v23() 3512 { 3513 emit fileSelectionUpdateRequested(); 3514 SelectedTaggedFileIterator it(getRootIndex(), 3515 getFileSelectionModel(), 3516 false); 3517 while (it.hasNext()) { 3518 TaggedFile* taggedFile = it.next(); 3519 taggedFile->readTags(false); 3520 if (taggedFile->hasTag(Frame::Tag_Id3v2) && !taggedFile->isChanged()) { 3521 QString tagFmt = taggedFile->getTagFormat(Frame::Tag_Id3v2); 3522 if (QString ext = taggedFile->getFileExtension(); 3523 tagFmt.length() >= 7 && tagFmt.startsWith(QLatin1String("ID3v2.")) && 3524 tagFmt[6] > QLatin1Char('3') && 3525 (ext == QLatin1String(".mp3") || ext == QLatin1String(".mp2") || 3526 ext == QLatin1String(".aac") || ext == QLatin1String(".wav") || 3527 ext == QLatin1String(".dsf") || ext == QLatin1String(".dff"))) { 3528 if (!(taggedFile->taggedFileFeatures() & TaggedFile::TF_ID3v23)) { 3529 FrameCollection frames; 3530 taggedFile->getAllFrames(Frame::Tag_Id3v2, frames); 3531 FrameFilter flt; 3532 flt.enableAll(); 3533 taggedFile->deleteFrames(Frame::Tag_Id3v2, flt); 3534 3535 // The file has to be reread to write ID3v2.3 tags 3536 taggedFile = FileProxyModel::readWithId3V23(taggedFile); 3537 3538 // Restore the frames 3539 FrameFilter frameFlt; 3540 frameFlt.enableAll(); 3541 taggedFile->setFrames(Frame::Tag_Id3v2, 3542 frames.copyEnabledFrames(frameFlt), false); 3543 } 3544 3545 // Write the file with ID3v2.3 tags 3546 bool renamed; 3547 int storedFeatures = taggedFile->activeTaggedFileFeatures(); 3548 taggedFile->setActiveTaggedFileFeatures(TaggedFile::TF_ID3v23); 3549 taggedFile->writeTags(true, &renamed, 3550 FileConfig::instance().preserveTime()); 3551 taggedFile->setActiveTaggedFileFeatures(storedFeatures); 3552 taggedFile->readTags(true); 3553 } 3554 } 3555 } 3556 emit selectedFilesUpdated(); 3557 } 3558 3559 /** 3560 * Get value of frame. 3561 * To get binary data like a picture, the name of a file to write can be 3562 * added after the @a name, e.g. "Picture:/path/to/file". 3563 * 3564 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 3565 * @param name name of frame (e.g. "Artist") 3566 */ 3567 QString Kid3Application::getFrame(Frame::TagVersion tagMask, 3568 const QString& name) const 3569 { 3570 QString frameName(name); 3571 QString dataFileName, fieldName; 3572 int index = 0; 3573 Frame::ExtendedType explicitType; 3574 if (frameName.startsWith(QLatin1Char('!'))) { 3575 frameName.remove(0, 1); 3576 explicitType = Frame::ExtendedType(Frame::FT_Other, frameName); 3577 } 3578 extractFileFieldIndex(frameName, dataFileName, fieldName, index); 3579 bool isRatingStars = false; 3580 if (frameName.toLower() == QLatin1String("ratingstars")) { 3581 frameName.truncate(6); // Reduce to "rating" 3582 isRatingStars = true; 3583 } 3584 Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask); 3585 if (tagNr >= Frame::Tag_NumValues) 3586 return QString(); 3587 3588 FrameTableModel* ft = m_framesModel[tagNr]; 3589 const FrameCollection& frames = ft->frames(); 3590 if (auto it = explicitType.getType() == Frame::FT_UnknownFrame 3591 ? frames.findByName(frameName, index) 3592 : frames.findByExtendedType(explicitType, index); 3593 it != frames.cend()) { 3594 QString frmName(it->getName()); 3595 if (!dataFileName.isEmpty() && 3596 (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0) { 3597 if (it->getType() == Frame::FT_Picture || 3598 frmName.startsWith(QLatin1String("GEOB"))) { 3599 PictureFrame::writeDataToFile(*it, dataFileName); 3600 } else if (bool isSylt = false; 3601 (isSylt = frmName.startsWith(QLatin1String("SYLT")) || 3602 frmName == QLatin1String("Chapters")) || 3603 frmName.startsWith(QLatin1String("ETCO"))) { 3604 QFile file(dataFileName); 3605 if (file.open(QIODevice::WriteOnly)) { 3606 TimeEventModel timeEventModel; 3607 if (isSylt) { 3608 timeEventModel.setType(TimeEventModel::SynchronizedLyrics); 3609 timeEventModel.fromSyltFrame(it->getFieldList()); 3610 } else { 3611 timeEventModel.setType(TimeEventModel::EventTimingCodes); 3612 timeEventModel.fromEtcoFrame(it->getFieldList()); 3613 } 3614 QTextStream stream(&file); 3615 if (QString codecName = FileConfig::instance().textEncoding(); 3616 codecName != QLatin1String("System")) { 3617 #if QT_VERSION >= 0x060000 3618 if (auto encoding = QStringConverter::encodingForName(codecName.toLatin1())) { 3619 stream.setEncoding(*encoding); 3620 } 3621 #else 3622 stream.setCodec(codecName.toLatin1()); 3623 #endif 3624 } 3625 timeEventModel.toLrcFile(stream, frames.getTitle(), 3626 frames.getArtist(), frames.getAlbum()); 3627 file.close(); 3628 } 3629 } else if (fieldName.isEmpty()) { 3630 it->writeValueToFile(dataFileName); 3631 } 3632 } 3633 if (!fieldName.isEmpty()) { 3634 if (fieldName == QLatin1String("selected")) { 3635 const int frameIndex = it->getIndex(); 3636 if (const int row = frameIndex >= 0 3637 ? ft->getRowWithFrameIndex(frameIndex) 3638 : std::distance(frames.cbegin(), it); 3639 row != -1) { 3640 return QLatin1String(ft->index(row, FrameTableModel::CI_Enable) 3641 .data(Qt::CheckStateRole).toInt() == Qt::Checked 3642 ? "1" : "0"); 3643 } 3644 return QString(); 3645 } 3646 return Frame::getField(*it, fieldName).toString(); 3647 } 3648 if (isRatingStars) { 3649 bool ok; 3650 int rating = it->getValue().toInt(&ok); 3651 if (ok) { 3652 return QString::number(TagConfig::instance().starCountFromRating( 3653 rating, ratingTypeName(*it))); 3654 } 3655 } 3656 return it->getValue(); 3657 } 3658 return QString(); 3659 } 3660 3661 /** 3662 * Get names and values of all frames. 3663 * 3664 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 3665 * 3666 * @return map containing frame values. 3667 */ 3668 QVariantMap Kid3Application::getAllFrames(Frame::TagVersion tagMask) const 3669 { 3670 QVariantMap map; 3671 Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask); 3672 if (tagNr >= Frame::Tag_NumValues) 3673 return QVariantMap(); 3674 3675 FrameTableModel* ft = m_framesModel[tagNr]; 3676 const FrameCollection& frames = ft->frames(); 3677 for (auto it = frames.cbegin(); it != frames.cend(); ++it) { 3678 QString name(it->getName()); 3679 if (int nlPos = name.indexOf(QLatin1Char('\n')); nlPos > 0) { 3680 // probably "TXXX - User defined text information\nDescription" or 3681 // "WXXX - User defined URL link\nDescription" 3682 name = name.mid(nlPos + 1); 3683 #if QT_VERSION >= 0x060000 3684 } else if (name.mid(4, 3) == QLatin1String(" - ")) { 3685 #else 3686 } else if (name.midRef(4, 3) == QLatin1String(" - ")) { 3687 #endif 3688 // probably "ID3-ID - Description" 3689 name = name.left(4); 3690 } 3691 map.insert(name, it->getValue()); 3692 } 3693 return map; 3694 } 3695 3696 /** 3697 * Set value of frame. 3698 * For tag 2 (@a tagMask 2), if no frame with @a name exists, a new frame 3699 * is added, if @a value is empty, the frame is deleted. 3700 * To add binary data like a picture, a file can be added after the 3701 * @a name, e.g. "Picture:/path/to/file". 3702 * 3703 * @param tagMask tag bit (1 for tag 1, 2 for tag 2) 3704 * @param name name of frame (e.g. "Artist") 3705 * @param value value of frame 3706 * 3707 * @return true if ok. 3708 */ 3709 bool Kid3Application::setFrame(Frame::TagVersion tagMask, 3710 const QString& name, const QString& value) 3711 { 3712 Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask); 3713 if (tagNr >= Frame::Tag_NumValues) 3714 return false; 3715 3716 FrameTableModel* ft = m_framesModel[tagNr]; 3717 if (name == QLatin1String("*.selected")) { 3718 ft->setAllCheckStates(!value.isEmpty() && value != QLatin1String("0") 3719 && value != QLatin1String("false")); 3720 return true; 3721 } 3722 3723 QString frameName(name); 3724 QString dataFileName, fieldName; 3725 int index = 0; 3726 Frame::ExtendedType explicitType; 3727 if (frameName.startsWith(QLatin1Char('!'))) { 3728 frameName.remove(0, 1); 3729 explicitType = Frame::ExtendedType(Frame::FT_Other, frameName); 3730 } 3731 extractFileFieldIndex(frameName, dataFileName, fieldName, index); 3732 bool isRatingStars = false; 3733 if (frameName.toLower() == QLatin1String("ratingstars")) { 3734 frameName.truncate(6); // Reduce to "rating" 3735 isRatingStars = true; 3736 } 3737 FrameCollection frames(ft->frames()); 3738 if (auto it = explicitType.getType() == Frame::FT_UnknownFrame 3739 ? frames.findByName(frameName, index) 3740 : frames.findByExtendedType(explicitType, index); 3741 it != frames.end()) { 3742 QString frmName(it->getName()); 3743 if (!dataFileName.isEmpty() && 3744 (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0) { 3745 if (it->getType() == Frame::FT_Picture) { 3746 deleteFrame(tagNr, frmName, index); 3747 PictureFrame frame; 3748 PictureFrame::setDescription(frame, value); 3749 PictureFrame::setDataFromFile(frame, dataFileName); 3750 PictureFrame::setMimeTypeFromFileName(frame, dataFileName); 3751 PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig()); 3752 addFrame(tagNr, &frame); 3753 } else if (frmName.startsWith(QLatin1String("GEOB"))) { 3754 Frame frame(*it); 3755 deleteFrame(tagNr, frmName, index); 3756 Frame::setField(frame, Frame::ID_MimeType, 3757 PictureFrame::getMimeTypeForFile(dataFileName)); 3758 Frame::setField(frame, Frame::ID_Filename, 3759 QFileInfo(dataFileName).fileName()); 3760 Frame::setField(frame, Frame::ID_Description, value); 3761 PictureFrame::setDataFromFile(frame, dataFileName); 3762 addFrame(tagNr, &frame); 3763 } else if (bool isSylt = false; 3764 (isSylt = frmName.startsWith(QLatin1String("SYLT")) || 3765 frmName == QLatin1String("Chapters")) || 3766 frmName.startsWith(QLatin1String("ETCO"))) { 3767 QFile file(dataFileName); 3768 if (file.open(QIODevice::ReadOnly)) { 3769 QTextStream stream(&file); 3770 Frame frame(*it); 3771 Frame::setField(frame, Frame::ID_Description, value); 3772 deleteFrame(tagNr, frmName, index); 3773 TimeEventModel timeEventModel; 3774 if (isSylt) { 3775 timeEventModel.setType(TimeEventModel::SynchronizedLyrics); 3776 timeEventModel.fromLrcFile(stream); 3777 timeEventModel.toSyltFrame(frame.fieldList()); 3778 } else { 3779 timeEventModel.setType(TimeEventModel::EventTimingCodes); 3780 timeEventModel.fromLrcFile(stream); 3781 timeEventModel.toEtcoFrame(frame.fieldList()); 3782 } 3783 file.close(); 3784 addFrame(tagNr, &frame); 3785 } 3786 } else if (fieldName.isEmpty()) { 3787 auto& frame = const_cast<Frame&>(*it); 3788 frame.setValueFromFile(dataFileName); 3789 ft->transferFrames(frames); 3790 ft->selectChangedFrames(); 3791 emit fileSelectionUpdateRequested(); 3792 emit selectedFilesUpdated(); 3793 } 3794 } else if (value.isEmpty() && fieldName.isEmpty() && 3795 (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0) { 3796 deleteFrame(tagNr, frmName, index); 3797 } else { 3798 auto& frame = const_cast<Frame&>(*it); 3799 if (fieldName.isEmpty()) { 3800 QString val(value); 3801 if (isRatingStars) { 3802 bool ok; 3803 if (int starCount = value.toInt(&ok); 3804 ok && starCount >= 0 && starCount <= 5) { 3805 val = QString::number(TagConfig::instance().starCountToRating( 3806 starCount, ratingTypeName(*it))); 3807 } else { 3808 return false; 3809 } 3810 } 3811 frame.setValueIfChanged(val); 3812 } else { 3813 if (fieldName == QLatin1String("selected")) { 3814 const int frameIndex = frame.getIndex(); 3815 if (const int row = frameIndex >= 0 3816 ? ft->getRowWithFrameIndex(frameIndex) 3817 : std::distance(frames.cbegin(), it); 3818 row != -1) { 3819 ft->setData(ft->index(row, FrameTableModel::CI_Enable), 3820 !value.isEmpty() && value != QLatin1String("0") 3821 && value != QLatin1String("false") 3822 ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole); 3823 return true; 3824 } 3825 } else { 3826 if (TaggedFile* taggedFile = getSelectedFile(); 3827 taggedFile && Frame::setField(frame, fieldName, value)) { 3828 taggedFile->setFrame(tagNr, frame); 3829 } 3830 } 3831 } 3832 ft->transferFrames(frames); 3833 ft->selectChangedFrames(); 3834 emit fileSelectionUpdateRequested(); 3835 emit selectedFilesUpdated(); 3836 } 3837 return true; 3838 } 3839 if (tagMask & (Frame::TagV2 | Frame::TagV3)) { 3840 Frame frame(explicitType.getType() == Frame::FT_UnknownFrame 3841 ? Frame::ExtendedType(frameName) : explicitType, value, -1); 3842 QString frmName(frame.getInternalName()); 3843 if (!dataFileName.isEmpty()) { 3844 if (frame.getType() == Frame::FT_Picture) { 3845 PictureFrame::setFields(frame); 3846 PictureFrame::setDescription(frame, value); 3847 PictureFrame::setDataFromFile(frame, dataFileName); 3848 PictureFrame::setMimeTypeFromFileName(frame, dataFileName); 3849 PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig()); 3850 } else if (frmName.startsWith(QLatin1String("GEOB"))) { 3851 PictureFrame::setGeobFields( 3852 frame, Frame::TE_ISO8859_1, 3853 PictureFrame::getMimeTypeForFile(dataFileName), 3854 QFileInfo(dataFileName).fileName(), value); 3855 PictureFrame::setDataFromFile(frame, dataFileName); 3856 } else if (bool isSylt = false; 3857 (isSylt = frmName.startsWith(QLatin1String("SYLT")) || 3858 frmName == QLatin1String("Chapters")) || 3859 frmName.startsWith(QLatin1String("ETCO"))) { 3860 QFile file(dataFileName); 3861 if (file.open(QIODevice::ReadOnly)) { 3862 Frame::Field field; 3863 Frame::FieldList& fields = frame.fieldList(); 3864 fields.clear(); 3865 field.m_id = Frame::ID_Description; 3866 field.m_value = value; 3867 fields.append(field); 3868 field.m_id = Frame::ID_Data; 3869 #if QT_VERSION >= 0x060000 3870 field.m_value = QVariant(QMetaType(QMetaType::QVariantList)); 3871 #else 3872 field.m_value = QVariant(QVariant::List); 3873 #endif 3874 fields.append(field); 3875 QTextStream stream(&file); 3876 TimeEventModel timeEventModel; 3877 if (isSylt) { 3878 timeEventModel.setType(TimeEventModel::SynchronizedLyrics); 3879 timeEventModel.fromLrcFile(stream); 3880 timeEventModel.toSyltFrame(frame.fieldList()); 3881 } else { 3882 timeEventModel.setType(TimeEventModel::EventTimingCodes); 3883 timeEventModel.fromLrcFile(stream); 3884 timeEventModel.toEtcoFrame(frame.fieldList()); 3885 } 3886 file.close(); 3887 } 3888 } else if (fieldName.isEmpty()) { 3889 frame.setValueFromFile(dataFileName); 3890 } 3891 } else if (value.isEmpty()) { 3892 // Do not add an empty frame 3893 return false; 3894 } 3895 if (!fieldName.isEmpty()) { 3896 if (TaggedFile* taggedFile = getSelectedFile()) { 3897 frame.setValue(QString()); 3898 taggedFile->addFieldList(tagNr, frame); 3899 if (!Frame::setField(frame, fieldName, value)) { 3900 return false; 3901 } 3902 } 3903 } 3904 if (isRatingStars) { 3905 bool ok; 3906 if (int starCount = value.toInt(&ok); 3907 ok && starCount >= 0 && starCount <= 5) { 3908 frame.setValue(QString::number(TagConfig::instance().starCountToRating( 3909 starCount, ratingTypeName(frame, getSelectedFile(), tagNr)))); 3910 } else { 3911 return false; 3912 } 3913 } 3914 addFrame(tagNr, &frame); 3915 return true; 3916 } 3917 return false; 3918 } 3919 3920 /** 3921 * Get data from picture frame. 3922 * @return picture data, empty if not found. 3923 */ 3924 QByteArray Kid3Application::getPictureData() const 3925 { 3926 QByteArray data; 3927 const FrameCollection& frames = m_framesModel[Frame::Tag_Picture]->frames(); 3928 if (auto it = frames.findByExtendedType( 3929 Frame::ExtendedType(Frame::FT_Picture)); 3930 it != frames.cend()) { 3931 PictureFrame::getData(*it, data); 3932 } 3933 return data; 3934 } 3935 3936 /** 3937 * Set data in picture frame. 3938 * @param data picture data 3939 */ 3940 void Kid3Application::setPictureData(const QByteArray& data) 3941 { 3942 const FrameCollection& frames = m_framesModel[Frame::Tag_Picture]->frames(); 3943 auto it = frames.findByExtendedType( 3944 Frame::ExtendedType(Frame::FT_Picture)); 3945 PictureFrame frame; 3946 if (it != frames.cend()) { 3947 frame = PictureFrame(*it); 3948 deleteFrame(Frame::Tag_Picture, QLatin1String("Picture")); 3949 } 3950 if (!data.isEmpty()) { 3951 PictureFrame::setData(frame, data); 3952 PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig()); 3953 addFrame(Frame::Tag_Picture, &frame); 3954 } 3955 } 3956 3957 /** 3958 * Update state when file is about to be played. 3959 * @param filePath path to file 3960 */ 3961 void Kid3Application::onAboutToPlay(const QString& filePath) 3962 { 3963 #ifdef Q_OS_WIN32 3964 // Phonon on Windows cannot play if the file is open. 3965 closeFileHandle(filePath); 3966 #endif 3967 if (GuiConfig::instance().selectFileOnPlayEnabled()) { 3968 selectFile(filePath); 3969 } 3970 } 3971 3972 /** 3973 * Close the file handle of a tagged file. 3974 * @param filePath path to file 3975 */ 3976 void Kid3Application::closeFileHandle(const QString& filePath) 3977 { 3978 if (QModelIndex index = m_fileProxyModel->index(filePath); index.isValid()) { 3979 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) { 3980 taggedFile->closeFileHandle(); 3981 } 3982 } 3983 } 3984 3985 /** 3986 * Set a frame editor object to act as the frame editor. 3987 * @param frameEditor frame editor object, null to disable 3988 */ 3989 void Kid3Application::setFrameEditor(FrameEditorObject* frameEditor) 3990 { 3991 if (m_frameEditor != frameEditor) { 3992 IFrameEditor* editor; 3993 bool storeCurrentEditor = false; 3994 if (frameEditor) { 3995 if (!m_frameEditor) { 3996 storeCurrentEditor = true; 3997 } 3998 editor = frameEditor; 3999 } else { 4000 editor = m_storedFrameEditor; 4001 } 4002 FOR_ALL_TAGS(tagNr) { 4003 if (tagNr != Frame::Tag_Id3v1) { 4004 FrameList* framelist = m_framelist[tagNr]; 4005 if (storeCurrentEditor) { 4006 m_storedFrameEditor = framelist->frameEditor(); 4007 storeCurrentEditor = false; 4008 } 4009 framelist->setFrameEditor(editor); 4010 } 4011 } 4012 m_frameEditor = frameEditor; 4013 emit frameEditorChanged(); 4014 } 4015 } 4016 4017 /** 4018 * Remove frame editor. 4019 * Has to be called in the destructor of the frame editor to avoid a dangling 4020 * pointer to a deleted object. 4021 * @param frameEditor frame editor 4022 */ 4023 void Kid3Application::removeFrameEditor(const IFrameEditor* frameEditor) 4024 { 4025 if (m_storedFrameEditor == frameEditor) { 4026 m_storedFrameEditor = nullptr; 4027 } 4028 if (m_frameEditor == frameEditor) { 4029 setFrameEditor(nullptr); 4030 } 4031 } 4032 4033 /** 4034 * Get the numbers of the selected rows in a list suitable for scripting. 4035 * @return list with row numbers. 4036 */ 4037 QVariantList Kid3Application::getFileSelectionRows() const 4038 { 4039 QVariantList rows; 4040 const auto indexes = m_fileSelectionModel->selectedRows(); 4041 rows.reserve(indexes.size()); 4042 for (const QModelIndex& index : indexes) { 4043 rows.append(index.row()); 4044 } 4045 return rows; 4046 } 4047 4048 /** 4049 * Set the file selection from a list of model indexes. 4050 * @param indexes list of model indexes suitable for scripting 4051 */ 4052 void Kid3Application::setFileSelectionIndexes(const QVariantList& indexes) 4053 { 4054 QItemSelection selection; 4055 QModelIndex firstIndex; 4056 for (const QVariant& var : indexes) { 4057 QModelIndex index = var.toModelIndex(); 4058 if (!firstIndex.isValid()) { 4059 firstIndex = index; 4060 } 4061 selection.select(index, index); 4062 } 4063 disconnect(m_fileSelectionModel, 4064 &QItemSelectionModel::selectionChanged, 4065 this, &Kid3Application::fileSelectionChanged); 4066 m_fileSelectionModel->select(selection, 4067 QItemSelectionModel::Clear | QItemSelectionModel::Select | 4068 QItemSelectionModel::Rows); 4069 if (firstIndex.isValid()) { 4070 m_fileSelectionModel->setCurrentIndex(firstIndex, 4071 QItemSelectionModel::Select | QItemSelectionModel::Rows); 4072 } 4073 connect(m_fileSelectionModel, 4074 &QItemSelectionModel::selectionChanged, 4075 this, &Kid3Application::fileSelectionChanged); 4076 } 4077 4078 /** 4079 * Set the image provider. 4080 * @param imageProvider image provider 4081 */ 4082 void Kid3Application::setImageProvider(ImageDataProvider* imageProvider) { 4083 m_imageProvider = imageProvider; 4084 } 4085 4086 /** 4087 * If an image provider is used, update its picture and change the 4088 * coverArtImageId property if the picture of the selection changed. 4089 * This can be used to change a QML image. 4090 */ 4091 void Kid3Application::updateCoverArtImageId() 4092 { 4093 // Only perform expensive picture operations if the signal is used 4094 // (when using a QML image provider). 4095 if (m_imageProvider && 4096 receivers(SIGNAL(coverArtImageIdChanged(QString))) > 0) { 4097 setCoverArtImageData(m_selection->getPicture()); 4098 } 4099 } 4100 4101 /** 4102 * Set picture data for image provider. 4103 * @param picture picture data 4104 */ 4105 void Kid3Application::setCoverArtImageData(const QByteArray& picture) 4106 { 4107 if (picture != m_imageProvider->getImageData()) { 4108 m_imageProvider->setImageData(picture); 4109 setNextCoverArtImageId(); 4110 emit coverArtImageIdChanged(m_coverArtImageId); 4111 } 4112 } 4113 4114 /** 4115 * Set the coverArtImageId property to a new value. 4116 * This can be used to trigger an update of QML images. 4117 */ 4118 void Kid3Application::setNextCoverArtImageId() { 4119 static quint32 nr = 0; 4120 m_coverArtImageId = QString(QLatin1String("image://kid3/data/%1")) 4121 .arg(nr++, 8, 16, QLatin1Char('0')); 4122 } 4123 4124 /** 4125 * Open a file select dialog to get a file name. 4126 * For script support, is only supported when a GUI is available. 4127 * @param caption dialog caption 4128 * @param dir working directory 4129 * @param filter file type filter 4130 * @param saveFile true to open a save file dialog 4131 * @return selected file, empty if canceled. 4132 */ 4133 QString Kid3Application::selectFileName(const QString& caption, const QString& dir, 4134 const QString& filter, bool saveFile) 4135 { 4136 return saveFile 4137 ? m_platformTools->getSaveFileName(nullptr, caption, dir, filter, nullptr) 4138 : m_platformTools->getOpenFileName(nullptr, caption, dir, filter, nullptr); 4139 } 4140 4141 /** 4142 * Open a file select dialog to get a directory name. 4143 * For script support, is only supported when a GUI is available. 4144 * @param caption dialog caption 4145 * @param dir working directory 4146 * @return selected directory, empty if canceled. 4147 */ 4148 QString Kid3Application::selectDirName(const QString& caption, 4149 const QString& dir) 4150 { 4151 return m_platformTools->getExistingDirectory(nullptr, caption, dir); 4152 } 4153 4154 /** 4155 * Check if application is running with a graphical user interface. 4156 * @return true if application has a GUI. 4157 */ 4158 bool Kid3Application::hasGui() const 4159 { 4160 return m_platformTools->hasGui(); 4161 }