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