1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  * ----
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; see the file COPYING.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "umsdevice.h"
25 #include "tags/tags.h"
26 #include "models/musiclibraryitemsong.h"
27 #include "models/musiclibraryitemalbum.h"
28 #include "models/musiclibraryitemartist.h"
29 #include "models/musiclibraryitemroot.h"
30 #include "models/mpdlibrarymodel.h"
31 #include "devicepropertiesdialog.h"
32 #include "devicepropertieswidget.h"
33 #include "support/utils.h"
34 #include "mpd-interface/mpdparseutils.h"
35 #include "mpd-interface/mpdconnection.h"
36 #include "encoders.h"
37 #include "transcodingjob.h"
38 #include "actiondialog.h"
39 #include "gui/covers.h"
40 #include "support/thread.h"
41 #include <QDir>
42 #include <QFile>
43 #include <QFileInfo>
44 #include <QTextStream>
45 #include <QTimer>
46 
47 const QLatin1String FsDevice::constCantataCacheFile("/.cache");
48 const QLatin1String FsDevice::constCantataSettingsFile("/.cantata");
49 const QLatin1String FsDevice::constMusicFilenameSchemeKey("music_filenamescheme");
50 const QLatin1String FsDevice::constVfatSafeKey("vfat_safe");
51 const QLatin1String FsDevice::constAsciiOnlyKey("ascii_only");
52 const QLatin1String FsDevice::constIgnoreTheKey("ignore_the");
53 const QLatin1String FsDevice::constReplaceSpacesKey("replace_spaces");
54 const QLatin1String FsDevice::constCoverFileNameKey("cover_filename"); // Cantata extension!
55 const QLatin1String FsDevice::constCoverMaxSizeKey("cover_maxsize"); // Cantata extension!
56 const QLatin1String FsDevice::constVariousArtistsFixKey("fix_various_artists"); // Cantata extension!
57 const QLatin1String FsDevice::constTranscoderKey("transcoder"); // Cantata extension!
58 const QLatin1String FsDevice::constUseCacheKey("use_cache"); // Cantata extension!
59 const QLatin1String FsDevice::constDefCoverFileName("cover.jpg");
60 const QLatin1String FsDevice::constAutoScanKey("auto_scan"); // Cantata extension!
61 
MusicScanner(const QString & id)62 MusicScanner::MusicScanner(const QString &id)
63     : QObject(nullptr)
64     , stopRequested(false)
65     , count(0)
66 {
67     thread=new Thread(metaObject()->className()+QLatin1String("::")+id);
68     moveToThread(thread);
69     thread->start();
70 }
71 
~MusicScanner()72 MusicScanner::~MusicScanner()
73 {
74     stop();
75 }
76 
scan(const QString & folder,const QString & cacheFile,bool readCache,const QSet<FileOnlySong> & existingSongs)77 void MusicScanner::scan(const QString &folder, const QString &cacheFile, bool readCache, const QSet<FileOnlySong> &existingSongs)
78 {
79     if (!cacheFile.isEmpty() && readCache) {
80         MusicLibraryItemRoot *lib=new MusicLibraryItemRoot;
81         readProgress(0.0);
82         if (lib->fromXML(cacheFile, folder)) {
83             if (!stopRequested) {
84                 emit libraryUpdated(lib);
85             } else {
86                 delete lib;
87             }
88             return;
89         } else {
90             delete lib;
91         }
92     }
93 
94     if (stopRequested) {
95         return;
96     }
97     count=0;
98     MusicLibraryItemRoot *library = new MusicLibraryItemRoot;
99     QString topLevel=Utils::fixPath(QDir(folder).absolutePath());
100     QSet<FileOnlySong> existing=existingSongs;
101     timer.start();
102     scanFolder(library, topLevel, topLevel, existing, 0);
103 
104     if (!stopRequested) {
105         if (!cacheFile.isEmpty()) {
106             writeProgress(0.0);
107             library->toXML(cacheFile, this);
108         }
109         emit libraryUpdated(library);
110     } else {
111         delete library;
112     }
113 }
114 
saveCache(const QString & cache,MusicLibraryItemRoot * lib)115 void MusicScanner::saveCache(const QString &cache, MusicLibraryItemRoot *lib)
116 {
117     writeProgress(0.0);
118     lib->toXML(cache, this);
119     emit cacheSaved();
120 }
121 
stop()122 void MusicScanner::stop()
123 {
124     stopRequested=true;
125     thread->stop();
126     thread=nullptr;
127 }
128 
scanFolder(MusicLibraryItemRoot * library,const QString & topLevel,const QString & f,QSet<FileOnlySong> & existing,int level)129 void MusicScanner::scanFolder(MusicLibraryItemRoot *library, const QString &topLevel, const QString &f,
130                               QSet<FileOnlySong> &existing, int level)
131 {
132     if (stopRequested) {
133         return;
134     }
135     if (level<4) {
136         QDir d(f);
137         QFileInfoList entries=d.entryInfoList(QDir::Files|QDir::NoSymLinks|QDir::Dirs|QDir::NoDotAndDotDot);
138         MusicLibraryItemArtist *artistItem = nullptr;
139         MusicLibraryItemAlbum *albumItem = nullptr;
140         for (const QFileInfo &info: entries) {
141             if (stopRequested) {
142                 return;
143             }
144             if (info.isDir()) {
145                 scanFolder(library, topLevel, info.absoluteFilePath(), existing, level+1);
146             } else if(info.isReadable()) {
147                 Song song;
148                 QString fname=info.absoluteFilePath().mid(topLevel.length());
149 
150                 if (fname.endsWith(".jpg", Qt::CaseInsensitive) || fname.endsWith(".png", Qt::CaseInsensitive) ||
151                     fname.endsWith(".lyrics", Qt::CaseInsensitive) || fname.endsWith(".pamp", Qt::CaseInsensitive)) {
152                     continue;
153                 }
154                 song.file=fname;
155                 QSet<FileOnlySong>::iterator it=existing.find(song);
156                 if (existing.end()==it) {
157                     song=Tags::read(info.absoluteFilePath());
158                     song.file=fname;
159                 } else {
160                     song=*it;
161                     existing.erase(it);
162                 }
163                 if (song.isEmpty()) {
164                     continue;
165                 }
166                 count++;
167                 if (timer.elapsed()>=1500 || 0==(count%5)) {
168                     timer.restart();
169                     emit songCount(count);
170                 }
171 
172                 song.fillEmptyFields();
173                 song.populateSorts();
174                 song.size=info.size();
175                 if (!artistItem || song.albumArtistOrComposer()!=artistItem->data()) {
176                     artistItem = library->artist(song);
177                 }
178                 if (!albumItem || albumItem->parentItem()!=artistItem || song.albumName()!=albumItem->data()) {
179                     albumItem = artistItem->album(song);
180                 }
181                 albumItem->append(new MusicLibraryItemSong(song, albumItem));
182             }
183         }
184     }
185 }
186 
readProgress(double pc)187 void MusicScanner::readProgress(double pc)
188 {
189     emit readingCache(pc);
190 }
191 
writeProgress(double pc)192 void MusicScanner::writeProgress(double pc)
193 {
194     emit savingCache(pc);
195 }
196 
readOpts(const QString & fileName,DeviceOptions & opts,bool readAll)197 bool FsDevice::readOpts(const QString &fileName, DeviceOptions &opts, bool readAll)
198 {
199     QFile file(fileName);
200 
201     opts=DeviceOptions(constDefCoverFileName);
202     if (file.open(QIODevice::ReadOnly|QIODevice::Text)) {
203         QTextStream in(&file);
204         while (!in.atEnd()) {
205             QString line = in.readLine();
206             if (line.startsWith(constCoverFileNameKey+"=")) {
207                 opts.coverName=line.section('=', 1, 1);
208             } if (line.startsWith(constCoverMaxSizeKey+"=")) {
209                 opts.coverMaxSize=line.section('=', 1, 1).toUInt();
210                 opts.checkCoverSize();
211             } else if(line.startsWith(constVariousArtistsFixKey+"=")) {
212                 opts.fixVariousArtists=QLatin1String("true")==line.section('=', 1, 1);
213             } else if (line.startsWith(constTranscoderKey+"="))  {
214                 QStringList parts=line.section('=', 1, 1).split(',');
215                 if (parts.size()>=3) {
216                     opts.transcoderCodec=parts.at(0);
217                     opts.transcoderValue=parts.at(1).toInt();
218                     if (parts.size()>=4) {
219                         if (QLatin1String("true")==parts.at(3)) {
220                             opts.transcoderWhen=DeviceOptions::TW_IfLossess;
221                         } else if (QLatin1String("true")==parts.at(2)) {
222                             opts.transcoderWhen=DeviceOptions::TW_IfDifferent;
223                         } else {
224                             opts.transcoderWhen=DeviceOptions::TW_Always;
225                         }
226                     } else {
227                         const QString &val = parts.at(2);
228                         if (QLatin1String("true")==val) {
229                             opts.transcoderWhen=DeviceOptions::TW_IfDifferent;
230                         } else if (QLatin1String("false")==val) {
231                             opts.transcoderWhen=DeviceOptions::TW_Always;
232                         } else {
233                             opts.transcoderWhen=(DeviceOptions::TranscodeWhen)val.toInt();
234                         }
235                     }
236                 }
237             } else if (line.startsWith(constUseCacheKey+"=")) {
238                 opts.useCache=QLatin1String("true")==line.section('=', 1, 1);
239             } else if (line.startsWith(constAutoScanKey+"=")) {
240                 opts.autoScan=QLatin1String("true")==line.section('=', 1, 1);
241             } else if (readAll) {
242                 // For UMS these are stored in .is_audio_player - for Amarok compatability!
243                 if (line.startsWith(constMusicFilenameSchemeKey+"=")) {
244                     QString scheme = line.section('=', 1, 1);
245                     //protect against empty setting.
246                     if (!scheme.isEmpty() ) {
247                         opts.scheme = scheme;
248                     }
249                 } else if (line.startsWith(constVfatSafeKey+"="))  {
250                     opts.vfatSafe = QLatin1String("true")==line.section('=', 1, 1);
251                 } else if (line.startsWith(constAsciiOnlyKey+"=")) {
252                     opts.asciiOnly = QLatin1String("true")==line.section('=', 1, 1);
253                 } else if (line.startsWith(constIgnoreTheKey+"=")) {
254                     opts.ignoreThe = QLatin1String("true")==line.section('=', 1, 1);
255                 } else if (line.startsWith(constReplaceSpacesKey+"="))  {
256                     opts.replaceSpaces = QLatin1String("true")==line.section('=', 1, 1);
257                 }
258             }
259         }
260 
261         return true;
262     }
263     return false;
264 }
265 
toString(bool b)266 static inline QString toString(bool b)
267 {
268     return b ? QLatin1String("true") : QLatin1String("false");
269 }
270 
writeOpts(const QString & fileName,const DeviceOptions & opts,bool writeAll)271 void FsDevice::writeOpts(const QString &fileName, const DeviceOptions &opts, bool writeAll)
272 {
273     DeviceOptions def(constDefCoverFileName);
274     // If we are just using the defaults, then mayas wel lremove the file!
275     if ( (writeAll && opts==def) ||
276          (!writeAll && opts.coverName==constDefCoverFileName && 0==opts.coverMaxSize && opts.fixVariousArtists!=def.fixVariousArtists &&
277           opts.transcoderCodec.isEmpty() && opts.useCache==def.useCache && opts.autoScan!=def.autoScan)) {
278         if (QFile::exists(fileName)) {
279             QFile::remove(fileName);
280         }
281         return;
282     }
283 
284     QFile file(fileName);
285     if (file.open(QIODevice::WriteOnly|QIODevice::Text)) {
286 
287         QTextStream out(&file);
288         if (writeAll) {
289             if (opts.scheme!=def.scheme) {
290                 out << constMusicFilenameSchemeKey << '=' << opts.scheme << '\n';
291             }
292             if (opts.vfatSafe!=def.vfatSafe) {
293                 out << constVfatSafeKey << '=' << toString(opts.vfatSafe) << '\n';
294             }
295             if (opts.asciiOnly!=def.asciiOnly) {
296                 out << constAsciiOnlyKey << '=' << toString(opts.asciiOnly) << '\n';
297             }
298             if (opts.ignoreThe!=def.ignoreThe) {
299                 out << constIgnoreTheKey << '=' << toString(opts.ignoreThe) << '\n';
300             }
301             if (opts.replaceSpaces!=def.replaceSpaces) {
302                 out << constReplaceSpacesKey << '=' << toString(opts.replaceSpaces) << '\n';
303             }
304         }
305 
306         // NOTE: If any options are added/changed - take care of the "if ( (writeAll..." block above!!!
307         if (opts.coverName!=constDefCoverFileName) {
308             out << constCoverFileNameKey << '=' << opts.coverName << '\n';
309         }
310         if (0!=opts.coverMaxSize) {
311             out << constCoverMaxSizeKey << '=' << opts.coverMaxSize << '\n';
312         }
313         if (opts.fixVariousArtists!=def.fixVariousArtists) {
314             out << constVariousArtistsFixKey << '=' << toString(opts.fixVariousArtists) << '\n';
315         }
316         if (!opts.transcoderCodec.isEmpty()) {
317             out << constTranscoderKey << '=' << opts.transcoderCodec << ',' << opts.transcoderValue
318                 << ',' << opts.transcoderWhen << '\n';
319         }
320         if (opts.useCache!=def.useCache) {
321             out << constUseCacheKey << '=' << toString(opts.useCache) << '\n';
322         }
323         if (opts.autoScan!=def.autoScan) {
324             out << constAutoScanKey << '=' << toString(opts.autoScan) << '\n';
325         }
326     }
327 }
328 
FsDevice(MusicLibraryModel * m,Solid::Device & dev)329 FsDevice::FsDevice(MusicLibraryModel *m, Solid::Device &dev)
330     : Device(m, dev)
331     , state(Idle)
332     , scanned(false)
333     , cacheProgress(-1)
334     , scanner(nullptr)
335 {
336 }
337 
FsDevice(MusicLibraryModel * m,const QString & name,const QString & id)338 FsDevice::FsDevice(MusicLibraryModel *m, const QString &name, const QString &id)
339     : Device(m, name, id)
340     , state(Idle)
341     , scanned(false)
342     , cacheProgress(-1)
343     , scanner(nullptr)
344 {
345 }
346 
~FsDevice()347 FsDevice::~FsDevice() {
348     stopScanner();
349 }
350 
rescan(bool full)351 void FsDevice::rescan(bool full)
352 {
353     spaceInfo.setDirty();
354     // If this is the first scan (scanned=false) and we are set to use cache, attempt to load that before scanning
355     if (isIdle()) {
356         if (full) {
357             removeCache();
358             clear();
359         }
360         startScanner(full);
361         scanned=true;
362     }
363 }
364 
stop()365 void FsDevice::stop()
366 {
367      if (nullptr!=scanner) {
368          stopScanner();
369      }
370 }
371 
addSong(const Song & s,bool overwrite,bool copyCover)372 void FsDevice::addSong(const Song &s, bool overwrite, bool copyCover)
373 {
374     jobAbortRequested=false;
375     if (!isConnected()) {
376         emit actionStatus(NotConnected);
377         return;
378     }
379 
380     needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
381 
382     if (!overwrite) {
383         Song check=s;
384 
385         if (needToFixVa) {
386             Device::fixVariousArtists(QString(), check, true);
387         }
388         if (songExists(check)) {
389             emit actionStatus(SongExists);
390             return;
391         }
392     }
393 
394     if (!QFile::exists(s.file)) {
395         emit actionStatus(SourceFileDoesNotExist);
396         return;
397     }
398 
399     currentDestFile=audioFolder+opts.createFilename(s);
400     Encoders::Encoder encoder;
401 
402     transcoding = false;
403     if (!opts.transcoderCodec.isEmpty()) {
404         encoder=Encoders::getEncoder(opts.transcoderCodec);
405         if (encoder.codec.isEmpty()) {
406             emit actionStatus(CodecNotAvailable);
407             return;
408         }
409 
410         transcoding = !opts.transcoderCodec.isEmpty() &&
411                          (DeviceOptions::TW_IfDifferent!=opts.transcoderWhen || encoder.isDifferent(s.file)) &&
412                          (DeviceOptions::TW_IfLossess!=opts.transcoderWhen || Device::isLossless(s.file));
413 
414         if (transcoding) {
415             currentDestFile=encoder.changeExtension(currentDestFile);
416         }
417     }
418 
419     if (!overwrite && QFile::exists(currentDestFile)) {
420         emit actionStatus(FileExists);
421         return;
422     }
423 
424     QDir dir(Utils::getDir(currentDestFile));
425     if(!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), QString())) {
426         emit actionStatus(DirCreationFaild);
427         return;
428     }
429     currentSong=s;
430 
431     if (transcoding) {
432         TranscodingJob *job=new TranscodingJob(encoder, opts.transcoderValue, s.file, currentDestFile, copyCover ? opts : DeviceOptions(Device::constNoCover),
433                                                (needToFixVa ? CopyJob::OptsApplyVaFix : CopyJob::OptsNone)|
434                                                    (Device::RemoteFs==devType() ? CopyJob::OptsFixLocal : CopyJob::OptsNone),
435                                                currentSong);
436         connect(job, SIGNAL(result(int)), SLOT(addSongResult(int)));
437         connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
438         job->start();
439     } else {
440         CopyJob *job=new CopyJob(s.file, currentDestFile, copyCover ? opts : DeviceOptions(Device::constNoCover),
441                                  (needToFixVa ? CopyJob::OptsApplyVaFix : CopyJob::OptsNone)|(Device::RemoteFs==devType() ? CopyJob::OptsFixLocal : CopyJob::OptsNone),
442                                  currentSong);
443         connect(job, SIGNAL(result(int)), SLOT(addSongResult(int)));
444         connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
445         job->start();
446     }
447 }
448 
copySongTo(const Song & s,const QString & musicPath,bool overwrite,bool copyCover)449 void FsDevice::copySongTo(const Song &s, const QString &musicPath, bool overwrite, bool copyCover)
450 {
451     jobAbortRequested=false;
452     if (!isConnected()) {
453         emit actionStatus(NotConnected);
454         return;
455     }
456 
457     needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
458 
459     if (!overwrite) {
460         Song check=s;
461 
462         if (needToFixVa) {
463             Device::fixVariousArtists(QString(), check, false);
464         }
465         if (MpdLibraryModel::self()->songExists(check)) {
466             emit actionStatus(SongExists);
467             return;
468         }
469     }
470 
471     QString source=audioFolder+s.file;
472 
473     if (!QFile::exists(source)) {
474         emit actionStatus(SourceFileDoesNotExist);
475         return;
476     }
477 
478     QString baseDir=MPDConnection::self()->getDetails().dir;
479     if (!overwrite && QFile::exists(baseDir+musicPath)) {
480         emit actionStatus(FileExists);
481         return;
482     }
483 
484     currentDestFile=baseDir+musicPath;
485     QDir dir(Utils::getDir(currentDestFile));
486     if (!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), baseDir)) {
487         emit actionStatus(DirCreationFaild);
488         return;
489     }
490 
491     currentSong=s;
492     // Pass an empty filename as covername, so that Covers::copyCover knows this is TO MPD...
493     CopyJob *job=new CopyJob(source, currentDestFile, copyCover ? DeviceOptions(QString()) : DeviceOptions(Device::constNoCover),
494                              needToFixVa ? CopyJob::OptsUnApplyVaFix : CopyJob::OptsNone, currentSong);
495     connect(job, SIGNAL(result(int)), SLOT(copySongToResult(int)));
496     connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
497     job->start();
498 }
499 
removeSong(const Song & s)500 void FsDevice::removeSong(const Song &s)
501 {
502     jobAbortRequested=false;
503     if (!isConnected()) {
504         emit actionStatus(NotConnected);
505         return;
506     }
507 
508     if (!QFile::exists(audioFolder+s.file)) {
509         emit actionStatus(SourceFileDoesNotExist);
510         return;
511     }
512 
513     currentSong=s;
514     DeleteJob *job=new DeleteJob(audioFolder+s.file);
515     connect(job, SIGNAL(result(int)), SLOT(removeSongResult(int)));
516     job->start();
517 }
518 
cleanDirs(const QSet<QString> & dirs)519 void FsDevice::cleanDirs(const QSet<QString> &dirs)
520 {
521     CleanJob *job=new CleanJob(dirs, audioFolder, opts.coverName);
522     connect(job, SIGNAL(result(int)), SLOT(cleanDirsResult(int)));
523     connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
524     job->start();
525 }
526 
requestCover(const Song & s)527 Covers::Image FsDevice::requestCover(const Song &s)
528 {
529     Covers::Image i;
530     QString songFile=audioFolder+s.file;
531     QString dirName=Utils::getDir(songFile);
532 
533     if (QFile::exists(dirName+opts.coverName)) {
534         QImage img(dirName+opts.coverName);
535         if (!img.isNull()) {
536             emit cover(s, img);
537             return Covers::Image(img, dirName+opts.coverName);
538         }
539     }
540 
541     QStringList files=QDir(dirName).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.png"), QDir::Files|QDir::Readable);
542     for (const QString &fileName: files) {
543         QImage img(dirName+fileName);
544 
545         if (!img.isNull()) {
546             emit cover(s, img);
547             return Covers::Image(img, dirName+fileName);
548         }
549     }
550     return Covers::Image();
551 }
552 
percent(int pc)553 void FsDevice::percent(int pc)
554 {
555     if (jobAbortRequested && 100!=pc) {
556         FileJob *job=qobject_cast<FileJob *>(sender());
557         if (job) {
558             job->stop();
559         }
560         return;
561     }
562     emit progress(pc);
563 }
564 
addSongResult(int status)565 void FsDevice::addSongResult(int status)
566 {
567     CopyJob *job=qobject_cast<CopyJob *>(sender());
568     FileJob::finished(job);
569     spaceInfo.setDirty();
570 
571     if (jobAbortRequested) {
572         if (job && job->wasStarted() && QFile::exists(currentDestFile)) {
573             QFile::remove(currentDestFile);
574         }
575         return;
576     }
577     if (Ok!=status) {
578         emit actionStatus(status);
579     } else {
580         currentSong.file=currentDestFile.mid(audioFolder.length());
581         if (needToFixVa) {
582             currentSong.fixVariousArtists();
583         }
584         addSongToList(currentSong);
585         emit actionStatus(Ok, job && job->coverCopied());
586     }
587 }
588 
copySongToResult(int status)589 void FsDevice::copySongToResult(int status)
590 {
591     CopyJob *job=qobject_cast<CopyJob *>(sender());
592     FileJob::finished(job);
593     spaceInfo.setDirty();
594     if (jobAbortRequested) {
595         if (job && job->wasStarted() && QFile::exists(currentDestFile)) {
596             QFile::remove(currentDestFile);
597         }
598         return;
599     }
600     if (Ok!=status) {
601         emit actionStatus(status);
602     } else {
603         currentSong.file=currentDestFile.mid(MPDConnection::self()->getDetails().dir.length());
604         QString origPath;
605         if (MPDConnection::self()->isMopidy()) {
606             origPath=currentSong.file;
607             currentSong.file=Song::encodePath(currentSong.file);
608         }
609         if (needToFixVa) {
610             currentSong.revertVariousArtists();
611         }
612         Utils::setFilePerms(currentDestFile);
613 //        MusicLibraryModel::self()->addSongToList(currentSong);
614 //        DirViewModel::self()->addFileToList(origPath.isEmpty() ? currentSong.file : origPath,
615 //                                            origPath.isEmpty() ? QString() : currentSong.file);
616         emit actionStatus(Ok, job && job->coverCopied());
617     }
618 }
619 
removeSongResult(int status)620 void FsDevice::removeSongResult(int status)
621 {
622     FileJob::finished(sender());
623     spaceInfo.setDirty();
624     if (jobAbortRequested) {
625         return;
626     }
627     if (Ok!=status) {
628         emit actionStatus(status);
629     } else {
630         removeSongFromList(currentSong);
631         emit actionStatus(Ok);
632     }
633 }
634 
cleanDirsResult(int status)635 void FsDevice::cleanDirsResult(int status)
636 {
637     FileJob::finished(sender());
638     spaceInfo.setDirty();
639     if (jobAbortRequested) {
640         return;
641     }
642     emit actionStatus(status);
643 }
644 
initScaner()645 void FsDevice::initScaner()
646 {
647     if (!scanner) {
648         static bool registeredTypes=false;
649 
650         if (!registeredTypes) {
651              qRegisterMetaType<QSet<FileOnlySong> >("QSet<FileOnlySong>");
652              registeredTypes=true;
653         }
654         scanner=new MusicScanner(data());
655         connect(scanner, SIGNAL(libraryUpdated(MusicLibraryItemRoot *)), this, SLOT(libraryUpdated(MusicLibraryItemRoot *)));
656         connect(scanner, SIGNAL(songCount(int)), this, SLOT(songCount(int)));
657         connect(scanner, SIGNAL(cacheSaved()), this, SLOT(savedCache()));
658         connect(scanner, SIGNAL(savingCache(int)), this, SLOT(savingCache(int)));
659         connect(scanner, SIGNAL(readingCache(int)), this, SLOT(readingCache(int)));
660         connect(this, SIGNAL(scan(const QString &, const QString &, bool, const QSet<FileOnlySong> &)), scanner, SLOT(scan(const QString &, const QString &, bool, const QSet<FileOnlySong> &)));
661         connect(this, SIGNAL(saveCache(const QString &, MusicLibraryItemRoot *)), scanner, SLOT(saveCache(const QString &, MusicLibraryItemRoot *)));
662     }
663 }
664 
startScanner(bool fullScan)665 void FsDevice::startScanner(bool fullScan)
666 {
667     stopScanner();
668     initScaner();
669     QSet<FileOnlySong> existingSongs;
670     if (!fullScan) {
671         QSet<Song> songs=allSongs();
672 
673         for (const Song &s: songs) {
674             existingSongs.insert(FileOnlySong(s));
675         }
676     }
677     state=Updating;
678     emit scan(audioFolder, opts.useCache ? cacheFileName() : QString(), !scanned, existingSongs);
679     setStatusMessage(tr("Updating..."));
680     emit updating(id(), true);
681 }
682 
stopScanner()683 void FsDevice::stopScanner()
684 {
685     state=Idle;
686     if (!scanner) {
687         return;
688     }
689     disconnect(scanner, SIGNAL(libraryUpdated(MusicLibraryItemRoot *)), this, SLOT(libraryUpdated(MusicLibraryItemRoot *)));
690     disconnect(scanner, SIGNAL(songCount(int)), this, SLOT(songCount(int)));
691     disconnect(scanner, SIGNAL(cacheSaved()), this, SLOT(savedCache()));
692     disconnect(scanner, SIGNAL(savingCache(int)), this, SLOT(savingCache(int)));
693     disconnect(scanner, SIGNAL(readingCache(int)), this, SLOT(readingCache(int)));
694     scanner->deleteLater();
695     scanner=nullptr;
696 }
697 
clear() const698 void FsDevice::clear() const
699 {
700     if (childCount()) {
701         FsDevice *that=const_cast<FsDevice *>(this);
702         that->update=new MusicLibraryItemRoot();
703         that->applyUpdate();
704         that->scanned=false;
705     }
706 }
707 
libraryUpdated(MusicLibraryItemRoot * lib)708 void FsDevice::libraryUpdated(MusicLibraryItemRoot *lib)
709 {
710     cacheProgress=-1;
711     if (update) {
712         delete update;
713     }
714     update=lib;
715     setStatusMessage(QString());
716     state=Idle;
717     emit updating(id(), false);
718 }
719 
cacheFileName() const720 QString FsDevice::cacheFileName() const
721 {
722     if (audioFolder.isEmpty()) {
723         setAudioFolder();
724     }
725     return audioFolder+constCantataCacheFile+".xml.gz";
726 }
727 
saveCache()728 void FsDevice::saveCache()
729 {
730     if (opts.useCache) {
731         state=SavingCache;
732         initScaner();
733         emit saveCache(cacheFileName(), this);
734     }
735 }
736 
savedCache()737 void FsDevice::savedCache()
738 {
739     state=Idle;
740     cacheProgress=-1;
741     setStatusMessage(QString());
742     emit cacheSaved();
743 }
744 
removeCache()745 void FsDevice::removeCache()
746 {
747     QString cacheFile(cacheFileName());
748     if (QFile::exists(cacheFile)) {
749         QFile::remove(cacheFile);
750     }
751 }
752 
readingCache(int pc)753 void FsDevice::readingCache(int pc)
754 {
755     cacheStatus(tr("Reading cache"), pc);
756 }
757 
savingCache(int pc)758 void FsDevice::savingCache(int pc)
759 {
760     cacheStatus(tr("Saving cache"), pc);
761 }
762 
cacheStatus(const QString & msg,int prog)763 void FsDevice::cacheStatus(const QString &msg, int prog)
764 {
765     if (prog!=cacheProgress) {
766         cacheProgress=prog;
767         setStatusMessage(tr("%1 %2%","Message percent").arg(msg).arg(cacheProgress));
768     }
769 }
770 
771 #include "moc_fsdevice.cpp"
772