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