1 /**
2  * \file scriptutils.cpp
3  * QML support functions.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 21 Sep 2014
8  *
9  * Copyright (C) 2014-2018  Urs Fleisch
10  *
11  * This file is part of Kid3.
12  *
13  * Kid3 is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * Kid3 is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "scriptutils.h"
28 #include <memory>
29 #include <QMetaProperty>
30 #include <QCoreApplication>
31 #include <QFile>
32 #include <QDir>
33 #include <QProcess>
34 #include <QImage>
35 #include <QBuffer>
36 #include <QCryptographicHash>
37 #include <QJSEngine>
38 #include <QStandardPaths>
39 #include <QStorageInfo>
40 #include "pictureframe.h"
41 #include "saferename.h"
42 #include "mainwindowconfig.h"
43 #include "config.h"
44 
45 namespace {
46 
47 /**
48  * Create a string list from a NULL terminated array of C strings.
49  */
cstringArrayToStringList(const char * const * strs)50 QStringList cstringArrayToStringList(const char* const* strs)
51 {
52   QStringList result;
53   while (*strs) {
54     result.append(QCoreApplication::translate("@default", *strs++));
55   }
56   return result;
57 }
58 
59 }
60 
61 
ScriptUtils(QObject * parent)62 ScriptUtils::ScriptUtils(QObject *parent) : QObject(parent)
63 {
64 }
65 
toStringList(const QList<QUrl> & urls)66 QStringList ScriptUtils::toStringList(const QList<QUrl>& urls)
67 {
68   QStringList paths;
69   paths.reserve(urls.size());
70   for (const QUrl& url : urls) {
71     paths.append(url.toLocalFile());
72   }
73   return paths;
74 }
75 
toPersistentModelIndexList(const QVariantList & lst)76 QList<QPersistentModelIndex> ScriptUtils::toPersistentModelIndexList(const QVariantList& lst)
77 {
78   QList<QPersistentModelIndex> indexes;
79   indexes.reserve(lst.size());
80   for (const QVariant& var : lst) {
81     indexes.append(var.toModelIndex());
82   }
83   return indexes;
84 }
85 
getRoleData(QObject * modelObj,int row,const QByteArray & roleName,const QModelIndex & parent)86 QVariant ScriptUtils::getRoleData(
87     QObject* modelObj, int row, const QByteArray& roleName,
88     const QModelIndex& parent)
89 {
90   if (auto model = qobject_cast<QAbstractItemModel*>(modelObj)) {
91     QHash<int,QByteArray> roleHash = model->roleNames();
92     for (auto it = roleHash.constBegin(); it != roleHash.constEnd(); ++it) {
93       if (it.value() == roleName) {
94         return model->index(row, 0, parent).data(it.key());
95       }
96     }
97   }
98   return QVariant();
99 }
100 
setRoleData(QObject * modelObj,int row,const QByteArray & roleName,const QVariant & value,const QModelIndex & parent)101 bool ScriptUtils::setRoleData(
102     QObject* modelObj, int row, const QByteArray& roleName,
103     const QVariant& value, const QModelIndex& parent)
104 {
105   if (auto model = qobject_cast<QAbstractItemModel*>(modelObj)) {
106     QHash<int,QByteArray> roleHash = model->roleNames();
107     for (auto it = roleHash.constBegin(); it != roleHash.constEnd(); ++it) {
108       if (it.value() == roleName) {
109         return model->setData(model->index(row, 0, parent), value, it.key());
110       }
111     }
112   }
113   return false;
114 }
115 
getIndexRoleData(const QModelIndex & index,const QByteArray & roleName)116 QVariant ScriptUtils::getIndexRoleData(const QModelIndex& index,
117                                        const QByteArray& roleName)
118 {
119   if (const QAbstractItemModel* model = index.model()) {
120     QHash<int,QByteArray> roleHash = model->roleNames();
121     for (auto it = roleHash.constBegin(); it != roleHash.constEnd(); ++it) {
122       if (it.value() == roleName) {
123         return index.data(it.key());
124       }
125     }
126   }
127   return QVariant();
128 }
129 
properties(QObject * obj)130 QString ScriptUtils::properties(QObject* obj)
131 {
132   QString str;
133   const QMetaObject* meta;
134   if (obj && (meta = obj->metaObject()) != nullptr) {
135     str += QLatin1String("className: ");
136     str += QString::fromLatin1(meta->className());
137     for (int i = 0; i < meta->propertyCount(); i++) {
138       QMetaProperty property = meta->property(i);
139       const char* name = property.name();
140       QVariant value = obj->property(name);
141       str += QLatin1Char('\n');
142       str += QString::fromLatin1(name);
143       str += QLatin1String(": ");
144       str += value.toString();
145     }
146   }
147   return str;
148 }
149 
150 /**
151  * String list of frame field ID names.
152  */
getFieldIdNames()153 QStringList ScriptUtils::getFieldIdNames()
154 {
155   return cstringArrayToStringList(Frame::Field::getFieldIdNames());
156 }
157 
158 /**
159  * String list of text encoding names.
160  */
getTextEncodingNames()161 QStringList ScriptUtils::getTextEncodingNames()
162 {
163   return cstringArrayToStringList(Frame::Field::getTextEncodingNames());
164 }
165 
166 /**
167  * String list of timestamp format names.
168  */
getTimestampFormatNames()169 QStringList ScriptUtils::getTimestampFormatNames()
170 {
171   return cstringArrayToStringList(Frame::Field::getTimestampFormatNames());
172 }
173 
174 /**
175  * String list of picture type names.
176  */
getPictureTypeNames()177 QStringList ScriptUtils::getPictureTypeNames()
178 {
179   return cstringArrayToStringList(PictureFrame::getPictureTypeNames());
180 }
181 
182 /**
183  * String list of content type names.
184  */
getContentTypeNames()185 QStringList ScriptUtils::getContentTypeNames()
186 {
187   return cstringArrayToStringList(Frame::Field::getContentTypeNames());
188 }
189 
190 /**
191  * Write data to a file.
192  * @param filePath path to file
193  * @param data data to write
194  * @return true if ok.
195  */
writeFile(const QString & filePath,const QByteArray & data)196 bool ScriptUtils::writeFile(const QString& filePath, const QByteArray& data)
197 {
198   bool ok = false;
199   QFile file(filePath);
200   if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
201     ok = file.write(data) > 0;
202     file.close();
203   }
204   return ok;
205 }
206 
207 /**
208  * Read data from file
209  * @param filePath path to file
210  * @return data read, empty if failed.
211  */
readFile(const QString & filePath)212 QByteArray ScriptUtils::readFile(const QString& filePath)
213 {
214   QByteArray data;
215   QFile file(filePath);
216   if (file.open(QIODevice::ReadOnly)) {
217     data = file.readAll();
218     file.close();
219   }
220   return data;
221 }
222 
223 /**
224  * Remove file.
225  * @param filePath path to file
226  * @return true if ok.
227  */
removeFile(const QString & filePath)228 bool ScriptUtils::removeFile(const QString& filePath)
229 {
230   return QFile::remove(filePath);
231 }
232 
233 /**
234  * Check if file exists.
235  * @param filePath path to file
236  * @return true if file exists.
237  */
fileExists(const QString & filePath)238 bool ScriptUtils::fileExists(const QString& filePath)
239 {
240   return QFile::exists(filePath);
241 }
242 
243 /**
244  * Check if file is writable.
245  * @param filePath path to file
246  * @return true if file is writable.
247  */
fileIsWritable(const QString & filePath)248 bool ScriptUtils::fileIsWritable(const QString& filePath)
249 {
250   return QFileInfo(filePath).isWritable();
251 }
252 
253 /**
254  * Get permissions of file.
255  * @param filePath path to file
256  * @return mode bits of file, e.g. 0x644.
257  */
getFilePermissions(const QString & filePath)258 int ScriptUtils::getFilePermissions(const QString& filePath)
259 {
260   return static_cast<int>(QFile::permissions(filePath));
261 }
262 
263 /**
264  * Set permissions of file.
265  * @param filePath path to file
266  * @param modeBits mode bits of file, e.g. 0x644
267  * @return true if ok.
268  */
setFilePermissions(const QString & filePath,int modeBits)269 bool ScriptUtils::setFilePermissions(const QString& filePath, int modeBits)
270 {
271   return QFile::setPermissions(filePath, QFile::Permissions(modeBits));
272 }
273 
274 /**
275  * @brief Get type of file.
276  * @param filePath path to file
277  * @return "/" for directories, "@" for symlinks, "*" for executables,
278  *         " " for files.
279  */
classifyFile(const QString & filePath)280 QString ScriptUtils::classifyFile(const QString& filePath)
281 {
282   QFileInfo fi(filePath);
283   if (fi.isSymLink()) {
284     return QLatin1String("@");
285   } else if (fi.isDir()) {
286     return QLatin1String("/");
287   } else if (fi.isExecutable()) {
288     return QLatin1String("*");
289   } else if (fi.isFile()) {
290     return QLatin1String(" ");
291   } else {
292     return QString();
293   }
294 }
295 
296 /**
297  * Rename file.
298  * @param oldName old name
299  * @param newName new name
300  * @return true if ok.
301  */
renameFile(const QString & oldName,const QString & newName)302 bool ScriptUtils::renameFile(const QString& oldName, const QString& newName)
303 {
304   return Utils::safeRename(oldName, newName);
305 }
306 
307 /**
308  * Copy file.
309  * @param source path to source file
310  * @param dest path to destination file
311  * @return true if ok.
312  */
copyFile(const QString & source,const QString & dest)313 bool ScriptUtils::copyFile(const QString& source, const QString& dest)
314 {
315   return QFile::copy(source, dest);
316 }
317 
318 /**
319  * Create directory.
320  * @param path path to new directory
321  * @return true if ok.
322  */
makeDir(const QString & path)323 bool ScriptUtils::makeDir(const QString& path)
324 {
325   return QDir().mkpath(path);
326 }
327 
328 /**
329  * Remove directory.
330  * @param path path to directory to remove
331  * @return true if ok.
332  */
removeDir(const QString & path)333 bool ScriptUtils::removeDir(const QString& path)
334 {
335   return QDir().rmpath(path);
336 }
337 
338 /**
339  * Get path of temporary directory.
340  * @return temporary directory.
341  */
tempPath()342 QString ScriptUtils::tempPath()
343 {
344   return QDir::tempPath();
345 }
346 
347 /**
348  * Get directory containing the user's music.
349  * @return music directory.
350  */
musicPath()351 QString ScriptUtils::musicPath()
352 {
353   return QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
354 }
355 
356 /**
357  * Get list of currently mounted filesystems.
358  * @return list with storage information maps containing the keys
359  * name, displayName, isValid, isReadOnly, isReady, rootPath,
360  * blockSize, mbytesAvailable, mbytesFree, mbytesTotal.
361  */
mountedVolumes()362 QVariantList ScriptUtils::mountedVolumes()
363 {
364   QVariantList result;
365   for (const QStorageInfo& si : QStorageInfo::mountedVolumes()) {
366     QVariantMap map;
367     map.insert(QLatin1String("name"), si.name());
368     map.insert(QLatin1String("displayName"), si.displayName());
369     map.insert(QLatin1String("isValid"), si.isValid());
370     map.insert(QLatin1String("isReadOnly"), si.isReadOnly());
371     map.insert(QLatin1String("isReady"), si.isReady());
372     map.insert(QLatin1String("rootPath"), si.rootPath());
373 #if QT_VERSION >= 0x050600
374     map.insert(QLatin1String("blockSize"), si.blockSize());
375 #endif
376     map.insert(QLatin1String("mbytesAvailable"),
377                static_cast<int>(si.bytesAvailable() / (1024 * 1024)));
378     map.insert(QLatin1String("mbytesFree"),
379                static_cast<int>(si.bytesFree() / (1024 * 1024)));
380     map.insert(QLatin1String("mbytesTotal"),
381                static_cast<int>(si.bytesTotal() / (1024 * 1024)));
382     result.append(map);
383   }
384   return result;
385 }
386 
387 /**
388  * List directory entries.
389  * @param path directory path
390  * @param nameFilters list of name filters, e.g. ["*.jpg", "*.png"]
391  * @param classify if true, add /, @, * for directories, symlinks, executables
392  * @return list of directory entries.
393  */
listDir(const QString & path,const QStringList & nameFilters,bool classify)394 QStringList ScriptUtils::listDir(
395     const QString& path, const QStringList& nameFilters, bool classify)
396 {
397   QStringList dirList;
398   const QFileInfoList entries = QDir(path).entryInfoList(nameFilters);
399   dirList.reserve(entries.size());
400   for (const QFileInfo& fi : entries) {
401     QString fileName = fi.fileName();
402     if (classify) {
403       if (fi.isDir()) fileName += QLatin1Char('/');
404       else if (fi.isSymLink()) fileName += QLatin1Char('@');
405       else if (fi.isExecutable()) fileName += QLatin1Char('*');
406     }
407     dirList.append(fileName);
408   }
409   return dirList;
410 }
411 
412 /**
413  * Synchronously start a system command.
414  * @param program executable
415  * @param args arguments
416  * @param msecs timeout in milliseconds, -1 for no timeout
417  * @return [exit code, standard output, standard error], empty list on timeout.
418  */
system(const QString & program,const QStringList & args,int msecs)419 QVariantList ScriptUtils::system(
420     const QString& program, const QStringList& args, int msecs)
421 {
422   QProcess proc;
423   proc.start(program, args);
424   if (proc.waitForFinished(msecs)) {
425     return QVariantList()
426         << proc.exitCode()
427         << QString::fromLocal8Bit(proc.readAllStandardOutput())
428         << QString::fromLocal8Bit(proc.readAllStandardError());
429   }
430   return QVariantList();
431 }
432 
systemAsync(const QString & program,const QStringList & args,QJSValue callback)433 void ScriptUtils::systemAsync(
434     const QString& program, const QStringList& args, QJSValue callback)
435 {
436   QProcess* proc = new QProcess(this);
437   auto conn = std::make_shared<QMetaObject::Connection>();
438 #if QT_VERSION >= 0x050d00
439   *conn = QObject::connect(
440         proc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(
441           &QProcess::finished),
442         this, [proc, conn, callback, this](int exitCode, QProcess::ExitStatus) mutable {
443 #else
444   *conn = QObject::connect(
445         proc, static_cast<void (QProcess::*)(int)>(&QProcess::finished),
446         this, [proc, conn, callback, this](int exitCode) mutable {
447 #endif
448     QObject::disconnect(*conn);
449     if (!callback.isUndefined()) {
450       QVariantList result{
451         exitCode,
452         QString::fromLocal8Bit(proc->readAllStandardOutput()),
453         QString::fromLocal8Bit(proc->readAllStandardError())
454       };
455       callback.call({qjsEngine(this)->toScriptValue(result)});
456     }
457   });
458   proc->start(program, args);
459 }
460 
461 /**
462  * Get value of environment variable.
463  * @param varName variable name
464  * @return value.
465  */
466 QByteArray ScriptUtils::getEnv(const QByteArray& varName)
467 {
468   return qgetenv(varName.constData());
469 }
470 
471 /**
472  * Set value of environment variable.
473  * @param varName variable name
474  * @param value value to set
475  * @return true if value could be set.
476  */
477 bool ScriptUtils::setEnv(const QByteArray& varName, const QByteArray& value)
478 {
479   return qputenv(varName, value);
480 }
481 
482 /**
483  * Get version of Kid3.
484  * @return Kid3 version string, e.g. "3.3.0".
485  */
486 QString ScriptUtils::getKid3Version()
487 {
488   return QLatin1String(VERSION);
489 }
490 
491 /**
492  * Get release year of Kid3.
493  * @return Kid3 year string, e.g. "2015".
494  */
495 QString ScriptUtils::getKid3ReleaseYear()
496 {
497   return QLatin1String(RELEASE_YEAR);
498 }
499 
500 /**
501  * Get version of Qt.
502  * @return Qt version string, e.g. "5.4.1".
503  */
504 QString ScriptUtils::getQtVersion()
505 {
506   return QString::fromLatin1(qVersion());
507 }
508 
509 /**
510  * Get hex string of the MD5 hash of data.
511  * This is a replacement for Qt::md5(), which does only work with strings.
512  * @param data data bytes
513  * @return MD5 sum.
514  */
515 QString ScriptUtils::getDataMd5(const QByteArray& data)
516 {
517   QByteArray result = QCryptographicHash::hash(data, QCryptographicHash::Md5);
518   return QLatin1String(result.toHex());
519 }
520 
521 /**
522  * Get size of byte array.
523  * @param data data bytes
524  * @return number of bytes in @a data.
525  */
526 int ScriptUtils::getDataSize(const QByteArray& data)
527 {
528   return data.size();
529 }
530 
531 /**
532  * Create an image from data bytes.
533  * @param data data bytes
534  * @param format image format, default is "JPG"
535  * @return image variant.
536  */
537 QVariant ScriptUtils::dataToImage(const QByteArray& data,
538                                   const QByteArray& format)
539 {
540   QImage img(QImage::fromData(data, format.constData()));
541   return QVariant::fromValue(img);
542 }
543 
544 /**
545  * Get data bytes from image.
546  * @param var image variant
547  * @param format image format, default is "JPG"
548  * @return data bytes.
549  */
550 QByteArray ScriptUtils::dataFromImage(const QVariant& var,
551                                       const QByteArray& format)
552 {
553   QByteArray data;
554   QImage img(var.value<QImage>());
555   if (!img.isNull()) {
556     QBuffer buffer(&data);
557     buffer.open(QIODevice::WriteOnly);
558     img.save(&buffer, format.constData());
559   }
560   return data;
561 }
562 
563 /**
564  * Load an image from a file.
565  * @param filePath path to file
566  * @return image variant.
567  */
568 QVariant ScriptUtils::loadImage(const QString& filePath)
569 {
570   QImage img(filePath);
571   return QVariant::fromValue(img);
572 }
573 
574 /**
575  * Save an image to a file.
576  * @param var image variant
577  * @param filePath path to file
578  * @param format image format, default is "JPG"
579  * @return true if ok.
580  */
581 bool ScriptUtils::saveImage(const QVariant& var, const QString& filePath,
582                             const QByteArray& format)
583 {
584   QImage img(var.value<QImage>());
585   if (!img.isNull()) {
586     return img.save(filePath, format.constData());
587   }
588   return false;
589 }
590 
591 /**
592  * Get properties of an image.
593  * @param var image variant
594  * @return map containing "width", "height", "depth" and "colorCount",
595  * empty if invalid image.
596  */
597 QVariantMap ScriptUtils::imageProperties(const QVariant& var)
598 {
599   QVariantMap map;
600   QImage img(var.value<QImage>());
601   if (!img.isNull()) {
602     map.insert(QLatin1String("width"), img.width());
603     map.insert(QLatin1String("height"), img.height());
604     map.insert(QLatin1String("depth"), img.depth());
605     map.insert(QLatin1String("colorCount"), img.colorCount());
606   }
607   return map;
608 }
609 
610 /**
611  * Scale an image.
612  * @param var image variant
613  * @param width scaled width, -1 to keep aspect ratio
614  * @param height scaled height, -1 to keep aspect ratio
615  * @return scaled image variant.
616  */
617 QVariant ScriptUtils::scaleImage(const QVariant& var, int width, int height)
618 {
619   QImage img(var.value<QImage>());
620   if (!img.isNull()) {
621     if (width > 0 && height > 0) {
622       return QVariant::fromValue(img.scaled(width, height,
623                             Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
624     } else if (width > 0) {
625       return QVariant::fromValue(img.scaledToWidth(width,
626                                                    Qt::SmoothTransformation));
627     } else if (height > 0) {
628       return QVariant::fromValue(img.scaledToHeight(height,
629                                                     Qt::SmoothTransformation));
630     }
631   }
632   return QVariant();
633 }
634