1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  */
7 /*
8  * Copyright (c) 2008 Sander Knopper (sander AT knopper DOT tk) and
9  *                    Roeland Douma (roeland AT rullzer DOT com)
10  *
11  * This file is part of QtMPC.
12  *
13  * QtMPC is free software: you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation, either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * QtMPC is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with QtMPC.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "mpdconnection.h"
28 #include "mpdparseutils.h"
29 #include "models/streamsmodel.h"
30 #ifdef ENABLE_SIMPLE_MPD_SUPPORT
31 #include "mpduser.h"
32 #endif
33 #include "gui/settings.h"
34 #include "support/globalstatic.h"
35 #include "support/configuration.h"
36 #include <QStringList>
37 #include <QTimer>
38 #include <QDir>
39 #include <QHostInfo>
40 #include <QDateTime>
41 #include <QPropertyAnimation>
42 #include <QCoreApplication>
43 #include <QUdpSocket>
44 #include <complex>
45 #include "support/thread.h"
46 #include "cuefile.h"
47 #if defined Q_OS_LINUX && defined QT_QTDBUS_FOUND
48 #include "dbus/powermanagement.h"
49 #elif defined Q_OS_MAC && defined IOKIT_FOUND
50 #include "mac/powermanagement.h"
51 #endif
52 #include <algorithm>
53 #include <QDebug>
54 static bool debugEnabled=false;
55 #define DBUG if (debugEnabled) qWarning() << "MPDConnection" << QThread::currentThreadId()
enableDebug()56 void MPDConnection::enableDebug()
57 {
58     debugEnabled=true;
59 }
60 
61 // Uncomment the following to report error strings in MPDStatus to the UI
62 // ...disabled, as stickers (for ratings) can cause lots of errors to be reported - and these all need clearing, etc.
63 // #define REPORT_MPD_ERRORS
64 static const int constSocketCommsTimeout=2000;
65 static const int constMaxReadAttempts=4;
66 static const int constMaxFilesPerAddCommand=2000;
67 static const int constConnTimer=5000;
68 
69 static const QByteArray constOkValue("OK");
70 static const QByteArray constOkMpdValue("OK MPD");
71 static const QByteArray constOkNlValue("OK\n");
72 static const QByteArray constAckValue("ACK");
73 static const QByteArray constIdleChangedKey("changed: ");
74 static const QByteArray constIdleDbValue("database");
75 static const QByteArray constIdleUpdateValue("update");
76 static const QByteArray constIdleStoredPlaylistValue("stored_playlist");
77 static const QByteArray constIdlePlaylistValue("playlist");
78 static const QByteArray constIdlePlayerValue("player");
79 static const QByteArray constIdleMixerValue("mixer");
80 static const QByteArray constIdleOptionsValue("options");
81 static const QByteArray constIdleOutputValue("output");
82 static const QByteArray constIdleStickerValue("sticker");
83 static const QByteArray constIdleSubscriptionValue("subscription");
84 static const QByteArray constIdleMessageValue("message");
85 static const QByteArray constDynamicIn("cantata-dynamic-in");
86 static const QByteArray constDynamicOut("cantata-dynamic-out");
87 static const QByteArray constRatingSticker("rating");
88 
socketTimeout(int dataSize)89 static inline int socketTimeout(int dataSize)
90 {
91     static const int constDataBlock=256;
92     return ((dataSize/constDataBlock)+((dataSize%constDataBlock) ? 1 : 0))*constSocketCommsTimeout;
93 }
94 
log(const QByteArray & data)95 static QByteArray log(const QByteArray &data)
96 {
97     if (data.length()<256) {
98         return data;
99     } else {
100         return data.left(96) + "... ..." + data.right(96) + " (" + QByteArray::number(data.length()) + " bytes)";
101     }
102 }
103 
104 GLOBAL_STATIC(MPDConnection, instance)
105 
106 const QString MPDConnection::constModifiedSince=QLatin1String("modified-since");
107 const int MPDConnection::constMaxPqChanges=1000;
108 const QString MPDConnection::constStreamsPlayListName=QLatin1String("[Radio Streams]");
109 const QString MPDConnection::constPlaylistPrefix=QLatin1String("playlist:");
110 const QString MPDConnection::constDirPrefix=QLatin1String("dir:");
111 
quote(int val)112 QByteArray MPDConnection::quote(int val)
113 {
114     return '\"'+QByteArray::number(val)+'\"';
115 }
116 
encodeName(const QString & name)117 QByteArray MPDConnection::encodeName(const QString &name)
118 {
119     return '\"'+name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"")+'\"';
120 }
121 
readFromSocket(MpdSocket & socket,int timeout=constSocketCommsTimeout)122 static QByteArray readFromSocket(MpdSocket &socket, int timeout=constSocketCommsTimeout)
123 {
124     QByteArray data;
125     int attempt=0;
126     while (QAbstractSocket::ConnectedState==socket.state()) {
127         while (0==socket.bytesAvailable() && QAbstractSocket::ConnectedState==socket.state()) {
128             DBUG << (void *)(&socket) << "Waiting for read data, attempt" << attempt;
129             if (socket.waitForReadyRead(timeout)) {
130                 break;
131             }
132             DBUG << (void *)(&socket) << "Wait for read failed - " << socket.errorString();
133             if (++attempt>=constMaxReadAttempts) {
134                 DBUG << "ERROR: Timedout waiting for response";
135                 socket.close();
136                 return QByteArray();
137             }
138         }
139 
140         data.append(socket.readAll());
141 
142         if (data.endsWith(constOkNlValue) || data.startsWith(constOkValue) || data.startsWith(constAckValue)) {
143             break;
144         }
145     }
146     DBUG << (void *)(&socket) << "Read:" << log(data) << ", socket state:" << socket.state();
147 
148     return data;
149 }
150 
readReply(MpdSocket & socket,int timeout=constSocketCommsTimeout)151 static MPDConnection::Response readReply(MpdSocket &socket, int timeout=constSocketCommsTimeout)
152 {
153     QByteArray data = readFromSocket(socket, timeout);
154     return MPDConnection::Response(data.endsWith(constOkNlValue), data);
155 }
156 
Response(bool o,const QByteArray & d)157 MPDConnection::Response::Response(bool o, const QByteArray &d)
158     : ok(o)
159     , data(d)
160 {
161 }
162 
getError(const QByteArray & command)163 QString MPDConnection::Response::getError(const QByteArray &command)
164 {
165     if (ok || data.isEmpty()) {
166         return QString();
167     }
168 
169     if (!ok && data.size()>0) {
170         int cmdEnd=data.indexOf("} ");
171         if (-1==cmdEnd) {
172             return tr("Unknown")+QLatin1String(" (")+command+QLatin1Char(')');
173         } else {
174             cmdEnd+=2;
175             QString rv=data.mid(cmdEnd, data.length()-(data.endsWith('\n') ? cmdEnd+1 : cmdEnd));
176             if (data.contains("{listplaylists}")) {
177                 // NOTE: NOT translated, as refers to config item
178                 return QLatin1String("playlist_directory - ")+rv;
179             }
180 
181             // If we are reporting a stream error, remove any stream name added by Cantata...
182             int start=rv.indexOf(QLatin1String("http://"));
183             if (start>0) {
184                 int pos=rv.indexOf(QChar('#'), start+6);
185                 if (-1!=pos) {
186                     rv=rv.left(pos);
187                 }
188             }
189 
190             return rv;
191         }
192     }
193     return data;
194 }
195 
MPDConnectionDetails()196 MPDConnectionDetails::MPDConnectionDetails()
197     : port(6600)
198     , dirReadable(false)
199     , applyReplayGain(true)
200     , allowLocalStreaming(true)
201     , autoUpdate(false)
202 {
203 }
204 
getName() const205 QString MPDConnectionDetails::getName() const
206 {
207     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
208     return name.isEmpty() ? QObject::tr("Default") : (name==MPDUser::constName ? MPDUser::translatedName() : name);
209     #else
210     return name.isEmpty() ? QObject::tr("Default") : name;
211     #endif
212 }
213 
description() const214 QString MPDConnectionDetails::description() const
215 {
216     if (hostname.isEmpty()) {
217         return getName();
218     } else if (hostname.startsWith('/') || hostname.startsWith('~')) {
219         return getName();
220     } else {
221         return QObject::tr("\"%1\" (%2:%3)", "name (host:port)").arg(getName()).arg(hostname).arg(QString::number(port));
222     }
223 }
224 
operator =(const MPDConnectionDetails & o)225 MPDConnectionDetails & MPDConnectionDetails::operator=(const MPDConnectionDetails &o)
226 {
227     name=o.name;
228     hostname=o.hostname;
229     port=o.port;
230     password=o.password;
231     dir=o.dir;
232     dirReadable=o.dirReadable;
233     #ifdef ENABLE_HTTP_STREAM_PLAYBACK
234     streamUrl=o.streamUrl;
235     #endif
236     replayGain=o.replayGain;
237     applyReplayGain=o.applyReplayGain;
238     allowLocalStreaming=o.allowLocalStreaming;
239     autoUpdate=o.autoUpdate;
240     return *this;
241 }
242 
setDirReadable()243 void MPDConnectionDetails::setDirReadable()
244 {
245     dirReadable=Utils::isDirReadable(dir);
246 }
247 
MPDConnection()248 MPDConnection::MPDConnection()
249     : isInitialConnect(true)
250     , thread(nullptr)
251     , ver(0)
252     , canUseStickers(false)
253     , sock(this)
254     , idleSocket(this)
255     , lastStatusPlayQueueVersion(0)
256     , lastUpdatePlayQueueVersion(0)
257     , state(State_Blank)
258     , isListingMusic(false)
259     , reconnectTimer(nullptr)
260     , reconnectStart(0)
261     , stopAfterCurrent(false)
262     , currentSongId(-1)
263     , songPos(0)
264     , unmuteVol(-1)
265     , isUpdatingDb(false)
266     , volumeFade(nullptr)
267     , fadeDuration(0)
268     , restoreVolume(-1)
269 {
270     qRegisterMetaType<time_t>("time_t");
271     qRegisterMetaType<Song>("Song");
272     qRegisterMetaType<Output>("Output");
273     qRegisterMetaType<Playlist>("Playlist");
274     qRegisterMetaType<QList<Song> >("QList<Song>");
275     qRegisterMetaType<QList<Output> >("QList<Output>");
276     qRegisterMetaType<QList<Playlist> >("QList<Playlist>");
277     qRegisterMetaType<QList<quint32> >("QList<quint32>");
278     qRegisterMetaType<QList<qint32> >("QList<qint32>");
279     qRegisterMetaType<QList<quint8> >("QList<quint8>");
280     qRegisterMetaType<QSet<qint32> >("QSet<qint32>");
281     qRegisterMetaType<QSet<QString> >("QSet<QString>");
282     qRegisterMetaType<QAbstractSocket::SocketState >("QAbstractSocket::SocketState");
283     qRegisterMetaType<MPDStatsValues>("MPDStatsValues");
284     qRegisterMetaType<MPDStatusValues>("MPDStatusValues");
285     qRegisterMetaType<MPDConnectionDetails>("MPDConnectionDetails");
286     qRegisterMetaType<QMap<qint32, quint8> >("QMap<qint32, quint8>");
287     qRegisterMetaType<Stream>("Stream");
288     qRegisterMetaType<QList<Stream> >("QList<Stream>");
289     #if (defined Q_OS_LINUX && defined QT_QTDBUS_FOUND) || (defined Q_OS_MAC && defined IOKIT_FOUND)
290     connect(PowerManagement::self(), SIGNAL(resuming()), this, SLOT(reconnect()));
291     #endif
292     MPDParseUtils::setSingleTracksFolders(Configuration().get("singleTracksFolders", QStringList()).toSet());
293 }
294 
~MPDConnection()295 MPDConnection::~MPDConnection()
296 {
297     if (State_Connected==state && fadingVolume()) {
298         sendCommand("stop");
299         stopVolumeFade();
300     }
301 //     disconnect(&sock, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
302     disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
303     disconnect(&idleSocket, SIGNAL(readyRead()), this, SLOT(idleDataReady()));
304     sock.disconnectFromHost();
305     idleSocket.disconnectFromHost();
306 }
307 
start()308 void MPDConnection::start()
309 {
310     if (!thread) {
311         thread=new Thread(metaObject()->className());
312         connTimer=thread->createTimer(this);
313         connTimer->setSingleShot(false);
314         moveToThread(thread);
315         connect(thread, SIGNAL(finished()), connTimer, SLOT(stop()));
316         connect(connTimer, SIGNAL(timeout()), SLOT(getStatus()));
317         thread->start();
318     }
319 }
320 
stop()321 void MPDConnection::stop()
322 {
323     stopPlaying();
324     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
325     if (details.name==MPDUser::constName && Settings::self()->stopOnExit()) {
326         MPDUser::self()->stop();
327     }
328     #endif
329 
330     if (thread) {
331         thread->deleteTimer(connTimer);
332         connTimer=nullptr;
333         thread->stop();
334         thread=nullptr;
335     }
336 }
337 
localFilePlaybackSupported() const338 bool MPDConnection::localFilePlaybackSupported() const
339 {
340     return details.isLocal() ||
341            (ver>=CANTATA_MAKE_VERSION(0, 19, 0) && /*handlers.contains(QLatin1String("file")) &&*/
342            (QLatin1String("127.0.0.1")==details.hostname || QLatin1String("localhost")==details.hostname));
343 }
344 
connectToMPD(MpdSocket & socket,bool enableIdle)345 MPDConnection::ConnectionReturn MPDConnection::connectToMPD(MpdSocket &socket, bool enableIdle)
346 {
347     if (QAbstractSocket::ConnectedState!=socket.state()) {
348         DBUG << (void *)(&socket) << "Connecting" << (enableIdle ? "(idle)" : "(std)");
349         if (details.isEmpty()) {
350             DBUG << "no hostname and/or port supplied.";
351             return Failed;
352         }
353 
354         socket.connectToHost(details.hostname, details.port);
355         if (socket.waitForConnected(constSocketCommsTimeout)) {
356             DBUG << (void *)(&socket) << "established";
357             QByteArray recvdata = readFromSocket(socket);
358 
359             if (recvdata.isEmpty()) {
360                 DBUG << (void *)(&socket) << "Couldn't connect";
361                 return Failed;
362             }
363 
364             if (recvdata.startsWith(constOkMpdValue)) {
365                 DBUG << (void *)(&socket) << "Received identification string";
366             }
367 
368             lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=0;
369             playQueueIds.clear();
370             emit cantataStreams(QList<Song>(), false);
371             int min, maj, patch;
372             if (3==sscanf(&(recvdata.constData()[7]), "%3d.%3d.%3d", &maj, &min, &patch)) {
373                 long v=((maj&0xFF)<<16)+((min&0xFF)<<8)+(patch&0xFF);
374                 if (v!=ver) {
375                     ver=v;
376                 }
377             }
378 
379             recvdata.clear();
380 
381             if (!details.password.isEmpty()) {
382                 DBUG << (void *)(&socket) << "setting password...";
383                 socket.write("password "+details.password.toUtf8()+'\n');
384                 socket.waitForBytesWritten(constSocketCommsTimeout);
385                 if (!readReply(socket).ok) {
386                     DBUG << (void *)(&socket) << "password rejected";
387                     socket.close();
388                     return IncorrectPassword;
389                 }
390             }
391 
392             if (enableIdle) {
393                 dynamicId.clear();
394                 setupRemoteDynamic();
395                 connect(&socket, SIGNAL(readyRead()), this, SLOT(idleDataReady()), Qt::QueuedConnection);
396                 connect(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)), Qt::QueuedConnection);
397                 DBUG << (void *)(&socket) << "Enabling idle";
398                 socket.write("idle\n");
399                 socket.waitForBytesWritten();
400             }
401             return Success;
402         } else {
403             DBUG << (void *)(&socket) << "Couldn't connect - " << socket.errorString() << socket.error();
404             return convertSocketCode(socket);
405         }
406     }
407 
408 //     DBUG << "Already connected" << (enableIdle ? "(idle)" : "(std)");
409     return Success;
410 }
411 
convertSocketCode(MpdSocket & socket)412 MPDConnection::ConnectionReturn MPDConnection::convertSocketCode(MpdSocket &socket)
413 {
414     switch (socket.error()) {
415     case QAbstractSocket::ProxyAuthenticationRequiredError:
416     case QAbstractSocket::ProxyConnectionRefusedError:
417     case QAbstractSocket::ProxyConnectionClosedError:
418     case QAbstractSocket::ProxyConnectionTimeoutError:
419     case QAbstractSocket::ProxyNotFoundError:
420     case QAbstractSocket::ProxyProtocolError:
421         return MPDConnection::ProxyError;
422     default:
423         if (QNetworkProxy::DefaultProxy!=socket.proxyType() && QNetworkProxy::NoProxy!=socket.proxyType()) {
424             return MPDConnection::ProxyError;
425         }
426         if (socket.errorString().contains(QLatin1String("proxy"), Qt::CaseInsensitive)) {
427             return MPDConnection::ProxyError;
428         }
429         return MPDConnection::Failed;
430     }
431 }
432 
errorString(ConnectionReturn status) const433 QString MPDConnection::errorString(ConnectionReturn status) const
434 {
435     switch (status) {
436     default:
437     case Success:           return QString();
438     case Failed:            return tr("Connection to %1 failed").arg(details.description());
439     case ProxyError:        return tr("Connection to %1 failed - please check your proxy settings").arg(details.description());
440     case IncorrectPassword: return tr("Connection to %1 failed - incorrect password").arg(details.description());
441     }
442 }
443 
connectToMPD()444 MPDConnection::ConnectionReturn MPDConnection::connectToMPD()
445 {
446     connTimer->stop();
447     if (State_Connected==state && (QAbstractSocket::ConnectedState!=sock.state() || QAbstractSocket::ConnectedState!=idleSocket.state())) {
448         DBUG << "Something has gone wrong with sockets, so disconnect";
449         disconnectFromMPD();
450     }
451     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
452     int maxConnAttempts=MPDUser::constName==details.name ? 2 : 1;
453     #else
454     int maxConnAttempts=1;
455     #endif
456     ConnectionReturn status=Failed;
457     for (int connAttempt=0; connAttempt<maxConnAttempts; ++connAttempt) {
458         if (Success==(status=connectToMPD(sock)) && Success==(status=connectToMPD(idleSocket, true))) {
459             state=State_Connected;
460             emit socketAddress(sock.address());
461         } else {
462             disconnectFromMPD();
463             state=State_Disconnected;
464             #ifdef ENABLE_SIMPLE_MPD_SUPPORT
465             if (0==connAttempt && MPDUser::constName==details.name) {
466                 DBUG << "Restart personal mpd";
467                 MPDUser::self()->stop();
468                 MPDUser::self()->start();
469             }
470             #endif
471         }
472     }
473     connTimer->start(constConnTimer);
474     return status;
475 }
476 
disconnectFromMPD()477 void MPDConnection::disconnectFromMPD()
478 {
479     DBUG << "disconnectFromMPD";
480     connTimer->stop();
481     disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
482     disconnect(&idleSocket, SIGNAL(readyRead()), this, SLOT(idleDataReady()));
483     if (QAbstractSocket::ConnectedState==sock.state()) {
484         sock.disconnectFromHost();
485     }
486     if (QAbstractSocket::ConnectedState==idleSocket.state()) {
487         idleSocket.disconnectFromHost();
488     }
489     sock.close();
490     idleSocket.close();
491     state=State_Disconnected;
492     ver=0;
493     playQueueIds.clear();
494     streamIds.clear();
495     lastStatusPlayQueueVersion=0;
496     lastUpdatePlayQueueVersion=0;
497     currentSongId=0;
498     songPos=0;
499     serverInfo.reset();
500     isUpdatingDb=false;
501     emit socketAddress(QString());
502 }
503 
504 // This function is mainly intended to be called after the computer (laptop) has been 'resumed'
reconnect()505 void MPDConnection::reconnect()
506 {
507     if (reconnectTimer && reconnectTimer->isActive()) {
508         return;
509     }
510     if (0==reconnectStart && isConnected()) {
511         disconnectFromMPD();
512     }
513 
514     if (isConnected()) { // Perhaps the user pressed a button which caused the reconnect???
515         reconnectStart=0;
516         return;
517     }
518     time_t now=time(nullptr);
519     ConnectionReturn status=connectToMPD();
520     switch (status) {
521     case Success:
522         // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so use the values read from Cantata's config.
523         if (replaygainSupported() && details.applyReplayGain && !details.replayGain.isEmpty()) {
524             sendCommand("replay_gain_mode "+details.replayGain.toLatin1());
525         }
526         getStatus();
527         getStats();
528         getUrlHandlers();
529         getTagTypes();
530         getStickerSupport();
531         playListInfo();
532         outputs();
533         reconnectStart=0;
534         determineIfaceIp();
535         emit stateChanged(true);
536         break;
537     case Failed:
538         if (0==reconnectStart || std::abs(now-reconnectStart)<15) {
539             if (0==reconnectStart) {
540                 reconnectStart=now;
541             }
542             if (!reconnectTimer) {
543                 reconnectTimer=new QTimer(this);
544                 reconnectTimer->setSingleShot(true);
545                 connect(reconnectTimer, SIGNAL(timeout()), this, SLOT(reconnect()), Qt::QueuedConnection);
546             }
547             if (std::abs(now-reconnectStart)>1) {
548                 emit info(tr("Connecting to %1").arg(details.description()));
549             }
550             reconnectTimer->start(500);
551         } else {
552             emit stateChanged(false);
553             emit error(errorString(Failed), true);
554             reconnectStart=0;
555         }
556         break;
557     default:
558         emit stateChanged(false);
559         emit error(errorString(status), true);
560         reconnectStart=0;
561         break;
562     }
563 }
564 
setDetails(const MPDConnectionDetails & d)565 void MPDConnection::setDetails(const MPDConnectionDetails &d)
566 {
567     // Can't change connection whilst listing music collection...
568     if (isListingMusic) {
569         emit connectionNotChanged(details.name);
570         return;
571     }
572 
573     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
574     bool isUser=d.name==MPDUser::constName;
575     const MPDConnectionDetails &det=isUser ? MPDUser::self()->details(true) : d;
576     #else
577     const MPDConnectionDetails &det=d;
578     #endif
579     bool changedDir=det.dir!=details.dir;
580     bool diffName=det.name!=details.name;
581     bool diffDetails=det!=details;
582     #ifdef ENABLE_HTTP_STREAM_PLAYBACK
583     bool diffStreamUrl=det.streamUrl!=details.streamUrl;
584     #endif
585 
586     details=det;
587     // Issue #1041 - If this is a user MPD, then the call to MPDUser::self()->details() will clear the replayGain setting
588     // We can safely use that of the passed in details.
589     details.replayGain=d.replayGain;
590 
591     if (diffDetails || State_Connected!=state) {
592         emit connectionChanged(details);
593         DBUG << "setDetails" << details.hostname << details.port << (details.password.isEmpty() ? false : true);
594         if (State_Connected==state && fadingVolume()) {
595             sendCommand("stop");
596             stopVolumeFade();
597         }
598         disconnectFromMPD();
599         DBUG << "call connectToMPD";
600         unmuteVol=-1;
601         toggleStopAfterCurrent(false);
602         #ifdef ENABLE_SIMPLE_MPD_SUPPORT
603         if (isUser) {
604             MPDUser::self()->start();
605         }
606         #endif
607         if (isUpdatingDb) {
608             isUpdatingDb=false;
609             emit updatedDatabase(); // Stop any spinners...
610         }
611         ConnectionReturn status=connectToMPD();
612         switch (status) {
613         case Success:
614             // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so use the values read from Cantata's config.
615             if (replaygainSupported() && details.applyReplayGain && !details.replayGain.isEmpty()) {
616                 sendCommand("replay_gain_mode "+details.replayGain.toLatin1());
617             }
618             serverInfo.detect();
619             getStatus();
620             getStats();
621             getUrlHandlers();
622             getTagTypes();
623             getStickerSupport();
624             playListInfo();
625             outputs();
626             determineIfaceIp();
627             emit stateChanged(true);
628             break;
629         default:
630             emit stateChanged(false);
631             emit error(errorString(status), true);
632             if (isInitialConnect) {
633                 reconnect();
634             }
635         }
636     } else if (diffName) {
637          emit stateChanged(true);
638     }
639     #ifdef ENABLE_HTTP_STREAM_PLAYBACK
640     if (diffStreamUrl) {
641         emit streamUrl(details.streamUrl);
642     }
643     #endif
644     if (changedDir) {
645         emit dirChanged();
646     }
647     isInitialConnect = false;
648 }
649 
650 //void MPDConnection::disconnectMpd()
651 //{
652 //    if (State_Connected==state) {
653 //        disconnectFromMPD();
654 //        emit stateChanged(false);
655 //    }
656 //}
657 
sendCommand(const QByteArray & command,bool emitErrors,bool retry)658 MPDConnection::Response MPDConnection::sendCommand(const QByteArray &command, bool emitErrors, bool retry)
659 {
660     connTimer->stop();
661     static bool reconnected=false; // If we reconnect, and send playlistinfo - dont want that call causing reconnects, and recursion!
662     DBUG << (void *)(&sock) << "sendCommand:" << log(command) << emitErrors << retry;
663 
664     if (!isConnected()) {
665         if ("stop"!=command) {
666             emit error(tr("Failed to send command to %1 - not connected").arg(details.description()), true);
667         }
668         return Response(false);
669     }
670 
671     if (QAbstractSocket::ConnectedState!=sock.state() && !reconnected) {
672         DBUG << (void *)(&sock) << "Socket (state:" << sock.state() << ") need to reconnect";
673         sock.close();
674         ConnectionReturn status=connectToMPD();
675         if (Success!=status) {
676             disconnectFromMPD();
677             emit stateChanged(false);
678             emit error(errorString(status), true);
679             return Response(false);
680         } else {
681             // Refresh playqueue...
682             reconnected=true;
683             playListInfo();
684             getStatus();
685             reconnected=false;
686         }
687     }
688 
689     Response response;
690     if (-1==sock.write(command+'\n')) {
691         DBUG << "Failed to write";
692         // If we fail to write, dont wait for bytes to be written!!
693         response=Response(false);
694         sock.close();
695     } else {
696         int timeout=socketTimeout(command.length());
697         DBUG << "Timeout (ms):" << timeout;
698         sock.waitForBytesWritten(timeout);
699         DBUG << "Socket state after write:" << (int)sock.state();
700         response=readReply(sock, timeout);
701     }
702 
703     if (!response.ok) {
704         DBUG << log(command) << "failed";
705         if (response.data.isEmpty() && retry && QAbstractSocket::ConnectedState!=sock.state() && !reconnected) {
706             // Try one more time...
707             // This scenario, where socket seems to be closed during/after 'write' seems to occur more often
708             // when dynamizer is running. However, simply reconnecting seems to resolve the issue.
709             return sendCommand(command, emitErrors, false);
710         }
711         clearError();
712         if (emitErrors) {
713             bool emitError=true;
714             // Mopidy returns "incorrect arguments" for commands it does not support. The docs state that crossfade and replaygain mode
715             // setting commands are not supported. So, if we get this error then just ignore it.
716             if (!isMpd() && (command.startsWith("crossfade ") || command.startsWith("replay_gain_mode "))) {
717                 emitError=false;
718             } else if (isMpd() && command.startsWith("albumart ")) {
719                 // MPD will report a generic "file not found" error if it can't find album art; this can happen
720                 // several times in a large playlist so hide this from the GUI (but report it using DBUG here).
721                 emitError=false;
722                 const auto start = command.indexOf(' ');
723                 const auto end = command.lastIndexOf(' ') - start;
724                 if (start > 0 && (end > 0 && (start + end) < command.length())) {
725                     const QString filename = command.mid(start, end);
726                     DBUG << "MPD reported no album art for" << filename;
727                 } else {
728                     DBUG << "MPD albumart command was malformed:" << command;
729                 }
730             }
731             if (emitError) {
732                 if ((command.startsWith("add ") || command.startsWith("command_list_begin\nadd ")) && -1!=command.indexOf("\"file:///")) {
733                     if (details.isLocal() && -1!=response.data.indexOf("Permission denied")) {
734                         emit error(tr("Failed to load. Please check user \"mpd\" has read permission."));
735                     } else if (!details.isLocal() && -1!=response.data.indexOf("Access denied")) {
736                         emit error(tr("Failed to load. MPD can only play local files if connected via a local socket."));
737                     } else if (!response.getError(command).isEmpty()) {
738                         emit error(tr("MPD reported the following error: %1").arg(response.getError(command)));
739                     } else {
740                         disconnectFromMPD();
741                         emit stateChanged(false);
742                         emit error(tr("Failed to send command. Disconnected from %1").arg(details.description()), true);
743                     }
744                 } else if (!response.getError(command).isEmpty()) {
745                     emit error(tr("MPD reported the following error: %1").arg(response.getError(command)));
746                 } /*else if ("listallinfo"==command && ver>=CANTATA_MAKE_VERSION(0,18,0)) {
747                     disconnectFromMPD();
748                     emit stateChanged(false);
749                     emit error(tr("Failed to load library. Please increase \"max_output_buffer_size\" in MPD's config file."));
750                 } */ else {
751                     disconnectFromMPD();
752                     emit stateChanged(false);
753                     emit error(tr("Failed to send command. Disconnected from %1").arg(details.description()), true);
754                 }
755             }
756         }
757     }
758     DBUG << (void *)(&sock) << "sendCommand - sent";
759     if (QAbstractSocket::ConnectedState==sock.state()) {
760         connTimer->start(constConnTimer);
761     } else {
762         connTimer->stop();
763     }
764     return response;
765 }
766 
767 /*
768  * Playlist commands
769  */
isPlaylist(const QString & file)770 bool MPDConnection::isPlaylist(const QString &file)
771 {
772     static QSet<QString> extensions = QSet<QString>() << QLatin1String("asx") << QLatin1String("cue")
773                                                       << QLatin1String("m3u") << QLatin1String("m3u8")
774                                                       << QLatin1String("pls") << QLatin1String("xspf");
775 
776     int pos=file.lastIndexOf('.');
777     return pos>0 ? extensions.contains(file.mid(pos+1).toLower()) : false;
778 }
779 
add(const QStringList & files,int action,quint8 priority,bool decreasePriority)780 void MPDConnection::add(const QStringList &files, int action, quint8 priority, bool decreasePriority)
781 {
782     add(files, 0, 0, action, priority, decreasePriority);
783 }
784 
add(const QStringList & files,quint32 pos,quint32 size,int action,quint8 priority,bool decreasePriority)785 void MPDConnection::add(const QStringList &files, quint32 pos, quint32 size, int action, quint8 priority, bool decreasePriority)
786 {
787     QList<quint8> prioList;
788     if (priority>0) {
789         prioList << priority;
790     }
791     add(files, pos, size, action, prioList, decreasePriority);
792 }
793 
add(const QStringList & origList,quint32 pos,quint32 size,int action,const QList<quint8> & priority)794 void MPDConnection::add(const QStringList &origList, quint32 pos, quint32 size, int action, const QList<quint8> &priority)
795 {
796     add(origList, pos, size, action, priority, false);
797 }
798 
add(const QStringList & origList,quint32 pos,quint32 size,int action,QList<quint8> priority,bool decreasePriority)799 void MPDConnection::add(const QStringList &origList, quint32 pos, quint32 size, int action, QList<quint8> priority, bool decreasePriority)
800 {
801     quint32 playPos=0;
802     if (0==pos && 0==size && (AddAfterCurrent==action || AppendAndPlay==action || AddAndPlay==action)) {
803         Response response=sendCommand("status");
804         if (response.ok) {
805             MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
806             if (AppendAndPlay==action) {
807                 playPos=sv.playlistLength;
808             } else if (AddAndPlay==action) {
809                 pos=0;
810                 size=sv.playlistLength;
811             } else {
812                 pos=sv.song+1;
813                 size=sv.playlistLength;
814             }
815         }
816     }
817     toggleStopAfterCurrent(false);
818     if (Replace==action || ReplaceAndplay==action) {
819         clear();
820         getStatus();
821     }
822 
823     QStringList files;
824     for (const QString &file: origList) {
825         if (file.startsWith(constDirPrefix)) {
826             files+=getAllFiles(file.mid(constDirPrefix.length()));
827         } else if (file.startsWith(constPlaylistPrefix)) {
828             files+=getPlaylistFiles(file.mid(constPlaylistPrefix.length()));
829         } else {
830             files.append(file);
831         }
832     }
833 
834     QList<QStringList> fileLists;
835     if (priority.count()<=1 && files.count()>constMaxFilesPerAddCommand) {
836         int numChunks=(files.count()/constMaxFilesPerAddCommand)+(files.count()%constMaxFilesPerAddCommand ? 1 : 0);
837         for (int i=0; i<numChunks; ++i) {
838             fileLists.append(files.mid(i*constMaxFilesPerAddCommand, constMaxFilesPerAddCommand));
839         }
840     } else {
841         fileLists.append(files);
842     }
843 
844     int curSize = size;
845     int curPos = pos;
846     //    bool addedFile=false;
847     bool havePlaylist=false;
848 
849     if (1==priority.count() && decreasePriority) {
850         quint8 prio=priority.at(0);
851         priority.clear();
852         for (int i=0; i<files.count(); ++i) {
853             priority.append(prio);
854             if (prio>1) {
855                 prio--;
856             }
857         }
858     }
859 
860     bool usePrio=!priority.isEmpty() && canUsePriority() && (1==priority.count() || priority.count()==files.count());
861     quint8 singlePrio=usePrio && 1==priority.count() ? priority.at(0) : 0;
862     QStringList cStreamFiles;
863     bool sentOk=false;
864 
865     if (usePrio && Append==action && 0==curPos) {
866         curPos=playQueueIds.size();
867     }
868 
869     for (const QStringList &files: fileLists) {
870         QByteArray send = "command_list_begin\n";
871 
872         for (int i = 0; i < files.size(); i++) {
873             QString fileName=files.at(i);
874             if (fileName.startsWith(QLatin1String("http://")) && fileName.contains(QLatin1String("cantata=song"))) {
875                 cStreamFiles.append(fileName);
876             }
877             if (CueFile::isCue(fileName)) {
878                 send += "load "+CueFile::getLoadLine(fileName)+'\n';
879             } else {
880                 if (isPlaylist(fileName)) {
881                     send+="load ";
882                     havePlaylist=true;
883                 } else {
884                     //                addedFile=true;
885                     send += "add ";
886                 }
887                 send += encodeName(fileName)+'\n';
888             }
889             if (!havePlaylist) {
890                 if (0!=size) {
891                     send += "move "+quote(curSize)+" "+quote(curPos)+'\n';
892                 }
893                 if (usePrio && !havePlaylist) {
894                     send += "prio "+quote(singlePrio || i>=priority.count() ? singlePrio : priority.at(i))+" "+quote(curPos)+" "+quote(curPos)+'\n';
895                 }
896             }
897             curSize++;
898             curPos++;
899         }
900 
901         send += "command_list_end";
902         sentOk=sendCommand(send).ok;
903         if (!sentOk) {
904             break;
905         }
906     }
907 
908     if (sentOk) {
909         if (!cStreamFiles.isEmpty()) {
910             emit cantataStreams(cStreamFiles);
911         }
912 
913         if ((ReplaceAndplay==action || AddAndPlay==action) /*&& addedFile */&& !files.isEmpty()) {
914             // Dont emit error if play fails, might be that playlist was not loaded...
915             playFirstTrack(false);
916         }
917 
918         if (AppendAndPlay==action) {
919             startPlayingSong(playPos);
920         }
921         emit added(files);
922     }
923 }
924 
populate(const QStringList & files,const QList<quint8> & priority)925 void MPDConnection::populate(const QStringList &files, const QList<quint8> &priority)
926 {
927     add(files, 0, 0, Replace, priority);
928 }
929 
addAndPlay(const QString & file)930 void MPDConnection::addAndPlay(const QString &file)
931 {
932     toggleStopAfterCurrent(false);
933     Response response=sendCommand("status");
934     if (response.ok) {
935         MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
936         QByteArray send = "command_list_begin\n";
937         if (CueFile::isCue(file)) {
938             send += "load "+CueFile::getLoadLine(file)+'\n';
939         } else {
940             send+="add "+encodeName(file)+'\n';
941         }
942         send+="play "+quote(sv.playlistLength)+'\n';
943         send+="command_list_end";
944         sendCommand(send);
945     }
946 }
947 
clear()948 void MPDConnection::clear()
949 {
950     toggleStopAfterCurrent(false);
951     if (sendCommand("clear").ok) {
952         lastUpdatePlayQueueVersion=0;
953         playQueueIds.clear();
954         emit cantataStreams(QList<Song>(), false);
955     }
956 }
957 
removeSongs(const QList<qint32> & items)958 void MPDConnection::removeSongs(const QList<qint32> &items)
959 {
960     toggleStopAfterCurrent(false);
961     QByteArray send = "command_list_begin\n";
962     for (qint32 i: items) {
963         send += "deleteid "+quote(i)+'\n';
964     }
965 
966     send += "command_list_end";
967     sendCommand(send);
968 }
969 
move(quint32 from,quint32 to)970 void MPDConnection::move(quint32 from, quint32 to)
971 {
972     toggleStopAfterCurrent(false);
973     sendCommand("move "+quote(from)+' '+quote(to));
974 }
975 
move(const QList<quint32> & items,quint32 pos,quint32 size)976 void MPDConnection::move(const QList<quint32> &items, quint32 pos, quint32 size)
977 {
978     doMoveInPlaylist(QString(), items, pos, size);
979     #if 0
980     QByteArray send = "command_list_begin\n";
981     QList<quint32> moveItems;
982 
983     moveItems.append(items);
984     std::sort(moveItems.begin(), moveItems.end());
985 
986     int posOffset = 0;
987 
988     //first move all items (starting with the biggest) to the end so we don't have to deal with changing rownums
989     for (int i = moveItems.size() - 1; i >= 0; i--) {
990         if (moveItems.at(i) < pos && moveItems.at(i) != size - 1) {
991             // we are moving away an item that resides before the destinatino row, manipulate destination row
992             posOffset++;
993         }
994         send += "move ";
995         send += quote(moveItems.at(i));
996         send += " ";
997         send += quote(size - 1);
998         send += '\n';
999     }
1000     //now move all of them to the destination position
1001     for (int i = moveItems.size() - 1; i >= 0; i--) {
1002         send += "move ";
1003         send += quote(size - 1 - i);
1004         send += " ";
1005         send += quote(pos - posOffset);
1006         send += '\n';
1007     }
1008 
1009     send += "command_list_end";
1010     sendCommand(send);
1011     #endif
1012 }
1013 
setOrder(const QList<quint32> & items)1014 void MPDConnection::setOrder(const QList<quint32> &items)
1015 {
1016     QByteArray cmd("move ");
1017     QByteArray send;
1018     QList<qint32> positions;
1019     quint32 numChanges=0;
1020     for (qint32 i=0; i<items.count(); ++i) {
1021         positions.append(i);
1022     }
1023 
1024     for (qint32 to=0; to<items.count(); ++to) {
1025         qint32 from=positions.indexOf(items.at(to));
1026         if (from!=to) {
1027             if (send.isEmpty()) {
1028                 send = "command_list_begin\n";
1029             }
1030             send += cmd;
1031             send += quote(from);
1032             send += " ";
1033             send += quote(to);
1034             send += '\n';
1035             positions.move(from, to);
1036             numChanges++;
1037         }
1038     }
1039 
1040     if (!send.isEmpty()) {
1041         send += "command_list_end";
1042         // If there are more than X changes made to the playqueue, then doing partial updates
1043         // can hang the UI. Therefore, set lastUpdatePlayQueueVersion to 0 to cause playlistInfo
1044         // to be used to do a complete update.
1045         if (sendCommand(send).ok && numChanges>constMaxPqChanges) {
1046             lastUpdatePlayQueueVersion=0;
1047         }
1048     }
1049 }
1050 
shuffle()1051 void MPDConnection::shuffle()
1052 {
1053     toggleStopAfterCurrent(false);
1054     sendCommand("shuffle");
1055 }
1056 
shuffle(quint32 from,quint32 to)1057 void MPDConnection::shuffle(quint32 from, quint32 to)
1058 {
1059     toggleStopAfterCurrent(false);
1060     sendCommand("shuffle "+quote(from)+':'+quote(to+1));
1061 }
1062 
currentSong()1063 void MPDConnection::currentSong()
1064 {
1065     Response response=sendCommand("currentsong");
1066     if (response.ok) {
1067         emit currentSongUpdated(MPDParseUtils::parseSong(response.data, MPDParseUtils::Loc_PlayQueue));
1068     }
1069 }
1070 
1071 /*
1072  * Call "plchangesposid" to recieve a list of positions+ids that have been changed since the last update.
1073  * If we have ids in this list that we don't know about, then these are new songs - so we call
1074  * "playlistinfo <pos>" to get the song information.
1075  *
1076  * Any songs that are know about, will actually be sent with empty data - as the playqueue model will
1077  * already hold these songs.
1078  */
playListChanges()1079 void MPDConnection::playListChanges()
1080 {
1081     DBUG << "playListChanges" << lastUpdatePlayQueueVersion << playQueueIds.size();
1082     if (0==lastUpdatePlayQueueVersion || 0==playQueueIds.size()) {
1083         playListInfo();
1084         return;
1085     }
1086 
1087     QByteArray data = "plchangesposid "+quote(lastUpdatePlayQueueVersion);
1088     Response status=sendCommand("status"); // We need an updated status so as to detect deletes at end of list...
1089     Response response=sendCommand(data, false);
1090     if (response.ok && status.ok && isPlayQueueIdValid()) {
1091         MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
1092         lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist;
1093         emitStatusUpdated(sv);
1094         QList<MPDParseUtils::IdPos> changes=MPDParseUtils::parseChanges(response.data);
1095         if (!changes.isEmpty()) {
1096             if (changes.count()>constMaxPqChanges) {
1097                 playListInfo();
1098                 return;
1099             }
1100             bool first=true;
1101             quint32 firstPos=0;
1102             QList<Song> songs;
1103             QList<Song> newCantataStreams;
1104             QList<qint32> ids;
1105             QSet<qint32> prevIds=playQueueIds.toSet();
1106             QSet<qint32> strmIds;
1107 
1108             for (const MPDParseUtils::IdPos &idp: changes) {
1109                 if (first) {
1110                     first=false;
1111                     firstPos=idp.pos;
1112                     if (idp.pos!=0) {
1113                         for (quint32 i=0; i<idp.pos; ++i) {
1114                             Song s;
1115                             if (i>=(unsigned int)playQueueIds.count()) { // Just for safety...
1116                                 playListInfo();
1117                                 return;
1118                             }
1119                             s.id=playQueueIds.at(i);
1120                             songs.append(s);
1121                             ids.append(s.id);
1122                             if (streamIds.contains(s.id)) {
1123                                 strmIds.insert(s.id);
1124                             }
1125                         }
1126                     }
1127                 }
1128 
1129                 if (prevIds.contains(idp.id) && !streamIds.contains(idp.id)) {
1130                     Song s;
1131                     s.id=idp.id;
1132 //                     s.pos=idp.pos;
1133                     songs.append(s);
1134                 } else {
1135                     // New song!
1136                     data = "playlistinfo ";
1137                     data += quote(idp.pos);
1138                     response=sendCommand(data);
1139                     if (!response.ok) {
1140                         playListInfo();
1141                         return;
1142                     }
1143                     Song s=MPDParseUtils::parseSong(response.data, MPDParseUtils::Loc_PlayQueue);
1144                     s.id=idp.id;
1145 //                     s.pos=idp.pos;
1146                     songs.append(s);
1147                     if (s.isCdda()) {
1148                         newCantataStreams.append(s);
1149                     } else if (s.isStream()) {
1150                         if (s.isCantataStream()) {
1151                             newCantataStreams.append(s);
1152                         } else {
1153                             strmIds.insert(s.id);
1154                         }
1155                     }
1156                 }
1157                 ids.append(idp.id);
1158             }
1159 
1160             // Dont think this section is ever called, but leave here to be safe!!!
1161             // For some reason if we have 10 songs in our playlist and we move pos 2 to pos 1, MPD sends all ids from pos 1 onwards
1162             if (firstPos+changes.size()<=sv.playlistLength && (sv.playlistLength<=(unsigned int)playQueueIds.length())) {
1163                 for (quint32 i=firstPos+changes.size(); i<sv.playlistLength; ++i) {
1164                     Song s;
1165                     s.id=playQueueIds.at(i);
1166                     songs.append(s);
1167                     ids.append(s.id);
1168                     if (streamIds.contains(s.id)) {
1169                         strmIds.insert(s.id);
1170                     }
1171                 }
1172             }
1173 
1174             playQueueIds=ids;
1175             streamIds=strmIds;
1176             if (!newCantataStreams.isEmpty()) {
1177                 emit cantataStreams(newCantataStreams, true);
1178             }
1179             QSet<qint32> removed=prevIds-playQueueIds.toSet();
1180             if (!removed.isEmpty()) {
1181                 emit removedIds(removed);
1182             }
1183             emit playlistUpdated(songs, false);
1184             if (songs.isEmpty()) {
1185                 stopVolumeFade();
1186             }
1187             return;
1188         }
1189     }
1190 
1191     playListInfo();
1192 }
1193 
playListInfo()1194 void MPDConnection::playListInfo()
1195 {
1196     Response response=sendCommand("playlistinfo");
1197     QList<Song> songs;
1198     if (response.ok) {
1199         lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion;
1200         songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_PlayQueue);
1201         playQueueIds.clear();
1202         streamIds.clear();
1203 
1204         QList<Song> cStreams;
1205         for (const Song &s: songs) {
1206             playQueueIds.append(s.id);
1207             if (s.isCdda()) {
1208                 cStreams.append(s);
1209             } else if (s.isStream()) {
1210                 if (s.isCantataStream()) {
1211                     cStreams.append(s);
1212                 } else {
1213                     streamIds.insert(s.id);
1214                 }
1215             }
1216         }
1217         emit cantataStreams(cStreams, false);
1218         if (songs.isEmpty()) {
1219             stopVolumeFade();
1220         }
1221         Response status=sendCommand("status");
1222         if (status.ok) {
1223             MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
1224             lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist;
1225             emitStatusUpdated(sv);
1226         }
1227     }
1228     emit playlistUpdated(songs, true);
1229 }
1230 
1231 /*
1232  * Playback commands
1233  */
setCrossFade(int secs)1234 void MPDConnection::setCrossFade(int secs)
1235 {
1236     sendCommand("crossfade "+quote(secs));
1237 }
1238 
setReplayGain(const QString & v)1239 void MPDConnection::setReplayGain(const QString &v)
1240 {
1241     if (replaygainSupported()) {
1242         // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so store in Cantata's config file.
1243         Settings::self()->saveReplayGain(details.name, v);
1244         sendCommand("replay_gain_mode "+v.toLatin1());
1245     }
1246 }
1247 
getReplayGain()1248 void MPDConnection::getReplayGain()
1249 {
1250     if (replaygainSupported()) {
1251         QStringList lines=QString(sendCommand("replay_gain_status").data).split('\n', QString::SkipEmptyParts);
1252 
1253         if (2==lines.count() && "OK"==lines[1] && lines[0].startsWith(QLatin1String("replay_gain_mode: "))) {
1254             QString mode=lines[0].mid(18);
1255             // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so store in Cantata's config file.
1256             Settings::self()->saveReplayGain(details.name, mode);
1257             emit replayGain(mode);
1258         } else {
1259             emit replayGain(QString());
1260         }
1261     }
1262 }
1263 
goToNext()1264 void MPDConnection::goToNext()
1265 {
1266     toggleStopAfterCurrent(false);
1267     Response status=sendCommand("status");
1268     if (status.ok) {
1269         MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
1270         if (MPDState_Stopped!=sv.state && -1!=sv.nextSongId) {
1271             stopVolumeFade();
1272             sendCommand("next");
1273         }
1274     }
1275 }
1276 
value(bool b)1277 static inline QByteArray value(bool b)
1278 {
1279     return MPDConnection::quote(b ? 1 : 0);
1280 }
1281 
setPause(bool toggle)1282 void MPDConnection::setPause(bool toggle)
1283 {
1284     toggleStopAfterCurrent(false);
1285     stopVolumeFade();
1286     sendCommand("pause "+value(toggle));
1287 }
1288 
play()1289 void MPDConnection::play()
1290 {
1291     playFirstTrack(true);
1292 }
1293 
playFirstTrack(bool emitErrors)1294 void MPDConnection::playFirstTrack(bool emitErrors)
1295 {
1296     toggleStopAfterCurrent(false);
1297     stopVolumeFade();
1298     sendCommand("play 0", emitErrors);
1299 }
1300 
seek(qint32 offset)1301 void MPDConnection::seek(qint32 offset)
1302 {
1303     if (0==offset) {
1304         QObject *s=sender();
1305         offset=s ? s->property("offset").toInt() : 0;
1306         if (0==offset) {
1307             return;
1308         }
1309     }
1310     toggleStopAfterCurrent(false);
1311     Response response=sendCommand("status");
1312     if (response.ok) {
1313         MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
1314         if (-1==sv.songId) {
1315             return;
1316         }
1317         if (offset>0) {
1318             if (sv.timeElapsed+offset<sv.timeTotal) {
1319                 setSeek(sv.song, sv.timeElapsed+offset);
1320             } else {
1321                 goToNext();
1322             }
1323         } else {
1324             if (sv.timeElapsed+offset>=0) {
1325                 setSeek(sv.song, sv.timeElapsed+offset);
1326             } else {
1327                 // Not sure about this!!!
1328                 /*goToPrevious();*/
1329                 setSeek(sv.song, 0);
1330             }
1331         }
1332     }
1333 }
1334 
startPlayingSong(quint32 song)1335 void MPDConnection::startPlayingSong(quint32 song)
1336 {
1337     toggleStopAfterCurrent(false);
1338     sendCommand("play "+quote(song));
1339 }
1340 
startPlayingSongId(qint32 songId)1341 void MPDConnection::startPlayingSongId(qint32 songId)
1342 {
1343     toggleStopAfterCurrent(false);
1344     stopVolumeFade();
1345     sendCommand("playid "+quote(songId));
1346 }
1347 
goToPrevious()1348 void MPDConnection::goToPrevious()
1349 {
1350     toggleStopAfterCurrent(false);
1351     stopVolumeFade();
1352     Response status=sendCommand("status");
1353     if (status.ok) {
1354         MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
1355         if (sv.timeElapsed>4) {
1356             setSeekId(sv.songId, 0);
1357             return;
1358         }
1359     }
1360     sendCommand("previous");
1361 }
1362 
setConsume(bool toggle)1363 void MPDConnection::setConsume(bool toggle)
1364 {
1365     sendCommand("consume "+value(toggle));
1366 }
1367 
setRandom(bool toggle)1368 void MPDConnection::setRandom(bool toggle)
1369 {
1370     sendCommand("random "+value(toggle));
1371 }
1372 
setRepeat(bool toggle)1373 void MPDConnection::setRepeat(bool toggle)
1374 {
1375     sendCommand("repeat "+value(toggle));
1376 }
1377 
setSingle(bool toggle)1378 void MPDConnection::setSingle(bool toggle)
1379 {
1380     sendCommand("single "+value(toggle));
1381 }
1382 
setSeek(quint32 song,quint32 time)1383 void MPDConnection::setSeek(quint32 song, quint32 time)
1384 {
1385     sendCommand("seek "+quote(song)+' '+quote(time));
1386 }
1387 
setSeekId(qint32 songId,quint32 time)1388 void MPDConnection::setSeekId(qint32 songId, quint32 time)
1389 {
1390     if (-1==songId) {
1391         songId=currentSongId;
1392     }
1393     if (-1==songId) {
1394         return;
1395     }
1396     if (songId!=currentSongId || 0==time) {
1397         toggleStopAfterCurrent(false);
1398     }
1399     if (sendCommand("seekid "+quote(songId)+' '+quote(time)).ok) {
1400         if (stopAfterCurrent && songId==currentSongId && songPos>time) {
1401             songPos=time;
1402         }
1403     }
1404 }
1405 
setVolume(int vol)1406 void MPDConnection::setVolume(int vol) //Range accepted by MPD: 0-100
1407 {
1408     if (-1==vol) {
1409         if (restoreVolume>=0) {
1410             sendCommand("setvol "+quote(restoreVolume), false);
1411         }
1412         if (volumeFade) {
1413             sendCommand("stop");
1414         }
1415         restoreVolume=-1;
1416     } else if (vol>=0) {
1417         unmuteVol=-1;
1418         sendCommand("setvol "+quote(vol), false);
1419     }
1420 }
1421 
toggleMute()1422 void MPDConnection::toggleMute()
1423 {
1424     if (unmuteVol>0) {
1425         sendCommand("setvol "+quote(unmuteVol), false);
1426         unmuteVol=-1;
1427     } else {
1428         Response status=sendCommand("status");
1429         if (status.ok) {
1430             MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
1431             if (sv.volume>0) {
1432                 unmuteVol=sv.volume;
1433                 sendCommand("setvol "+quote(0), false);
1434             }
1435         }
1436     }
1437 }
1438 
stopPlaying(bool afterCurrent)1439 void MPDConnection::stopPlaying(bool afterCurrent)
1440 {
1441     toggleStopAfterCurrent(afterCurrent);
1442     if (!stopAfterCurrent) {
1443         if (!startVolumeFade()) {
1444             sendCommand("stop");
1445         }
1446     }
1447 }
1448 
clearStopAfter()1449 void MPDConnection::clearStopAfter()
1450 {
1451     toggleStopAfterCurrent(false);
1452 }
1453 
getStats()1454 void MPDConnection::getStats()
1455 {
1456     Response response=sendCommand("stats");
1457     if (response.ok) {
1458         MPDStatsValues stats=MPDParseUtils::parseStats(response.data);
1459         dbUpdate=stats.dbUpdate;
1460         if (isMopidy()) {
1461             // Set version to 1 so that SQL cache is updated - it uses 0 as intial value
1462             dbUpdate=stats.dbUpdate=1;
1463         }
1464         emit statsUpdated(stats);
1465     }
1466 }
1467 
getStatus()1468 void MPDConnection::getStatus()
1469 {
1470    Response response=sendCommand("status");
1471     if (response.ok) {
1472         MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
1473         lastStatusPlayQueueVersion=sv.playlist;
1474         if (currentSongId!=sv.songId) {
1475             stopVolumeFade();
1476         }
1477         if (stopAfterCurrent && (currentSongId!=sv.songId || (songPos>0 && sv.timeElapsed<(qint32)songPos))) {
1478             stopVolumeFade();
1479             if (sendCommand("stop").ok) {
1480                 sv.state=MPDState_Stopped;
1481             }
1482             toggleStopAfterCurrent(false);
1483         }
1484         currentSongId=sv.songId;
1485         if (!isUpdatingDb && -1!=sv.updatingDb) {
1486             isUpdatingDb=true;
1487             emit updatingDatabase();
1488         } else if (isUpdatingDb && -1==sv.updatingDb) {
1489             isUpdatingDb=false;
1490             emit updatedDatabase();
1491         }
1492         emitStatusUpdated(sv);
1493 
1494         // If playlist length does not match number of IDs, then refresh
1495         if (sv.playlistLength!=playQueueIds.length()) {
1496             playListInfo();
1497         }
1498     }
1499 }
1500 
getUrlHandlers()1501 void MPDConnection::getUrlHandlers()
1502 {
1503     Response response=sendCommand("urlhandlers");
1504     if (response.ok) {
1505         handlers=MPDParseUtils::parseList(response.data, QByteArray("handler: ")).toSet();
1506         DBUG << handlers;
1507     }
1508 }
1509 
getTagTypes()1510 void MPDConnection::getTagTypes()
1511 {
1512     Response response=sendCommand("tagtypes");
1513     if (response.ok) {
1514         tagTypes=MPDParseUtils::parseList(response.data, QByteArray("tagtype: ")).toSet();
1515     }
1516 }
1517 
getCover(const Song & song)1518 void MPDConnection::getCover(const Song &song)
1519 {
1520     int dataToRead = -1;
1521     int imageSize = 0;
1522     QByteArray imageData;
1523     bool firstRun = true;
1524     QString path=Utils::getDir(song.file);
1525     while (dataToRead != 0) {
1526         Response response=sendCommand("albumart "+encodeName(path)+" "+QByteArray::number(firstRun ? 0 : (imageSize - dataToRead)));
1527         if (!response.ok) {
1528             DBUG << "albumart query failed";
1529             break;
1530         }
1531 
1532         static const QByteArray constSize("size: ");
1533         static const QByteArray constBinary("binary: ");
1534 
1535         auto sizeStart = strstr(response.data.constData(), constSize.constData());
1536         if (!sizeStart) {
1537             DBUG << "Failed to get size start";
1538             break;
1539         }
1540         auto sizeEnd = strchr(sizeStart, '\n');
1541         if (!sizeEnd) {
1542             DBUG << "Failed to get size end";
1543             break;
1544         }
1545 
1546         auto chunkSizeStart = strstr(sizeEnd, constBinary.constData());
1547         if (!chunkSizeStart) {
1548             DBUG << "Failed to get chunk size start";
1549             break;
1550         }
1551         auto chunkSizeEnd = strchr(chunkSizeStart, '\n');
1552         if (!chunkSizeEnd) {
1553             DBUG << "Failed to chunk size end";
1554             break;
1555         }
1556 
1557         if (firstRun) {
1558             imageSize = QByteArray(sizeStart+constSize.length(), sizeEnd-(sizeStart+constSize.length())).toUInt();
1559             imageData.reserve(imageSize);
1560             dataToRead = imageSize;
1561             firstRun = false;
1562             DBUG << "image size" << imageSize;
1563         }
1564 
1565         int chunkSize = QByteArray(chunkSizeStart+constBinary.length(), chunkSizeEnd-(chunkSizeStart+constBinary.length())).toUInt();
1566         DBUG << "chunk size" << chunkSize;
1567 
1568         int startOfChunk=(chunkSizeEnd+1)-response.data.constData();
1569         if (startOfChunk+chunkSize > response.data.length()) {
1570             DBUG << "Invalid chunk size";
1571             break;
1572         }
1573 
1574         imageData.append(chunkSizeEnd+1, chunkSize);
1575         dataToRead -= chunkSize;
1576     }
1577 
1578     DBUG << dataToRead << imageData.size();
1579     emit albumArt(song, 0==dataToRead ? imageData : QByteArray());
1580 }
1581 
1582 /*
1583  * Data is written during idle.
1584  * Retrieve it and parse it
1585  */
idleDataReady()1586 void MPDConnection::idleDataReady()
1587 {
1588     DBUG << "idleDataReady";
1589     if (0==idleSocket.bytesAvailable()) {
1590         return;
1591     }
1592     parseIdleReturn(readFromSocket(idleSocket));
1593 }
1594 
1595 /*
1596  * Socket state has changed, currently we only use this to gracefully
1597  * handle disconnects.
1598  */
onSocketStateChanged(QAbstractSocket::SocketState socketState)1599 void MPDConnection::onSocketStateChanged(QAbstractSocket::SocketState socketState)
1600 {
1601     if (socketState == QAbstractSocket::ClosingState){
1602         bool wasConnected=State_Connected==state;
1603         disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
1604         DBUG << "onSocketStateChanged";
1605         if (QAbstractSocket::ConnectedState==idleSocket.state()) {
1606             idleSocket.disconnectFromHost();
1607         }
1608         idleSocket.close();
1609         ConnectionReturn status=Success;
1610         if (wasConnected && Success!=(status=connectToMPD(idleSocket, true))) {
1611             // Failed to connect idle socket - so close *both*
1612             disconnectFromMPD();
1613             emit stateChanged(false);
1614             emit error(errorString(status), true);
1615         }
1616         if (QAbstractSocket::ConnectedState==idleSocket.state()) {
1617             connect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)), Qt::QueuedConnection);
1618         }
1619     }
1620 }
1621 
1622 /*
1623  * Parse data returned by the mpd deamon on an idle commond.
1624  */
parseIdleReturn(const QByteArray & data)1625 void MPDConnection::parseIdleReturn(const QByteArray &data)
1626 {
1627     DBUG << "parseIdleReturn:" << data;
1628 
1629     Response response(data.endsWith(constOkNlValue), data);
1630     if (!response.ok) {
1631         DBUG << "idle failed? reconnect";
1632         disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
1633         if (QAbstractSocket::ConnectedState==idleSocket.state()) {
1634             idleSocket.disconnectFromHost();
1635         }
1636         idleSocket.close();
1637         ConnectionReturn status=connectToMPD(idleSocket, true);
1638         if (Success!=status) {
1639             // Failed to connect idle socket - so close *both*
1640             disconnectFromMPD();
1641             emit stateChanged(false);
1642             emit error(errorString(status), true);
1643         }
1644         return;
1645     }
1646 
1647     QList<QByteArray> lines = data.split('\n');
1648 
1649     /*
1650      * See http://www.musicpd.org/doc/protocol/ch02.html
1651      */
1652     bool playListUpdated=false;
1653     bool statusUpdated=false;
1654     for (const QByteArray &line: lines) {
1655         if (line.startsWith(constIdleChangedKey)) {
1656             QByteArray value=line.mid(constIdleChangedKey.length());
1657             if (constIdleDbValue==value) {
1658                 getStats();
1659                 getStatus();
1660                 playListInfo();
1661                 playListUpdated=true;
1662             } else if (constIdleUpdateValue==value) {
1663                 getStats();
1664                 getStatus();
1665             } else if (constIdleStoredPlaylistValue==value) {
1666                 listPlaylists();
1667                 listStreams();
1668             } else if (constIdlePlaylistValue==value) {
1669                 if (!playListUpdated) {
1670                     playListChanges();
1671                 }
1672             } else if (!statusUpdated && (constIdlePlayerValue==value || constIdleMixerValue==value || constIdleOptionsValue==value)) {
1673                 getStatus();
1674                 getReplayGain();
1675                 statusUpdated=true;
1676             } else if (constIdleOutputValue==value) {
1677                 outputs();
1678             } else if (constIdleStickerValue==value) {
1679                 emit stickerDbChanged();
1680             } else if (constIdleSubscriptionValue==value) {
1681                 //if (dynamicId.isEmpty()) {
1682                     setupRemoteDynamic();
1683                 //}
1684             } else if (constIdleMessageValue==value) {
1685                 readRemoteDynamicMessages();
1686             }
1687         }
1688     }
1689 
1690     DBUG << (void *)(&idleSocket) << "write idle";
1691     idleSocket.write("idle\n");
1692     idleSocket.waitForBytesWritten();
1693 }
1694 
outputs()1695 void MPDConnection::outputs()
1696 {
1697     Response response=sendCommand("outputs");
1698     if (response.ok) {
1699         emit outputsUpdated(MPDParseUtils::parseOuputs(response.data));
1700     }
1701 }
1702 
enableOutput(quint32 id,bool enable)1703 void MPDConnection::enableOutput(quint32 id, bool enable)
1704 {
1705     if (sendCommand((enable ? "enableoutput " : "disableoutput ")+quote(id)).ok) {
1706         outputs();
1707     }
1708 }
1709 
1710 /*
1711  * Admin commands
1712  */
updateMaybe()1713 void MPDConnection::updateMaybe()
1714 {
1715     if (!details.autoUpdate) {
1716         update();
1717     }
1718 }
1719 
update()1720 void MPDConnection::update()
1721 {
1722     if (isMopidy()) {
1723         // Mopidy does not support MPD's update command. So, when user presses update DB, what we
1724         // just reload the library.
1725         loadLibrary();
1726     } else {
1727         sendCommand("update");
1728     }
1729 }
1730 
1731 /*
1732  * Database commands
1733  */
loadLibrary()1734 void MPDConnection::loadLibrary()
1735 {
1736     DBUG << "loadLibrary";
1737     isListingMusic=true;
1738     emit updatingLibrary(dbUpdate);
1739     QList<Song> songs;
1740     recursivelyListDir("/", songs);
1741     emit updatedLibrary();
1742     isListingMusic=false;
1743 }
1744 
listFolder(const QString & folder)1745 void MPDConnection::listFolder(const QString &folder)
1746 {
1747     DBUG << "listFolder" << folder;
1748     bool topLevel="/"==folder || ""==folder;
1749     Response response=sendCommand(topLevel ? "lsinfo" : ("lsinfo "+encodeName(folder)));
1750     QStringList subFolders;
1751     QList<Song> songs;
1752     if (response.ok) {
1753         MPDParseUtils::parseDirItems(response.data, QString(), ver, songs, folder, subFolders, MPDParseUtils::Loc_Browse);
1754     }
1755     emit folderContents(folder, subFolders, songs);
1756 }
1757 
1758 /*
1759  * Playlists commands
1760  */
1761 // void MPDConnection::listPlaylist(const QString &name)
1762 // {
1763 //     QByteArray data = "listplaylist ";
1764 //     data += encodeName(name);
1765 //     sendCommand(data);
1766 // }
1767 
listPlaylists()1768 void MPDConnection::listPlaylists()
1769 {
1770     // Don't report errors here. If user has disabled playlists, then MPD will report an error
1771     // Issues #1090 #1284
1772     Response response=sendCommand("listplaylists", false);
1773     if (response.ok) {
1774         QList<Playlist> playlists=MPDParseUtils::parsePlaylists(response.data);
1775         playlists.removeAll((Playlist(constStreamsPlayListName)));
1776         emit playlistsRetrieved(playlists);
1777     }
1778 }
1779 
playlistInfo(const QString & name)1780 void MPDConnection::playlistInfo(const QString &name)
1781 {
1782     Response response=sendCommand("listplaylistinfo "+encodeName(name));
1783     if (response.ok) {
1784         emit playlistInfoRetrieved(name, MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Playlists));
1785     }
1786 }
1787 
loadPlaylist(const QString & name,bool replace)1788 void MPDConnection::loadPlaylist(const QString &name, bool replace)
1789 {
1790     if (replace) {
1791         clear();
1792         getStatus();
1793     }
1794 
1795     if (sendCommand("load "+encodeName(name)).ok) {
1796         if (replace) {
1797             playFirstTrack(false);
1798         }
1799         emit playlistLoaded(name);
1800     }
1801 }
1802 
renamePlaylist(const QString oldName,const QString newName)1803 void MPDConnection::renamePlaylist(const QString oldName, const QString newName)
1804 {
1805     if (sendCommand("rename "+encodeName(oldName)+' '+encodeName(newName), false).ok) {
1806         emit playlistRenamed(oldName, newName);
1807     } else {
1808         emit error(tr("Failed to rename <b>%1</b> to <b>%2</b>").arg(oldName).arg(newName));
1809     }
1810 }
1811 
removePlaylist(const QString & name)1812 void MPDConnection::removePlaylist(const QString &name)
1813 {
1814     sendCommand("rm "+encodeName(name));
1815 }
1816 
savePlaylist(const QString & name,bool overwrite)1817 void MPDConnection::savePlaylist(const QString &name, bool overwrite)
1818 {
1819     if (overwrite) {
1820         sendCommand("rm "+encodeName(name), false);
1821     }
1822     if (!sendCommand("save "+encodeName(name), false).ok) {
1823         emit error(tr("Failed to save <b>%1</b>").arg(name));
1824     }
1825 }
1826 
addToPlaylist(const QString & name,const QStringList & songs,quint32 pos,quint32 size)1827 void MPDConnection::addToPlaylist(const QString &name, const QStringList &songs, quint32 pos, quint32 size)
1828 {
1829     if (songs.isEmpty()) {
1830         return;
1831     }
1832 
1833     if (!name.isEmpty()) {
1834         for (const QString &song: songs) {
1835             if (CueFile::isCue(song)) {
1836                 emit error(tr("You cannot add parts of a cue sheet to a playlist!")+QLatin1String(" (")+song+QLatin1Char(')'));
1837                 return;
1838             } else if (isPlaylist(song)) {
1839                 emit error(tr("You cannot add a playlist to another playlist!")+QLatin1String(" (")+song+QLatin1Char(')'));
1840                 return;
1841             }
1842         }
1843     }
1844 
1845     QStringList files;
1846     for (const QString &file: songs) {
1847         if (file.startsWith(constDirPrefix)) {
1848             files+=getAllFiles(file.mid(constDirPrefix.length()));
1849         } else if (file.startsWith(constPlaylistPrefix)) {
1850             files+=getPlaylistFiles(file.mid(constPlaylistPrefix.length()));
1851         } else {
1852             files.append(file);
1853         }
1854     }
1855 
1856     QByteArray encodedName=encodeName(name);
1857     QByteArray send = "command_list_begin\n";
1858     for (const QString &s: files) {
1859         send += "playlistadd "+encodedName+" "+encodeName(s)+'\n';
1860     }
1861     send += "command_list_end";
1862 
1863     if (sendCommand(send).ok) {
1864         if (size>0) {
1865             QList<quint32> items;
1866             for(int i=0; i<songs.count(); ++i) {
1867                 items.append(size+i);
1868             }
1869             doMoveInPlaylist(name, items, pos, size+songs.count());
1870         }
1871     } else {
1872         playlistInfo(name);
1873     }
1874 }
1875 
removeFromPlaylist(const QString & name,const QList<quint32> & positions)1876 void MPDConnection::removeFromPlaylist(const QString &name, const QList<quint32> &positions)
1877 {
1878     if (positions.isEmpty()) {
1879         return;
1880     }
1881 
1882     QByteArray encodedName=encodeName(name);
1883     QList<quint32> sorted=positions;
1884     QList<quint32> removed;
1885 
1886     std::sort(sorted.begin(), sorted.end());
1887 
1888     for (int i=sorted.count()-1; i>=0; --i) {
1889         quint32 idx=sorted.at(i);
1890         QByteArray data = "playlistdelete ";
1891         data += encodedName;
1892         data += " ";
1893         data += quote(idx);
1894         if (sendCommand(data).ok) {
1895             removed.prepend(idx);
1896         } else {
1897             break;
1898         }
1899     }
1900 
1901     if (removed.count()) {
1902         emit removedFromPlaylist(name, removed);
1903     }
1904 //     playlistInfo(name);
1905 }
1906 
setPriority(const QList<qint32> & ids,quint8 priority,bool decreasePriority)1907 void MPDConnection::setPriority(const QList<qint32> &ids, quint8 priority, bool decreasePriority)
1908 {
1909     if (canUsePriority()) {
1910         QMap<qint32, quint8> tracks;
1911         QByteArray send = "command_list_begin\n";
1912 
1913         for (quint32 id: ids) {
1914             tracks.insert(id, priority);
1915             send += "prioid "+quote(priority)+" "+quote(id)+'\n';
1916             if (decreasePriority && priority>0) {
1917                 priority--;
1918             }
1919         }
1920 
1921         send += "command_list_end";
1922         if (sendCommand(send).ok) {
1923             emit prioritySet(tracks);
1924         }
1925     }
1926 }
1927 
search(const QString & field,const QString & value,int id)1928 void MPDConnection::search(const QString &field, const QString &value, int id)
1929 {
1930     QList<Song> songs;
1931     QByteArray cmd;
1932 
1933     if (field==constModifiedSince) {
1934         time_t v=0;
1935         if (QRegExp("\\d*").exactMatch(value)) {
1936             v=QDateTime(QDateTime::currentDateTime().date()).toTime_t()-(value.toInt()*24*60*60);
1937         } else if (QRegExp("^((19|20)\\d\\d)[-/](0[1-9]|1[012])[-/](0[1-9]|[12][0-9]|3[01])$").exactMatch(value)) {
1938             QDateTime dt=QDateTime::fromString(QString(value).replace("/", "-"), Qt::ISODate);
1939             if (dt.isValid()) {
1940                 v=dt.toTime_t();
1941             }
1942         }
1943         if (v>0) {
1944             cmd="find "+field.toLatin1()+" "+quote(v);
1945         }
1946     } else {
1947         cmd="search "+field.toLatin1()+" "+encodeName(value);
1948     }
1949 
1950     if (!cmd.isEmpty()) {
1951         Response response=sendCommand(cmd);
1952         if (response.ok) {
1953             songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search);
1954 
1955             if (QLatin1String("any")==field) {
1956                 // When searching on 'any' MPD ignores filename/paths! So, do another
1957                 // search on these, and combine results.
1958                 response=sendCommand("search file "+encodeName(value));
1959                 if (response.ok) {
1960                     QList<Song> otherSongs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search);
1961                     if (!otherSongs.isEmpty()) {
1962                         QSet<QString> fileNames;
1963                         for (const auto &s: songs) {
1964                             fileNames.insert(s.file);
1965                         }
1966                         for (const auto &s: otherSongs) {
1967                             if (!fileNames.contains(s.file)) {
1968                                 songs.append(s);
1969                             }
1970                         }
1971                     }
1972                 }
1973             }
1974             std::sort(songs.begin(), songs.end());
1975         }
1976     }
1977     emit searchResponse(id, songs);
1978 }
1979 
search(const QByteArray & query,const QString & id)1980 void MPDConnection::search(const QByteArray &query, const QString &id)
1981 {
1982     QList<Song> songs;
1983     if (query.isEmpty()) {
1984         Response response=sendCommand("list albumartist", false, false);
1985         if (response.ok) {
1986             QList<QByteArray> lines = response.data.split('\n');
1987             for (const QByteArray &line: lines) {
1988                 if (line.startsWith("AlbumArtist: ")) {
1989                     Response resp = sendCommand("find albumartist " + encodeName(QString::fromUtf8(line.mid(13))) , false, false);
1990                     if (resp.ok) {
1991                         songs += MPDParseUtils::parseSongs(resp.data, MPDParseUtils::Loc_Search);
1992                     }
1993                 }
1994             }
1995         }
1996     } else if (query.startsWith("RATING:")) {
1997         QList<QByteArray> parts = query.split(':');
1998         if (3==parts.length()) {
1999             Response response=sendCommand("sticker find song \"\" rating", false, false);
2000             if (response.ok) {
2001                 int min = parts.at(1).toInt();
2002                 int max = parts.at(2).toInt();
2003                 QList<MPDParseUtils::Sticker> stickers=MPDParseUtils::parseStickers(response.data, constRatingSticker);
2004                 if (!stickers.isEmpty()) {
2005                     for (const MPDParseUtils::Sticker &sticker: stickers) {
2006                         if (!sticker.file.isEmpty() && !sticker.value.isEmpty()) {
2007                             int val = sticker.value.toInt();
2008                             if (val>=min && val<=max) {
2009                                 Response resp = sendCommand("find file " + encodeName(QString::fromUtf8(sticker.file)) , false, false);
2010                                 if (resp.ok) {
2011                                     songs += MPDParseUtils::parseSong(resp.data, MPDParseUtils::Loc_Search);
2012                                 }
2013                             }
2014                         }
2015                     }
2016                 }
2017             }
2018         }
2019     } else {
2020         Response response=sendCommand(query);
2021         if (response.ok) {
2022             songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search);
2023         }
2024     }
2025     emit searchResponse(id, songs);
2026 }
2027 
listStreams()2028 void MPDConnection::listStreams()
2029 {
2030     Response response=sendCommand("listplaylistinfo "+encodeName(constStreamsPlayListName), false);
2031     QList<Stream> streams;
2032     if (response.ok) {
2033         QList<Song> songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Streams);
2034         for (const Song &song: songs) {
2035             streams.append(Stream(song.file, song.name()));
2036         }
2037     }
2038     clearError();
2039     emit streamList(streams);
2040 }
2041 
saveStream(const QString & url,const QString & name)2042 void MPDConnection::saveStream(const QString &url, const QString &name)
2043 {
2044     if (sendCommand("playlistadd "+encodeName(constStreamsPlayListName)+" "+encodeName(MPDParseUtils::addStreamName(url, name, true))).ok) {
2045         emit savedStream(url, name);
2046     }
2047 }
2048 
removeStreams(const QList<quint32> & positions)2049 void MPDConnection::removeStreams(const QList<quint32> &positions)
2050 {
2051     if (positions.isEmpty()) {
2052         return;
2053     }
2054 
2055     QByteArray encodedName=encodeName(constStreamsPlayListName);
2056     QList<quint32> sorted=positions;
2057     QList<quint32> removed;
2058 
2059     std::sort(sorted.begin(), sorted.end());
2060 
2061     for (int i=sorted.count()-1; i>=0; --i) {
2062         quint32 idx=sorted.at(i);
2063         QByteArray data = "playlistdelete ";
2064         data += encodedName;
2065         data += " ";
2066         data += quote(idx);
2067         if (sendCommand(data).ok) {
2068             removed.prepend(idx);
2069         } else {
2070             break;
2071         }
2072     }
2073 
2074     emit removedStreams(removed);
2075 }
2076 
editStream(const QString & url,const QString & name,quint32 position)2077 void MPDConnection::editStream(const QString &url, const QString &name, quint32 position)
2078 {
2079     QByteArray encodedName=encodeName(constStreamsPlayListName);
2080     if (sendCommand("playlistdelete "+encodedName+" "+quote(position)).ok &&
2081         sendCommand("playlistadd "+encodedName+" "+encodeName(MPDParseUtils::addStreamName(url, name))).ok) {
2082 //        emit editedStream(url, name, position);
2083         listStreams();
2084     }
2085 }
2086 
sendClientMessage(const QString & channel,const QString & msg,const QString & clientName)2087 void MPDConnection::sendClientMessage(const QString &channel, const QString &msg, const QString &clientName)
2088 {
2089     if (!sendCommand("sendmessage "+channel.toUtf8()+" "+msg.toUtf8(), false).ok) {
2090         emit error(tr("Failed to send '%1' to %2. Please check %2 is registered with MPD.").arg(msg).arg(clientName.isEmpty() ? channel : clientName));
2091         emit clientMessageFailed(channel, msg);
2092     }
2093 }
2094 
sendDynamicMessage(const QStringList & msg)2095 void MPDConnection::sendDynamicMessage(const QStringList &msg)
2096 {
2097     // Check whether cantata-dynamic is still alive, by seeing if its channel is still open...
2098     if (1==msg.count() && QLatin1String("ping")==msg.at(0)) {
2099         Response response=sendCommand("channels");
2100         if (!response.ok || !MPDParseUtils::parseList(response.data, QByteArray("channel: ")).toSet().contains(constDynamicIn)) {
2101             emit dynamicSupport(false);
2102         }
2103         return;
2104     }
2105 
2106     QByteArray data;
2107     for (QString part: msg) {
2108         if (data.isEmpty()) {
2109             data+='\"'+part.toUtf8()+':'+dynamicId;
2110         } else {
2111             part=part.replace('\"', "{q}");
2112             part=part.replace("{", "{ob}");
2113             part=part.replace("}", "{cb}");
2114             part=part.replace("\n", "{n}");
2115             part=part.replace(":", "{c}");
2116             data+=':'+part.toUtf8();
2117         }
2118     }
2119 
2120     data+='\"';
2121     if (!sendCommand("sendmessage "+constDynamicIn+" "+data).ok) {
2122         emit dynamicSupport(false);
2123     }
2124 }
2125 
moveInPlaylist(const QString & name,const QList<quint32> & items,quint32 pos,quint32 size)2126 void MPDConnection::moveInPlaylist(const QString &name, const QList<quint32> &items, quint32 pos, quint32 size)
2127 {
2128     if (doMoveInPlaylist(name, items, pos, size)) {
2129         emit movedInPlaylist(name, items, pos);
2130     }
2131 //     playlistInfo(name);
2132 }
2133 
doMoveInPlaylist(const QString & name,const QList<quint32> & items,quint32 pos,quint32 size)2134 bool MPDConnection::doMoveInPlaylist(const QString &name, const QList<quint32> &items, quint32 pos, quint32 size)
2135 {
2136     if (name.isEmpty()) {
2137         toggleStopAfterCurrent(false);
2138     }
2139     QByteArray cmd = name.isEmpty() ? "move " : ("playlistmove "+encodeName(name)+" ");
2140     QByteArray send = "command_list_begin\n";
2141     QList<quint32> moveItems=items;
2142     int posOffset = 0;
2143 
2144     std::sort(moveItems.begin(), moveItems.end());
2145 
2146     //first move all items (starting with the biggest) to the end so we don't have to deal with changing rownums
2147     for (int i = moveItems.size() - 1; i >= 0; i--) {
2148         if (moveItems.at(i) < pos && moveItems.at(i) != size - 1) {
2149             // we are moving away an item that resides before the destination row, manipulate destination row
2150             posOffset++;
2151         }
2152         send += cmd;
2153         send += quote(moveItems.at(i));
2154         send += " ";
2155         send += quote(size - 1);
2156         send += '\n';
2157     }
2158     //now move all of them to the destination position
2159     for (int i = moveItems.size() - 1; i >= 0; i--) {
2160         send += cmd;
2161         send += quote(size - 1 - i);
2162         send += " ";
2163         send += quote(pos - posOffset);
2164         send += '\n';
2165     }
2166 
2167     send += "command_list_end";
2168     return sendCommand(send).ok;
2169 }
2170 
toggleStopAfterCurrent(bool afterCurrent)2171 void MPDConnection::toggleStopAfterCurrent(bool afterCurrent)
2172 {
2173     if (afterCurrent!=stopAfterCurrent) {
2174         stopAfterCurrent=afterCurrent;
2175         songPos=0;
2176         if (stopAfterCurrent && 1==playQueueIds.count()) {
2177             Response response=sendCommand("status");
2178             if (response.ok) {
2179                 MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
2180                 songPos=sv.timeElapsed;
2181             }
2182         }
2183         emit stopAfterCurrentChanged(stopAfterCurrent);
2184     }
2185 }
2186 
recursivelyListDir(const QString & dir,QList<Song> & songs)2187 bool MPDConnection::recursivelyListDir(const QString &dir, QList<Song> &songs)
2188 {
2189     bool topLevel="/"==dir || ""==dir;
2190 
2191     if (topLevel && isMpd()) {
2192         // UPnP database backend does not list separate metadata items, so if "list genre" returns
2193         // empty response assume this is a UPnP backend and dont attempt to get rest of data...
2194         // Although we dont use "list XXX", lsinfo will return duplciate items (due to the way most
2195         // UPnP servers returing directories of classifications - Genre/Album/Tracks, Artist/Album/Tracks,
2196         // etc...
2197         Response response=sendCommand("list genre", false, false);
2198         if (!response.ok || response.data.split('\n').length()<3) { // 2 lines - OK and blank
2199             // ..just to be 100% sure, check no artists either...
2200             response=sendCommand("list artist", false, false);
2201             if (!response.ok || response.data.split('\n').length()<3) { // 2 lines - OK and blank
2202                 return false;
2203             }
2204         }
2205     }
2206 
2207     Response response=sendCommand(topLevel
2208                                     ? serverInfo.getTopLevelLsinfo()
2209                                     : ("lsinfo "+encodeName(dir)));
2210     if (response.ok) {
2211         QStringList subDirs;
2212         MPDParseUtils::parseDirItems(response.data, details.dir, ver, songs, dir, subDirs, MPDParseUtils::Loc_Library);
2213         if (songs.count()>=200){
2214             QCoreApplication::processEvents();
2215             QList<Song> *copy=new QList<Song>();
2216             *copy << songs;
2217             emit librarySongs(copy);
2218             songs.clear();
2219         }
2220         for (const QString &sub: subDirs) {
2221             recursivelyListDir(sub, songs);
2222         }
2223 
2224         if (topLevel && !songs.isEmpty()) {
2225             QList<Song> *copy=new QList<Song>();
2226             *copy << songs;
2227             emit librarySongs(copy);
2228         }
2229         return true;
2230     } else {
2231         return false;
2232     }
2233 }
2234 
getPlaylistFiles(const QString & name)2235 QStringList MPDConnection::getPlaylistFiles(const QString &name)
2236 {
2237     QStringList files;
2238     Response response=sendCommand("listplaylistinfo "+encodeName(name));
2239     if (response.ok) {
2240         QList<Song> songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Playlists);
2241         emit playlistInfoRetrieved(name, songs);
2242         for (const Song &s: songs) {
2243             files.append(s.file);
2244         }
2245     }
2246     return files;
2247 }
2248 
getAllFiles(const QString & dir)2249 QStringList MPDConnection::getAllFiles(const QString &dir)
2250 {
2251     QStringList files;
2252     Response response=sendCommand("lsinfo "+encodeName(dir));
2253     if (response.ok) {
2254         QStringList subDirs;
2255         QList<Song> songs;
2256         MPDParseUtils::parseDirItems(response.data, details.dir, ver, songs, dir, subDirs, MPDParseUtils::Loc_Browse);
2257         for (const Song &song: songs) {
2258             if (Song::Playlist!=song.type) {
2259                 files.append(song.file);
2260             }
2261         }
2262         for (const QString &sub: subDirs) {
2263             files+=getAllFiles(sub);
2264         }
2265     }
2266 
2267     return files;
2268 }
2269 
checkRemoteDynamicSupport()2270 bool MPDConnection::checkRemoteDynamicSupport()
2271 {
2272     if (ver>=CANTATA_MAKE_VERSION(0,17,0)) {
2273         Response response;
2274         if (-1!=idleSocket.write("channels\n")) {
2275             idleSocket.waitForBytesWritten(constSocketCommsTimeout);
2276             response=readReply(idleSocket);
2277             if (response.ok) {
2278                 return MPDParseUtils::parseList(response.data, QByteArray("channel: ")).toSet().contains(constDynamicIn);
2279             }
2280         }
2281     }
2282     return false;
2283 }
2284 
subscribe(const QByteArray & channel)2285 bool MPDConnection::subscribe(const QByteArray &channel)
2286 {
2287     if (-1!=idleSocket.write("subscribe \""+channel+"\"\n")) {
2288         idleSocket.waitForBytesWritten(constSocketCommsTimeout);
2289         Response response=readReply(idleSocket);
2290         if (response.ok || response.data.startsWith("ACK [56@0]")) { // ACK => already subscribed...
2291             DBUG << "Created subscription to " << channel;
2292             return true;
2293         } else {
2294             DBUG << "Failed to subscribe to " << channel;
2295         }
2296     } else {
2297         DBUG << "Failed to create subscribe to " << channel;
2298     }
2299     return false;
2300 }
2301 
setupRemoteDynamic()2302 void MPDConnection::setupRemoteDynamic()
2303 {
2304     if (checkRemoteDynamicSupport()) {
2305         DBUG << "cantata-dynamic is running";
2306         if (subscribe(constDynamicOut)) {
2307             if (dynamicId.isEmpty()) {
2308                 dynamicId=QHostInfo::localHostName().toLatin1()+'.'+QHostInfo::localDomainName().toLatin1()+'-'+QByteArray::number(QCoreApplication::applicationPid());
2309                 if (!subscribe(constDynamicOut+'-'+dynamicId)) {
2310                     dynamicId.clear();
2311                 }
2312             }
2313         }
2314     } else {
2315         DBUG << "remote dynamic is not supported";
2316     }
2317     emit dynamicSupport(!dynamicId.isEmpty());
2318 }
2319 
readRemoteDynamicMessages()2320 void MPDConnection::readRemoteDynamicMessages()
2321 {
2322     if (-1!=idleSocket.write("readmessages\n")) {
2323         idleSocket.waitForBytesWritten(constSocketCommsTimeout);
2324         Response response=readReply(idleSocket);
2325         if (response.ok) {
2326             MPDParseUtils::MessageMap messages=MPDParseUtils::parseMessages(response.data);
2327             if (!messages.isEmpty()) {
2328                 QList<QByteArray> channels=QList<QByteArray>() << constDynamicOut << constDynamicOut+'-'+dynamicId;
2329                 for (const QByteArray &channel: channels) {
2330                     if (messages.contains(channel)) {
2331                         for (const QString &m: messages[channel]) {
2332                             if (!m.isEmpty()) {
2333                                 DBUG << "Received message " << m;
2334                                 QStringList parts=m.split(':', QString::SkipEmptyParts);
2335                                 QStringList message;
2336                                 for (QString part: parts) {
2337                                     part=part.replace("{c}", ":");
2338                                     part=part.replace("{n}", "\n");
2339                                     part=part.replace("{cb}", "}");
2340                                     part=part.replace("{ob}", "{");
2341                                     part=part.replace("{q}", "\"");
2342                                     message.append(part);
2343                                 }
2344                                 emit dynamicResponse(message);
2345                             }
2346                         }
2347                     }
2348                 }
2349             }
2350         }
2351     }
2352 }
2353 
getVolume()2354 int MPDConnection::getVolume()
2355 {
2356     Response response=sendCommand("status");
2357     if (response.ok) {
2358         MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
2359         return sv.volume;
2360     }
2361     return -1;
2362 }
2363 
setRating(const QString & file,quint8 val)2364 void MPDConnection::setRating(const QString &file, quint8 val)
2365 {
2366     if (val>Song::Rating_Max) {
2367         return;
2368     }
2369 
2370     if (!canUseStickers) {
2371         emit error(tr("Cannot store ratings, as the 'sticker' MPD command is not supported."));
2372         return;
2373     }
2374 
2375     bool ok=0==val
2376                 ? sendCommand("sticker delete song "+encodeName(file)+' '+constRatingSticker, 0!=val).ok
2377                 : sendCommand("sticker set song "+encodeName(file)+' '+constRatingSticker+' '+quote(val)).ok;
2378 
2379     if (!ok && 0==val) {
2380         clearError();
2381     }
2382 
2383     if (ok) {
2384         emit rating(file, val);
2385     } else {
2386         getRating(file);
2387     }
2388 }
2389 
setRating(const QStringList & files,quint8 val)2390 void MPDConnection::setRating(const QStringList &files, quint8 val)
2391 {
2392     if (1==files.count()) {
2393         setRating(files.at(0), val);
2394         return;
2395     }
2396 
2397     if (!canUseStickers) {
2398         emit error(tr("Cannot store ratings, as the 'sticker' MPD command is not supported."));
2399         return;
2400     }
2401 
2402     QList<QStringList> fileLists;
2403     if (files.count()>constMaxFilesPerAddCommand) {
2404         int numChunks=(files.count()/constMaxFilesPerAddCommand)+(files.count()%constMaxFilesPerAddCommand ? 1 : 0);
2405         for (int i=0; i<numChunks; ++i) {
2406             fileLists.append(files.mid(i*constMaxFilesPerAddCommand, constMaxFilesPerAddCommand));
2407         }
2408     } else {
2409         fileLists.append(files);
2410     }
2411 
2412     bool ok=true;
2413     for (const QStringList &list: fileLists) {
2414         QByteArray cmd = "command_list_begin\n";
2415 
2416         for (const QString &f: list) {
2417             if (0==val) {
2418                 cmd+="sticker delete song "+encodeName(f)+' '+constRatingSticker+'\n';
2419             } else {
2420                 cmd+="sticker set song "+encodeName(f)+' '+constRatingSticker+' '+quote(val)+'\n';
2421             }
2422         }
2423 
2424         cmd += "command_list_end";
2425         ok=sendCommand(cmd, 0!=val).ok;
2426         if (!ok) {
2427             break;
2428         }
2429     }
2430 
2431     if (!ok && 0==val) {
2432         clearError();
2433     }
2434 }
2435 
getRating(const QString & file)2436 void MPDConnection::getRating(const QString &file)
2437 {
2438     quint8 r=0;
2439     if (canUseStickers) {
2440         Response resp=sendCommand("sticker get song "+encodeName(file)+' '+constRatingSticker, false);
2441         if (resp.ok) {
2442             QByteArray val=MPDParseUtils::parseSticker(resp.data, constRatingSticker);
2443             if (!val.isEmpty()) {
2444                 r=val.toUInt();
2445             }
2446         } else { // Ignore errors about uknown sticker...
2447             clearError();
2448         }
2449         if (r>Song::Rating_Max) {
2450             r=0;
2451         }
2452     }
2453     emit rating(file, r);
2454 }
2455 
getStickerSupport()2456 void MPDConnection::getStickerSupport()
2457 {
2458     Response response=sendCommand("commands");
2459     canUseStickers=response.ok &&
2460         MPDParseUtils::parseList(response.data, QByteArray("command: ")).toSet().contains("sticker");
2461 }
2462 
fadingVolume()2463 bool MPDConnection::fadingVolume()
2464 {
2465     return volumeFade && QPropertyAnimation::Running==volumeFade->state();
2466 }
2467 
startVolumeFade()2468 bool MPDConnection::startVolumeFade()
2469 {
2470     if (fadeDuration<=MinFade) {
2471         return false;
2472     }
2473 
2474     restoreVolume=getVolume();
2475     if (restoreVolume<5) {
2476         return false;
2477     }
2478 
2479     if (!volumeFade) {
2480         volumeFade = new QPropertyAnimation(this, "volume");
2481         volumeFade->setDuration(fadeDuration);
2482     }
2483 
2484     if (QPropertyAnimation::Running!=volumeFade->state()) {
2485         volumeFade->setStartValue(restoreVolume);
2486         volumeFade->setEndValue(-1);
2487         volumeFade->start();
2488     }
2489     return true;
2490 }
2491 
stopVolumeFade()2492 void MPDConnection::stopVolumeFade()
2493 {
2494     if (fadingVolume()) {
2495         volumeFade->stop();
2496         setVolume(restoreVolume);
2497         restoreVolume=-1;
2498     }
2499 }
2500 
emitStatusUpdated(MPDStatusValues & v)2501 void MPDConnection::emitStatusUpdated(MPDStatusValues &v)
2502 {
2503     if (restoreVolume>=0) {
2504         v.volume=restoreVolume;
2505     }
2506     #ifndef REPORT_MPD_ERRORS
2507     v.error=QString();
2508     #endif
2509     emit statusUpdated(v);
2510     if (!v.error.isEmpty()) {
2511         clearError();
2512     }
2513 }
2514 
clearError()2515 void MPDConnection::clearError()
2516 {
2517     #ifdef REPORT_MPD_ERRORS
2518     if (isConnected()) {
2519         DBUG << __FUNCTION__;
2520         if (-1!=sock.write("clearerror\n")) {
2521             sock.waitForBytesWritten(500);
2522             readReply(sock);
2523         }
2524     }
2525     #endif
2526 }
2527 
determineIfaceIp()2528 void MPDConnection::determineIfaceIp()
2529 {
2530     static const QLatin1String ip4Local("127.0.0.1");
2531     if (!details.isLocal() && !details.hostname.isEmpty() && ip4Local!=details.hostname && QLatin1String("localhost")!=details.hostname) {
2532         QUdpSocket testSocket(this);
2533         testSocket.connectToHost(details.hostname, 1, QIODevice::ReadOnly);
2534         QString addr=testSocket.localAddress().toString();
2535         testSocket.close();
2536         if (!addr.isEmpty()) {
2537             DBUG << addr;
2538             emit ifaceIp(addr);
2539             return;
2540         }
2541     }
2542     DBUG << ip4Local;
2543     emit ifaceIp(ip4Local);
2544 }
2545 
MpdSocket(QObject * parent)2546 MpdSocket::MpdSocket(QObject *parent)
2547     : QObject(parent)
2548     , tcp(nullptr)
2549     , local(nullptr)
2550 {
2551 }
2552 
~MpdSocket()2553 MpdSocket::~MpdSocket()
2554 {
2555     deleteTcp();
2556     deleteLocal();
2557 }
2558 
connectToHost(const QString & hostName,quint16 port,QIODevice::OpenMode mode)2559 void MpdSocket::connectToHost(const QString &hostName, quint16 port, QIODevice::OpenMode mode)
2560 {
2561     DBUG << "connectToHost" << hostName << port;
2562     if (hostName.startsWith('/') || hostName.startsWith('~')/* || hostName.startsWith('@')*/) {
2563         deleteTcp();
2564         if (!local) {
2565             local = new QLocalSocket(this);
2566             connect(local, SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), this, SLOT(localStateChanged(QLocalSocket::LocalSocketState)));
2567             connect(local, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
2568         }
2569         DBUG << "Connecting to LOCAL socket";
2570         QString host = Utils::tildaToHome(hostName);
2571         /*if ('@'==host[0]) {
2572             host[0]='\0';
2573         }*/
2574         local->connectToServer(host, mode);
2575     } else {
2576         deleteLocal();
2577         if (!tcp) {
2578             tcp = new QTcpSocket(this);
2579             connect(tcp, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
2580             connect(tcp, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
2581         }
2582         DBUG << "Connecting to TCP socket";
2583         tcp->connectToHost(hostName, port, mode);
2584     }
2585 }
2586 
localStateChanged(QLocalSocket::LocalSocketState state)2587 void MpdSocket::localStateChanged(QLocalSocket::LocalSocketState state)
2588 {
2589     emit stateChanged((QAbstractSocket::SocketState)state);
2590 }
2591 
deleteTcp()2592 void MpdSocket::deleteTcp()
2593 {
2594     if (tcp) {
2595         disconnect(tcp, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
2596         disconnect(tcp, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
2597         tcp->deleteLater();
2598         tcp=nullptr;
2599     }
2600 }
2601 
deleteLocal()2602 void MpdSocket::deleteLocal()
2603 {
2604     if (local) {
2605         disconnect(local, SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), this, SLOT(localStateChanged(QLocalSocket::LocalSocketState)));
2606         disconnect(local, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
2607         local->deleteLater();
2608         local=nullptr;
2609     }
2610 }
2611 
2612 // ONLY use this method to detect Non-MPD servers. The code which uses this will default to MPD
2613 MPDServerInfo::ResponseParameter MPDServerInfo::lsinfoResponseParameters[] = {
2614     // github
2615     { "{lsinfo} Directory info not found for virtual-path '/", true, MPDServerInfo::ForkedDaapd, "forked-daapd" },
2616     // { "ACK [50@0] {lsinfo} Not found", false, MPDServerInfo::Mopidy, "Mopidy" },
2617     // ubuntu 16.10
2618     { "OK", false, MPDServerInfo::ForkedDaapd, "forked-daapd" }
2619 };
2620 
detect()2621 void MPDServerInfo::detect() {
2622     MPDConnection *conn;
2623 
2624     if (!isUndetermined()) {
2625         return;
2626     }
2627 
2628     conn = MPDConnection::self();
2629 
2630     if (isUndetermined()) {
2631         MPDConnection::Response response=conn->sendCommand("stats");
2632         if (response.ok) {
2633             MPDStatsValues stats=MPDParseUtils::parseStats(response.data);
2634             if (0==stats.artists && 0==stats.albums && 0==stats.songs
2635                 && 0==stats.uptime && 0==stats.playtime && 0==stats.dbPlaytime
2636                 && 0==stats.dbUpdate) {
2637                 setServerType(Mopidy);
2638                 serverName = "Mopidy";
2639             }
2640         }
2641     }
2642 
2643     if (isUndetermined()) {
2644         MPDConnection::Response response=conn->sendCommand(lsinfoCommand, false, false);
2645         QList<QByteArray> lines = response.data.split('\n');
2646         bool match = false;
2647         int indx;
2648         for (const QByteArray &line: lines) {
2649             for (indx=0; indx<sizeof(lsinfoResponseParameters)/sizeof(ResponseParameter); ++indx) {
2650                 ResponseParameter &rp = lsinfoResponseParameters[indx];
2651                 if (rp.isSubstring) {
2652                     match = line.toLower().contains(rp.response.toLower());
2653                 } else {
2654                     match = line.toLower() == rp.response.toLower();
2655                 }
2656                 if (match) {
2657                     setServerType(rp.serverType);
2658                     serverName = rp.name;
2659                     break;
2660                 }
2661             }
2662             // first line is currently enough
2663             break;
2664         }
2665     }
2666 
2667     if (isUndetermined()) {
2668         // Default to MPD if cannot determine otherwise. Cantata is an *MPD* client first and foremost.
2669         setServerType(Mpd);
2670         serverName = "MPD";
2671     }
2672 
2673     DBUG << "detected serverType:" << getServerName() << "(" << getServerType() << ")";
2674 
2675     if (isMopidy()) {
2676         topLevelLsinfo = "lsinfo \"Local media\"";
2677     }
2678 
2679     if (isForkedDaapd()) {
2680         topLevelLsinfo = "lsinfo file:";
2681 
2682         QByteArray message = "sendmessage rating \"";
2683         message += "rating ";                           // sticker name
2684         message += QString().number(Song::Rating_Max);  // max rating
2685         message += " ";
2686         message += QString().number(Song::Rating_Step); // rating step (optional)
2687         message += "\"";
2688         conn->sendCommand(message, false, false);
2689     }
2690 }
2691 
reset()2692 void MPDServerInfo::reset() {
2693     setServerType(MPDServerInfo::Undetermined);
2694     serverName = "undetermined";
2695     topLevelLsinfo = "lsinfo";
2696 }
2697 
2698 #include "moc_mpdconnection.cpp"
2699