1 /**
2  * \file scriptinterface.cpp
3  * D-Bus script adaptor.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 20 Dec 2007
8  *
9  * Copyright (C) 2007-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 "scriptinterface.h"
28 #ifdef HAVE_QTDBUS
29 #include <QDBusMessage>
30 #include <QDBusConnection>
31 #include <QFileInfo>
32 #include <QCoreApplication>
33 #include <QItemSelectionModel>
34 #include "kid3application.h"
35 #include "taggedfile.h"
36 #include "frametablemodel.h"
37 #include "filefilter.h"
38 #include "pictureframe.h"
39 #include "fileproxymodel.h"
40 #include "modeliterator.h"
41 #include "batchimportconfig.h"
42 #include "batchimportprofile.h"
43 #include "fileconfig.h"
44 
45 /**
46  * Constructor.
47  *
48  * @param app parent application
49  */
ScriptInterface(Kid3Application * app)50 ScriptInterface::ScriptInterface(Kid3Application* app)
51   : QDBusAbstractAdaptor(app), m_app(app)
52 {
53   setObjectName(QLatin1String("ScriptInterface"));
54   setAutoRelaySignals(true);
55 }
56 
57 /**
58  * Open file or directory.
59  *
60  * @param path path to file or directory
61  *
62  * @return true if ok.
63  */
openDirectory(const QString & path)64 bool ScriptInterface::openDirectory(const QString& path)
65 {
66   return m_app->openDirectory({path}, true);
67 }
68 
69 /**
70  * Unload all tags.
71  * The tags of all files which are not modified or selected are freed to
72  * reclaim their memory.
73  */
unloadAllTags()74 void ScriptInterface::unloadAllTags()
75 {
76   m_app->unloadAllTags();
77 }
78 
79 /**
80  * Save all modified files.
81  *
82  * @return true if ok,
83  *         else the error message is available using getErrorMessage().
84  */
save()85 bool ScriptInterface::save()
86 {
87   QStringList errorFiles = m_app->saveDirectory();
88   if (errorFiles.isEmpty()) {
89     m_errorMsg.clear();
90     return true;
91   } else {
92     m_errorMsg = QLatin1String("Error while writing file:\n") +
93         errorFiles.join(QLatin1String("\n"));
94     return false;
95   }
96 }
97 
98 /**
99  * Get a detailed error message provided by some methods.
100  *
101  * @return detailed error message.
102  */
getErrorMessage() const103 QString ScriptInterface::getErrorMessage() const
104 {
105   return m_errorMsg;
106 }
107 
108 /**
109  * Revert changes in the selected files.
110  */
revert()111 void ScriptInterface::revert()
112 {
113   m_app->revertFileModifications();
114 }
115 
116 /**
117  * Import tags from a file.
118  *
119  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
120  * @param path    path of file, "clipboard" for import from clipboard
121  * @param fmtIdx  index of format
122  *
123  * @return true if ok.
124  */
importFromFile(int tagMask,const QString & path,int fmtIdx)125 bool ScriptInterface::importFromFile(int tagMask, const QString& path,
126                                      int fmtIdx)
127 {
128   return m_app->importTags(Frame::tagVersionCast(tagMask), path, fmtIdx);
129 }
130 
131 /**
132  * Import from tags.
133  *
134  * @param tagMask tag mask
135  * @param source format to get source text from tags
136  * @param extraction regular expression with frame names and captures to
137  * extract from source text
138  */
importFromTags(int tagMask,const QString & source,const QString & extraction)139 void ScriptInterface::importFromTags(int tagMask,
140                                      const QString& source,
141                                      const QString& extraction)
142 {
143   m_app->importFromTags(Frame::tagVersionCast(tagMask), source, extraction);
144 }
145 
146 /**
147  * Import from tags on selected files.
148  *
149  * @param tagMask tag mask
150  * @param source format to get source text from tags
151  * @param extraction regular expression with frame names and captures to
152  * extract from source text
153  *
154  * @return extracted values for "%{__return}(.+)", empty if not used.
155  */
importFromTagsToSelection(int tagMask,const QString & source,const QString & extraction)156 QStringList ScriptInterface::importFromTagsToSelection(int tagMask,
157                                                       const QString& source,
158                                                       const QString& extraction)
159 {
160   return m_app->importFromTagsToSelection(Frame::tagVersionCast(tagMask),
161                                           source, extraction);
162 }
163 
164 /**
165  * Start an automatic batch import.
166  *
167  * @param tagMask tag mask (bit 0 for tag 1, bit 1 for tag 2)
168  * @param profileName name of batch import profile to use
169  *
170  * @return true if profile found.
171  */
batchImport(int tagMask,const QString & profileName)172 bool ScriptInterface::batchImport(int tagMask, const QString& profileName)
173 {
174   return m_app->batchImport(profileName, Frame::tagVersionCast(tagMask));
175 }
176 
177 /**
178  * Download album cover art into the picture frame of the selected files.
179  *
180  * @param url           URL of picture file or album art resource
181  * @param allFilesInDir true to add the image to all files in the directory
182  */
downloadAlbumArt(const QString & url,bool allFilesInDir)183 void ScriptInterface::downloadAlbumArt(const QString& url, bool allFilesInDir)
184 {
185   m_app->downloadImage(url, allFilesInDir);;
186 }
187 
188 /**
189  * Export tags to a file.
190  *
191  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
192  * @param path    path of file, "clipboard" for export to clipboard
193  * @param fmtIdx  index of format
194  *
195  * @return true if ok.
196  */
exportToFile(int tagMask,const QString & path,int fmtIdx)197 bool ScriptInterface::exportToFile(int tagMask, const QString& path, int fmtIdx)
198 {
199   return m_app->exportTags(Frame::tagVersionCast(tagMask), path, fmtIdx);
200 }
201 
202 /**
203  * Create a playlist.
204  *
205  * @return true if ok.
206  */
createPlaylist()207 bool ScriptInterface::createPlaylist()
208 {
209   return m_app->writePlaylist();
210 }
211 
212 /**
213  * Get items of a playlist.
214  * @param path path to playlist file
215  * @return list of absolute paths to playlist items.
216  */
getPlaylistItems(const QString & path)217 QStringList ScriptInterface::getPlaylistItems(const QString& path)
218 {
219   return m_app->getPlaylistItems(path);
220 }
221 
222 /**
223  * Set items of a playlist.
224  * @param path path to playlist file
225  * @param items list of absolute paths to playlist items
226  * @return true if ok, false if not all @a items were found and added or
227  *         saving failed.
228  */
setPlaylistItems(const QString & path,const QStringList & items)229 bool ScriptInterface::setPlaylistItems(const QString& path,
230                                        const QStringList& items)
231 {
232   return m_app->setPlaylistItems(path, items);
233 }
234 
235 /**
236  * Quit the application.
237  */
quit()238 void ScriptInterface::quit()
239 {
240   selectAll();
241   revert();
242   QCoreApplication::quit();
243 }
244 
245 /**
246  * Select all files.
247  */
selectAll()248 void ScriptInterface::selectAll()
249 {
250   m_app->selectAllFiles();
251 }
252 
253 /**
254  * Deselect all files.
255  */
deselectAll()256 void ScriptInterface::deselectAll()
257 {
258   m_app->deselectAllFiles();
259 }
260 
261 /**
262  * Set the first file as the current file.
263  *
264  * @return true if there is a first file.
265  */
firstFile()266 bool ScriptInterface::firstFile()
267 {
268   return m_app->firstFile(false);
269 }
270 
271 /**
272  * Set the previous file as the current file.
273  *
274  * @return true if there is a previous file.
275  */
previousFile()276 bool ScriptInterface::previousFile()
277 {
278   return m_app->previousFile(false);
279 }
280 
281 /**
282  * Set the next file as the current file.
283  *
284  * @return true if there is a next file.
285  */
nextFile()286 bool ScriptInterface::nextFile()
287 {
288   return m_app->nextFile(false);
289 }
290 
291 /**
292  * Select the first file.
293  *
294  * @return true if there is a first file.
295  */
selectFirstFile()296 bool ScriptInterface::selectFirstFile()
297 {
298   return m_app->firstFile(true);
299 }
300 
301 /**
302  * Select the previous file.
303  *
304  * @return true if there is a previous file.
305  */
selectPreviousFile()306 bool ScriptInterface::selectPreviousFile()
307 {
308   return m_app->previousFile(true);
309 }
310 
311 /**
312  * Select the next file.
313  *
314  * @return true if there is a next file.
315  */
selectNextFile()316 bool ScriptInterface::selectNextFile()
317 {
318   return m_app->nextFile(true);
319 }
320 
321 /**
322  * Select the current file.
323  *
324  * @return true if there is a current file.
325  */
selectCurrentFile()326 bool ScriptInterface::selectCurrentFile()
327 {
328  return m_app->selectCurrentFile(true);
329 }
330 
331 /**
332  * Expand the current file item if it is a directory.
333  * A file list item is a directory if getFileName() returns a name with
334  * '/' as the last character.
335  * The directory is fetched but not expanded in the GUI. To expand it in the
336  * GUI, call nextFile() or selectNextFile() after expandDirectory().
337  *
338  * @return true if current file item is a directory.
339  */
expandDirectory()340 bool ScriptInterface::expandDirectory()
341 {
342   QModelIndex index(m_app->getFileSelectionModel()->currentIndex());
343   if (!FileProxyModel::getPathIfIndexOfDir(index).isNull()) {
344     m_app->expandDirectory(index);
345     return true;
346   }
347   return false;
348 }
349 
350 /**
351  * Expand the file list.
352  */
expandFileList()353 void ScriptInterface::expandFileList()
354 {
355   m_app->requestExpandFileList();
356 }
357 
358 /**
359  * Apply the file name format.
360  */
applyFilenameFormat()361 void ScriptInterface::applyFilenameFormat()
362 {
363   m_app->applyFilenameFormat();
364 }
365 
366 /**
367  * Apply the tag format.
368  */
applyTagFormat()369 void ScriptInterface::applyTagFormat()
370 {
371   m_app->applyTagFormat();
372 }
373 
374 /**
375  * Apply text encoding.
376  */
applyTextEncoding()377 void ScriptInterface::applyTextEncoding()
378 {
379   m_app->applyTextEncoding();
380 }
381 
382 /**
383  * Set the directory name from the tags.
384  *
385  * @param tagMask tag mask (bit 0 for tag 1, bit 1 for tag 2)
386  * @param format  directory name format
387  * @param create  true to create, false to rename
388  *
389  * @return true if ok,
390  *         else the error message is available using getErrorMessage().
391  */
setDirNameFromTag(int tagMask,const QString & format,bool create)392 bool ScriptInterface::setDirNameFromTag(int tagMask, const QString& format,
393                                         bool create)
394 {
395   connect(m_app, &Kid3Application::renameActionsScheduled,
396           this, &ScriptInterface::onRenameActionsScheduled);
397   if (m_app->renameDirectory(Frame::tagVersionCast(tagMask), format,
398                              create)) {
399     return true;
400   } else {
401     disconnect(m_app, &Kid3Application::renameActionsScheduled,
402                this, &ScriptInterface::onRenameActionsScheduled);
403     return false;
404   }
405 }
406 
onRenameActionsScheduled()407 void ScriptInterface::onRenameActionsScheduled()
408 {
409   disconnect(m_app, &Kid3Application::renameActionsScheduled,
410              this, &ScriptInterface::onRenameActionsScheduled);
411   m_errorMsg = m_app->performRenameActions();
412   if (!m_errorMsg.isEmpty()) {
413     m_errorMsg = QLatin1String("Error while renaming:\n") + m_errorMsg;
414   }
415 }
416 
417 /**
418  * Set subsequent track numbers in the selected files.
419  *
420  * @param tagMask      tag mask (bit 0 for tag 1, bit 1 for tag 2)
421  * @param firstTrackNr number to use for first file
422  */
numberTracks(int tagMask,int firstTrackNr)423 void ScriptInterface::numberTracks(int tagMask, int firstTrackNr)
424 {
425   m_app->numberTracks(firstTrackNr, 0, Frame::tagVersionCast(tagMask));
426 }
427 
428 /**
429  * Filter the files.
430  *
431  * @param expression filter expression
432  */
filter(const QString & expression)433 void ScriptInterface::filter(const QString& expression)
434 {
435   m_app->applyFilter(expression);
436 }
437 
438 /**
439  * Convert ID3v2.3 tags to ID3v2.4.
440  */
convertToId3v24()441 void ScriptInterface::convertToId3v24()
442 {
443   m_app->convertToId3v24();
444 }
445 
446 /**
447  * Convert ID3v2.4 tags to ID3v2.3.
448  */
convertToId3v23()449 void ScriptInterface::convertToId3v23()
450 {
451   m_app->convertToId3v23();
452 }
453 
454 /**
455  * Get path of directory.
456  *
457  * @return absolute path of directory.
458  */
getDirectoryName()459 QString ScriptInterface::getDirectoryName()
460 {
461   return m_app->getDirPath();
462 }
463 
464 /**
465  * Get name of current file.
466  *
467  * @return absolute file name, ends with "/" if it is a directory.
468  */
getFileName()469 QString ScriptInterface::getFileName()
470 {
471   return m_app->getFileNameOfSelectedFile();
472 }
473 
474 /**
475  * Set name of selected file.
476  * The file will be renamed when the directory is saved.
477  *
478  * @param name file name.
479  */
setFileName(const QString & name)480 void ScriptInterface::setFileName(const QString& name)
481 {
482   m_app->setFileNameOfSelectedFile(name);
483 }
484 
485 /**
486  * Set format to use when setting the filename from the tags.
487  *
488  * @param format file name format
489  * @see setFileNameFromTag()
490  */
setFileNameFormat(const QString & format)491 void ScriptInterface::setFileNameFormat(const QString& format)
492 {
493   FileConfig::instance().setToFilenameFormat(format);
494 }
495 
496 /**
497  * Set the file names of the selected files from the tags.
498  *
499  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
500  * @see setFileNameFormat()
501  */
setFileNameFromTag(int tagMask)502 void ScriptInterface::setFileNameFromTag(int tagMask)
503 {
504   m_app->getFilenameFromTags(Frame::tagVersionCast(tagMask));
505 }
506 
507 /**
508  * Get value of frame.
509  * To get binary data like a picture, the name of a file to write can be
510  * added after the @a name, e.g. "Picture:/path/to/file".
511  *
512  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
513  * @param name    name of frame (e.g. "Artist")
514  */
getFrame(int tagMask,const QString & name)515 QString ScriptInterface::getFrame(int tagMask, const QString& name)
516 {
517   return m_app->getFrame(Frame::tagVersionCast(tagMask), name);
518 }
519 
520 /**
521  * Set value of frame.
522  * For tag 2 (@a tagMask 2), if no frame with @a name exists, a new frame
523  * is added, if @a value is empty, the frame is deleted.
524  * To add binary data like a picture, a file can be added after the
525  * @a name, e.g. "Picture:/path/to/file".
526  *
527  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
528  * @param name    name of frame (e.g. "Artist")
529  * @param value   value of frame
530  */
setFrame(int tagMask,const QString & name,const QString & value)531 bool ScriptInterface::setFrame(int tagMask, const QString& name,
532                            const QString& value)
533 {
534   return m_app->setFrame(Frame::tagVersionCast(tagMask), name, value);
535 }
536 
537 /**
538  * Get all frames of a tag.
539  *
540  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
541  *
542  * @return list with alternating frame names and values.
543  */
getTag(int tagMask)544 QStringList ScriptInterface::getTag(int tagMask)
545 {
546   Frame::TagNumber tagNr =
547       Frame::tagNumberFromMask(Frame::tagVersionCast(tagMask));
548   if (tagNr >= Frame::Tag_NumValues)
549     return QStringList();
550 
551   QStringList lst;
552   FrameTableModel* ft = m_app->frameModel(tagNr);
553   for (auto it = ft->frames().cbegin(); it != ft->frames().cend(); ++it) {
554     lst << it->getName();
555     lst << it->getValue();
556   }
557   return lst;
558 }
559 
560 /**
561  * Get technical information about file.
562  * Properties are Format, Bitrate, Samplerate, Channels, Duration,
563  * Channel Mode, VBR, Tag 1, Tag 2.
564  * Properties which are not available are omitted.
565  *
566  * @return list with alternating property names and values.
567  */
getInformation()568 QStringList ScriptInterface::getInformation()
569 {
570   QStringList lst;
571   QModelIndex index = m_app->getFileSelectionModel()->currentIndex();
572   if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
573     TaggedFile::DetailInfo info;
574     taggedFile->getDetailInfo(info);
575     if (info.valid) {
576       lst << QLatin1String("Format") << info.format;
577       if (info.bitrate > 0 && info.bitrate < 16384) {
578         lst << QLatin1String("Bitrate") << QString::number(info.bitrate);
579       }
580       if (info.sampleRate > 0) {
581         lst << QLatin1String("Samplerate") << QString::number(info.sampleRate);
582       }
583       if (info.channels > 0) {
584         lst << QLatin1String("Channels") << QString::number(info.channels);
585       }
586       if (info.duration > 0) {
587         lst << QLatin1String("Duration") << QString::number(info.duration);
588       }
589       if (info.channelMode == TaggedFile::DetailInfo::CM_Stereo ||
590           info.channelMode == TaggedFile::DetailInfo::CM_JointStereo) {
591         lst << QLatin1String("Channel Mode") <<
592           (info.channelMode == TaggedFile::DetailInfo::CM_Stereo ?
593            QLatin1String("Stereo") : QLatin1String("Joint Stereo"));
594       }
595       if (info.vbr) {
596         lst << QLatin1String("VBR") << QLatin1String("1");
597       }
598     }
599     FOR_ALL_TAGS(tagNr) {
600       QString tag = taggedFile->getTagFormat(tagNr);
601       if (!tag.isEmpty()) {
602         lst << QLatin1String("Tag ") + Frame::tagNumberToString(tagNr) << tag; // clazy:exclude=reserve-candidates
603       }
604     }
605   }
606   return lst;
607 }
608 
609 /**
610  * Set tag from file name.
611  *
612  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
613  */
setTagFromFileName(int tagMask)614 void ScriptInterface::setTagFromFileName(int tagMask)
615 {
616   m_app->getTagsFromFilename(Frame::tagVersionCast(tagMask));
617 }
618 
619 /**
620  * Set tag from other tag.
621  *
622  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
623  */
setTagFromOtherTag(int tagMask)624 void ScriptInterface::setTagFromOtherTag(int tagMask)
625 {
626   m_app->copyToOtherTag(Frame::tagVersionCast(tagMask));
627 }
628 
629 /**
630  * Copy tag.
631  *
632  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
633  */
copyTag(int tagMask)634 void ScriptInterface::copyTag(int tagMask)
635 {
636   m_app->copyTags(Frame::tagVersionCast(tagMask));
637 }
638 
639 /**
640  * Paste tag.
641  *
642  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
643  */
pasteTag(int tagMask)644 void ScriptInterface::pasteTag(int tagMask)
645 {
646   m_app->pasteTags(Frame::tagVersionCast(tagMask));
647 }
648 
649 /**
650  * Remove tag.
651  *
652  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
653  */
removeTag(int tagMask)654 void ScriptInterface::removeTag(int tagMask)
655 {
656   m_app->removeTags(Frame::tagVersionCast(tagMask));
657 }
658 
659 /**
660  * Reparse the configuration.
661  * Automated configuration changes are possible by modifying
662  * the configuration file and then reparsing the configuration.
663  */
reparseConfiguration()664 void ScriptInterface::reparseConfiguration()
665 {
666   m_app->readConfig();
667 }
668 
669 /**
670  * Play selected audio files.
671  */
playAudio()672 void ScriptInterface::playAudio()
673 {
674   m_app->playAudio();
675 }
676 
677 #endif // HAVE_QTDBUS
678