/* * Cantata * * Copyright (c) 2011-2017 Craig Drummond * * ---- * * 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 "dynamic.h" #include "config.h" #include "support/utils.h" #include "mpd-interface/mpdconnection.h" #include "widgets/icons.h" #include "models/roles.h" #include "network/networkaccessmanager.h" #include "gui/settings.h" #include "support/actioncollection.h" #include "support/globalstatic.h" #include #include #include #include #include #include #include #include #include #include #include static bool debugEnabled=false; #define DBUG if (debugEnabled) qWarning() << metaObject()->className() << __FUNCTION__ void Dynamic::enableDebug() { debugEnabled=true; } static const QString constDir=QLatin1String("dynamic"); static const QString constExtension=QLatin1String(".rules"); static const QString constActiveRules=QLatin1String("rules"); static const QString constLockFile=QLatin1String("lock"); static const QString constPingCmd=QLatin1String("ping"); static const QString constListCmd=QLatin1String("list"); static const QString constStatusCmd=QLatin1String("status"); static const QString constSaveCmd=QLatin1String("save"); static const QString constDeleteCmd=QLatin1String("delete"); static const QString constSetActiveCmd=QLatin1String("setActive"); static const QString constControlCmd=QLatin1String("control"); static QString remoteError(const QStringList &status) { if (!status.isEmpty()) { switch (status.at(0).toInt()) { case 1: return QObject::tr("Empty filename."); case 2: return QObject::tr("Invalid filename. (%1)").arg(status.length()<2 ? QString() : status.at(2)); case 3: return QObject::tr("Failed to save %1.").arg(status.length()<2 ? QString() : status.at(2)); case 4: return QObject::tr("Failed to delete rules file. (%1)").arg(status.length()<2 ? QString() : status.at(2)); case 5: return QObject::tr("Invalid command. (%1)").arg(status.length()<2 ? QString() : status.at(2)); case 6: return QObject::tr("Could not remove active rules link."); case 7: return QObject::tr("Active rules is not a link."); case 8: return QObject::tr("Could not create active rules link."); case 9: return QObject::tr("Rules file, %1, does not exist.").arg(status.length()<2 ? QString() : status.at(2)); case 10: return QObject::tr("Incorrect arguments supplied."); case 11: return QObject::tr("Unknown method called."); } } return QObject::tr("Unknown error"); } Dynamic::Command Dynamic::toCommand(const QString &cmd) { if (constListCmd==cmd) { return List; } if (constStatusCmd==cmd) { return Status; } if (constSaveCmd==cmd) { return Save; } if (constDeleteCmd==cmd) { return Del; } if (constSetActiveCmd==cmd) { return SetActive; } if (constControlCmd==cmd) { return Control; } return Unknown; } QString Dynamic::toString(Command cmd) { switch (cmd) { case Unknown: return QString(); case Ping: return constPingCmd; case List: return constListCmd; case Status: return constStatusCmd; case Save: return constSaveCmd; case Del: return constDeleteCmd; case SetActive: return constSetActiveCmd; case Control: return constControlCmd; } return QString(); } GLOBAL_STATIC(Dynamic, instance) const QString Dynamic::constRuleKey=QLatin1String("Rule"); const QString Dynamic::constArtistKey=QLatin1String("Artist"); const QString Dynamic::constSimilarArtistsKey=QLatin1String("SimilarArtists"); const QString Dynamic::constAlbumArtistKey=QLatin1String("AlbumArtist"); const QString Dynamic::constComposerKey=QLatin1String("Composer"); const QString Dynamic::constCommentKey=QLatin1String("Comment"); const QString Dynamic::constAlbumKey=QLatin1String("Album"); const QString Dynamic::constTitleKey=QLatin1String("Title"); const QString Dynamic::constGenreKey=QLatin1String("Genre"); const QString Dynamic::constDateKey=QLatin1String("Date"); const QString Dynamic::constRatingKey=QLatin1String("Rating"); const QString Dynamic::constDurationKey=QLatin1String("Duration"); const QString Dynamic::constFileKey=QLatin1String("File"); const QString Dynamic::constExactKey=QLatin1String("Exact"); const QString Dynamic::constExcludeKey=QLatin1String("Exclude"); const QChar Dynamic::constRangeSep=QLatin1Char('-'); const QChar constKeyValSep=QLatin1Char(':'); const QString constOk=QLatin1String("0"); const QString constFilename=QLatin1String("FILENAME:"); Dynamic::Dynamic() : localTimer(0) , usingRemote(false) , remoteTimer(0) , remotePollingEnabled(false) , statusTime(0) , currentJob(0) , currentCommand(Unknown) { icn=Icons::self()->dynamicRuleIcon; loadLocal(); connect(this, SIGNAL(clear()), MPDConnection::self(), SLOT(clear())); connect(MPDConnection::self(), SIGNAL(dynamicSupport(bool)), this, SLOT(remoteDynamicSupported(bool))); connect(this, SIGNAL(remoteMessage(QStringList)), MPDConnection::self(), SLOT(sendDynamicMessage(QStringList))); connect(MPDConnection::self(), SIGNAL(dynamicResponse(QStringList)), this, SLOT(remoteResponse(QStringList))); QTimer::singleShot(500, this, SLOT(checkHelper())); startAction = ActionCollection::get()->createAction("startdynamic", tr("Start Dynamic Playlist"), Icons::self()->replacePlayQueueIcon); stopAction = ActionCollection::get()->createAction("stopdynamic", tr("Stop Dynamic Mode"), Icons::self()->stopDynamicIcon); } QString Dynamic::name() const { return QLatin1String("dynamic"); } QString Dynamic::title() const { return tr("Dynamic Playlists"); } QString Dynamic::descr() const { return tr("Dynamically generated playlists"); } QVariant Dynamic::headerData(int, Qt::Orientation, int) const { return QVariant(); } int Dynamic::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : entryList.count(); } bool Dynamic::hasChildren(const QModelIndex &parent) const { return !parent.isValid(); } QModelIndex Dynamic::parent(const QModelIndex &) const { return QModelIndex(); } QModelIndex Dynamic::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid() || !hasIndex(row, column, parent) || row>=entryList.count()) { return QModelIndex(); } return createIndex(row, column); } #define IS_ACTIVE(E) !currentEntry.isEmpty() && (E)==currentEntry && (!isRemote() || QLatin1String("IDLE")!=lastState) QVariant Dynamic::data(const QModelIndex &index, int role) const { if (!index.isValid()) { switch (role) { case Cantata::Role_TitleText: return title(); case Cantata::Role_SubText: return descr(); case Qt::DecorationRole: return icon(); } return QVariant(); } if (index.parent().isValid() || index.row()>=entryList.count()) { return QVariant(); } switch (role) { case Qt::ToolTipRole: if (!Settings::self()->infoTooltips()) { return QVariant(); } case Qt::DisplayRole: return entryList.at(index.row()).name; case Qt::DecorationRole: return IS_ACTIVE(entryList.at(index.row()).name) ? Icons::self()->replacePlayQueueIcon : Icons::self()->dynamicListIcon; case Cantata::Role_SubText: { const Entry &e=entryList.at(index.row()); return tr("%n Rule(s)", "", e.rules.count())+(e.haveRating() ? tr(" - Rating: %1..%2") .arg((double)e.ratingFrom/Song::Rating_Step).arg((double)e.ratingTo/Song::Rating_Step) : QString()); } case Cantata::Role_Actions: { QVariant v; v.setValue >(QList() << (IS_ACTIVE(entryList.at(index.row()).name) ? stopAction : startAction)); return v; } default: return QVariant(); } } Qt::ItemFlags Dynamic::flags(const QModelIndex &index) const { if (index.isValid()) { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } return Qt::NoItemFlags; } Dynamic::Entry Dynamic::entry(const QString &e) { if (!e.isEmpty()) { QList::Iterator it=find(e); if (it!=entryList.end()) { return *it; } } return Entry(); } bool Dynamic::save(const Entry &e) { if (e.name.isEmpty()) { return false; } QString string; QTextStream str(&string); if (e.ratingFrom!=0 || e.ratingTo!=0) { str << constRatingKey << constKeyValSep << e.ratingFrom << constRangeSep << e.ratingTo<< '\n'; } if (e.minDuration!=0 || e.maxDuration!=0) { str << constDurationKey << constKeyValSep << e.minDuration << constRangeSep << e.maxDuration<< '\n'; } foreach (const Rule &rule, e.rules) { if (!rule.isEmpty()) { str << constRuleKey << '\n'; Rule::ConstIterator it(rule.constBegin()); Rule::ConstIterator end(rule.constEnd()); for (; it!=end; ++it) { str << it.key() << constKeyValSep << it.value() << '\n'; } } } if (isRemote()) { if (sendCommand(Save, QStringList() << e.name << string)) { currentSave=e; return true; } return false; } QFile f(Utils::dataDir(constDir, true)+e.name+constExtension); if (f.open(QIODevice::WriteOnly|QIODevice::Text)) { QTextStream out(&f); out.setCodec("UTF-8"); out << string; updateEntry(e); return true; } return false; } void Dynamic::updateEntry(const Entry &e) { QList::Iterator it=find(e.name); if (it!=entryList.end()) { entryList.replace(it-entryList.begin(), e); QModelIndex idx=index(it-entryList.begin(), 0, QModelIndex()); emit dataChanged(idx, idx); } else { beginInsertRows(QModelIndex(), entryList.count(), entryList.count()); entryList.append(e); endInsertRows(); } } void Dynamic::del(const QString &name) { if (isRemote()) { if (sendCommand(Del, QStringList() << name)) { currentDelete=name; } return; } QList::Iterator it=find(name); if (it==entryList.end()) { return; } QString fName(Utils::dataDir(constDir, false)+name+constExtension); bool isCurrent=currentEntry==name; if (!QFile::exists(fName) || QFile::remove(fName)) { if (isCurrent) { stop(); } beginRemoveRows(QModelIndex(), it-entryList.begin(), it-entryList.begin()); entryList.erase(it); endRemoveRows(); return; } } void Dynamic::start(const QString &name) { if (isRemote()) { sendCommand(SetActive, QStringList() << name << "1"); return; } if (Utils::findExe("perl").isEmpty()) { emit error(tr("You need to install \"perl\" on your system in order for Cantata's dynamic mode to function.")); return; } QString fName(Utils::dataDir(constDir, false)+name+constExtension); if (!QFile::exists(fName)) { emit error(tr("Failed to locate rules file - %1").arg(fName)); return; } QString rules(Utils::cacheDir(constDir, true)+constActiveRules); QFile::remove(rules); if (QFile::exists(rules)) { emit error(tr("Failed to remove previous rules file - %1").arg(rules)); return; } if (!QFile::link(fName, rules)) { emit error(tr("Failed to install rules file - %1 -> %2").arg(fName).arg(rules)); return; } int i=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry); QModelIndex idx=index(i, 0, QModelIndex()); currentEntry=name; if (idx.isValid()) { emit dataChanged(idx, idx); } i=entryList.indexOf(currentEntry); idx=index(i, 0, QModelIndex()); if (idx.isValid()) { emit dataChanged(idx, idx); } if (isRunning()) { emit clear(); return; } if (controlApp(true)) { emit running(isRunning()); emit clear(); return; } } void Dynamic::stop(bool sendClear) { if (isRemote()) { if (sendClear) { sendCommand(Control, QStringList() << "stop" << "1"); } else { sendCommand(Control, QStringList() << "stop"); } return; } #if !defined Q_OS_WIN int i=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry); QModelIndex idx=index(i, 0, QModelIndex()); int pid=getPid(); if (!pid) { if (sendClear) { emit clear(); } currentEntry=QString(); emit running(false); if (idx.isValid()) { emit dataChanged(idx, idx); } return; } if (0!=::kill(pid, 0)) { if (sendClear) { emit clear(); } currentEntry=QString(); emit running(false); if (idx.isValid()) { emit dataChanged(idx, idx); } return; } if (controlApp(false)) { if (sendClear) { emit clear(); } currentEntry=QString(); emit running(isRunning()); if (idx.isValid()) { emit dataChanged(idx, idx); } return; } #endif } void Dynamic::toggle(const QString &name) { if(name==currentEntry) { stop(); } else { start(name); } } bool Dynamic::isRunning() { #if defined Q_OS_WIN return false; #else int pid=getPid(); return pid ? 0==::kill(pid, 0) : false; #endif } void Dynamic::enableRemotePolling(bool e) { remotePollingEnabled=e; if (remoteTimer && remoteTimer->isActive()) { if (remotePollingEnabled) { checkIfRemoteIsRunning(); } remoteTimer->start(remotePollingEnabled ? 5000 : 30000); } } int Dynamic::getPid() const { QFile pidFile(Utils::cacheDir(constDir, false)+constLockFile); if (pidFile.open(QIODevice::ReadOnly|QIODevice::Text)) { QTextStream str(&pidFile); int pid=0; str >> pid; return pid; } return 0; } bool Dynamic::controlApp(bool isStart) { QString cmd=CANTATA_SYS_SCRIPTS_DIR+QLatin1String("cantata-dynamic"); QProcess process; if (isStart) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); MPDConnectionDetails details=MPDConnection::self()->getDetails(); env.insert("MPD_HOST", details.password.isEmpty() ? details.hostname : (details.password+'@'+details.hostname)); env.insert("MPD_PORT", QString::number(details.port)); process.setProcessEnvironment(env); } process.start(cmd, QStringList() << QLatin1String(isStart ? "start" : "stop"), QIODevice::WriteOnly); if (!localTimer) { localTimer=new QTimer(this); connect(localTimer, SIGNAL(timeout()), SLOT(checkHelper())); } bool rv=process.waitForFinished(1000); localTimer->start(1000); return rv; } QList::Iterator Dynamic::find(const QString &e) { QList::Iterator it(entryList.begin()); QList::Iterator end(entryList.end()); for (; it!=end; ++it) { if ((*it).name==e) { break; } } return it; } void Dynamic::loadLocal() { beginResetModel(); entryList.clear(); currentEntry=QString(); // Load all current enttries... QString dirName=Utils::dataDir(constDir); QDir d(dirName); if (d.exists()) { QStringList rulesFiles=d.entryList(QStringList() << QChar('*')+constExtension); foreach (const QString &rf, rulesFiles) { QFile f(dirName+rf); if (f.open(QIODevice::ReadOnly|QIODevice::Text)) { QStringList keys=QStringList() << constArtistKey << constSimilarArtistsKey << constAlbumArtistKey << constDateKey << constExactKey << constAlbumKey << constTitleKey << constGenreKey << constFileKey << constExcludeKey; Entry e; e.name=rf.left(rf.length()-constExtension.length()); Rule r; QTextStream in(&f); in.setCodec("UTF-8"); QStringList lines = in.readAll().split('\n', QString::SkipEmptyParts); foreach (const QString &line, lines) { QString str=line.trimmed(); if (str.isEmpty() || str.startsWith('#')) { continue; } if (str==constRuleKey) { if (!r.isEmpty()) { e.rules.append(r); r.clear(); } } else if (str.startsWith(constRatingKey+constKeyValSep)) { QStringList vals=str.mid(constRatingKey.length()+1).split(constRangeSep); if (2==vals.count()) { e.ratingFrom=vals.at(0).toUInt(); e.ratingTo=vals.at(1).toUInt(); } } else if (str.startsWith(constDurationKey+constKeyValSep)) { QStringList vals=str.mid(constDurationKey.length()+1).split(constRangeSep); if (2==vals.count()) { e.minDuration=vals.at(0).toUInt(); e.maxDuration=vals.at(1).toUInt(); } } else { foreach (const QString &k, keys) { if (str.startsWith(k+constKeyValSep)) { r.insert(k, str.mid(k.length()+1)); } } } } if (!r.isEmpty()) { e.rules.append(r); r.clear(); } entryList.append(e); } } } endResetModel(); } void Dynamic::parseRemote(const QStringList &response) { DBUG << response; beginResetModel(); entryList.clear(); currentEntry=QString(); QStringList keys=QStringList() << constArtistKey << constSimilarArtistsKey << constAlbumArtistKey << constDateKey << constExactKey << constAlbumKey << constTitleKey << constGenreKey << constFileKey << constExcludeKey; Entry e; Rule r; foreach (const QString &part, response) { QStringList lines=part.split('\n', QString::SkipEmptyParts); foreach (const QString &s, lines) { QString str=s.trimmed(); if (str.isEmpty() || str.startsWith('#')) { continue; } if (str.startsWith(constFilename)) { if (!e.name.isEmpty()) { if (!r.isEmpty()) { e.rules.append(r); } entryList.append(e); } e.name=str.mid(9, str.length()-15); // Remove extension... e.ratingFrom=e.ratingTo=0; e.rules.clear(); r.clear(); } else if (str==constRuleKey) { if (!r.isEmpty()) { e.rules.append(r); r.clear(); } } else if (str.startsWith(constRatingKey+constKeyValSep)) { QStringList vals=str.mid(constRatingKey.length()+1).split(constRangeSep); if (2==vals.count()) { e.ratingFrom=vals.at(0).toUInt(); e.ratingTo=vals.at(1).toUInt(); } } else if (str.startsWith(constDurationKey+constKeyValSep)) { QStringList vals=str.mid(constDurationKey.length()+1).split(constRangeSep); if (2==vals.count()) { e.minDuration=vals.at(0).toUInt(); e.maxDuration=vals.at(1).toUInt(); } } else { foreach (const QString &k, keys) { if (str.startsWith(k+constKeyValSep)) { r.insert(k, str.mid(k.length()+1)); } } } } } if (!e.name.isEmpty()) { if (!r.isEmpty()) { e.rules.append(r); } entryList.append(e); } endResetModel(); } void Dynamic::parseStatus(QStringList response) { DBUG << response; if (response.isEmpty()) { return; } QString state=response.takeAt(0); QString prevEntry=currentEntry; int st=statusTime; if (!response.isEmpty()) { st=response.takeAt(0).toInt(); } if (!response.isEmpty()) { currentEntry=response.takeAt(0); } bool stateChanged=lastState!=state; bool noSongs=QLatin1String("NO_SONGS")==state; bool terminated=false; DBUG << "lastState" << lastState << "state" << state << "statusTime(before)" << statusTime << "statusTime(now)" << st; if (stateChanged) { lastState=state; if (noSongs) { bool sendError=!currentEntry.isEmpty(); emit running(true); if (sendError) { emit error(QLatin1String("NO_SONGS")); } //sendCommand(Control, QStringList() << "stop"); } else if (QLatin1String("HAVE_SONGS")==state || QLatin1String("STARTING")==state) { emit running(true); } else if (QLatin1String("IDLE")==state) { currentEntry.clear(); emit running(false); } else if (QLatin1String("TERMINATED")==state) { terminated=true; currentEntry.clear(); emit running(false); emit error(tr("Dynamizer has been terminated.")); pollRemoteHelper(); currentEntry=QString(); } } if (prevEntry!=currentEntry) { int prev=prevEntry.isEmpty() ? -1 : entryList.indexOf(prevEntry); int cur=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry); if (-1!=prev) { QModelIndex idx=index(prev, 0, QModelIndex()); emit dataChanged(idx, idx); } if (-1!=cur) { QModelIndex idx=index(cur, 0, QModelIndex()); emit dataChanged(idx, idx); } } else if (stateChanged) { int row=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry); if (-1!=row) { QModelIndex idx=index(row, 0, QModelIndex()); emit dataChanged(idx, idx); } } if (st>statusTime && !terminated) { statusTime=st; sendCommand(List, QStringList() << "1"); } } bool Dynamic::sendCommand(Command cmd, const QStringList &args) { DBUG << toString(cmd) << args; if (Ping==cmd) { emit remoteMessage(QStringList() << toString(cmd)); return true; } if (Status==currentCommand) { if (cmd==Status) { return true; } currentCommand=Unknown; } if (Status!=cmd && (Save==currentCommand || Del==currentCommand)) { emit error(tr("Awaiting response for previous command. (%1)").arg(Save==cmd ? tr("Saving rule") : tr("Deleting rule"))); return false; } currentCommand=cmd; emit remoteMessage(QStringList() << toString(cmd) << args); if (List==cmd) { emit loadingList(); } return true; } void Dynamic::checkHelper() { if (isRemote()) { return; } if (!isRunning()) { emit running(false); int i=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry); currentEntry=QString(); if (i>-1) { QModelIndex idx=index(i, 0, QModelIndex()); emit dataChanged(idx, idx); } if (localTimer) { localTimer->stop(); } } else { if (localTimer && localTimer->isActive()) { static const int constAppCheck=15*1000; if (localTimer->interval()start(constAppCheck); } } else { // No timer => app startup! // Attempt to read current name... QFileInfo inf(Utils::cacheDir(constDir, false)+constActiveRules); if (inf.exists() && inf.isSymLink()) { QString link=inf.readLink(); if (!link.isEmpty()) { QString fname=QFileInfo(link).fileName(); if (fname.endsWith(constExtension)) { currentEntry=fname.left(fname.length()-constExtension.length()); } } } emit running(true); } } } void Dynamic::pollRemoteHelper() { if (!remoteTimer) { remoteTimer=new QTimer(this); connect(remoteTimer, SIGNAL(timeout()), SLOT(checkIfRemoteIsRunning())); } beginResetModel(); entryList.clear(); currentEntry=QString(); endResetModel(); remoteTimer->start(remotePollingEnabled ? 5000 : 30000); } void Dynamic::checkIfRemoteIsRunning() { if (isRemote()) { if (remotePollingEnabled) { sendCommand(Ping); } } else if (remoteTimer) { remoteTimer->stop(); } } void Dynamic::updateRemoteStatus() { if (isRemote()) { sendCommand(Status); } } void Dynamic::remoteResponse(QStringList msg) { DBUG << msg; if (msg.isEmpty()) { return; } QString cmd=msg.takeAt(0); Command msgCmd=toCommand(cmd); switch (msgCmd) { case List: parseRemote(msg); emit loadedList(); break; case Status: parseStatus(msg); break; case Save: if (!msg.isEmpty() && msg.at(0)==constOk) { updateEntry(currentSave); emit saved(true); } else { emit error(tr("Failed to save %1. (%2)").arg(currentSave.name).arg(remoteError(msg))); emit saved(false); } currentSave.name=QString(); currentSave.rules.clear(); break; case Del: if (!msg.isEmpty() && msg.at(0)==constOk) { QList::Iterator it=find(currentDelete); if (it!=entryList.end()) { beginRemoveRows(QModelIndex(), it-entryList.begin(), it-entryList.begin()); entryList.erase(it); endRemoveRows(); } else { emit error(tr("Failed to delete rules file. (%1)").arg(remoteError(msg))); } } else { emit error(tr("Failed to delete rules file. (%1)").arg(remoteError(msg))); emit saved(false); } currentDelete.clear(); break; case Control: if (msg.isEmpty() || msg.at(0)!=constOk) { emit error(tr("Failed to control dynamizer state. (%1)").arg(remoteError(msg))); } break; case SetActive: if (!msg.isEmpty() && msg.at(0)==constOk) { QTimer::singleShot(1000, this, SLOT(updateRemoteStatus())); } else { emit error(tr("Failed to set the current dynamic rules. (%1)").arg(remoteError(msg))); } break; default: break; } if (msgCmd==currentCommand) { currentCommand=Unknown; } } void Dynamic::remoteDynamicSupported(bool s) { if (usingRemote==s) { return; } // Stop any local dynamizer before switching to remote... if (!isRemote()) { stop(); } usingRemote=s; if (s) { if (localTimer) { localTimer->stop(); } pollRemoteHelper(); sendCommand(List); sendCommand(Status); } else { if (remoteTimer) { remoteTimer->stop(); } loadLocal(); // #ifndef Q_OS_WIN // emit error(tr("Communication with the remote dynamizer has been lost, reverting to local mode.")); // #endif } }