File indexing completed on 2024-05-19 04:55:49

0001 /**
0002  * \file clicommand.cpp
0003  * Command line interface commands.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 11 Aug 2013
0008  *
0009  * Copyright (C) 2013-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 "clicommand.h"
0028 #include <functional>
0029 #include <QStringList>
0030 #include <QStringBuilder>
0031 #include <QTimer>
0032 #include <QDir>
0033 #include <QItemSelectionModel>
0034 #include <QMetaProperty>
0035 #include "kid3cli.h"
0036 #include "kid3application.h"
0037 #include "fileproxymodel.h"
0038 #include "frametablemodel.h"
0039 #include "filefilter.h"
0040 #include "importconfig.h"
0041 #include "exportconfig.h"
0042 #include "filterconfig.h"
0043 #include "fileconfig.h"
0044 #include "rendirconfig.h"
0045 #include "batchimportconfig.h"
0046 #include "formatconfig.h"
0047 #include "networkconfig.h"
0048 #include "numbertracksconfig.h"
0049 #include "playlistconfig.h"
0050 #include "tagconfig.h"
0051 #include "batchimporter.h"
0052 #include "downloadclient.h"
0053 #include "dirrenamer.h"
0054 
0055 namespace {
0056 
0057 /** Default command timeout in milliseconds. */
0058 constexpr int DEFAULT_TIMEOUT_MS = 3000;
0059 
0060 /**
0061  * Available names for groups in config command.
0062  * If this list is modified, adapt also the cfgFuncs in getConfig().
0063  */
0064 const QStringList configNames{
0065   QLatin1String("BatchImport"),
0066   QLatin1String("Export"),
0067   QLatin1String("File"),
0068   QLatin1String("FilenameFormat"),
0069   QLatin1String("Filter"),
0070   QLatin1String("Import"),
0071   QLatin1String("Network"),
0072   QLatin1String("NumberTracks"),
0073   QLatin1String("Playlist"),
0074   QLatin1String("RenameFolder"),
0075   QLatin1String("Tag"),
0076   QLatin1String("TagFormat")
0077 };
0078 
0079 /** Properties which shall not be displayed as config options. */
0080 const QStringList excludedConfigPropertyNames{
0081   QLatin1String("objectName"),
0082   QLatin1String("windowGeometry"),
0083   QLatin1String("exportWindowGeometry"),
0084   QLatin1String("importServer"),
0085   QLatin1String("importVisibleColumns"),
0086   QLatin1String("importWindowGeometry"),
0087   QLatin1String("browseCoverArtWindowGeometry"),
0088   QLatin1String("quickAccessFrames"),
0089   QLatin1String("quickAccessFrameOrder"),
0090   QLatin1String("taggedFileFeatures")
0091 };
0092 
0093 /**
0094  * Get a configuration object for a given group name.
0095  * @param name group name
0096  * @return QObject with configuration options as properties.
0097  */
0098 GeneralConfig* getConfig(const QString& name)
0099 {
0100   int idx = configNames.indexOf(name);
0101   if (idx == -1) {
0102     return nullptr;
0103   }
0104 
0105   // Change this list together with configNames.
0106   static const std::function<GeneralConfig*()> cfgFuncs[] = {
0107     [] { return &BatchImportConfig::instance(); },
0108     [] { return &ExportConfig::instance(); },
0109     [] { return &FileConfig::instance(); },
0110     [] { return &FilenameFormatConfig::instance(); },
0111     [] { return &FilterConfig::instance(); },
0112     [] { return &ImportConfig::instance(); },
0113     [] { return &NetworkConfig::instance(); },
0114     [] { return &NumberTracksConfig::instance(); },
0115     [] { return &PlaylistConfig::instance(); },
0116     [] { return &RenDirConfig::instance(); },
0117     [] { return &TagConfig::instance(); },
0118     [] { return &TagFormatConfig::instance(); }
0119   };
0120   return cfgFuncs[idx]();
0121 }
0122 
0123 /**
0124  * Convert an integer value to the corresponding enum name string.
0125  * @param group config group
0126  * @param option config option
0127  * @param value enum value as integer
0128  * @return enum value as string, original int value if invalid.
0129  */
0130 QVariant configIntToEnumName(const QString& group, const QString& option,
0131                              const QVariant& value)
0132 {
0133   const int enumVal = value.toInt();
0134   if (option == QLatin1String("importDest") ||
0135       option == QLatin1String("exportSource") ||
0136       option == QLatin1String("numberTracksDestination")) {
0137     QString tagMaskStr;
0138     for (Frame::TagNumber tagNr :
0139          Frame::tagNumbersFromMask(Frame::tagVersionCast(enumVal))) {
0140       tagMaskStr += Frame::tagNumberToString(tagNr);
0141     }
0142     return tagMaskStr;
0143   }
0144   if (option == QLatin1String("caseConversion")) {
0145     const QMetaObject metaObj = FormatConfig::staticMetaObject;
0146     if (const char* key = metaObj.enumerator(
0147       metaObj.indexOfEnumerator("CaseConversion")).valueToKey(enumVal)) {
0148       return QString::fromLatin1(key);
0149     }
0150   } else if (group == QLatin1String("Playlist") &&
0151              option == QLatin1String("location")) {
0152     const QMetaObject metaObj = PlaylistConfig::staticMetaObject;
0153     if (const char* key = metaObj.enumerator(
0154           metaObj.indexOfEnumerator("PlaylistLocation")).valueToKey(enumVal)) {
0155       return QString::fromLatin1(key);
0156     }
0157   } else if (group == QLatin1String("Playlist") &&
0158              option == QLatin1String("format")) {
0159     const QMetaObject metaObj = PlaylistConfig::staticMetaObject;
0160     if (const char* key = metaObj.enumerator(
0161       metaObj.indexOfEnumerator("PlaylistFormat")).valueToKey(enumVal)) {
0162       return QString::fromLatin1(key);
0163     }
0164   } else if (group == QLatin1String("Tag") &&
0165              option == QLatin1String("id3v2Version")) {
0166     const QMetaObject metaObj = TagConfig::staticMetaObject;
0167     if (const char* key = metaObj.enumerator(
0168           metaObj.indexOfEnumerator("Id3v2Version")).valueToKey(enumVal)) {
0169       return QString::fromLatin1(key);
0170     }
0171   } else if (group == QLatin1String("Tag") &&
0172              option == QLatin1String("textEncoding")) {
0173     const QMetaObject metaObj = TagConfig::staticMetaObject;
0174     if (const char* key = metaObj.enumerator(
0175           metaObj.indexOfEnumerator("TextEncoding")).valueToKey(enumVal)) {
0176       return QString::fromLatin1(key);
0177     }
0178   }
0179   return value;
0180 }
0181 
0182 /**
0183  * Convert an enum value name to the corresponding integer value.
0184  * @param group config group
0185  * @param option config option
0186  * @param value enum name as variant
0187  * @return enum value as integer, original string value if invalid.
0188  */
0189 QVariant configIntFromEnumName(const QString& group, const QString& option,
0190                                const QVariant& value)
0191 {
0192   const QString enumName = value.toString();
0193   int val;
0194   bool ok;
0195   if (option == QLatin1String("importDest") ||
0196       option == QLatin1String("exportSource") ||
0197       option == QLatin1String("numberTracksDestination")) {
0198     val = 0;
0199     if (!enumName.isEmpty() && enumName.at(0).isDigit()) {
0200       FOR_ALL_TAGS(tagNr) {
0201         if (enumName.contains(Frame::tagNumberToString(tagNr))) {
0202           val |= Frame::tagVersionFromNumber(tagNr);
0203         }
0204       }
0205       if (val != 0) {
0206         return val;
0207       }
0208     }
0209   } else if (option == QLatin1String("caseConversion")) {
0210     const QMetaObject metaObj = FormatConfig::staticMetaObject;
0211     val = metaObj.enumerator(metaObj.indexOfEnumerator("CaseConversion"))
0212         .keyToValue(enumName.toLatin1(), &ok);
0213     if (ok) {
0214       return val;
0215     }
0216   } else if (group == QLatin1String("Playlist") &&
0217              option == QLatin1String("location")) {
0218     const QMetaObject metaObj = PlaylistConfig::staticMetaObject;
0219     val = metaObj.enumerator(metaObj.indexOfEnumerator("PlaylistLocation"))
0220         .keyToValue(enumName.toLatin1(), &ok);
0221     if (ok) {
0222       return val;
0223     }
0224   } else if (group == QLatin1String("Playlist") &&
0225              option == QLatin1String("format")) {
0226     const QMetaObject metaObj = PlaylistConfig::staticMetaObject;
0227     val = metaObj.enumerator(metaObj.indexOfEnumerator("PlaylistFormat"))
0228         .keyToValue(enumName.toLatin1(), &ok);
0229     if (ok) {
0230       return val;
0231     }
0232   } else if (group == QLatin1String("Tag") &&
0233              option == QLatin1String("id3v2Version")) {
0234     const QMetaObject metaObj = TagConfig::staticMetaObject;
0235     val = metaObj.enumerator(metaObj.indexOfEnumerator("Id3v2Version"))
0236         .keyToValue(enumName.toLatin1(), &ok);
0237     if (ok) {
0238       return val;
0239     }
0240   } else if (group == QLatin1String("Tag") &&
0241              option == QLatin1String("textEncoding")) {
0242     const QMetaObject metaObj = TagConfig::staticMetaObject;
0243     val = metaObj.enumerator(metaObj.indexOfEnumerator("TextEncoding"))
0244         .keyToValue(enumName.toLatin1(), &ok);
0245     if (ok) {
0246       return val;
0247     }
0248   }
0249   val = enumName.toInt(&ok);
0250   if (ok) {
0251     return val;
0252   }
0253   return QVariant();
0254 }
0255 
0256 }
0257 
0258 /**
0259  * Constructor.
0260  * @param processor command line processor
0261  * @param name name with which command is invoked
0262  * @param help help text for command
0263  * @param argspec argument specification
0264  */
0265 CliCommand::CliCommand(Kid3Cli* processor,
0266                        const QString& name, const QString& help,
0267                        const QString& argspec)
0268   : QObject(processor), m_processor(processor), m_name(name), m_help(help),
0269     m_argspec(argspec), m_timerId(0), m_timeoutMs(DEFAULT_TIMEOUT_MS), m_result(0)
0270 {
0271 }
0272 
0273 /**
0274  * Reset state to defaults.
0275  */
0276 void CliCommand::clear()
0277 {
0278   if (m_timerId != 0) {
0279     killTimer(m_timerId);
0280     m_timerId = 0;
0281   }
0282   cli()->finishWriting();
0283   m_errorMsg.clear();
0284   m_args.clear();
0285   m_result = 0;
0286 }
0287 
0288 /**
0289  * Execute command.
0290  */
0291 void CliCommand::execute()
0292 {
0293   if (m_timerId != 0) {
0294     killTimer(m_timerId);
0295     m_timerId = 0;
0296   }
0297   int msec = m_processor->getTimeout();
0298   if (msec == 0) {
0299     msec = getTimeout();
0300   }
0301   if (msec > 0) {
0302     m_timerId = startTimer(msec);
0303   }
0304   connectResultSignal();
0305   startCommand();
0306 }
0307 
0308 /**
0309  * Terminate command.
0310  */
0311 void CliCommand::terminate() {
0312   if (m_timerId != 0) {
0313     killTimer(m_timerId);
0314     m_timerId = 0;
0315   }
0316   disconnectResultSignal();
0317   emit finished();
0318 }
0319 
0320 /**
0321  * Connect signals used to emit finished().
0322  * This method is called after startCommand(). The default implementation
0323  * invokes terminate() in the event loop. It can be overridden to connect
0324  * signals connected to terminate() to signal termination of the command.
0325  */
0326 void CliCommand::connectResultSignal()
0327 {
0328   QTimer::singleShot(0, this, &CliCommand::terminate);
0329 }
0330 
0331 /**
0332  * Disconnect signals used to emit finished().
0333  * This method is called from terminate(). The default implementation
0334  * does nothing. It can be overridden to disconnect signals connected
0335  * in connectResultSignal().
0336  */
0337 void CliCommand::disconnectResultSignal()
0338 {
0339 }
0340 
0341 /**
0342  * Called on timeout.
0343  */
0344 void CliCommand::timerEvent(QTimerEvent*) {
0345   setError(tr("Timeout"));
0346   terminate();
0347 }
0348 
0349 /**
0350  * Get parameter for task mask.
0351  * @param nr index in args()
0352  * @param useDefault if true use cli()->tagMask() if no parameter found
0353  * @return tag versions.
0354  */
0355 Frame::TagVersion CliCommand::getTagMaskParameter(int nr,
0356                                                   bool useDefault) const
0357 {
0358   int tagMask = 0;
0359   if (m_args.size() > nr) {
0360     if (const QString& tagStr = m_args.at(nr);
0361         !tagStr.isEmpty() && tagStr.at(0).isDigit()) {
0362       FOR_ALL_TAGS(tagNr) {
0363         if (tagStr.contains(Frame::tagNumberToString(tagNr))) {
0364           tagMask |= Frame::tagVersionFromNumber(tagNr);
0365         }
0366       }
0367       if (tagMask == 0)
0368         tagMask = tagStr.toInt();
0369     }
0370   }
0371   if (tagMask == 0 && useDefault) {
0372     tagMask = m_processor->tagMask();
0373   }
0374   return Frame::tagVersionCast(tagMask);
0375 }
0376 
0377 /**
0378  * Show usage of command.
0379  */
0380 void CliCommand::showUsage()
0381 {
0382   cli()->writeHelp(name(), true);
0383   setError(QLatin1String("_Usage"));
0384 }
0385 
0386 
0387 
0388 HelpCommand::HelpCommand(Kid3Cli* processor)
0389   : CliCommand(processor, QLatin1String("help"), tr("Help"),
0390                QLatin1String("[S]\nS = ") + tr("Command name"))
0391 {
0392 }
0393 
0394 void HelpCommand::startCommand()
0395 {
0396   cli()->writeHelp(args().size() > 1 ? args().at(1) : QString());
0397 }
0398 
0399 
0400 TimeoutCommand::TimeoutCommand(Kid3Cli* processor)
0401   : CliCommand(processor, QLatin1String("timeout"), tr("Overwrite timeout"),
0402                QLatin1String("[S]\nS = \"default\" | \"off\" | ") + tr("Time") +
0403                QLatin1String(" [ms]"))
0404 {
0405 }
0406 
0407 void TimeoutCommand::startCommand()
0408 {
0409   int cliTimeout = cli()->getTimeout();
0410   if (args().size() > 1) {
0411     if (const QString& val = args().at(1); val == QLatin1String("off")) {
0412       cliTimeout = -1;
0413     } else if (val == QLatin1String("default")) {
0414       cliTimeout = 0;
0415     } else {
0416       QString msStr = val;
0417       if (msStr.endsWith(QLatin1String("ms"))) {
0418         msStr.truncate(msStr.length() - 2);
0419       }
0420       bool ok;
0421       if (int ms = msStr.toInt(&ok); ok && ms > 0) {
0422         cliTimeout = ms;
0423       }
0424     }
0425     cli()->setTimeout(cliTimeout);
0426   }
0427   QString value;
0428   if (cliTimeout < 0) {
0429     value = QLatin1String("off");
0430   } else if (cliTimeout == 0) {
0431     value = QLatin1String("default");
0432   } else {
0433     value = QString::number(cliTimeout);
0434     value += QLatin1String(" ms");
0435   }
0436   cli()->writeResult(QVariantMap{{QLatin1String("timeout"), value}});
0437 }
0438 
0439 
0440 QuitCommand::QuitCommand(Kid3Cli* processor)
0441   : CliCommand(processor, QLatin1String("exit"), tr("Quit application"),
0442                QLatin1String("[S]\nS = \"force\""))
0443 {
0444 }
0445 
0446 void QuitCommand::startCommand()
0447 {
0448   if (cli()->app()->isModified() && !cli()->app()->getDirName().isEmpty()) {
0449     if (!(args().size() > 1 && args().at(1) == QLatin1String("force"))) {
0450       cli()->writeResult(tr("The current folder has been modified.") %
0451                          QLatin1Char('\n') %
0452                          tr("Type 'exit force' to quit."));
0453       terminate();
0454       return;
0455     }
0456   }
0457   disconnect(this, &CliCommand::finished, cli(), &Kid3Cli::onCommandFinished);
0458   cli()->terminate();
0459 }
0460 
0461 void QuitCommand::connectResultSignal()
0462 {
0463   // Do not signal finished() to avoid printing prompt.
0464 }
0465 
0466 
0467 CdCommand::CdCommand(Kid3Cli* processor)
0468   : CliCommand(processor, QLatin1String("cd"), tr("Change folder"),
0469                QLatin1String("[P]"))
0470 {
0471 }
0472 
0473 void CdCommand::startCommand()
0474 {
0475   QStringList paths;
0476   if (args().size() > 1) {
0477     paths = args().mid(1);
0478   } else {
0479     paths.append(QDir::homePath());
0480   }
0481   if (!cli()->openDirectory(Kid3Cli::expandWildcards(paths))) {
0482     setError(tr("%1 does not exist").arg(paths.join(QLatin1String(", "))));
0483     terminate();
0484   }
0485 }
0486 
0487 void CdCommand::connectResultSignal()
0488 {
0489   connect(cli()->app(), &Kid3Application::directoryOpened,
0490     this, &CdCommand::terminate);
0491 }
0492 
0493 void CdCommand::disconnectResultSignal()
0494 {
0495   disconnect(cli()->app(), &Kid3Application::directoryOpened,
0496     this, &CdCommand::terminate);
0497 }
0498 
0499 
0500 PwdCommand::PwdCommand(Kid3Cli* processor)
0501   : CliCommand(processor, QLatin1String("pwd"),
0502                tr("Print the filename of the current folder"))
0503 {
0504 }
0505 
0506 void PwdCommand::startCommand()
0507 {
0508   QString path = cli()->app()->getDirPath();
0509   if (path.isNull()) {
0510     path = QDir::currentPath();
0511     cli()->app()->openDirectory({path});
0512   }
0513   cli()->writeResult(path);
0514 }
0515 
0516 
0517 LsCommand::LsCommand(Kid3Cli* processor)
0518   : CliCommand(processor, QLatin1String("ls"), tr("Folder list"))
0519 {
0520   setTimeout(10000);
0521 }
0522 
0523 void LsCommand::startCommand()
0524 {
0525   cli()->writeFileList();
0526 }
0527 
0528 
0529 SaveCommand::SaveCommand(Kid3Cli* processor)
0530   : CliCommand(processor, QLatin1String("save"), tr("Saves the changed files"))
0531 {
0532 }
0533 
0534 void SaveCommand::startCommand()
0535 {
0536   QStringList errorDescriptions;
0537   if (const QStringList errorFiles = cli()->app()->saveDirectory(&errorDescriptions);
0538       errorFiles.isEmpty()) {
0539     cli()->updateSelection();
0540   } else {
0541     setError(tr("Error while writing file:\n") +
0542              Kid3Application::mergeStringLists(errorFiles, errorDescriptions,
0543                                                QLatin1String(": "))
0544              .join(QLatin1String("\n")));
0545   }
0546 }
0547 
0548 
0549 SelectCommand::SelectCommand(Kid3Cli* processor)
0550   : CliCommand(processor, QLatin1String("select"), tr("Select file"),
0551                QLatin1String("[P|S]\n"
0552                 "S = \"all\" | \"none\" | \"first\" | \"previous\" | \"next\""))
0553 {
0554 }
0555 
0556 void SelectCommand::startCommand()
0557 {
0558   if (args().size() > 1) {
0559     if (const QString& param = args().at(1); param == QLatin1String("all")) {
0560       cli()->app()->selectAllFiles();
0561     } else if (param == QLatin1String("none")) {
0562       cli()->app()->deselectAllFiles();
0563     } else if (param == QLatin1String("first")) {
0564       setResult(cli()->app()->firstFile(true) ? 0 : 1);
0565     } else if (param == QLatin1String("previous")) {
0566       setResult(cli()->app()->previousFile(true) ? 0 : 1);
0567     } else if (param == QLatin1String("next")) {
0568       setResult(cli()->app()->nextFile(true) ? 0 : 1);
0569     } else {
0570       if (QStringList paths = args().mid(1);
0571           !cli()->selectFile(Kid3Cli::expandWildcards(paths))) {
0572         setError(tr("%1 not found").arg(paths.join(QLatin1String(", "))));
0573       }
0574     }
0575   } else {
0576     cli()->updateSelection();
0577   }
0578 }
0579 
0580 
0581 TagCommand::TagCommand(Kid3Cli* processor)
0582   : CliCommand(processor, QLatin1String("tag"), tr("Select tag"),
0583                QLatin1String("[T]"))
0584 {
0585 }
0586 
0587 void TagCommand::startCommand()
0588 {
0589   if (Frame::TagVersion tagMask = getTagMaskParameter(1, false);
0590       tagMask != Frame::TagNone) {
0591     cli()->setTagMask(tagMask);
0592   } else {
0593     cli()->writeTagMask();
0594   }
0595 }
0596 
0597 
0598 GetCommand::GetCommand(Kid3Cli* processor)
0599   : CliCommand(processor, QLatin1String("get"), tr("Get tag frame"),
0600                QLatin1String("[N|S] [T]\nS = \"all\""))
0601 {
0602 }
0603 
0604 void GetCommand::startCommand()
0605 {
0606   int numArgs = args().size();
0607   QString name = numArgs > 1
0608       ? Frame::getNameForTranslatedFrameName(args().at(1))
0609       : QLatin1String("all");
0610   Frame::TagVersion tagMask = getTagMaskParameter(2);
0611   if (name == QLatin1String("all")) {
0612     cli()->writeFileInformation(tagMask);
0613   } else {
0614     for (Frame::TagNumber tagNr : Frame::tagNumbersFromMask(tagMask)) {
0615       if (QString value = cli()->app()->getFrame(
0616           Frame::tagVersionFromNumber(tagNr), name);
0617           !(tagNr == Frame::Tag_1 ? value.isEmpty() : value.isNull())) {
0618         cli()->writeResult(value);
0619         break;
0620       }
0621     }
0622   }
0623 }
0624 
0625 
0626 SetCommand::SetCommand(Kid3Cli* processor)
0627   : CliCommand(processor, QLatin1String("set"), tr("Set tag frame"),
0628                QLatin1String("N V [T]"))
0629 {
0630 }
0631 
0632 void SetCommand::startCommand()
0633 {
0634   if (int numArgs = args().size(); numArgs > 2) {
0635     QString name = Frame::getNameForTranslatedFrameName(args().at(1));
0636     const QString& value = args().at(2);
0637     if (Frame::TagVersion tagMask = getTagMaskParameter(3);
0638         cli()->app()->setFrame(tagMask, name, value)) {
0639       if (!name.endsWith(QLatin1String(".selected"))) {
0640         cli()->updateSelectedFiles();
0641         cli()->updateSelection();
0642       }
0643     } else if (!value.isEmpty()) {
0644       setError(tr("Could not set \"%1\" for %2").arg(value, name));
0645     }
0646   } else {
0647     showUsage();
0648   }
0649 }
0650 
0651 
0652 RevertCommand::RevertCommand(Kid3Cli* processor)
0653   : CliCommand(processor, QLatin1String("revert"),
0654                tr("Revert"))
0655 {
0656 }
0657 
0658 void RevertCommand::startCommand()
0659 {
0660   cli()->app()->revertFileModifications();
0661 }
0662 
0663 
0664 ImportCommand::ImportCommand(Kid3Cli* processor)
0665   : CliCommand(processor, QLatin1String("import"),
0666                tr("Import from file"),
0667                QLatin1String("P S [T]\nP S = ") +
0668                tr("File path") + QLatin1Char(' ') + tr("Format name") +
0669                QLatin1String(" | tags ") + tr("Source") + QLatin1Char(' ') +
0670                tr("Extraction"))
0671 {
0672 }
0673 
0674 void ImportCommand::startCommand()
0675 {
0676   if (int numArgs = args().size();
0677       numArgs > 3 && args().at(1).startsWith(QLatin1String("tags"))) {
0678     const QString& source = args().at(2);
0679     const QString& extraction = args().at(3);
0680     Frame::TagVersion tagMask = getTagMaskParameter(4);
0681     if (args().at(1).contains(QLatin1String("sel"))) {
0682       if (QStringList returnValues =
0683           cli()->app()->importFromTagsToSelection(tagMask, source, extraction);
0684           !returnValues.isEmpty()) {
0685         cli()->writeResult(returnValues);
0686       }
0687     } else {
0688       cli()->app()->importFromTags(tagMask, source, extraction);
0689     }
0690   } else if (numArgs > 2) {
0691     const QString& path = args().at(1);
0692     const QString& fmtName = args().at(2);
0693     bool ok;
0694     int fmtIdx = fmtName.toInt(&ok);
0695     if (!ok) {
0696       fmtIdx = ImportConfig::instance().importFormatNames().indexOf(fmtName);
0697       if (fmtIdx == -1) {
0698         QString errMsg = tr("%1 not found.").arg(fmtName);
0699         errMsg += QLatin1Char('\n');
0700         errMsg += tr("Available");
0701         errMsg += QLatin1String(": ");
0702         errMsg += ImportConfig::instance().importFormatNames().join(
0703               QLatin1String(", "));
0704         errMsg += QLatin1Char('.');
0705         setError(errMsg);
0706         return;
0707       }
0708     }
0709     if (Frame::TagVersion tagMask = getTagMaskParameter(3);
0710         !cli()->app()->importTags(tagMask, path, fmtIdx)) {
0711       setError(tr("Error"));
0712     }
0713   } else {
0714     showUsage();
0715   }
0716 }
0717 
0718 
0719 BatchImportCommand::BatchImportCommand(Kid3Cli* processor)
0720   : CliCommand(processor, QLatin1String("autoimport"),
0721                tr("Automatic import"), QLatin1String("[S] [T]\nS = ") +
0722                tr("Profile name"))
0723 {
0724   setTimeout(60000);
0725 }
0726 
0727 void BatchImportCommand::startCommand()
0728 {
0729   int numArgs = args().size();
0730   const QString& profileName = numArgs > 1
0731       ? args().at(1) : QLatin1String("All");
0732   if (Frame::TagVersion tagMask = getTagMaskParameter(2);
0733       !cli()->app()->batchImport(profileName, tagMask)) {
0734     QString errMsg = tr("%1 not found.").arg(profileName);
0735     errMsg += QLatin1Char('\n');
0736     errMsg += tr("Available");
0737     errMsg += QLatin1String(": ");
0738     errMsg += BatchImportConfig::instance().profileNames().join(
0739           QLatin1String(", "));
0740     errMsg += QLatin1Char('.');
0741     setError(errMsg);
0742     terminate();
0743   }
0744 }
0745 
0746 void BatchImportCommand::connectResultSignal()
0747 {
0748   BatchImporter* importer = cli()->app()->getBatchImporter();
0749   connect(importer, &BatchImporter::reportImportEvent,
0750           this, &BatchImportCommand::onReportImportEvent);
0751   connect(importer, &BatchImporter::finished,
0752           this, &BatchImportCommand::terminate);
0753 }
0754 
0755 void BatchImportCommand::disconnectResultSignal()
0756 {
0757   BatchImporter* importer = cli()->app()->getBatchImporter();
0758   disconnect(importer, &BatchImporter::reportImportEvent,
0759              this, &BatchImportCommand::onReportImportEvent);
0760   disconnect(importer, &BatchImporter::finished,
0761              this, &BatchImportCommand::terminate);
0762 }
0763 
0764 void BatchImportCommand::onReportImportEvent(int type, const QString& text)
0765 {
0766   QString typeStr;
0767   switch (type) {
0768   case BatchImporter::ReadingDirectory:
0769     typeStr = QLatin1String("readingDirectory");
0770     break;
0771   case BatchImporter::Started:
0772     typeStr = QLatin1String("started");
0773     break;
0774   case BatchImporter::SourceSelected:
0775     typeStr = QLatin1String("source");
0776     break;
0777   case BatchImporter::QueryingAlbumList:
0778     typeStr = QLatin1String("querying");
0779     break;
0780   case BatchImporter::FetchingTrackList:
0781   case BatchImporter::FetchingCoverArt:
0782     typeStr = QLatin1String("fetching");
0783     break;
0784   case BatchImporter::TrackListReceived:
0785     typeStr = QLatin1String("data received");
0786     break;
0787   case BatchImporter::CoverArtReceived:
0788     typeStr = QLatin1String("cover");
0789     break;
0790   case BatchImporter::Finished:
0791     typeStr = QLatin1String("finished");
0792     break;
0793   case BatchImporter::Aborted:
0794     typeStr = QLatin1String("aborted");
0795     break;
0796   case BatchImporter::Error:
0797     typeStr = QLatin1String("error");
0798   }
0799   QVariantMap event{{QLatin1String("type"), typeStr}};
0800   if (!text.isEmpty()) {
0801     event.insert(QLatin1String("data"), text);
0802   }
0803   cli()->writeResult(QVariantMap{{QLatin1String("event"), event}});
0804 }
0805 
0806 
0807 AlbumArtCommand::AlbumArtCommand(Kid3Cli* processor)
0808   : CliCommand(processor, QLatin1String("albumart"),
0809                tr("Download album cover artwork"),
0810                QLatin1String("U [S]\nS = \"all\""))
0811 {
0812   setTimeout(10000);
0813 }
0814 
0815 void AlbumArtCommand::startCommand()
0816 {
0817   if (int numArgs = args().size(); numArgs > 1) {
0818     const QString& url = args().at(1);
0819     cli()->app()->downloadImage(url,
0820                   numArgs > 2 && args().at(2) == QLatin1String("all"));
0821   } else {
0822     showUsage();
0823     terminate();
0824   }
0825 }
0826 
0827 void AlbumArtCommand::connectResultSignal()
0828 {
0829   DownloadClient* downloadClient = cli()->app()->getDownloadClient();
0830   connect(downloadClient, &DownloadClient::downloadFinished,
0831           this, &AlbumArtCommand::onDownloadFinished);
0832 }
0833 
0834 void AlbumArtCommand::disconnectResultSignal()
0835 {
0836   DownloadClient* downloadClient = cli()->app()->getDownloadClient();
0837   disconnect(downloadClient,
0838              &DownloadClient::downloadFinished,
0839              this, &AlbumArtCommand::onDownloadFinished);
0840 }
0841 
0842 void AlbumArtCommand::onDownloadFinished(
0843     const QByteArray& data, const QString& mimeType, const QString& url)
0844 {
0845   cli()->app()->imageDownloaded(data, mimeType, url);
0846   terminate();
0847 }
0848 
0849 
0850 ExportCommand::ExportCommand(Kid3Cli* processor)
0851   : CliCommand(processor, QLatin1String("export"),
0852                tr("Export to file"),
0853                QLatin1String("P S [T]\nS = ") + tr("Format name"))
0854 {
0855 }
0856 
0857 void ExportCommand::startCommand()
0858 {
0859   if (int numArgs = args().size(); numArgs > 2) {
0860     const QString& path = args().at(1);
0861     const QString& fmtName = args().at(2);
0862     bool ok;
0863     int fmtIdx = fmtName.toInt(&ok);
0864     if (!ok) {
0865       fmtIdx = ExportConfig::instance().exportFormatNames().indexOf(fmtName);
0866       if (fmtIdx == -1) {
0867         QString errMsg = tr("%1 not found.").arg(fmtName);
0868         errMsg += QLatin1Char('\n');
0869         errMsg += tr("Available");
0870         errMsg += QLatin1String(": ");
0871         errMsg += ExportConfig::instance().exportFormatNames().join(
0872               QLatin1String(", "));
0873         errMsg += QLatin1Char('.');
0874         setError(errMsg);
0875         return;
0876       }
0877     }
0878     if (Frame::TagVersion tagMask = getTagMaskParameter(3);
0879         !cli()->app()->exportTags(tagMask, path, fmtIdx)) {
0880       setError(tr("Error"));
0881     }
0882   } else {
0883     showUsage();
0884   }
0885 }
0886 
0887 
0888 PlaylistCommand::PlaylistCommand(Kid3Cli* processor)
0889   : CliCommand(processor, QLatin1String("playlist"), tr("Create playlist"))
0890 {
0891 }
0892 
0893 void PlaylistCommand::startCommand()
0894 {
0895   if (!cli()->app()->writePlaylist()) {
0896     setError(tr("Error"));
0897   }
0898 }
0899 
0900 
0901 FilenameFormatCommand::FilenameFormatCommand(Kid3Cli* processor)
0902   : CliCommand(processor, QLatin1String("filenameformat"),
0903                tr("Apply filename format"))
0904 {
0905 }
0906 
0907 void FilenameFormatCommand::startCommand()
0908 {
0909   cli()->app()->applyFilenameFormat();
0910 }
0911 
0912 
0913 TagFormatCommand::TagFormatCommand(Kid3Cli* processor)
0914   : CliCommand(processor, QLatin1String("tagformat"), tr("Apply tag format"))
0915 {
0916 }
0917 
0918 void TagFormatCommand::startCommand()
0919 {
0920   cli()->app()->applyTagFormat();
0921 }
0922 
0923 
0924 TextEncodingCommand::TextEncodingCommand(Kid3Cli* processor)
0925   : CliCommand(processor, QLatin1String("textencoding"),
0926                tr("Apply text encoding"))
0927 {
0928 }
0929 
0930 void TextEncodingCommand::startCommand()
0931 {
0932   cli()->app()->applyTextEncoding();
0933 }
0934 
0935 
0936 RenameDirectoryCommand::RenameDirectoryCommand(Kid3Cli* processor)
0937   : CliCommand(processor, QLatin1String("renamedir"), tr("Rename folder"),
0938        QLatin1String("[F] [S] [T]\nS = \"create\" | \"rename\" | \"dryrun\"")),
0939     m_dryRun(false)
0940 {
0941 }
0942 
0943 void RenameDirectoryCommand::startCommand()
0944 {
0945   Frame::TagVersion tagMask = Frame::TagNone;
0946   QString format;
0947   bool create = false;
0948   m_dryRun = false;
0949   for (int i = 1; i < args().size(); ++i) {
0950     bool ok = false;
0951     if (tagMask == Frame::TagNone) {
0952       tagMask = getTagMaskParameter(i, false);
0953       ok = tagMask != Frame::TagNone;
0954     }
0955     if (!ok) {
0956       if (const QString& param = args().at(i);
0957           param == QLatin1String("create")) {
0958         create = true;
0959       } else if (param == QLatin1String("rename")) {
0960         create = false;
0961       } else if (param == QLatin1String("dryrun")) {
0962         m_dryRun = true;
0963       } else if (format.isEmpty()) {
0964         format = param;
0965       }
0966     }
0967   }
0968   if (tagMask == Frame::TagNone) {
0969     tagMask = cli()->tagMask();
0970   }
0971   if (format.isEmpty()) {
0972     format = RenDirConfig::instance().dirFormat();
0973   }
0974 
0975   if (!cli()->app()->renameDirectory(tagMask, format, create)) {
0976     terminate();
0977   }
0978 }
0979 
0980 void RenameDirectoryCommand::connectResultSignal()
0981 {
0982   DirRenamer* renamer = cli()->app()->getDirRenamer();
0983   connect(renamer, &DirRenamer::actionScheduled,
0984           this, &RenameDirectoryCommand::onActionScheduled);
0985   connect(cli()->app(), &Kid3Application::renameActionsScheduled,
0986           this, &RenameDirectoryCommand::onRenameActionsScheduled);
0987 }
0988 
0989 void RenameDirectoryCommand::disconnectResultSignal()
0990 {
0991   DirRenamer* renamer = cli()->app()->getDirRenamer();
0992   disconnect(renamer, &DirRenamer::actionScheduled,
0993              this, &RenameDirectoryCommand::onActionScheduled);
0994   disconnect(cli()->app(), &Kid3Application::renameActionsScheduled,
0995              this, &RenameDirectoryCommand::onRenameActionsScheduled);
0996 }
0997 
0998 void RenameDirectoryCommand::onActionScheduled(const QStringList& actionStrs)
0999 {
1000   QVariantMap event{{QLatin1String("type"), actionStrs.at(0)}};
1001   QVariantMap data;
1002   if (actionStrs.size() > 1) {
1003     data.insert(QLatin1String("source"), actionStrs.at(1));
1004   }
1005   if (actionStrs.size() > 2) {
1006     data.insert(QLatin1String("destination"), actionStrs.at(2));
1007   }
1008   if (!data.isEmpty()) {
1009     event.insert(QLatin1String("data"), data);
1010   }
1011   cli()->writeResult(QVariantMap{{QLatin1String("event"), event}});
1012 }
1013 
1014 void RenameDirectoryCommand::onRenameActionsScheduled()
1015 {
1016   if (!m_dryRun) {
1017     if (QString errMsg = cli()->app()->performRenameActions(); errMsg.isEmpty()) {
1018       cli()->app()->deselectAllFiles();
1019     } else {
1020       setError(errMsg);
1021     }
1022   }
1023   terminate();
1024 }
1025 
1026 
1027 NumberTracksCommand::NumberTracksCommand(Kid3Cli* processor)
1028   : CliCommand(processor, QLatin1String("numbertracks"), tr("Number tracks"),
1029                QLatin1String("[S] [T]\nS = ") + tr("Track number"))
1030 {
1031 }
1032 
1033 void NumberTracksCommand::startCommand()
1034 {
1035   int numArgs = args().size();
1036   int firstTrackNr = 1;
1037   bool ok = false;
1038   if (numArgs > 1) {
1039     firstTrackNr = args().at(1).toInt(&ok);
1040   }
1041   if (!ok) {
1042     firstTrackNr = 1;
1043   }
1044   Frame::TagVersion tagMask = getTagMaskParameter(2);
1045   Kid3Application::NumberTrackOptions options;
1046   options |= Kid3Application::NumberTracksEnabled;
1047   options |= Kid3Application::NumberTracksResetCounterForEachDirectory;
1048   cli()->app()->numberTracks(firstTrackNr, 0, tagMask, options);
1049 }
1050 
1051 
1052 FilterCommand::FilterCommand(Kid3Cli* processor)
1053   : CliCommand(processor, QLatin1String("filter"), tr("Filter"),
1054                QLatin1String("F|S\nS = ") + tr("Filter name"))
1055 {
1056   setTimeout(60000);
1057 }
1058 
1059 void FilterCommand::startCommand()
1060 {
1061   if (args().size() > 1) {
1062     QString expression = args().at(1);
1063     if (int fltIdx = FilterConfig::instance().filterNames().indexOf(expression);
1064         fltIdx != -1) {
1065       expression = FilterConfig::instance().filterExpressions().at(fltIdx);
1066     } else if (!expression.isEmpty() &&
1067                !expression.contains(QLatin1Char('%'))) {
1068       // Probably an invalid expression
1069       QString errMsg = tr("%1 not found.").arg(expression);
1070       errMsg += QLatin1Char('\n');
1071       errMsg += tr("Available");
1072       errMsg += QLatin1String(": ");
1073       errMsg += FilterConfig::instance().filterNames().join(
1074             QLatin1String(", "));
1075       errMsg += QLatin1Char('.');
1076       setError(errMsg);
1077       terminate();
1078       return;
1079     }
1080     cli()->app()->applyFilter(expression);
1081   } else {
1082     showUsage();
1083     terminate();
1084   }
1085 }
1086 
1087 void FilterCommand::connectResultSignal()
1088 {
1089   connect(cli()->app(), &Kid3Application::fileFiltered,
1090           this, &FilterCommand::onFileFiltered);
1091 }
1092 
1093 void FilterCommand::disconnectResultSignal()
1094 {
1095   cli()->app()->abortFilter();
1096   disconnect(cli()->app(), &Kid3Application::fileFiltered,
1097              this, &FilterCommand::onFileFiltered);
1098 }
1099 
1100 void FilterCommand::onFileFiltered(int type, const QString& fileName)
1101 {
1102   QString typeStr;
1103   QString data;
1104   bool finish = false;
1105   switch (type) {
1106   case FileFilter::Started:
1107     typeStr = QLatin1String("started");
1108     break;
1109   case FileFilter::Directory:
1110     typeStr = QLatin1String("filterEntered");
1111     data = fileName;
1112     break;
1113   case FileFilter::ParseError:
1114     typeStr = QLatin1String("parseError");
1115     break;
1116   case FileFilter::FilePassed:
1117     typeStr = QLatin1String("filterPassed");
1118     data = fileName;
1119     break;
1120   case FileFilter::FileFilteredOut:
1121     typeStr = QLatin1String("filteredOut");
1122     data = fileName;
1123     break;
1124   case FileFilter::Finished:
1125     typeStr = QLatin1String("finished");
1126     finish = true;
1127     break;
1128   case FileFilter::Aborted:
1129     typeStr = QLatin1String("aborted");
1130     finish = true;
1131     break;
1132   }
1133   QVariantMap event{{QLatin1String("type"), typeStr}};
1134   if (!data.isEmpty()) {
1135     event.insert(QLatin1String("data"), data);
1136   }
1137   cli()->writeResult(QVariantMap{{QLatin1String("event"), event}});
1138   if (finish) {
1139     terminate();
1140   }
1141 }
1142 
1143 
1144 ToId3v24Command::ToId3v24Command(Kid3Cli* processor)
1145   : CliCommand(processor, QLatin1String("to24"), tr("Convert ID3v2.3 to ID3v2.4"))
1146 {
1147 }
1148 
1149 void ToId3v24Command::startCommand()
1150 {
1151   cli()->app()->convertToId3v24();
1152 }
1153 
1154 
1155 ToId3v23Command::ToId3v23Command(Kid3Cli* processor)
1156   : CliCommand(processor, QLatin1String("to23"), tr("Convert ID3v2.4 to ID3v2.3"))
1157 {
1158 }
1159 
1160 void ToId3v23Command::startCommand()
1161 {
1162   cli()->app()->convertToId3v23();
1163 }
1164 
1165 
1166 TagToFilenameCommand::TagToFilenameCommand(Kid3Cli* processor)
1167   : CliCommand(processor, QLatin1String("fromtag"), tr("Filename from tag"),
1168                QLatin1String("[F] [T]"))
1169 {
1170 }
1171 
1172 void TagToFilenameCommand::startCommand()
1173 {
1174   Frame::TagVersion tagMask = Frame::TagNone;
1175   QString format;
1176   for (int i = 1; i < qMin(args().size(), 3); ++i) {
1177     bool ok = false;
1178     if (tagMask == Frame::TagNone) {
1179       tagMask = getTagMaskParameter(i, false);
1180       ok = tagMask != Frame::TagNone;
1181     }
1182     if (!ok && format.isEmpty()) {
1183       format = args().at(i);
1184     }
1185   }
1186   if (tagMask == Frame::TagNone) {
1187     tagMask = cli()->tagMask();
1188   }
1189   if (!format.isEmpty()) {
1190     FileConfig::instance().setToFilenameFormat(format);
1191   }
1192   cli()->app()->getFilenameFromTags(tagMask);
1193 }
1194 
1195 
1196 FilenameToTagCommand::FilenameToTagCommand(Kid3Cli* processor)
1197   : CliCommand(processor, QLatin1String("totag"), tr("Tag from filename"),
1198                QLatin1String("[F] [T]"))
1199 {
1200 }
1201 
1202 void FilenameToTagCommand::startCommand()
1203 {
1204   Frame::TagVersion tagMask = Frame::TagNone;
1205   QString format;
1206   for (int i = 1; i < qMin(args().size(), 3); ++i) {
1207     bool ok = false;
1208     if (tagMask == Frame::TagNone) {
1209       tagMask = getTagMaskParameter(i, false);
1210       ok = tagMask != Frame::TagNone;
1211     }
1212     if (!ok && format.isEmpty()) {
1213       format = args().at(i);
1214     }
1215   }
1216   if (tagMask == Frame::TagNone) {
1217     tagMask = cli()->tagMask();
1218   }
1219   if (!format.isEmpty()) {
1220     FileConfig::instance().setFromFilenameFormat(format);
1221   }
1222   cli()->app()->getTagsFromFilename(tagMask);
1223 }
1224 
1225 
1226 TagToOtherTagCommand::TagToOtherTagCommand(Kid3Cli* processor)
1227   : CliCommand(processor, QLatin1String("syncto"), tr("Tag to other tag"),
1228                QLatin1String("T"))
1229 {
1230 }
1231 
1232 void TagToOtherTagCommand::startCommand()
1233 {
1234   if (Frame::TagVersion tagMask = getTagMaskParameter(1, false);
1235       tagMask != Frame::TagNone) {
1236     cli()->app()->copyToOtherTag(tagMask);
1237   } else {
1238     showUsage();
1239   }
1240 }
1241 
1242 
1243 CopyCommand::CopyCommand(Kid3Cli* processor)
1244   : CliCommand(processor, QLatin1String("copy"), tr("Copy"),
1245                QLatin1String("[T]"))
1246 {
1247 }
1248 
1249 void CopyCommand::startCommand()
1250 {
1251   Frame::TagVersion tagMask = getTagMaskParameter(1);
1252   cli()->app()->copyTags(tagMask);
1253 }
1254 
1255 
1256 PasteCommand::PasteCommand(Kid3Cli* processor)
1257   : CliCommand(processor, QLatin1String("paste"), tr("Paste"),
1258                QLatin1String("[T]"))
1259 {
1260 }
1261 
1262 void PasteCommand::startCommand()
1263 {
1264   Frame::TagVersion tagMask = getTagMaskParameter(1);
1265   cli()->app()->pasteTags(tagMask);
1266 }
1267 
1268 
1269 RemoveCommand::RemoveCommand(Kid3Cli* processor)
1270   : CliCommand(processor, QLatin1String("remove"), tr("Remove"),
1271                QLatin1String("[T]"))
1272 {
1273 }
1274 
1275 void RemoveCommand::startCommand()
1276 {
1277   Frame::TagVersion tagMask = getTagMaskParameter(1);
1278   cli()->app()->removeTags(tagMask);
1279 }
1280 
1281 
1282 ConfigCommand::ConfigCommand(Kid3Cli* processor)
1283   : CliCommand(processor, QLatin1String("config"), tr("Configure Kid3"),
1284                QLatin1String("[S]\nS = ") + tr("Group.Option Value"))
1285 {
1286 }
1287 
1288 void ConfigCommand::startCommand()
1289 {
1290   int numArgs = args().size();
1291   QString group, option;
1292   GeneralConfig* cfg = nullptr;
1293   QVariant value;
1294   if (numArgs > 1) {
1295     const QString& groupOption = args().at(1);
1296     if (int dotPos = groupOption.indexOf(QLatin1Char('.')); dotPos > 0) {
1297       group = groupOption.left(dotPos);
1298       option = groupOption.mid(dotPos + 1);
1299     } else {
1300       group = groupOption;
1301     }
1302     cfg = getConfig(group);
1303     if (!cfg) {
1304       setError(tr("%1 does not exist").arg(group));
1305       return;
1306     }
1307     if (!option.isNull()) {
1308       value = cfg->property(option.toLatin1());
1309       if (!value.isValid()) {
1310         setError(tr("%1 does not exist").arg(option));
1311         return;
1312       }
1313     }
1314   }
1315   if (numArgs > 2) {
1316     const QMetaObject* metaObj = nullptr;
1317     if (int propIdx = -1;
1318         !option.isNull() && (metaObj = cfg->metaObject()) != nullptr &&
1319         (propIdx = metaObj->indexOfProperty(option.toLatin1())) >= 0) {
1320 #if QT_VERSION >= 0x060000
1321       auto propType = metaObj->property(propIdx).typeId();
1322       if (propType == QMetaType::QStringList) {
1323         value = QVariant(args().mid(2));
1324       } else if (propType == QMetaType::Int) {
1325         value = configIntFromEnumName(group, option, args().at(2));
1326       } else if (propType == QMetaType::Bool) {
1327         value = QVariant(args().at(2)).toBool();
1328       } else {
1329         value = args().at(2);
1330       }
1331       if (value.typeId() == propType) {
1332         cfg->setProperty(option.toLatin1(), value);
1333         cli()->app()->applyChangedConfiguration();
1334         // The value is read back and will be displayed.
1335         value = cfg->property(option.toLatin1());
1336       } else {
1337         setError(tr("Invalid value %1").arg(value.toString()));
1338         return;
1339       }
1340 #else
1341       QVariant::Type propType = metaObj->property(propIdx).type();
1342       if (propType == QVariant::StringList) {
1343         value = QVariant(args().mid(2));
1344       } else if (propType == QVariant::Int) {
1345         value = configIntFromEnumName(group, option, args().at(2));
1346       } else if (propType == QVariant::Bool) {
1347         value = QVariant(args().at(2)).toBool();
1348       } else {
1349         value = args().at(2);
1350       }
1351       if (value.type() == propType) {
1352         cfg->setProperty(option.toLatin1(), value);
1353         cli()->app()->applyChangedConfiguration();
1354         // The value is read back and will be displayed.
1355         value = cfg->property(option.toLatin1());
1356       } else {
1357         setError(tr("Invalid value %1").arg(value.toString()));
1358         return;
1359       }
1360 #endif
1361     }
1362   }
1363   if (numArgs > 1) {
1364     if (option.isNull()) {
1365       if (auto metaObj = cfg->metaObject()) {
1366         QStringList propertyNames;
1367         for (int i = 0; i < metaObj->propertyCount(); ++i) {
1368           if (QString propertyName = QString::fromLatin1(metaObj->property(i).name());
1369               !excludedConfigPropertyNames.contains(propertyName)) {
1370             propertyNames.append(propertyName);
1371           }
1372         }
1373         cli()->writeResult(propertyNames);
1374       }
1375     } else {
1376 #if QT_VERSION >= 0x060000
1377       if (value.typeId() == QMetaType::QStringList) {
1378         cli()->writeResult(value.toStringList());
1379       } else if (value.typeId() == QMetaType::QVariantMap) {
1380         cli()->writeResult(value.toMap());
1381       } else if (value.typeId() == QMetaType::Int) {
1382         value = configIntToEnumName(group, option, value);
1383         cli()->writeResult(value.toString());
1384       } else if (value.typeId() == QMetaType::Bool) {
1385         cli()->writeResult(value.toBool());
1386       } else {
1387         cli()->writeResult(value.toString());
1388       }
1389 #else
1390       if (value.type() == QVariant::StringList) {
1391         cli()->writeResult(value.toStringList());
1392       } else if (value.type() == QVariant::Map) {
1393         cli()->writeResult(value.toMap());
1394       } else if (value.type() == QVariant::Int) {
1395         value = configIntToEnumName(group, option, value);
1396         cli()->writeResult(value.toString());
1397       } else if (value.type() == QVariant::Bool) {
1398         cli()->writeResult(value.toBool());
1399       } else {
1400         cli()->writeResult(value.toString());
1401       }
1402 #endif
1403     }
1404   } else {
1405     cli()->writeResult(configNames);
1406   }
1407 }
1408 
1409 
1410 ExecuteCommand::ExecuteCommand(Kid3Cli* processor)
1411   : CliCommand(processor, QLatin1String("execute"), tr("Execute command"),
1412                QLatin1String("S\nS = [@qml] ") + tr("Executable [arguments]"))
1413 {
1414   setTimeout(-1);
1415 }
1416 
1417 void ExecuteCommand::setCaption(const QString& title)
1418 {
1419   Q_UNUSED(title)
1420 }
1421 
1422 void ExecuteCommand::append(const QString& text)
1423 {
1424   cli()->writeLine(text);
1425 }
1426 
1427 void ExecuteCommand::scrollToBottom()
1428 {
1429 }
1430 
1431 void ExecuteCommand::startCommand()
1432 {
1433   if (args().size() > 1) {
1434     QString command = args().at(1);
1435     if (!m_process) {
1436       m_process.reset(new ExternalProcess(cli()->app(), this));
1437       connectResultSignal();
1438     }
1439     m_process->setOutputViewer(this);
1440     if (!m_process->launchCommand(command, args().mid(1), true)) {
1441       setError(tr("Could not execute ") + args().mid(1).join(QLatin1String(" ")));
1442       terminate();
1443     }
1444   } else {
1445     showUsage();
1446     terminate();
1447   }
1448 }
1449 
1450 void ExecuteCommand::connectResultSignal()
1451 {
1452   if (m_process) {
1453     connect(m_process.data(), &ExternalProcess::finished,
1454             this, &ExecuteCommand::terminate, Qt::UniqueConnection);
1455   }
1456 }
1457 
1458 void ExecuteCommand::disconnectResultSignal()
1459 {
1460   if (m_process) {
1461     disconnect(m_process.data(), &ExternalProcess::finished,
1462                this, &ExecuteCommand::terminate);
1463     // Avoid segfault when m_process is deleted at program termination
1464     m_process.reset();
1465   }
1466 }