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