2356 lines
75 KiB
C++
2356 lines
75 KiB
C++
/*
|
|
* Cantata
|
|
*
|
|
* Copyright (c) 2011-2017 Craig Drummond <craig.p.drummond@gmail.com>
|
|
*
|
|
*/
|
|
/*
|
|
* Copyright (c) 2008 Sander Knopper (sander AT knopper DOT tk) and
|
|
* Roeland Douma (roeland AT rullzer DOT com)
|
|
*
|
|
* This file is part of QtMPC.
|
|
*
|
|
* QtMPC is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* QtMPC is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with QtMPC. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "mpdconnection.h"
|
|
#include "mpdparseutils.h"
|
|
#include "models/streamsmodel.h"
|
|
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
|
|
#include "mpduser.h"
|
|
#include "gui/settings.h"
|
|
#endif
|
|
#include "support/globalstatic.h"
|
|
#include "support/configuration.h"
|
|
#include <QStringList>
|
|
#include <QTimer>
|
|
#include <QDir>
|
|
#include <QHostInfo>
|
|
#include <QDateTime>
|
|
#include <QPropertyAnimation>
|
|
#include <QCoreApplication>
|
|
#include <QUdpSocket>
|
|
#include "support/thread.h"
|
|
#include "cuefile.h"
|
|
#if defined Q_OS_LINUX && defined QT_QTDBUS_FOUND
|
|
#include "dbus/powermanagement.h"
|
|
#elif defined Q_OS_MAC && defined IOKIT_FOUND
|
|
#include "mac/powermanagement.h"
|
|
#endif
|
|
#include <QDebug>
|
|
static bool debugEnabled=false;
|
|
#define DBUG if (debugEnabled) qWarning() << "MPDConnection" << QThread::currentThreadId()
|
|
void MPDConnection::enableDebug()
|
|
{
|
|
debugEnabled=true;
|
|
}
|
|
|
|
// Uncomment the following to report error strings in MPDStatus to the UI
|
|
// ...disabled, as stickers (for ratings) can cause lots of errors to be reported - and these all need clearing, etc.
|
|
// #define REPORT_MPD_ERRORS
|
|
static const int constSocketCommsTimeout=250
|
|
;
|
|
static const int constMaxReadAttempts=4;
|
|
static int maxFilesPerAddCommand=1000;
|
|
static int seekStep=5;
|
|
static int constConnTimer=5000;
|
|
|
|
static const QByteArray constOkValue("OK");
|
|
static const QByteArray constOkMpdValue("OK MPD");
|
|
static const QByteArray constOkNlValue("OK\n");
|
|
static const QByteArray constAckValue("ACK");
|
|
static const QByteArray constIdleChangedKey("changed: ");
|
|
static const QByteArray constIdleDbValue("database");
|
|
static const QByteArray constIdleUpdateValue("update");
|
|
static const QByteArray constIdleStoredPlaylistValue("stored_playlist");
|
|
static const QByteArray constIdlePlaylistValue("playlist");
|
|
static const QByteArray constIdlePlayerValue("player");
|
|
static const QByteArray constIdleMixerValue("mixer");
|
|
static const QByteArray constIdleOptionsValue("options");
|
|
static const QByteArray constIdleOutputValue("output");
|
|
static const QByteArray constIdleStickerValue("sticker");
|
|
static const QByteArray constIdleSubscriptionValue("subscription");
|
|
static const QByteArray constIdleMessageValue("message");
|
|
static const QByteArray constDynamicIn("cantata-dynamic-in");
|
|
static const QByteArray constDynamicOut("cantata-dynamic-out");
|
|
static const QByteArray constRatingSticker("rating");
|
|
|
|
static inline int socketTimeout(int dataSize)
|
|
{
|
|
static const int constDataBlock=512;
|
|
return ((dataSize/constDataBlock)+((dataSize%constDataBlock) ? 1 : 0))*constSocketCommsTimeout;
|
|
}
|
|
|
|
static QByteArray log(const QByteArray &data)
|
|
{
|
|
if (data.length()<256) {
|
|
return data;
|
|
} else {
|
|
return data.left(96) + "... ..." + data.right(96) + " (" + QByteArray::number(data.length()) + " bytes)";
|
|
}
|
|
}
|
|
|
|
GLOBAL_STATIC(MPDConnection, instance)
|
|
|
|
const QString MPDConnection::constModifiedSince=QLatin1String("modified-since");
|
|
const int MPDConnection::constMaxPqChanges=1000;
|
|
const QString MPDConnection::constStreamsPlayListName=QLatin1String("[Radio Streams]");
|
|
const QString MPDConnection::constPlaylistPrefix=QLatin1String("playlist:");
|
|
const QString MPDConnection::constDirPrefix=QLatin1String("dir:");
|
|
|
|
QByteArray MPDConnection::quote(int val)
|
|
{
|
|
return '\"'+QByteArray::number(val)+'\"';
|
|
}
|
|
|
|
QByteArray MPDConnection::encodeName(const QString &name)
|
|
{
|
|
return '\"'+name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"")+'\"';
|
|
}
|
|
|
|
static QByteArray readFromSocket(MpdSocket &socket, int timeout=constSocketCommsTimeout)
|
|
{
|
|
QByteArray data;
|
|
int attempt=0;
|
|
while (QAbstractSocket::ConnectedState==socket.state()) {
|
|
while (0==socket.bytesAvailable() && QAbstractSocket::ConnectedState==socket.state()) {
|
|
DBUG << (void *)(&socket) << "Waiting for read data, attempt" << attempt;
|
|
if (socket.waitForReadyRead(timeout)) {
|
|
break;
|
|
}
|
|
DBUG << (void *)(&socket) << "Wait for read failed - " << socket.errorString();
|
|
if (++attempt>=constMaxReadAttempts) {
|
|
DBUG << "ERROR: Timedout waiting for response";
|
|
socket.close();
|
|
return QByteArray();
|
|
}
|
|
}
|
|
|
|
data.append(socket.readAll());
|
|
|
|
if (data.endsWith(constOkNlValue) || data.startsWith(constOkValue) || data.startsWith(constAckValue)) {
|
|
break;
|
|
}
|
|
}
|
|
DBUG << (void *)(&socket) << "Read:" << log(data) << ", socket state:" << socket.state();
|
|
|
|
return data;
|
|
}
|
|
|
|
static MPDConnection::Response readReply(MpdSocket &socket, int timeout=constSocketCommsTimeout)
|
|
{
|
|
QByteArray data = readFromSocket(socket, timeout);
|
|
return MPDConnection::Response(data.endsWith(constOkNlValue), data);
|
|
}
|
|
|
|
MPDConnection::Response::Response(bool o, const QByteArray &d)
|
|
: ok(o)
|
|
, data(d)
|
|
{
|
|
}
|
|
|
|
QString MPDConnection::Response::getError(const QByteArray &command)
|
|
{
|
|
if (ok || data.isEmpty()) {
|
|
return QString();
|
|
}
|
|
|
|
if (!ok && data.size()>0) {
|
|
int cmdEnd=data.indexOf("} ");
|
|
if (-1==cmdEnd) {
|
|
return tr("Unknown")+QLatin1String(" (")+command+QLatin1Char(')');
|
|
} else {
|
|
cmdEnd+=2;
|
|
QString rv=data.mid(cmdEnd, data.length()-(data.endsWith('\n') ? cmdEnd+1 : cmdEnd));
|
|
if (data.contains("{listplaylists}")) {
|
|
// NOTE: NOT translated, as refers to config item
|
|
return QLatin1String("playlist_directory - ")+rv;
|
|
}
|
|
|
|
// If we are reporting a stream error, remove any stream name added by Cantata...
|
|
int start=rv.indexOf(QLatin1String("http://"));
|
|
if (start>0) {
|
|
int pos=rv.indexOf(QChar('#'), start+6);
|
|
if (-1!=pos) {
|
|
rv=rv.left(pos);
|
|
}
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
MPDConnectionDetails::MPDConnectionDetails()
|
|
: port(6600)
|
|
, dirReadable(false)
|
|
{
|
|
}
|
|
|
|
QString MPDConnectionDetails::getName() const
|
|
{
|
|
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
|
|
return name.isEmpty() ? QObject::tr("Default") : (name==MPDUser::constName ? MPDUser::translatedName() : name);
|
|
#else
|
|
return name.isEmpty() ? QObject::tr("Default") : name;
|
|
#endif
|
|
}
|
|
|
|
QString MPDConnectionDetails::description() const
|
|
{
|
|
if (hostname.isEmpty()) {
|
|
return getName();
|
|
} else if (hostname.startsWith('/') || hostname.startsWith('~')) {
|
|
return getName();
|
|
} else {
|
|
return QObject::tr("\"%1\" (%2:%3)", "name (host:port)").arg(getName()).arg(hostname).arg(QString::number(port));
|
|
}
|
|
}
|
|
|
|
void MPDConnectionDetails::setDirReadable()
|
|
{
|
|
dirReadable=Utils::isDirReadable(dir);
|
|
}
|
|
|
|
MPDConnection::MPDConnection()
|
|
: thread(0)
|
|
, ver(0)
|
|
, canUseStickers(false)
|
|
, sock(this)
|
|
, idleSocket(this)
|
|
, lastStatusPlayQueueVersion(0)
|
|
, lastUpdatePlayQueueVersion(0)
|
|
, state(State_Blank)
|
|
, isListingMusic(false)
|
|
, reconnectTimer(0)
|
|
, reconnectStart(0)
|
|
, stopAfterCurrent(false)
|
|
, currentSongId(-1)
|
|
, songPos(0)
|
|
, unmuteVol(-1)
|
|
, mopidy(false)
|
|
, isUpdatingDb(false)
|
|
, volumeFade(0)
|
|
, fadeDuration(0)
|
|
, restoreVolume(-1)
|
|
{
|
|
qRegisterMetaType<time_t>("time_t");
|
|
qRegisterMetaType<Song>("Song");
|
|
qRegisterMetaType<Output>("Output");
|
|
qRegisterMetaType<Playlist>("Playlist");
|
|
qRegisterMetaType<QList<Song> >("QList<Song>");
|
|
qRegisterMetaType<QList<Output> >("QList<Output>");
|
|
qRegisterMetaType<QList<Playlist> >("QList<Playlist>");
|
|
qRegisterMetaType<QList<quint32> >("QList<quint32>");
|
|
qRegisterMetaType<QList<qint32> >("QList<qint32>");
|
|
qRegisterMetaType<QList<quint8> >("QList<quint8>");
|
|
qRegisterMetaType<QSet<qint32> >("QSet<qint32>");
|
|
qRegisterMetaType<QSet<QString> >("QSet<QString>");
|
|
qRegisterMetaType<QAbstractSocket::SocketState >("QAbstractSocket::SocketState");
|
|
qRegisterMetaType<MPDStatsValues>("MPDStatsValues");
|
|
qRegisterMetaType<MPDStatusValues>("MPDStatusValues");
|
|
qRegisterMetaType<MPDConnectionDetails>("MPDConnectionDetails");
|
|
qRegisterMetaType<Stream>("Stream");
|
|
qRegisterMetaType<QList<Stream> >("QList<Stream>");
|
|
#if (defined Q_OS_LINUX && defined QT_QTDBUS_FOUND) || (defined Q_OS_MAC && defined IOKIT_FOUND)
|
|
connect(PowerManagement::self(), SIGNAL(resuming()), this, SLOT(reconnect()));
|
|
#endif
|
|
Configuration cfg;
|
|
maxFilesPerAddCommand=cfg.get("mpdListSize", 10000, 100, 65535);
|
|
seekStep=cfg.get("seekStep", 5, 2, 60);
|
|
MPDParseUtils::setSingleTracksFolders(cfg.get("singleTracksFolders", QStringList()).toSet());
|
|
}
|
|
|
|
MPDConnection::~MPDConnection()
|
|
{
|
|
if (State_Connected==state && fadingVolume()) {
|
|
sendCommand("stop");
|
|
stopVolumeFade();
|
|
}
|
|
// disconnect(&sock, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
|
|
disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
|
|
disconnect(&idleSocket, SIGNAL(readyRead()), this, SLOT(idleDataReady()));
|
|
sock.disconnectFromHost();
|
|
idleSocket.disconnectFromHost();
|
|
}
|
|
|
|
void MPDConnection::start()
|
|
{
|
|
if (!thread) {
|
|
thread=new Thread(metaObject()->className());
|
|
connTimer=thread->createTimer(this);
|
|
connTimer->setSingleShot(false);
|
|
moveToThread(thread);
|
|
connect(thread, SIGNAL(finished()), connTimer, SLOT(stop()));
|
|
connect(connTimer, SIGNAL(timeout()), SLOT(getStatus()));
|
|
thread->start();
|
|
}
|
|
}
|
|
|
|
void MPDConnection::stop()
|
|
{
|
|
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
|
|
if (details.name==MPDUser::constName && Settings::self()->stopOnExit()) {
|
|
MPDUser::self()->stop();
|
|
}
|
|
#endif
|
|
|
|
if (thread) {
|
|
thread->deleteTimer(connTimer);
|
|
connTimer=0;
|
|
thread->stop();
|
|
thread=0;
|
|
}
|
|
}
|
|
|
|
bool MPDConnection::localFilePlaybackSupported() const
|
|
{
|
|
return details.isLocal() ||
|
|
(ver>=CANTATA_MAKE_VERSION(0, 19, 0) && /*handlers.contains(QLatin1String("file")) &&*/
|
|
(QLatin1String("127.0.0.1")==details.hostname || QLatin1String("localhost")==details.hostname));
|
|
}
|
|
|
|
MPDConnection::ConnectionReturn MPDConnection::connectToMPD(MpdSocket &socket, bool enableIdle)
|
|
{
|
|
if (QAbstractSocket::ConnectedState!=socket.state()) {
|
|
DBUG << (void *)(&socket) << "Connecting" << (enableIdle ? "(idle)" : "(std)");
|
|
if (details.isEmpty()) {
|
|
DBUG << "no hostname and/or port supplied.";
|
|
return Failed;
|
|
}
|
|
|
|
socket.connectToHost(details.hostname, details.port);
|
|
if (socket.waitForConnected(constSocketCommsTimeout)) {
|
|
DBUG << (void *)(&socket) << "established";
|
|
QByteArray recvdata = readFromSocket(socket);
|
|
|
|
if (recvdata.isEmpty()) {
|
|
DBUG << (void *)(&socket) << "Couldn't connect";
|
|
return Failed;
|
|
}
|
|
|
|
if (recvdata.startsWith(constOkMpdValue)) {
|
|
DBUG << (void *)(&socket) << "Received identification string";
|
|
}
|
|
|
|
lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=0;
|
|
playQueueIds.clear();
|
|
emit cantataStreams(QList<Song>(), false);
|
|
int min, maj, patch;
|
|
if (3==sscanf(&(recvdata.constData()[7]), "%3d.%3d.%3d", &maj, &min, &patch)) {
|
|
long v=((maj&0xFF)<<16)+((min&0xFF)<<8)+(patch&0xFF);
|
|
if (v!=ver) {
|
|
ver=v;
|
|
}
|
|
}
|
|
|
|
recvdata.clear();
|
|
|
|
if (!details.password.isEmpty()) {
|
|
DBUG << (void *)(&socket) << "setting password...";
|
|
socket.write("password "+details.password.toUtf8()+'\n');
|
|
socket.waitForBytesWritten(constSocketCommsTimeout);
|
|
if (!readReply(socket).ok) {
|
|
DBUG << (void *)(&socket) << "password rejected";
|
|
socket.close();
|
|
return IncorrectPassword;
|
|
}
|
|
}
|
|
|
|
if (enableIdle) {
|
|
dynamicId.clear();
|
|
setupRemoteDynamic();
|
|
connect(&socket, SIGNAL(readyRead()), this, SLOT(idleDataReady()), Qt::QueuedConnection);
|
|
connect(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)), Qt::QueuedConnection);
|
|
DBUG << (void *)(&socket) << "Enabling idle";
|
|
socket.write("idle\n");
|
|
socket.waitForBytesWritten();
|
|
}
|
|
return Success;
|
|
} else {
|
|
DBUG << (void *)(&socket) << "Couldn't connect - " << socket.errorString() << socket.error();
|
|
return convertSocketCode(socket);
|
|
}
|
|
}
|
|
|
|
// DBUG << "Already connected" << (enableIdle ? "(idle)" : "(std)");
|
|
return Success;
|
|
}
|
|
|
|
MPDConnection::ConnectionReturn MPDConnection::convertSocketCode(MpdSocket &socket)
|
|
{
|
|
switch (socket.error()) {
|
|
case QAbstractSocket::ProxyAuthenticationRequiredError:
|
|
case QAbstractSocket::ProxyConnectionRefusedError:
|
|
case QAbstractSocket::ProxyConnectionClosedError:
|
|
case QAbstractSocket::ProxyConnectionTimeoutError:
|
|
case QAbstractSocket::ProxyNotFoundError:
|
|
case QAbstractSocket::ProxyProtocolError:
|
|
return MPDConnection::ProxyError;
|
|
default:
|
|
if (QNetworkProxy::DefaultProxy!=socket.proxyType() && QNetworkProxy::NoProxy!=socket.proxyType()) {
|
|
return MPDConnection::ProxyError;
|
|
}
|
|
if (socket.errorString().contains(QLatin1String("proxy"), Qt::CaseInsensitive)) {
|
|
return MPDConnection::ProxyError;
|
|
}
|
|
return MPDConnection::Failed;
|
|
}
|
|
}
|
|
|
|
QString MPDConnection::errorString(ConnectionReturn status) const
|
|
{
|
|
switch (status) {
|
|
default:
|
|
case Success: return QString();
|
|
case Failed: return tr("Connection to %1 failed").arg(details.description());
|
|
case ProxyError: return tr("Connection to %1 failed - please check your proxy settings").arg(details.description());
|
|
case IncorrectPassword: return tr("Connection to %1 failed - incorrect password").arg(details.description());
|
|
}
|
|
}
|
|
|
|
MPDConnection::ConnectionReturn MPDConnection::connectToMPD()
|
|
{
|
|
connTimer->stop();
|
|
if (State_Connected==state && (QAbstractSocket::ConnectedState!=sock.state() || QAbstractSocket::ConnectedState!=idleSocket.state())) {
|
|
DBUG << "Something has gone wrong with sockets, so disconnect";
|
|
disconnectFromMPD();
|
|
}
|
|
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
|
|
int maxConnAttempts=MPDUser::constName==details.name ? 2 : 1;
|
|
#else
|
|
int maxConnAttempts=1;
|
|
#endif
|
|
ConnectionReturn status=Failed;
|
|
for (int connAttempt=0; connAttempt<maxConnAttempts; ++connAttempt) {
|
|
if (Success==(status=connectToMPD(sock)) && Success==(status=connectToMPD(idleSocket, true))) {
|
|
state=State_Connected;
|
|
emit socketAddress(sock.address());
|
|
} else {
|
|
disconnectFromMPD();
|
|
state=State_Disconnected;
|
|
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
|
|
if (0==connAttempt && MPDUser::constName==details.name) {
|
|
DBUG << "Restart personal mpd";
|
|
MPDUser::self()->stop();
|
|
MPDUser::self()->start();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
connTimer->start(constConnTimer);
|
|
return status;
|
|
}
|
|
|
|
void MPDConnection::disconnectFromMPD()
|
|
{
|
|
DBUG << "disconnectFromMPD";
|
|
connTimer->stop();
|
|
disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
|
|
disconnect(&idleSocket, SIGNAL(readyRead()), this, SLOT(idleDataReady()));
|
|
if (QAbstractSocket::ConnectedState==sock.state()) {
|
|
sock.disconnectFromHost();
|
|
}
|
|
if (QAbstractSocket::ConnectedState==idleSocket.state()) {
|
|
idleSocket.disconnectFromHost();
|
|
}
|
|
sock.close();
|
|
idleSocket.close();
|
|
state=State_Disconnected;
|
|
ver=0;
|
|
playQueueIds.clear();
|
|
streamIds.clear();
|
|
lastStatusPlayQueueVersion=0;
|
|
lastUpdatePlayQueueVersion=0;
|
|
currentSongId=0;
|
|
songPos=0;
|
|
mopidy=false;
|
|
isUpdatingDb=false;
|
|
emit socketAddress(QString());
|
|
}
|
|
|
|
// This function is mainly intended to be called after the computer (laptop) has been 'resumed'
|
|
void MPDConnection::reconnect()
|
|
{
|
|
if (reconnectTimer && reconnectTimer->isActive()) {
|
|
return;
|
|
}
|
|
if (0==reconnectStart) {
|
|
if (isConnected()) {
|
|
disconnectFromMPD();
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (isConnected()) { // Perhaps the user pressed a button which caused the reconnect???
|
|
reconnectStart=0;
|
|
return;
|
|
}
|
|
|
|
time_t now=time(NULL);
|
|
ConnectionReturn status=connectToMPD();
|
|
switch (status) {
|
|
case Success:
|
|
getStatus();
|
|
getStats();
|
|
getUrlHandlers();
|
|
getTagTypes();
|
|
getStickerSupport();
|
|
playListInfo();
|
|
outputs();
|
|
reconnectStart=0;
|
|
emit stateChanged(true);
|
|
break;
|
|
case Failed:
|
|
if (0==reconnectStart || abs(now-reconnectStart)<15) {
|
|
if (0==reconnectStart) {
|
|
reconnectStart=now;
|
|
}
|
|
if (!reconnectTimer) {
|
|
reconnectTimer=new QTimer(this);
|
|
reconnectTimer->setSingleShot(true);
|
|
connect(reconnectTimer, SIGNAL(timeout()), this, SLOT(reconnect()), Qt::QueuedConnection);
|
|
}
|
|
if (abs(now-reconnectStart)>1) {
|
|
emit info(tr("Connecting to %1").arg(details.description()));
|
|
}
|
|
reconnectTimer->start(500);
|
|
} else {
|
|
emit stateChanged(false);
|
|
emit error(errorString(Failed), true);
|
|
reconnectStart=0;
|
|
}
|
|
break;
|
|
default:
|
|
emit stateChanged(false);
|
|
emit error(errorString(status), true);
|
|
reconnectStart=0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void MPDConnection::setDetails(const MPDConnectionDetails &d)
|
|
{
|
|
// Can't change connection whilst listing music collection...
|
|
if (isListingMusic) {
|
|
emit connectionNotChanged(details.name);
|
|
return;
|
|
}
|
|
|
|
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
|
|
bool isUser=d.name==MPDUser::constName;
|
|
const MPDConnectionDetails &det=isUser ? MPDUser::self()->details(true) : d;
|
|
#else
|
|
const MPDConnectionDetails &det=d;
|
|
#endif
|
|
bool changedDir=det.dir!=details.dir;
|
|
bool diffName=det.name!=details.name;
|
|
bool diffDetails=det!=details;
|
|
#ifdef ENABLE_HTTP_STREAM_PLAYBACK
|
|
bool diffStreamUrl=det.streamUrl!=details.streamUrl;
|
|
#endif
|
|
|
|
details=det;
|
|
|
|
if (diffDetails || State_Connected!=state) {
|
|
emit connectionChanged(details);
|
|
DBUG << "setDetails" << details.hostname << details.port << (details.password.isEmpty() ? false : true);
|
|
if (State_Connected==state && fadingVolume()) {
|
|
sendCommand("stop");
|
|
stopVolumeFade();
|
|
}
|
|
disconnectFromMPD();
|
|
DBUG << "call connectToMPD";
|
|
unmuteVol=-1;
|
|
toggleStopAfterCurrent(false);
|
|
mopidy=false;
|
|
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
|
|
if (isUser) {
|
|
MPDUser::self()->start();
|
|
}
|
|
#endif
|
|
if (isUpdatingDb) {
|
|
isUpdatingDb=false;
|
|
emit updatedDatabase(); // Stop any spinners...
|
|
}
|
|
ConnectionReturn status=connectToMPD();
|
|
switch (status) {
|
|
case Success:
|
|
getStatus();
|
|
getStats();
|
|
getUrlHandlers();
|
|
getTagTypes();
|
|
getStickerSupport();
|
|
determineIfaceIp();
|
|
emit stateChanged(true);
|
|
break;
|
|
default:
|
|
emit error(errorString(status), true);
|
|
}
|
|
} else if (diffName) {
|
|
emit stateChanged(true);
|
|
}
|
|
#ifdef ENABLE_HTTP_STREAM_PLAYBACK
|
|
if (diffStreamUrl) {
|
|
emit streamUrl(details.streamUrl);
|
|
}
|
|
#endif
|
|
if (changedDir) {
|
|
emit dirChanged();
|
|
}
|
|
}
|
|
|
|
//void MPDConnection::disconnectMpd()
|
|
//{
|
|
// if (State_Connected==state) {
|
|
// disconnectFromMPD();
|
|
// emit stateChanged(false);
|
|
// }
|
|
//}
|
|
|
|
MPDConnection::Response MPDConnection::sendCommand(const QByteArray &command, bool emitErrors, bool retry)
|
|
{
|
|
connTimer->stop();
|
|
static bool reconnected=false; // If we reconnect, and send playlistinfo - dont want that call causing reconnects, and recursion!
|
|
DBUG << (void *)(&sock) << "sendCommand:" << log(command) << emitErrors << retry;
|
|
|
|
if (!isConnected()) {
|
|
emit error(tr("Failed to send command to %1 - not connected").arg(details.description()), true);
|
|
return Response(false);
|
|
}
|
|
|
|
if (QAbstractSocket::ConnectedState!=sock.state() && !reconnected) {
|
|
DBUG << (void *)(&sock) << "Socket (state:" << sock.state() << ") need to reconnect";
|
|
sock.close();
|
|
ConnectionReturn status=connectToMPD();
|
|
if (Success!=status) {
|
|
disconnectFromMPD();
|
|
emit stateChanged(false);
|
|
emit error(errorString(status), true);
|
|
return Response(false);
|
|
} else {
|
|
// Refresh playqueue...
|
|
reconnected=true;
|
|
playListInfo();
|
|
getStatus();
|
|
reconnected=false;
|
|
}
|
|
}
|
|
|
|
Response response;
|
|
if (-1==sock.write(command+'\n')) {
|
|
DBUG << "Failed to write";
|
|
// If we fail to write, dont wait for bytes to be written!!
|
|
response=Response(false);
|
|
sock.close();
|
|
} else {
|
|
int timeout=socketTimeout(command.length());
|
|
DBUG << "Timeout (ms):" << timeout;
|
|
sock.waitForBytesWritten(timeout);
|
|
DBUG << "Socket state after write:" << (int)sock.state();
|
|
response=readReply(sock, timeout);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
DBUG << log(command) << "failed";
|
|
if (response.data.isEmpty() && retry && QAbstractSocket::ConnectedState!=sock.state() && !reconnected) {
|
|
// Try one more time...
|
|
// This scenario, where socket seems to be closed during/after 'write' seems to occur more often
|
|
// when dynamizer is running. However, simply reconnecting seems to resolve the issue.
|
|
return sendCommand(command, emitErrors, false);
|
|
}
|
|
clearError();
|
|
if (emitErrors) {
|
|
bool emitError=true;
|
|
// Mopidy returns "incorrect arguments" for commands it does not support. The docs state that crossfade and replaygain mode
|
|
// setting commands are not supported. So, if we get this error then just ignore it.
|
|
if (mopidy && (command.startsWith("crossfade ") || command.startsWith("replay_gain_mode "))) {
|
|
emitError=false;
|
|
}
|
|
if (emitError) {
|
|
if ((command.startsWith("add ") || command.startsWith("command_list_begin\nadd ")) && -1!=command.indexOf("\"file:///")) {
|
|
if (details.isLocal() && -1!=response.data.indexOf("Permission denied")) {
|
|
emit error(tr("Failed to load. Please check user \"mpd\" has read permission."));
|
|
} else if (!details.isLocal() && -1!=response.data.indexOf("Access denied")) {
|
|
emit error(tr("Failed to load. MPD can only play local files if connected via a local socket."));
|
|
} else if (!response.getError(command).isEmpty()) {
|
|
emit error(tr("MPD reported the following error: %1").arg(response.getError(command)));
|
|
} else {
|
|
disconnectFromMPD();
|
|
emit stateChanged(false);
|
|
emit error(tr("Failed to send command. Disconnected from %1").arg(details.description()), true);
|
|
}
|
|
} else if (!response.getError(command).isEmpty()) {
|
|
emit error(tr("MPD reported the following error: %1").arg(response.getError(command)));
|
|
} /*else if ("listallinfo"==command && ver>=CANTATA_MAKE_VERSION(0,18,0)) {
|
|
disconnectFromMPD();
|
|
emit stateChanged(false);
|
|
emit error(tr("Failed to load library. Please increase \"max_output_buffer_size\" in MPD's config file."));
|
|
} */ else {
|
|
disconnectFromMPD();
|
|
emit stateChanged(false);
|
|
emit error(tr("Failed to send command. Disconnected from %1").arg(details.description()), true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DBUG << (void *)(&sock) << "sendCommand - sent";
|
|
if (QAbstractSocket::ConnectedState==sock.state()) {
|
|
connTimer->start(constConnTimer);
|
|
} else {
|
|
connTimer->stop();
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/*
|
|
* Playlist commands
|
|
*/
|
|
bool MPDConnection::isPlaylist(const QString &file)
|
|
{
|
|
return file.endsWith(QLatin1String(".asx"), Qt::CaseInsensitive) || file.endsWith(QLatin1String(".cue"), Qt::CaseInsensitive) ||
|
|
file.endsWith(QLatin1String(".m3u"), Qt::CaseInsensitive) || file.endsWith(QLatin1String(".pls"), Qt::CaseInsensitive) ||
|
|
file.endsWith(QLatin1String(".xspf"), Qt::CaseInsensitive);
|
|
}
|
|
|
|
void MPDConnection::add(const QStringList &files, int action, quint8 priority)
|
|
{
|
|
add(files, 0, 0, action, priority);
|
|
}
|
|
|
|
void MPDConnection::add(const QStringList &files, quint32 pos, quint32 size, int action, quint8 priority)
|
|
{
|
|
QList<quint8> prioList;
|
|
if (priority>0) {
|
|
prioList << priority;
|
|
}
|
|
add(files, pos, size, action, prioList);
|
|
}
|
|
|
|
void MPDConnection::add(const QStringList &origList, quint32 pos, quint32 size, int action, const QList<quint8> &priority)
|
|
{
|
|
quint32 playPos=0;
|
|
if (0==pos && 0==size && (AddAfterCurrent==action || AppendAndPlay==action || AddAndPlay==action)) {
|
|
Response response=sendCommand("status");
|
|
if (response.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
|
|
if (AppendAndPlay==action) {
|
|
playPos=sv.playlistLength;
|
|
} else if (AddAndPlay==action) {
|
|
pos=0;
|
|
size=sv.playlistLength;
|
|
} else {
|
|
pos=sv.song+1;
|
|
size=sv.playlistLength;
|
|
}
|
|
}
|
|
}
|
|
toggleStopAfterCurrent(false);
|
|
if (Replace==action || ReplaceAndplay==action) {
|
|
clear();
|
|
getStatus();
|
|
}
|
|
|
|
QStringList files;
|
|
foreach (const QString &file, origList) {
|
|
if (file.startsWith(constDirPrefix)) {
|
|
files+=getAllFiles(file.mid(constDirPrefix.length()));
|
|
} else if (file.startsWith(constPlaylistPrefix)) {
|
|
files+=getPlaylistFiles(file.mid(constPlaylistPrefix.length()));
|
|
} else {
|
|
files.append(file);
|
|
}
|
|
}
|
|
|
|
QList<QStringList> fileLists;
|
|
if (priority.count()<=1 && files.count()>maxFilesPerAddCommand) {
|
|
int numChunks=(files.count()/maxFilesPerAddCommand)+(files.count()%maxFilesPerAddCommand ? 1 : 0);
|
|
for (int i=0; i<numChunks; ++i) {
|
|
fileLists.append(files.mid(i*maxFilesPerAddCommand, maxFilesPerAddCommand));
|
|
}
|
|
} else {
|
|
fileLists.append(files);
|
|
}
|
|
|
|
int curSize = size;
|
|
int curPos = pos;
|
|
// bool addedFile=false;
|
|
bool havePlaylist=false;
|
|
bool usePrio=!priority.isEmpty() && canUsePriority() && (1==priority.count() || priority.count()==files.count());
|
|
quint8 singlePrio=usePrio && 1==priority.count() ? priority.at(0) : 0;
|
|
QStringList cStreamFiles;
|
|
bool sentOk=false;
|
|
|
|
if (usePrio && Append==action && 0==curPos) {
|
|
curPos=playQueueIds.size();
|
|
}
|
|
|
|
foreach (const QStringList &files, fileLists) {
|
|
QByteArray send = "command_list_begin\n";
|
|
|
|
for (int i = 0; i < files.size(); i++) {
|
|
QString fileName=files.at(i);
|
|
if (fileName.startsWith(QLatin1String("http://")) && fileName.contains(QLatin1String("cantata=song"))) {
|
|
cStreamFiles.append(fileName);
|
|
}
|
|
if (CueFile::isCue(fileName)) {
|
|
send += "load "+CueFile::getLoadLine(fileName)+'\n';
|
|
} else {
|
|
if (isPlaylist(fileName)) {
|
|
send+="load ";
|
|
havePlaylist=true;
|
|
} else {
|
|
// addedFile=true;
|
|
send += "add ";
|
|
}
|
|
send += encodeName(fileName)+'\n';
|
|
}
|
|
if (!havePlaylist) {
|
|
if (0!=size) {
|
|
send += "move "+quote(curSize)+" "+quote(curPos)+'\n';
|
|
}
|
|
if (usePrio && !havePlaylist) {
|
|
send += "prio "+quote(singlePrio || i>=priority.count() ? singlePrio : priority.at(i))+" "+quote(curPos)+" "+quote(curPos)+'\n';
|
|
}
|
|
}
|
|
curSize++;
|
|
curPos++;
|
|
}
|
|
|
|
send += "command_list_end";
|
|
sentOk=sendCommand(send).ok;
|
|
if (!sentOk) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (sentOk) {
|
|
if (!cStreamFiles.isEmpty()) {
|
|
emit cantataStreams(cStreamFiles);
|
|
}
|
|
|
|
if ((ReplaceAndplay==action || AddAndPlay==action) /*&& addedFile */&& !files.isEmpty()) {
|
|
// Dont emit error if play fails, might be that playlist was not loaded...
|
|
playFirstTrack(false);
|
|
}
|
|
|
|
if (AppendAndPlay==action) {
|
|
startPlayingSong(playPos);
|
|
}
|
|
emit added(files);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::populate(const QStringList &files, const QList<quint8> &priority)
|
|
{
|
|
add(files, 0, 0, Replace, priority);
|
|
}
|
|
|
|
void MPDConnection::addAndPlay(const QString &file)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
Response response=sendCommand("status");
|
|
if (response.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
|
|
QByteArray send = "command_list_begin\n";
|
|
send+="add "+encodeName(file)+'\n';
|
|
send+="play "+quote(sv.playlistLength)+'\n';
|
|
send+="command_list_end";
|
|
sendCommand(send);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::clear()
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
if (sendCommand("clear").ok) {
|
|
lastUpdatePlayQueueVersion=0;
|
|
playQueueIds.clear();
|
|
emit cantataStreams(QList<Song>(), false);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::removeSongs(const QList<qint32> &items)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
QByteArray send = "command_list_begin\n";
|
|
foreach (qint32 i, items) {
|
|
send += "deleteid "+quote(i)+'\n';
|
|
}
|
|
|
|
send += "command_list_end";
|
|
sendCommand(send);
|
|
}
|
|
|
|
void MPDConnection::move(quint32 from, quint32 to)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
sendCommand("move "+quote(from)+' '+quote(to));
|
|
}
|
|
|
|
void MPDConnection::move(const QList<quint32> &items, quint32 pos, quint32 size)
|
|
{
|
|
doMoveInPlaylist(QString(), items, pos, size);
|
|
#if 0
|
|
QByteArray send = "command_list_begin\n";
|
|
QList<quint32> moveItems;
|
|
|
|
moveItems.append(items);
|
|
qSort(moveItems);
|
|
|
|
int posOffset = 0;
|
|
|
|
//first move all items (starting with the biggest) to the end so we don't have to deal with changing rownums
|
|
for (int i = moveItems.size() - 1; i >= 0; i--) {
|
|
if (moveItems.at(i) < pos && moveItems.at(i) != size - 1) {
|
|
// we are moving away an item that resides before the destinatino row, manipulate destination row
|
|
posOffset++;
|
|
}
|
|
send += "move ";
|
|
send += quote(moveItems.at(i));
|
|
send += " ";
|
|
send += quote(size - 1);
|
|
send += '\n';
|
|
}
|
|
//now move all of them to the destination position
|
|
for (int i = moveItems.size() - 1; i >= 0; i--) {
|
|
send += "move ";
|
|
send += quote(size - 1 - i);
|
|
send += " ";
|
|
send += quote(pos - posOffset);
|
|
send += '\n';
|
|
}
|
|
|
|
send += "command_list_end";
|
|
sendCommand(send);
|
|
#endif
|
|
}
|
|
|
|
void MPDConnection::setOrder(const QList<quint32> &items)
|
|
{
|
|
QByteArray cmd("move ");
|
|
QByteArray send;
|
|
QList<qint32> positions;
|
|
quint32 numChanges=0;
|
|
for (qint32 i=0; i<items.count(); ++i) {
|
|
positions.append(i);
|
|
}
|
|
|
|
for (qint32 to=0; to<items.count(); ++to) {
|
|
qint32 from=positions.indexOf(items.at(to));
|
|
if (from!=to) {
|
|
if (send.isEmpty()) {
|
|
send = "command_list_begin\n";
|
|
}
|
|
send += cmd;
|
|
send += quote(from);
|
|
send += " ";
|
|
send += quote(to);
|
|
send += '\n';
|
|
positions.move(from, to);
|
|
numChanges++;
|
|
}
|
|
}
|
|
|
|
if (!send.isEmpty()) {
|
|
send += "command_list_end";
|
|
// If there are more than X changes made to the playqueue, then doing partial updates
|
|
// can hang the UI. Therefore, set lastUpdatePlayQueueVersion to 0 to cause playlistInfo
|
|
// to be used to do a complete update.
|
|
if (sendCommand(send).ok && numChanges>constMaxPqChanges) {
|
|
lastUpdatePlayQueueVersion=0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::shuffle()
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
sendCommand("shuffle");
|
|
}
|
|
|
|
void MPDConnection::shuffle(quint32 from, quint32 to)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
sendCommand("shuffle "+quote(from)+':'+quote(to+1));
|
|
}
|
|
|
|
void MPDConnection::currentSong()
|
|
{
|
|
Response response=sendCommand("currentsong");
|
|
if (response.ok) {
|
|
emit currentSongUpdated(MPDParseUtils::parseSong(response.data, MPDParseUtils::Loc_PlayQueue));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Call "plchangesposid" to recieve a list of positions+ids that have been changed since the last update.
|
|
* If we have ids in this list that we don't know about, then these are new songs - so we call
|
|
* "playlistinfo <pos>" to get the song information.
|
|
*
|
|
* Any songs that are know about, will actually be sent with empty data - as the playqueue model will
|
|
* already hold these songs.
|
|
*/
|
|
void MPDConnection::playListChanges()
|
|
{
|
|
DBUG << "playListChanges" << lastUpdatePlayQueueVersion << playQueueIds.size();
|
|
if (0==lastUpdatePlayQueueVersion || 0==playQueueIds.size()) {
|
|
playListInfo();
|
|
return;
|
|
}
|
|
|
|
QByteArray data = "plchangesposid "+quote(lastUpdatePlayQueueVersion);
|
|
Response status=sendCommand("status"); // We need an updated status so as to detect deletes at end of list...
|
|
Response response=sendCommand(data, false);
|
|
if (response.ok && status.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
|
|
lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist;
|
|
emitStatusUpdated(sv);
|
|
QList<MPDParseUtils::IdPos> changes=MPDParseUtils::parseChanges(response.data);
|
|
if (!changes.isEmpty()) {
|
|
if (changes.count()>constMaxPqChanges) {
|
|
playListInfo();
|
|
return;
|
|
}
|
|
bool first=true;
|
|
quint32 firstPos=0;
|
|
QList<Song> songs;
|
|
QList<Song> newCantataStreams;
|
|
QList<qint32> ids;
|
|
QSet<qint32> prevIds=playQueueIds.toSet();
|
|
QSet<qint32> strmIds;
|
|
|
|
foreach (const MPDParseUtils::IdPos &idp, changes) {
|
|
if (first) {
|
|
first=false;
|
|
firstPos=idp.pos;
|
|
if (idp.pos!=0) {
|
|
for (quint32 i=0; i<idp.pos; ++i) {
|
|
Song s;
|
|
if (i>=(unsigned int)playQueueIds.count()) { // Just for safety...
|
|
playListInfo();
|
|
return;
|
|
}
|
|
s.id=playQueueIds.at(i);
|
|
songs.append(s);
|
|
ids.append(s.id);
|
|
if (streamIds.contains(s.id)) {
|
|
strmIds.insert(s.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prevIds.contains(idp.id) && !streamIds.contains(idp.id)) {
|
|
Song s;
|
|
s.id=idp.id;
|
|
// s.pos=idp.pos;
|
|
songs.append(s);
|
|
} else {
|
|
// New song!
|
|
data = "playlistinfo ";
|
|
data += quote(idp.pos);
|
|
response=sendCommand(data);
|
|
if (!response.ok) {
|
|
playListInfo();
|
|
return;
|
|
}
|
|
Song s=MPDParseUtils::parseSong(response.data, MPDParseUtils::Loc_PlayQueue);
|
|
s.id=idp.id;
|
|
// s.pos=idp.pos;
|
|
songs.append(s);
|
|
if (s.isCdda()) {
|
|
newCantataStreams.append(s);
|
|
} else if (s.isStream()) {
|
|
if (s.isCantataStream()) {
|
|
newCantataStreams.append(s);
|
|
} else {
|
|
strmIds.insert(s.id);
|
|
}
|
|
}
|
|
}
|
|
ids.append(idp.id);
|
|
}
|
|
|
|
// Dont think this section is ever called, but leave here to be safe!!!
|
|
// 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
|
|
if (firstPos+changes.size()<=sv.playlistLength && (sv.playlistLength<=(unsigned int)playQueueIds.length())) {
|
|
for (quint32 i=firstPos+changes.size(); i<sv.playlistLength; ++i) {
|
|
Song s;
|
|
s.id=playQueueIds.at(i);
|
|
songs.append(s);
|
|
ids.append(s.id);
|
|
if (streamIds.contains(s.id)) {
|
|
strmIds.insert(s.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
playQueueIds=ids;
|
|
streamIds=strmIds;
|
|
if (!newCantataStreams.isEmpty()) {
|
|
emit cantataStreams(newCantataStreams, true);
|
|
}
|
|
QSet<qint32> removed=prevIds-playQueueIds.toSet();
|
|
if (!removed.isEmpty()) {
|
|
emit removedIds(removed);
|
|
}
|
|
emit playlistUpdated(songs, false);
|
|
if (songs.isEmpty()) {
|
|
stopVolumeFade();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
playListInfo();
|
|
}
|
|
|
|
void MPDConnection::playListInfo()
|
|
{
|
|
Response response=sendCommand("playlistinfo");
|
|
QList<Song> songs;
|
|
if (response.ok) {
|
|
lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion;
|
|
songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_PlayQueue);
|
|
playQueueIds.clear();
|
|
streamIds.clear();
|
|
|
|
QList<Song> cStreams;
|
|
foreach (const Song &s, songs) {
|
|
playQueueIds.append(s.id);
|
|
if (s.isCdda()) {
|
|
cStreams.append(s);
|
|
} else if (s.isStream()) {
|
|
if (s.isCantataStream()) {
|
|
cStreams.append(s);
|
|
} else {
|
|
streamIds.insert(s.id);
|
|
}
|
|
}
|
|
}
|
|
emit cantataStreams(cStreams, false);
|
|
if (songs.isEmpty()) {
|
|
stopVolumeFade();
|
|
}
|
|
Response status=sendCommand("status");
|
|
if (status.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
|
|
lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist;
|
|
emitStatusUpdated(sv);
|
|
}
|
|
}
|
|
emit playlistUpdated(songs, true);
|
|
}
|
|
|
|
/*
|
|
* Playback commands
|
|
*/
|
|
void MPDConnection::setCrossFade(int secs)
|
|
{
|
|
sendCommand("crossfade "+quote(secs));
|
|
}
|
|
|
|
void MPDConnection::setReplayGain(const QString &v)
|
|
{
|
|
if (replaygainSupported()) {
|
|
sendCommand("replay_gain_mode "+v.toLatin1());
|
|
}
|
|
}
|
|
|
|
void MPDConnection::getReplayGain()
|
|
{
|
|
if (replaygainSupported()) {
|
|
QStringList lines=QString(sendCommand("replay_gain_status").data).split('\n', QString::SkipEmptyParts);
|
|
|
|
if (2==lines.count() && "OK"==lines[1] && lines[0].startsWith(QLatin1String("replay_gain_mode: "))) {
|
|
emit replayGain(lines[0].mid(18));
|
|
} else {
|
|
emit replayGain(QString());
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::goToNext()
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
stopVolumeFade();
|
|
sendCommand("next");
|
|
}
|
|
|
|
static inline QByteArray value(bool b)
|
|
{
|
|
return MPDConnection::quote(b ? 1 : 0);
|
|
}
|
|
|
|
void MPDConnection::setPause(bool toggle)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
stopVolumeFade();
|
|
sendCommand("pause "+value(toggle));
|
|
}
|
|
|
|
void MPDConnection::play()
|
|
{
|
|
playFirstTrack(true);
|
|
}
|
|
|
|
void MPDConnection::playFirstTrack(bool emitErrors)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
stopVolumeFade();
|
|
sendCommand("play 0", emitErrors);
|
|
}
|
|
|
|
void MPDConnection::seek(bool fwd)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
Response response=sendCommand("status");
|
|
if (response.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
|
|
if (fwd) {
|
|
if (sv.timeElapsed+seekStep<sv.timeTotal) {
|
|
setSeek(sv.song, sv.timeElapsed+seekStep);
|
|
} else {
|
|
goToNext();
|
|
}
|
|
} else {
|
|
setSeek(sv.song, sv.timeElapsed>=seekStep ? sv.timeElapsed-seekStep : 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::startPlayingSong(quint32 song)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
sendCommand("play "+quote(song));
|
|
}
|
|
|
|
void MPDConnection::startPlayingSongId(qint32 songId)
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
stopVolumeFade();
|
|
sendCommand("playid "+quote(songId));
|
|
}
|
|
|
|
void MPDConnection::goToPrevious()
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
stopVolumeFade();
|
|
sendCommand("previous");
|
|
}
|
|
|
|
void MPDConnection::setConsume(bool toggle)
|
|
{
|
|
sendCommand("consume "+value(toggle));
|
|
}
|
|
|
|
void MPDConnection::setRandom(bool toggle)
|
|
{
|
|
sendCommand("random "+value(toggle));
|
|
}
|
|
|
|
void MPDConnection::setRepeat(bool toggle)
|
|
{
|
|
sendCommand("repeat "+value(toggle));
|
|
}
|
|
|
|
void MPDConnection::setSingle(bool toggle)
|
|
{
|
|
sendCommand("single "+value(toggle));
|
|
}
|
|
|
|
void MPDConnection::setSeek(quint32 song, quint32 time)
|
|
{
|
|
sendCommand("seek "+quote(song)+' '+quote(time));
|
|
}
|
|
|
|
void MPDConnection::setSeekId(qint32 songId, quint32 time)
|
|
{
|
|
if (-1==songId) {
|
|
songId=currentSongId;
|
|
}
|
|
if (-1==songId) {
|
|
return;
|
|
}
|
|
if (songId!=currentSongId || 0==time) {
|
|
toggleStopAfterCurrent(false);
|
|
}
|
|
if (sendCommand("seekid "+quote(songId)+' '+quote(time)).ok) {
|
|
if (stopAfterCurrent && songId==currentSongId && songPos>time) {
|
|
songPos=time;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::setVolume(int vol) //Range accepted by MPD: 0-100
|
|
{
|
|
if (-1==vol) {
|
|
if (restoreVolume>=0) {
|
|
sendCommand("setvol "+quote(restoreVolume), false);
|
|
}
|
|
if (volumeFade) {
|
|
sendCommand("stop");
|
|
}
|
|
restoreVolume=-1;
|
|
} else if (vol>=0) {
|
|
unmuteVol=-1;
|
|
sendCommand("setvol "+quote(vol), false);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::toggleMute()
|
|
{
|
|
if (unmuteVol>0) {
|
|
sendCommand("setvol "+quote(unmuteVol), false);
|
|
unmuteVol=-1;
|
|
} else {
|
|
Response status=sendCommand("status");
|
|
if (status.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
|
|
if (sv.volume>0) {
|
|
unmuteVol=sv.volume;
|
|
sendCommand("setvol "+quote(0), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::stopPlaying(bool afterCurrent)
|
|
{
|
|
toggleStopAfterCurrent(afterCurrent);
|
|
if (!stopAfterCurrent) {
|
|
if (!startVolumeFade()) {
|
|
sendCommand("stop");
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::clearStopAfter()
|
|
{
|
|
toggleStopAfterCurrent(false);
|
|
}
|
|
|
|
void MPDConnection::getStats()
|
|
{
|
|
Response response=sendCommand("stats");
|
|
if (response.ok) {
|
|
MPDStatsValues stats=MPDParseUtils::parseStats(response.data);
|
|
dbUpdate=stats.dbUpdate;
|
|
mopidy=0==stats.artists && 0==stats.albums && 0==stats.songs &&
|
|
0==stats.uptime && 0==stats.playtime && 0==stats.dbPlaytime && 0==dbUpdate;
|
|
if (mopidy) {
|
|
// Set version to 1 so that SQL cache is updated - it uses 0 as intial value
|
|
dbUpdate=stats.dbUpdate=1;
|
|
}
|
|
emit statsUpdated(stats);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::getStatus()
|
|
{
|
|
Response response=sendCommand("status");
|
|
if (response.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
|
|
lastStatusPlayQueueVersion=sv.playlist;
|
|
if (currentSongId!=sv.songId) {
|
|
stopVolumeFade();
|
|
}
|
|
if (stopAfterCurrent && (currentSongId!=sv.songId || (songPos>0 && sv.timeElapsed<(qint32)songPos))) {
|
|
stopVolumeFade();
|
|
if (sendCommand("stop").ok) {
|
|
sv.state=MPDState_Stopped;
|
|
}
|
|
toggleStopAfterCurrent(false);
|
|
}
|
|
currentSongId=sv.songId;
|
|
if (!isUpdatingDb && -1!=sv.updatingDb) {
|
|
isUpdatingDb=true;
|
|
emit updatingDatabase();
|
|
} else if (isUpdatingDb && -1==sv.updatingDb) {
|
|
isUpdatingDb=false;
|
|
emit updatedDatabase();
|
|
}
|
|
emitStatusUpdated(sv);
|
|
|
|
// If playlist length does not match number of IDs, then refresh
|
|
if (sv.playlistLength!=playQueueIds.length()) {
|
|
playListInfo();
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::getUrlHandlers()
|
|
{
|
|
Response response=sendCommand("urlhandlers");
|
|
if (response.ok) {
|
|
handlers=MPDParseUtils::parseList(response.data, QByteArray("handler: ")).toSet();
|
|
DBUG << handlers;
|
|
}
|
|
}
|
|
|
|
void MPDConnection::getTagTypes()
|
|
{
|
|
Response response=sendCommand("tagtypes");
|
|
if (response.ok) {
|
|
tagTypes=MPDParseUtils::parseList(response.data, QByteArray("tagtype: ")).toSet();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Data is written during idle.
|
|
* Retrieve it and parse it
|
|
*/
|
|
void MPDConnection::idleDataReady()
|
|
{
|
|
DBUG << "idleDataReady";
|
|
if (0==idleSocket.bytesAvailable()) {
|
|
return;
|
|
}
|
|
parseIdleReturn(readFromSocket(idleSocket));
|
|
}
|
|
|
|
/*
|
|
* Socket state has changed, currently we only use this to gracefully
|
|
* handle disconnects.
|
|
*/
|
|
void MPDConnection::onSocketStateChanged(QAbstractSocket::SocketState socketState)
|
|
{
|
|
if (socketState == QAbstractSocket::ClosingState){
|
|
bool wasConnected=State_Connected==state;
|
|
disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
|
|
DBUG << "onSocketStateChanged";
|
|
if (QAbstractSocket::ConnectedState==idleSocket.state()) {
|
|
idleSocket.disconnectFromHost();
|
|
}
|
|
idleSocket.close();
|
|
ConnectionReturn status=Success;
|
|
if (wasConnected && Success!=(status=connectToMPD(idleSocket, true))) {
|
|
// Failed to connect idle socket - so close *both*
|
|
disconnectFromMPD();
|
|
emit stateChanged(false);
|
|
emit error(errorString(status), true);
|
|
}
|
|
if (QAbstractSocket::ConnectedState==idleSocket.state()) {
|
|
connect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)), Qt::QueuedConnection);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Parse data returned by the mpd deamon on an idle commond.
|
|
*/
|
|
void MPDConnection::parseIdleReturn(const QByteArray &data)
|
|
{
|
|
DBUG << "parseIdleReturn:" << data;
|
|
|
|
Response response(data.endsWith(constOkNlValue), data);
|
|
if (!response.ok) {
|
|
DBUG << "idle failed? reconnect";
|
|
disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
|
|
if (QAbstractSocket::ConnectedState==idleSocket.state()) {
|
|
idleSocket.disconnectFromHost();
|
|
}
|
|
idleSocket.close();
|
|
ConnectionReturn status=connectToMPD(idleSocket, true);
|
|
if (Success!=status) {
|
|
// Failed to connect idle socket - so close *both*
|
|
disconnectFromMPD();
|
|
emit stateChanged(false);
|
|
emit error(errorString(status), true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
QList<QByteArray> lines = data.split('\n');
|
|
|
|
/*
|
|
* See http://www.musicpd.org/doc/protocol/ch02.html
|
|
*/
|
|
bool playListUpdated=false;
|
|
bool statusUpdated=false;
|
|
foreach(const QByteArray &line, lines) {
|
|
if (line.startsWith(constIdleChangedKey)) {
|
|
QByteArray value=line.mid(constIdleChangedKey.length());
|
|
if (constIdleDbValue==value) {
|
|
getStats();
|
|
getStatus();
|
|
playListInfo();
|
|
playListUpdated=true;
|
|
} else if (constIdleUpdateValue==value) {
|
|
getStats();
|
|
getStatus();
|
|
} else if (constIdleStoredPlaylistValue==value) {
|
|
listPlaylists();
|
|
listStreams();
|
|
} else if (constIdlePlaylistValue==value) {
|
|
if (!playListUpdated) {
|
|
playListChanges();
|
|
}
|
|
} else if (!statusUpdated && (constIdlePlayerValue==value || constIdleMixerValue==value || constIdleOptionsValue==value)) {
|
|
getStatus();
|
|
getReplayGain();
|
|
statusUpdated=true;
|
|
} else if (constIdleOutputValue==value) {
|
|
outputs();
|
|
} else if (constIdleStickerValue==value) {
|
|
emit stickerDbChanged();
|
|
} else if (constIdleSubscriptionValue==value) {
|
|
//if (dynamicId.isEmpty()) {
|
|
setupRemoteDynamic();
|
|
//}
|
|
} else if (constIdleMessageValue==value) {
|
|
readRemoteDynamicMessages();
|
|
}
|
|
}
|
|
}
|
|
|
|
DBUG << (void *)(&idleSocket) << "write idle";
|
|
idleSocket.write("idle\n");
|
|
idleSocket.waitForBytesWritten();
|
|
}
|
|
|
|
void MPDConnection::outputs()
|
|
{
|
|
Response response=sendCommand("outputs");
|
|
if (response.ok) {
|
|
emit outputsUpdated(MPDParseUtils::parseOuputs(response.data));
|
|
}
|
|
}
|
|
|
|
void MPDConnection::enableOutput(int id, bool enable)
|
|
{
|
|
if (sendCommand((enable ? "enableoutput " : "disableoutput ")+quote(id)).ok) {
|
|
outputs();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Admin commands
|
|
*/
|
|
void MPDConnection::update()
|
|
{
|
|
if (mopidy) {
|
|
// Mopidy does not support MPD's update command. So, when user presses update DB, what we
|
|
// just reload the library.
|
|
loadLibrary();
|
|
} else {
|
|
sendCommand("update");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Database commands
|
|
*/
|
|
void MPDConnection::loadLibrary()
|
|
{
|
|
DBUG << "loadLibrary";
|
|
isListingMusic=true;
|
|
emit updatingLibrary(dbUpdate);
|
|
QList<Song> songs;
|
|
recursivelyListDir("/", songs);
|
|
emit updatedLibrary();
|
|
isListingMusic=false;
|
|
}
|
|
|
|
void MPDConnection::listFolder(const QString &folder)
|
|
{
|
|
DBUG << "listFolder" << folder;
|
|
bool topLevel="/"==folder || ""==folder;
|
|
Response response=sendCommand(topLevel ? "lsinfo" : ("lsinfo "+encodeName(folder)));
|
|
QStringList subFolders;
|
|
QList<Song> songs;
|
|
if (response.ok) {
|
|
MPDParseUtils::parseDirItems(response.data, QString(), ver, songs, folder, subFolders, MPDParseUtils::Loc_Browse);
|
|
}
|
|
emit folderContents(folder, subFolders, songs);
|
|
}
|
|
|
|
/*
|
|
* Playlists commands
|
|
*/
|
|
// void MPDConnection::listPlaylist(const QString &name)
|
|
// {
|
|
// QByteArray data = "listplaylist ";
|
|
// data += encodeName(name);
|
|
// sendCommand(data);
|
|
// }
|
|
|
|
void MPDConnection::listPlaylists()
|
|
{
|
|
Response response=sendCommand("listplaylists");
|
|
if (response.ok) {
|
|
QList<Playlist> playlists=MPDParseUtils::parsePlaylists(response.data);
|
|
playlists.removeAll((Playlist(constStreamsPlayListName)));
|
|
emit playlistsRetrieved(playlists);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::playlistInfo(const QString &name)
|
|
{
|
|
Response response=sendCommand("listplaylistinfo "+encodeName(name));
|
|
if (response.ok) {
|
|
emit playlistInfoRetrieved(name, MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Playlists));
|
|
}
|
|
}
|
|
|
|
void MPDConnection::loadPlaylist(const QString &name, bool replace)
|
|
{
|
|
if (replace) {
|
|
clear();
|
|
getStatus();
|
|
}
|
|
|
|
if (sendCommand("load "+encodeName(name)).ok) {
|
|
if (replace) {
|
|
playFirstTrack(false);
|
|
}
|
|
emit playlistLoaded(name);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::renamePlaylist(const QString oldName, const QString newName)
|
|
{
|
|
if (sendCommand("rename "+encodeName(oldName)+' '+encodeName(newName), false).ok) {
|
|
emit playlistRenamed(oldName, newName);
|
|
} else {
|
|
emit error(tr("Failed to rename <b>%1</b> to <b>%2</b>").arg(oldName).arg(newName));
|
|
}
|
|
}
|
|
|
|
void MPDConnection::removePlaylist(const QString &name)
|
|
{
|
|
sendCommand("rm "+encodeName(name));
|
|
}
|
|
|
|
void MPDConnection::savePlaylist(const QString &name)
|
|
{
|
|
if (!sendCommand("save "+encodeName(name), false).ok) {
|
|
emit error(tr("Failed to save <b>%1</b>").arg(name));
|
|
}
|
|
}
|
|
|
|
void MPDConnection::addToPlaylist(const QString &name, const QStringList &songs, quint32 pos, quint32 size)
|
|
{
|
|
if (songs.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (!name.isEmpty()) {
|
|
foreach (const QString &song, songs) {
|
|
if (CueFile::isCue(song)) {
|
|
emit error(tr("You cannot add parts of a cue sheet to a playlist!")+QLatin1String(" (")+song+QLatin1Char(')'));
|
|
return;
|
|
} else if (isPlaylist(song)) {
|
|
emit error(tr("You cannot add a playlist to another playlist!")+QLatin1String(" (")+song+QLatin1Char(')'));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
QStringList files;
|
|
foreach (const QString &file, songs) {
|
|
if (file.startsWith(constDirPrefix)) {
|
|
files+=getAllFiles(file.mid(constDirPrefix.length()));
|
|
} else if (file.startsWith(constPlaylistPrefix)) {
|
|
files+=getPlaylistFiles(file.mid(constPlaylistPrefix.length()));
|
|
} else {
|
|
files.append(file);
|
|
}
|
|
}
|
|
|
|
QByteArray encodedName=encodeName(name);
|
|
QByteArray send = "command_list_begin\n";
|
|
foreach (const QString &s, files) {
|
|
send += "playlistadd "+encodedName+" "+encodeName(s)+'\n';
|
|
}
|
|
send += "command_list_end";
|
|
|
|
if (sendCommand(send).ok) {
|
|
if (size>0) {
|
|
QList<quint32> items;
|
|
for(int i=0; i<songs.count(); ++i) {
|
|
items.append(size+i);
|
|
}
|
|
doMoveInPlaylist(name, items, pos, size+songs.count());
|
|
}
|
|
} else {
|
|
playlistInfo(name);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::removeFromPlaylist(const QString &name, const QList<quint32> &positions)
|
|
{
|
|
if (positions.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QByteArray encodedName=encodeName(name);
|
|
QList<quint32> sorted=positions;
|
|
QList<quint32> removed;
|
|
|
|
qSort(sorted);
|
|
|
|
for (int i=sorted.count()-1; i>=0; --i) {
|
|
quint32 idx=sorted.at(i);
|
|
QByteArray data = "playlistdelete ";
|
|
data += encodedName;
|
|
data += " ";
|
|
data += quote(idx);
|
|
if (sendCommand(data).ok) {
|
|
removed.prepend(idx);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (removed.count()) {
|
|
emit removedFromPlaylist(name, removed);
|
|
}
|
|
// playlistInfo(name);
|
|
}
|
|
|
|
void MPDConnection::setPriority(const QList<qint32> &ids, quint8 priority)
|
|
{
|
|
if (canUsePriority()) {
|
|
QByteArray send = "command_list_begin\n";
|
|
|
|
foreach (quint32 id, ids) {
|
|
send += "prioid "+quote(priority)+" "+quote(id)+'\n';
|
|
}
|
|
|
|
send += "command_list_end";
|
|
if (sendCommand(send).ok) {
|
|
emit prioritySet(ids, priority);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPDConnection::search(const QString &field, const QString &value, int id)
|
|
{
|
|
QList<Song> songs;
|
|
QByteArray cmd;
|
|
|
|
if (field==constModifiedSince) {
|
|
time_t v=0;
|
|
if (QRegExp("\\d*").exactMatch(value)) {
|
|
v=QDateTime(QDateTime::currentDateTime().date()).toTime_t()-(value.toInt()*24*60*60);
|
|
} else if (QRegExp("^((19|20)\\d\\d)[-/](0[1-9]|1[012])[-/](0[1-9]|[12][0-9]|3[01])$").exactMatch(value)) {
|
|
QDateTime dt=QDateTime::fromString(QString(value).replace("/", "-"), Qt::ISODate);
|
|
if (dt.isValid()) {
|
|
v=dt.toTime_t();
|
|
}
|
|
}
|
|
if (v>0) {
|
|
cmd="find "+field.toLatin1()+" "+quote(v);
|
|
}
|
|
} else {
|
|
cmd="search "+field.toLatin1()+" "+encodeName(value);
|
|
}
|
|
|
|
if (!cmd.isEmpty()) {
|
|
Response response=sendCommand(cmd);
|
|
if (response.ok) {
|
|
songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search);
|
|
qSort(songs);
|
|
}
|
|
}
|
|
emit searchResponse(id, songs);
|
|
}
|
|
|
|
void MPDConnection::listStreams()
|
|
{
|
|
Response response=sendCommand("listplaylistinfo "+encodeName(constStreamsPlayListName), false);
|
|
QList<Stream> streams;
|
|
if (response.ok) {
|
|
QList<Song> songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Streams);
|
|
foreach (const Song &song, songs) {
|
|
streams.append(Stream(song.file, song.name()));
|
|
}
|
|
}
|
|
clearError();
|
|
emit streamList(streams);
|
|
}
|
|
|
|
void MPDConnection::saveStream(const QString &url, const QString &name)
|
|
{
|
|
if (sendCommand("playlistadd "+encodeName(constStreamsPlayListName)+" "+encodeName(MPDParseUtils::addStreamName(url, name))).ok) {
|
|
emit savedStream(url, name);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::removeStreams(const QList<quint32> &positions)
|
|
{
|
|
if (positions.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QByteArray encodedName=encodeName(constStreamsPlayListName);
|
|
QList<quint32> sorted=positions;
|
|
QList<quint32> removed;
|
|
|
|
qSort(sorted);
|
|
|
|
for (int i=sorted.count()-1; i>=0; --i) {
|
|
quint32 idx=sorted.at(i);
|
|
QByteArray data = "playlistdelete ";
|
|
data += encodedName;
|
|
data += " ";
|
|
data += quote(idx);
|
|
if (sendCommand(data).ok) {
|
|
removed.prepend(idx);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
emit removedStreams(removed);
|
|
}
|
|
|
|
void MPDConnection::editStream(const QString &url, const QString &name, quint32 position)
|
|
{
|
|
QByteArray encodedName=encodeName(constStreamsPlayListName);
|
|
if (sendCommand("playlistdelete "+encodedName+" "+quote(position)).ok &&
|
|
sendCommand("playlistadd "+encodedName+" "+encodeName(MPDParseUtils::addStreamName(url, name))).ok) {
|
|
// emit editedStream(url, name, position);
|
|
listStreams();
|
|
}
|
|
}
|
|
|
|
void MPDConnection::sendClientMessage(const QString &channel, const QString &msg, const QString &clientName)
|
|
{
|
|
if (!sendCommand("sendmessage "+channel.toUtf8()+" "+msg.toUtf8(), false).ok) {
|
|
emit error(tr("Failed to send '%1' to %2. Please check %2 is registered with MPD.").arg(msg).arg(clientName.isEmpty() ? channel : clientName));
|
|
emit clientMessageFailed(channel, msg);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::sendDynamicMessage(const QStringList &msg)
|
|
{
|
|
// Check whether cantata-dynamic is still alive, by seeing if its channel is still open...
|
|
if (1==msg.count() && QLatin1String("ping")==msg.at(0)) {
|
|
Response response=sendCommand("channels");
|
|
if (!response.ok || !MPDParseUtils::parseList(response.data, QByteArray("channel: ")).toSet().contains(constDynamicIn)) {
|
|
emit dynamicSupport(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
QByteArray data;
|
|
foreach (QString part, msg) {
|
|
if (data.isEmpty()) {
|
|
data+='\"'+part.toUtf8()+':'+dynamicId;
|
|
} else {
|
|
part=part.replace('\"', "{q}");
|
|
part=part.replace("{", "{ob}");
|
|
part=part.replace("}", "{cb}");
|
|
part=part.replace("\n", "{n}");
|
|
part=part.replace(":", "{c}");
|
|
data+=':'+part.toUtf8();
|
|
}
|
|
}
|
|
|
|
data+='\"';
|
|
if (!sendCommand("sendmessage "+constDynamicIn+" "+data).ok) {
|
|
emit dynamicSupport(false);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::moveInPlaylist(const QString &name, const QList<quint32> &items, quint32 pos, quint32 size)
|
|
{
|
|
if (doMoveInPlaylist(name, items, pos, size)) {
|
|
emit movedInPlaylist(name, items, pos);
|
|
}
|
|
// playlistInfo(name);
|
|
}
|
|
|
|
bool MPDConnection::doMoveInPlaylist(const QString &name, const QList<quint32> &items, quint32 pos, quint32 size)
|
|
{
|
|
if (name.isEmpty()) {
|
|
toggleStopAfterCurrent(false);
|
|
}
|
|
QByteArray cmd = name.isEmpty() ? "move " : ("playlistmove "+encodeName(name)+" ");
|
|
QByteArray send = "command_list_begin\n";
|
|
QList<quint32> moveItems=items;
|
|
int posOffset = 0;
|
|
|
|
qSort(moveItems);
|
|
|
|
//first move all items (starting with the biggest) to the end so we don't have to deal with changing rownums
|
|
for (int i = moveItems.size() - 1; i >= 0; i--) {
|
|
if (moveItems.at(i) < pos && moveItems.at(i) != size - 1) {
|
|
// we are moving away an item that resides before the destination row, manipulate destination row
|
|
posOffset++;
|
|
}
|
|
send += cmd;
|
|
send += quote(moveItems.at(i));
|
|
send += " ";
|
|
send += quote(size - 1);
|
|
send += '\n';
|
|
}
|
|
//now move all of them to the destination position
|
|
for (int i = moveItems.size() - 1; i >= 0; i--) {
|
|
send += cmd;
|
|
send += quote(size - 1 - i);
|
|
send += " ";
|
|
send += quote(pos - posOffset);
|
|
send += '\n';
|
|
}
|
|
|
|
send += "command_list_end";
|
|
return sendCommand(send).ok;
|
|
}
|
|
|
|
void MPDConnection::toggleStopAfterCurrent(bool afterCurrent)
|
|
{
|
|
if (afterCurrent!=stopAfterCurrent) {
|
|
stopAfterCurrent=afterCurrent;
|
|
songPos=0;
|
|
if (stopAfterCurrent && 1==playQueueIds.count()) {
|
|
Response response=sendCommand("status");
|
|
if (response.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
|
|
songPos=sv.timeElapsed;
|
|
}
|
|
}
|
|
emit stopAfterCurrentChanged(stopAfterCurrent);
|
|
}
|
|
}
|
|
|
|
bool MPDConnection::recursivelyListDir(const QString &dir, QList<Song> &songs)
|
|
{
|
|
bool topLevel="/"==dir || ""==dir;
|
|
|
|
if (topLevel && !mopidy) {
|
|
// UPnP database backend does not list separate metadata items, so if "list genre" returns
|
|
// empty response assume this is a UPnP backend and dont attempt to get rest of data...
|
|
// Although we dont use "list XXX", lsinfo will return duplciate items (due to the way most
|
|
// UPnP servers returing directories of classifications - Genre/Album/Tracks, Artist/Album/Tracks,
|
|
// etc...
|
|
Response response=sendCommand("list genre", false, false);
|
|
if (!response.ok || response.data.split('\n').length()<3) { // 2 lines - OK and blank
|
|
// ..just to be 100% sure, check no artists either...
|
|
response=sendCommand("list artist", false, false);
|
|
if (!response.ok || response.data.split('\n').length()<3) { // 2 lines - OK and blank
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Response response=sendCommand(topLevel
|
|
? QByteArray(mopidy ? "lsinfo \"Local media\"" : "lsinfo")
|
|
: ("lsinfo "+encodeName(dir)));
|
|
if (response.ok) {
|
|
QStringList subDirs;
|
|
MPDParseUtils::parseDirItems(response.data, details.dir, ver, songs, dir, subDirs, MPDParseUtils::Loc_Library);
|
|
if (songs.count()>=200){
|
|
QCoreApplication::processEvents();
|
|
QList<Song> *copy=new QList<Song>();
|
|
*copy << songs;
|
|
emit librarySongs(copy);
|
|
songs.clear();
|
|
}
|
|
foreach (const QString &sub, subDirs) {
|
|
if (!recursivelyListDir(sub, songs)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (topLevel && !songs.isEmpty()) {
|
|
QList<Song> *copy=new QList<Song>();
|
|
*copy << songs;
|
|
emit librarySongs(copy);
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
QStringList MPDConnection::getPlaylistFiles(const QString &name)
|
|
{
|
|
QStringList files;
|
|
Response response=sendCommand("listplaylistinfo "+encodeName(name));
|
|
if (response.ok) {
|
|
QList<Song> songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Playlists);
|
|
emit playlistInfoRetrieved(name, songs);
|
|
foreach (const Song &s, songs) {
|
|
files.append(s.file);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
QStringList MPDConnection::getAllFiles(const QString &dir)
|
|
{
|
|
QStringList files;
|
|
Response response=sendCommand("lsinfo "+encodeName(dir));
|
|
if (response.ok) {
|
|
QStringList subDirs;
|
|
QList<Song> songs;
|
|
MPDParseUtils::parseDirItems(response.data, details.dir, ver, songs, dir, subDirs, MPDParseUtils::Loc_Browse);
|
|
foreach (const Song &song, songs) {
|
|
if (Song::Playlist!=song.type) {
|
|
files.append(song.file);
|
|
}
|
|
}
|
|
foreach (const QString &sub, subDirs) {
|
|
files+=getAllFiles(sub);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
bool MPDConnection::checkRemoteDynamicSupport()
|
|
{
|
|
if (ver>=CANTATA_MAKE_VERSION(0,17,0)) {
|
|
Response response;
|
|
if (-1!=idleSocket.write("channels\n")) {
|
|
idleSocket.waitForBytesWritten(socketTimeout(9));
|
|
response=readReply(idleSocket);
|
|
if (response.ok) {
|
|
return MPDParseUtils::parseList(response.data, QByteArray("channel: ")).toSet().contains(constDynamicIn);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool MPDConnection::subscribe(const QByteArray &channel)
|
|
{
|
|
if (-1!=idleSocket.write("subscribe \""+channel+"\"\n")) {
|
|
idleSocket.waitForBytesWritten(socketTimeout(128));
|
|
Response response=readReply(idleSocket);
|
|
if (response.ok || response.data.startsWith("ACK [56@0]")) { // ACK => already subscribed...
|
|
DBUG << "Created subscription to " << channel;
|
|
return true;
|
|
} else {
|
|
DBUG << "Failed to subscribe to " << channel;
|
|
}
|
|
} else {
|
|
DBUG << "Failed to create subscribe to " << channel;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MPDConnection::setupRemoteDynamic()
|
|
{
|
|
if (checkRemoteDynamicSupport()) {
|
|
DBUG << "cantata-dynamic is running";
|
|
if (subscribe(constDynamicOut)) {
|
|
if (dynamicId.isEmpty()) {
|
|
dynamicId=QHostInfo::localHostName().toLatin1()+'.'+QHostInfo::localDomainName().toLatin1()+'-'+QByteArray::number(QCoreApplication::applicationPid());
|
|
if (!subscribe(constDynamicOut+'-'+dynamicId)) {
|
|
dynamicId.clear();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
DBUG << "remote dynamic is not supported";
|
|
}
|
|
emit dynamicSupport(!dynamicId.isEmpty());
|
|
}
|
|
|
|
void MPDConnection::readRemoteDynamicMessages()
|
|
{
|
|
if (-1!=idleSocket.write("readmessages\n")) {
|
|
idleSocket.waitForBytesWritten(socketTimeout(22));
|
|
Response response=readReply(idleSocket);
|
|
if (response.ok) {
|
|
MPDParseUtils::MessageMap messages=MPDParseUtils::parseMessages(response.data);
|
|
if (!messages.isEmpty()) {
|
|
QList<QByteArray> channels=QList<QByteArray>() << constDynamicOut << constDynamicOut+'-'+dynamicId;
|
|
foreach (const QByteArray &channel, channels) {
|
|
if (messages.contains(channel)) {
|
|
foreach (const QString &m, messages[channel]) {
|
|
if (!m.isEmpty()) {
|
|
DBUG << "Received message " << m;
|
|
QStringList parts=m.split(':', QString::SkipEmptyParts);
|
|
QStringList message;
|
|
foreach (QString part, parts) {
|
|
part=part.replace("{c}", ":");
|
|
part=part.replace("{n}", "\n");
|
|
part=part.replace("{cb}", "}");
|
|
part=part.replace("{ob}", "{");
|
|
part=part.replace("{q}", "\"");
|
|
message.append(part);
|
|
}
|
|
emit dynamicResponse(message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int MPDConnection::getVolume()
|
|
{
|
|
Response response=sendCommand("status");
|
|
if (response.ok) {
|
|
MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
|
|
return sv.volume;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void MPDConnection::setRating(const QString &file, quint8 val)
|
|
{
|
|
if (val>Song::Rating_Max) {
|
|
return;
|
|
}
|
|
|
|
if (!canUseStickers) {
|
|
emit error(tr("Cannot store ratings, as the 'sticker' MPD command is not supported."));
|
|
return;
|
|
}
|
|
|
|
bool ok=0==val
|
|
? sendCommand("sticker delete song "+encodeName(file)+' '+constRatingSticker, 0!=val).ok
|
|
: sendCommand("sticker set song "+encodeName(file)+' '+constRatingSticker+' '+quote(val)).ok;
|
|
|
|
if (!ok && 0==val) {
|
|
clearError();
|
|
}
|
|
|
|
if (ok) {
|
|
emit rating(file, val);
|
|
} else {
|
|
getRating(file);
|
|
}
|
|
}
|
|
|
|
void MPDConnection::setRating(const QStringList &files, quint8 val)
|
|
{
|
|
if (1==files.count()) {
|
|
setRating(files.at(0), val);
|
|
return;
|
|
}
|
|
|
|
if (!canUseStickers) {
|
|
emit error(tr("Cannot store ratings, as the 'sticker' MPD command is not supported."));
|
|
return;
|
|
}
|
|
|
|
QList<QStringList> fileLists;
|
|
if (files.count()>maxFilesPerAddCommand) {
|
|
int numChunks=(files.count()/maxFilesPerAddCommand)+(files.count()%maxFilesPerAddCommand ? 1 : 0);
|
|
for (int i=0; i<numChunks; ++i) {
|
|
fileLists.append(files.mid(i*maxFilesPerAddCommand, maxFilesPerAddCommand));
|
|
}
|
|
} else {
|
|
fileLists.append(files);
|
|
}
|
|
|
|
bool ok=true;
|
|
foreach (const QStringList &list, fileLists) {
|
|
QByteArray cmd = "command_list_begin\n";
|
|
|
|
foreach (const QString &f, list) {
|
|
if (0==val) {
|
|
cmd+="sticker delete song "+encodeName(f)+' '+constRatingSticker+'\n';
|
|
} else {
|
|
cmd+="sticker set song "+encodeName(f)+' '+constRatingSticker+' '+quote(val)+'\n';
|
|
}
|
|
}
|
|
|
|
cmd += "command_list_end";
|
|
ok=sendCommand(cmd, 0!=val).ok;
|
|
if (!ok) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok && 0==val) {
|
|
clearError();
|
|
}
|
|
}
|
|
|
|
void MPDConnection::getRating(const QString &file)
|
|
{
|
|
quint8 r=0;
|
|
if (canUseStickers) {
|
|
Response resp=sendCommand("sticker get song "+encodeName(file)+' '+constRatingSticker, false);
|
|
if (resp.ok) {
|
|
QByteArray val=MPDParseUtils::parseSticker(resp.data, constRatingSticker);
|
|
if (!val.isEmpty()) {
|
|
r=val.toUInt();
|
|
}
|
|
} else { // Ignore errors about uknown sticker...
|
|
clearError();
|
|
}
|
|
if (r>Song::Rating_Max) {
|
|
r=0;
|
|
}
|
|
}
|
|
emit rating(file, r);
|
|
}
|
|
|
|
void MPDConnection::getStickerSupport()
|
|
{
|
|
Response response=sendCommand("commands");
|
|
canUseStickers=response.ok &&
|
|
MPDParseUtils::parseList(response.data, QByteArray("command: ")).toSet().contains("sticker");
|
|
}
|
|
|
|
bool MPDConnection::fadingVolume()
|
|
{
|
|
return volumeFade && QPropertyAnimation::Running==volumeFade->state();
|
|
}
|
|
|
|
bool MPDConnection::startVolumeFade()
|
|
{
|
|
if (fadeDuration<=MinFade) {
|
|
return false;
|
|
}
|
|
|
|
restoreVolume=getVolume();
|
|
if (restoreVolume<5) {
|
|
return false;
|
|
}
|
|
|
|
if (!volumeFade) {
|
|
volumeFade = new QPropertyAnimation(this, "volume");
|
|
volumeFade->setDuration(fadeDuration);
|
|
}
|
|
|
|
if (QPropertyAnimation::Running!=volumeFade->state()) {
|
|
volumeFade->setStartValue(restoreVolume);
|
|
volumeFade->setEndValue(-1);
|
|
volumeFade->start();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void MPDConnection::stopVolumeFade()
|
|
{
|
|
if (fadingVolume()) {
|
|
volumeFade->stop();
|
|
setVolume(restoreVolume);
|
|
restoreVolume=-1;
|
|
}
|
|
}
|
|
|
|
void MPDConnection::emitStatusUpdated(MPDStatusValues &v)
|
|
{
|
|
if (restoreVolume>=0) {
|
|
v.volume=restoreVolume;
|
|
}
|
|
#ifndef REPORT_MPD_ERRORS
|
|
v.error=QString();
|
|
#endif
|
|
emit statusUpdated(v);
|
|
if (!v.error.isEmpty()) {
|
|
clearError();
|
|
}
|
|
}
|
|
|
|
void MPDConnection::clearError()
|
|
{
|
|
#ifdef REPORT_MPD_ERRORS
|
|
if (isConnected()) {
|
|
DBUG << __FUNCTION__;
|
|
if (-1!=sock.write("clearerror\n")) {
|
|
sock.waitForBytesWritten(500);
|
|
readReply(sock);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void MPDConnection::determineIfaceIp()
|
|
{
|
|
static const QLatin1String ip4Local("127.0.0.1");
|
|
if (!details.isLocal() && !details.hostname.isEmpty() && ip4Local!=details.hostname && QLatin1String("localhost")!=details.hostname) {
|
|
QUdpSocket testSocket(this);
|
|
testSocket.connectToHost(details.hostname, 1, QIODevice::ReadOnly);
|
|
QString addr=testSocket.localAddress().toString();
|
|
testSocket.close();
|
|
if (!addr.isEmpty()) {
|
|
DBUG << addr;
|
|
emit ifaceIp(addr);
|
|
return;
|
|
}
|
|
}
|
|
DBUG << ip4Local;
|
|
emit ifaceIp(ip4Local);
|
|
}
|
|
|
|
MpdSocket::MpdSocket(QObject *parent)
|
|
: QObject(parent)
|
|
, tcp(0)
|
|
, local(0)
|
|
{
|
|
}
|
|
|
|
MpdSocket::~MpdSocket()
|
|
{
|
|
deleteTcp();
|
|
deleteLocal();
|
|
}
|
|
|
|
void MpdSocket::connectToHost(const QString &hostName, quint16 port, QIODevice::OpenMode mode)
|
|
{
|
|
// qWarning() << "connectToHost" << hostName << port;
|
|
if (hostName.startsWith('/') || hostName.startsWith('~')) {
|
|
deleteTcp();
|
|
if (!local) {
|
|
local = new QLocalSocket(this);
|
|
connect(local, SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), this, SLOT(localStateChanged(QLocalSocket::LocalSocketState)));
|
|
connect(local, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
|
|
}
|
|
// qWarning() << "Connecting to LOCAL socket";
|
|
local->connectToServer(Utils::tildaToHome(hostName), mode);
|
|
} else {
|
|
deleteLocal();
|
|
if (!tcp) {
|
|
tcp = new QTcpSocket(this);
|
|
connect(tcp, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
|
|
connect(tcp, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
|
|
}
|
|
// qWarning() << "Connecting to TCP socket";
|
|
tcp->connectToHost(hostName, port, mode);
|
|
}
|
|
}
|
|
|
|
void MpdSocket::localStateChanged(QLocalSocket::LocalSocketState state)
|
|
{
|
|
emit stateChanged((QAbstractSocket::SocketState)state);
|
|
}
|
|
|
|
void MpdSocket::deleteTcp()
|
|
{
|
|
if (tcp) {
|
|
disconnect(tcp, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
|
|
disconnect(tcp, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
|
|
tcp->deleteLater();
|
|
tcp=0;
|
|
}
|
|
}
|
|
|
|
void MpdSocket::deleteLocal()
|
|
{
|
|
if (local) {
|
|
disconnect(local, SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), this, SLOT(localStateChanged(QLocalSocket::LocalSocketState)));
|
|
disconnect(local, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
|
|
local->deleteLater();
|
|
local=0;
|
|
}
|
|
}
|