Files
cantata/devices/fsdevice.cpp

773 lines
25 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2014 Craig Drummond <craig.p.drummond@gmail.com>
*
* ----
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "umsdevice.h"
#include "tags/tags.h"
#include "models/musiclibrarymodel.h"
#include "models/musiclibraryitemsong.h"
#include "models/musiclibraryitemalbum.h"
#include "models/musiclibraryitemartist.h"
#include "models/musiclibraryitemroot.h"
#include "models/dirviewmodel.h"
#include "devicepropertiesdialog.h"
#include "devicepropertieswidget.h"
#include "support/utils.h"
#include "mpd/mpdparseutils.h"
#include "mpd/mpdconnection.h"
#include "encoders.h"
#include "transcodingjob.h"
#include "actiondialog.h"
#include "support/localize.h"
#include "gui/covers.h"
#include "support/thread.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <QTimer>
#include <time.h>
const QLatin1String FsDevice::constCantataCacheFile("/.cache");
const QLatin1String FsDevice::constCantataSettingsFile("/.cantata");
const QLatin1String FsDevice::constMusicFilenameSchemeKey("music_filenamescheme");
const QLatin1String FsDevice::constVfatSafeKey("vfat_safe");
const QLatin1String FsDevice::constAsciiOnlyKey("ascii_only");
const QLatin1String FsDevice::constIgnoreTheKey("ignore_the");
const QLatin1String FsDevice::constReplaceSpacesKey("replace_spaces");
const QLatin1String FsDevice::constCoverFileNameKey("cover_filename"); // Cantata extension!
const QLatin1String FsDevice::constCoverMaxSizeKey("cover_maxsize"); // Cantata extension!
const QLatin1String FsDevice::constVariousArtistsFixKey("fix_various_artists"); // Cantata extension!
const QLatin1String FsDevice::constTranscoderKey("transcoder"); // Cantata extension!
const QLatin1String FsDevice::constUseCacheKey("use_cache"); // Cantata extension!
const QLatin1String FsDevice::constDefCoverFileName("cover.jpg");
const QLatin1String FsDevice::constAutoScanKey("auto_scan"); // Cantata extension!
MusicScanner::MusicScanner()
: QObject(0)
, stopRequested(false)
, count(0)
, lastUpdate(0)
, lastCacheProg(-1)
{
thread=new Thread(metaObject()->className());
moveToThread(thread);
thread->start();
}
MusicScanner::~MusicScanner()
{
stop();
}
void MusicScanner::scan(const QString &folder, const QString &cacheFile, bool readCache, const QSet<FileOnlySong> &existingSongs)
{
lastCacheProg=-1;
if (!cacheFile.isEmpty() && readCache) {
MusicLibraryItemRoot *lib=new MusicLibraryItemRoot;
MusicLibraryModel::convertCache(cacheFile);
readProgress(0.0);
if (lib->fromXML(cacheFile, QDateTime(), 0, folder)) {
lib->applyGrouping();
if (!stopRequested) {
emit libraryUpdated(lib);
} else {
delete lib;
}
return;
} else {
delete lib;
}
}
if (stopRequested) {
return;
}
count=0;
lastUpdate=0;
MusicLibraryItemRoot *library = new MusicLibraryItemRoot;
QString topLevel=Utils::fixPath(QDir(folder).absolutePath());
QSet<FileOnlySong> existing=existingSongs;
scanFolder(library, topLevel, topLevel, existing, 0);
if (!stopRequested) {
library->applyGrouping();
if (!cacheFile.isEmpty()) {
writeProgress(0.0);
library->toXML(cacheFile, QDateTime(), this);
}
emit libraryUpdated(library);
} else {
delete library;
}
}
void MusicScanner::saveCache(const QString &cache, MusicLibraryItemRoot *lib)
{
lastCacheProg=-1;
writeProgress(0.0);
lib->toXML(cache, QDateTime(), this);
emit cacheSaved();
}
void MusicScanner::stop()
{
stopRequested=true;
thread->stop();
thread=0;
}
void MusicScanner::scanFolder(MusicLibraryItemRoot *library, const QString &topLevel, const QString &f,
QSet<FileOnlySong> &existing, int level)
{
if (stopRequested) {
return;
}
if (level<4) {
QDir d(f);
QFileInfoList entries=d.entryInfoList(QDir::Files|QDir::NoSymLinks|QDir::Dirs|QDir::NoDotAndDotDot);
MusicLibraryItemArtist *artistItem = 0;
MusicLibraryItemAlbum *albumItem = 0;
foreach (const QFileInfo &info, entries) {
if (stopRequested) {
return;
}
if (info.isDir()) {
scanFolder(library, topLevel, info.absoluteFilePath(), existing, level+1);
} else if(info.isReadable()) {
Song song;
QString fname=info.absoluteFilePath().mid(topLevel.length());
if (fname.endsWith(".jpg", Qt::CaseInsensitive) || fname.endsWith(".png", Qt::CaseInsensitive) ||
fname.endsWith(".lyrics", Qt::CaseInsensitive) || fname.endsWith(".pamp", Qt::CaseInsensitive)) {
continue;
}
song.file=fname;
QSet<FileOnlySong>::iterator it=existing.find(song);
if (existing.end()==it) {
song=Tags::read(info.absoluteFilePath());
song.file=fname;
} else {
song=*it;
existing.erase(it);
}
if (song.isEmpty()) {
continue;
}
count++;
int t=time(NULL);
if ((t-lastUpdate)>=2 || 0==(count%5)) {
lastUpdate=t;
emit songCount(count);
}
song.fillEmptyFields();
song.size=info.size();
if (!artistItem || song.artistOrComposer()!=artistItem->data()) {
artistItem = library->artist(song);
}
if (!albumItem || albumItem->parentItem()!=artistItem || song.albumName()!=albumItem->data()) {
albumItem = artistItem->album(song);
}
MusicLibraryItemSong *songItem = new MusicLibraryItemSong(song, albumItem);
const QSet<QString> &songGenres=songItem->allGenres();
albumItem->append(songItem);
albumItem->addGenres(songGenres);
artistItem->addGenres(songGenres);
library->addGenres(songGenres);
}
}
}
}
void MusicScanner::readProgress(double pc)
{
int prog=(int)pc;
if (prog!=lastCacheProg) {
lastCacheProg=prog;
emit readingCache(lastCacheProg);
}
}
void MusicScanner::writeProgress(double pc)
{
int prog=(int)pc;
if (prog!=lastCacheProg) {
lastCacheProg=prog;
emit savingCache(lastCacheProg);
}
}
bool FsDevice::readOpts(const QString &fileName, DeviceOptions &opts, bool readAll)
{
QFile file(fileName);
opts=DeviceOptions(constDefCoverFileName);
if (file.open(QIODevice::ReadOnly|QIODevice::Text)) {
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
if (line.startsWith(constCoverFileNameKey+"=")) {
opts.coverName=line.section('=', 1, 1);
} if (line.startsWith(constCoverMaxSizeKey+"=")) {
opts.coverMaxSize=line.section('=', 1, 1).toUInt();
opts.checkCoverSize();
} else if(line.startsWith(constVariousArtistsFixKey+"=")) {
opts.fixVariousArtists=QLatin1String("true")==line.section('=', 1, 1);
} else if (line.startsWith(constTranscoderKey+"=")) {
QStringList parts=line.section('=', 1, 1).split(',');
if (3==parts.size()) {
opts.transcoderCodec=parts.at(0);
opts.transcoderValue=parts.at(1).toInt();
opts.transcoderWhenDifferent=QLatin1String("true")==parts.at(2);
}
} else if (line.startsWith(constUseCacheKey+"=")) {
opts.useCache=QLatin1String("true")==line.section('=', 1, 1);
} else if (line.startsWith(constAutoScanKey+"=")) {
opts.autoScan=QLatin1String("true")==line.section('=', 1, 1);
} else if (readAll) {
// For UMS these are stored in .is_audio_player - for Amarok compatability!
if (line.startsWith(constMusicFilenameSchemeKey+"=")) {
QString scheme = line.section('=', 1, 1);
//protect against empty setting.
if (!scheme.isEmpty() ) {
opts.scheme = scheme;
}
} else if (line.startsWith(constVfatSafeKey+"=")) {
opts.vfatSafe = QLatin1String("true")==line.section('=', 1, 1);
} else if (line.startsWith(constAsciiOnlyKey+"=")) {
opts.asciiOnly = QLatin1String("true")==line.section('=', 1, 1);
} else if (line.startsWith(constIgnoreTheKey+"=")) {
opts.ignoreThe = QLatin1String("true")==line.section('=', 1, 1);
} else if (line.startsWith(constReplaceSpacesKey+"=")) {
opts.replaceSpaces = QLatin1String("true")==line.section('=', 1, 1);
}
}
}
return true;
}
return false;
}
static inline QString toString(bool b)
{
return b ? QLatin1String("true") : QLatin1String("false");
}
void FsDevice::writeOpts(const QString &fileName, const DeviceOptions &opts, bool writeAll)
{
DeviceOptions def(constDefCoverFileName);
// If we are just using the defaults, then mayas wel lremove the file!
if ( (writeAll && opts==def) ||
(!writeAll && opts.coverName==constDefCoverFileName && 0==opts.coverMaxSize && opts.fixVariousArtists!=def.fixVariousArtists &&
opts.transcoderCodec.isEmpty() && opts.useCache==def.useCache && opts.autoScan!=def.autoScan)) {
if (QFile::exists(fileName)) {
QFile::remove(fileName);
}
return;
}
QFile file(fileName);
if (file.open(QIODevice::WriteOnly|QIODevice::Text)) {
QTextStream out(&file);
if (writeAll) {
if (opts.scheme!=def.scheme) {
out << constMusicFilenameSchemeKey << '=' << opts.scheme << '\n';
}
if (opts.scheme!=def.scheme) {
out << constVfatSafeKey << '=' << toString(opts.vfatSafe) << '\n';
}
if (opts.asciiOnly!=def.asciiOnly) {
out << constAsciiOnlyKey << '=' << toString(opts.asciiOnly) << '\n';
}
if (opts.ignoreThe!=def.ignoreThe) {
out << constIgnoreTheKey << '=' << toString(opts.ignoreThe) << '\n';
}
if (opts.replaceSpaces!=def.replaceSpaces) {
out << constReplaceSpacesKey << '=' << toString(opts.replaceSpaces) << '\n';
}
}
// NOTE: If any options are added/changed - take care of the "if ( (writeAll..." block above!!!
if (opts.coverName!=constDefCoverFileName) {
out << constCoverFileNameKey << '=' << opts.coverName << '\n';
}
if (0!=opts.coverMaxSize) {
out << constCoverMaxSizeKey << '=' << opts.coverMaxSize << '\n';
}
if (opts.fixVariousArtists!=def.fixVariousArtists) {
out << constVariousArtistsFixKey << '=' << toString(opts.fixVariousArtists) << '\n';
}
if (!opts.transcoderCodec.isEmpty()) {
out << constTranscoderKey << '=' << opts.transcoderCodec << ',' << opts.transcoderValue << ',' << toString(opts.transcoderWhenDifferent) << '\n';
}
if (opts.useCache!=def.useCache) {
out << constUseCacheKey << '=' << toString(opts.useCache) << '\n';
}
if (opts.autoScan!=def.autoScan) {
out << constAutoScanKey << '=' << toString(opts.autoScan) << '\n';
}
}
}
FsDevice::FsDevice(MusicModel *m, Solid::Device &dev)
: Device(m, dev)
, state(Idle)
, scanned(false)
, cacheProgress(-1)
, scanner(0)
{
}
FsDevice::FsDevice(MusicModel *m, const QString &name, const QString &id)
: Device(m, name, id)
, state(Idle)
, scanned(false)
, cacheProgress(-1)
, scanner(0)
{
}
FsDevice::~FsDevice() {
stopScanner();
}
void FsDevice::rescan(bool full)
{
spaceInfo.setDirty();
// If this is the first scan (scanned=false) and we are set to use cache, attempt to load that before scanning
if (isIdle()) {
if (full) {
removeCache();
clear();
}
startScanner(full);
scanned=true;
}
}
void FsDevice::stop()
{
if (0!=scanner) {
stopScanner();
}
}
void FsDevice::addSong(const Song &s, bool overwrite, bool copyCover)
{
jobAbortRequested=false;
if (!isConnected()) {
emit actionStatus(NotConnected);
return;
}
needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
if (!overwrite) {
Song check=s;
if (needToFixVa) {
Device::fixVariousArtists(QString(), check, true);
}
if (songExists(check)) {
emit actionStatus(SongExists);
return;
}
}
if (!QFile::exists(s.file)) {
emit actionStatus(SourceFileDoesNotExist);
return;
}
currentDestFile=audioFolder+opts.createFilename(s);
Encoders::Encoder encoder;
if (!opts.transcoderCodec.isEmpty()) {
encoder=Encoders::getEncoder(opts.transcoderCodec);
if (encoder.codec.isEmpty()) {
emit actionStatus(CodecNotAvailable);
return;
}
currentDestFile=encoder.changeExtension(currentDestFile);
}
if (!overwrite && QFile::exists(currentDestFile)) {
emit actionStatus(FileExists);
return;
}
QDir dir(Utils::getDir(currentDestFile));
if(!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), QString())) {
emit actionStatus(DirCreationFaild);
return;
}
currentSong=s;
if (encoder.codec.isEmpty() || (opts.transcoderWhenDifferent && !encoder.isDifferent(s.file))) {
transcoding=false;
CopyJob *job=new CopyJob(s.file, currentDestFile, copyCover ? opts : DeviceOptions(Device::constNoCover),
(needToFixVa ? CopyJob::OptsApplyVaFix : CopyJob::OptsNone)|(Device::RemoteFs==devType() ? CopyJob::OptsFixLocal : CopyJob::OptsNone),
currentSong);
connect(job, SIGNAL(result(int)), SLOT(addSongResult(int)));
connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
job->start();
} else {
transcoding=true;
TranscodingJob *job=new TranscodingJob(encoder, opts.transcoderValue, s.file, currentDestFile, copyCover ? opts : DeviceOptions(Device::constNoCover),
(needToFixVa ? CopyJob::OptsApplyVaFix : CopyJob::OptsNone)|
(Device::RemoteFs==devType() ? CopyJob::OptsFixLocal : CopyJob::OptsNone));
connect(job, SIGNAL(result(int)), SLOT(addSongResult(int)));
connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
job->start();
}
}
void FsDevice::copySongTo(const Song &s, const QString &musicPath, bool overwrite, bool copyCover)
{
jobAbortRequested=false;
if (!isConnected()) {
emit actionStatus(NotConnected);
return;
}
needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
if (!overwrite) {
Song check=s;
if (needToFixVa) {
Device::fixVariousArtists(QString(), check, false);
}
if (MusicLibraryModel::self()->songExists(check)) {
emit actionStatus(SongExists);
return;
}
}
QString source=audioFolder+s.file;
if (!QFile::exists(source)) {
emit actionStatus(SourceFileDoesNotExist);
return;
}
QString baseDir=MPDConnection::self()->getDetails().dir;
if (!overwrite && QFile::exists(baseDir+musicPath)) {
emit actionStatus(FileExists);
return;
}
currentDestFile=baseDir+musicPath;
QDir dir(Utils::getDir(currentDestFile));
if (!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), baseDir)) {
emit actionStatus(DirCreationFaild);
return;
}
currentSong=s;
// Pass an empty filename as covername, so that Covers::copyCover knows this is TO MPD...
CopyJob *job=new CopyJob(source, currentDestFile, copyCover ? DeviceOptions(QString()) : DeviceOptions(Device::constNoCover),
needToFixVa ? CopyJob::OptsUnApplyVaFix : CopyJob::OptsNone, currentSong);
connect(job, SIGNAL(result(int)), SLOT(copySongToResult(int)));
connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
job->start();
}
void FsDevice::removeSong(const Song &s)
{
jobAbortRequested=false;
if (!isConnected()) {
emit actionStatus(NotConnected);
return;
}
if (!QFile::exists(audioFolder+s.file)) {
emit actionStatus(SourceFileDoesNotExist);
return;
}
currentSong=s;
DeleteJob *job=new DeleteJob(audioFolder+s.file);
connect(job, SIGNAL(result(int)), SLOT(removeSongResult(int)));
job->start();
}
void FsDevice::cleanDirs(const QSet<QString> &dirs)
{
CleanJob *job=new CleanJob(dirs, audioFolder, opts.coverName);
connect(job, SIGNAL(result(int)), SLOT(cleanDirsResult(int)));
connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
job->start();
}
void FsDevice::requestCover(const Song &s)
{
QString songFile=audioFolder+s.file;
QString dirName=Utils::getDir(songFile);
if (QFile::exists(dirName+opts.coverName)) {
QImage img(dirName+opts.coverName);
if (!img.isNull()) {
emit cover(s, img);
return;
}
}
QStringList files=QDir(dirName).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.png"), QDir::Files|QDir::Readable);
foreach (const QString &fileName, files) {
QImage img(dirName+fileName);
if (!img.isNull()) {
emit cover(s, img);
return;
}
}
}
void FsDevice::percent(int pc)
{
if (jobAbortRequested && 100!=pc) {
FileJob *job=qobject_cast<FileJob *>(sender());
if (job) {
job->stop();
}
return;
}
emit progress(pc);
}
void FsDevice::addSongResult(int status)
{
CopyJob *job=qobject_cast<CopyJob *>(sender());
FileJob::finished(job);
spaceInfo.setDirty();
if (jobAbortRequested) {
if (job && job->wasStarted() && QFile::exists(currentDestFile)) {
QFile::remove(currentDestFile);
}
return;
}
if (Ok!=status) {
emit actionStatus(status);
} else {
currentSong.file=currentDestFile.mid(audioFolder.length());
if (needToFixVa) {
currentSong.fixVariousArtists();
}
addSongToList(currentSong);
emit actionStatus(Ok, job && job->coverCopied());
}
}
void FsDevice::copySongToResult(int status)
{
CopyJob *job=qobject_cast<CopyJob *>(sender());
FileJob::finished(job);
spaceInfo.setDirty();
if (jobAbortRequested) {
if (job && job->wasStarted() && QFile::exists(currentDestFile)) {
QFile::remove(currentDestFile);
}
return;
}
if (Ok!=status) {
emit actionStatus(status);
} else {
currentSong.file=currentDestFile.mid(MPDConnection::self()->getDetails().dir.length());
QString origPath;
if (MPDConnection::self()->isMopdidy()) {
origPath=currentSong.file;
currentSong.file=Song::encodePath(currentSong.file);
}
if (needToFixVa) {
currentSong.revertVariousArtists();
}
Utils::setFilePerms(currentDestFile);
MusicLibraryModel::self()->addSongToList(currentSong);
DirViewModel::self()->addFileToList(origPath.isEmpty() ? currentSong.file : origPath,
origPath.isEmpty() ? QString() : currentSong.file);
emit actionStatus(Ok, job && job->coverCopied());
}
}
void FsDevice::removeSongResult(int status)
{
FileJob::finished(sender());
spaceInfo.setDirty();
if (jobAbortRequested) {
return;
}
if (Ok!=status) {
emit actionStatus(status);
} else {
removeSongFromList(currentSong);
emit actionStatus(Ok);
}
}
void FsDevice::cleanDirsResult(int status)
{
FileJob::finished(sender());
spaceInfo.setDirty();
if (jobAbortRequested) {
return;
}
emit actionStatus(status);
}
void FsDevice::initScaner()
{
if (!scanner) {
static bool registeredTypes=false;
if (!registeredTypes) {
qRegisterMetaType<QSet<FileOnlySong> >("QSet<FileOnlySong>");
registeredTypes=true;
}
scanner=new MusicScanner();
connect(scanner, SIGNAL(libraryUpdated(MusicLibraryItemRoot *)), this, SLOT(libraryUpdated(MusicLibraryItemRoot *)));
connect(scanner, SIGNAL(songCount(int)), this, SLOT(songCount(int)));
connect(scanner, SIGNAL(cacheSaved()), this, SLOT(savedCache()));
connect(scanner, SIGNAL(savingCache(int)), this, SLOT(savingCache(int)));
connect(scanner, SIGNAL(readingCache(int)), this, SLOT(readingCache(int)));
connect(this, SIGNAL(scan(const QString &, const QString &, bool, const QSet<FileOnlySong> &)), scanner, SLOT(scan(const QString &, const QString &, bool, const QSet<FileOnlySong> &)));
connect(this, SIGNAL(saveCache(const QString &, MusicLibraryItemRoot *)), scanner, SLOT(saveCache(const QString &, MusicLibraryItemRoot *)));
}
}
void FsDevice::startScanner(bool fullScan)
{
stopScanner();
initScaner();
QSet<FileOnlySong> existingSongs;
if (!fullScan) {
QSet<Song> songs=allSongs();
foreach (const Song &s, songs) {
existingSongs.insert(FileOnlySong(s));
}
}
state=Updating;
emit scan(audioFolder, opts.useCache ? cacheFileName() : QString(), !scanned, existingSongs);
setStatusMessage(i18n("Updating..."));
emit updating(id(), true);
}
void FsDevice::stopScanner()
{
state=Idle;
if (!scanner) {
return;
}
disconnect(scanner, SIGNAL(libraryUpdated(MusicLibraryItemRoot *)), this, SLOT(libraryUpdated(MusicLibraryItemRoot *)));
disconnect(scanner, SIGNAL(songCount(int)), this, SLOT(songCount(int)));
disconnect(scanner, SIGNAL(cacheSaved()), this, SLOT(savedCache()));
disconnect(scanner, SIGNAL(savingCache(int)), this, SLOT(savingCache(int)));
disconnect(scanner, SIGNAL(readingCache(int)), this, SLOT(readingCache(int)));
scanner->deleteLater();
scanner=0;
}
void FsDevice::clear() const
{
if (childCount()) {
FsDevice *that=const_cast<FsDevice *>(this);
that->update=new MusicLibraryItemRoot();
that->applyUpdate();
that->scanned=false;
}
}
void FsDevice::libraryUpdated(MusicLibraryItemRoot *lib)
{
cacheProgress=-1;
if (update) {
delete update;
}
update=lib;
setStatusMessage(QString());
state=Idle;
emit updating(id(), false);
}
QString FsDevice::cacheFileName() const
{
if (audioFolder.isEmpty()) {
setAudioFolder();
}
return audioFolder+constCantataCacheFile+MusicLibraryModel::constLibraryCompressedExt;
}
void FsDevice::saveCache()
{
if (opts.useCache) {
state=SavingCache;
emit saveCache(cacheFileName(), this);
}
}
void FsDevice::savedCache()
{
state=Idle;
cacheProgress=-1;
setStatusMessage(QString());
emit cacheSaved();
}
void FsDevice::removeCache()
{
QString cacheFile(cacheFileName());
if (QFile::exists(cacheFile)) {
QFile::remove(cacheFile);
}
// Remove old (non-compressed) cache file as well...
QString oldCache=cacheFile;
oldCache.replace(MusicLibraryModel::constLibraryCompressedExt, MusicLibraryModel::constLibraryExt);
if (oldCache!=cacheFile && QFile::exists(oldCache)) {
QFile::remove(oldCache);
}
}
void FsDevice::readingCache(int pc)
{
cacheStatus(i18n("Reading cache"), pc);
}
void FsDevice::savingCache(int pc)
{
cacheStatus(i18n("Saving cache"), pc);
}
void FsDevice::cacheStatus(const QString &msg, int prog)
{
if (prog!=cacheProgress) {
cacheProgress=prog;
setStatusMessage(i18nc("Message percent", "%1 %2%", msg, cacheProgress));
}
}