/* * Cantata * * Copyright (c) 2011-2013 Craig Drummond * */ /* * 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 . */ #include "mpdconnection.h" #include "mpdparseutils.h" #include "mpduser.h" #include "localize.h" #include "utils.h" #ifdef ENABLE_KDE_SUPPORT #include #endif #include #include #include #include "thread.h" #include "settings.h" #include "cuefile.h" #include static bool debugEnabled=false; #define DBUG if (debugEnabled) qWarning() << "MPDConnection" << QThread::currentThreadId() void MPDConnection::enableDebug() { debugEnabled=true; } static const int constSocketCommsTimeout=2000; static const int constMaxReadAttempts=4; #ifdef ENABLE_KDE_SUPPORT K_GLOBAL_STATIC(MPDConnection, conn) #endif MPDConnection * MPDConnection::self() { #ifdef ENABLE_KDE_SUPPORT return conn; #else static MPDConnection *conn=0; if (!conn) { conn=new MPDConnection; } return conn; #endif } QByteArray MPDConnection::encodeName(const QString &name) { return '\"'+name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"")+'\"'; } static QByteArray readFromSocket(MpdSocket &socket) { QByteArray data; while (QAbstractSocket::ConnectedState==socket.state()) { int attempt=0; while (0==socket.bytesAvailable() && QAbstractSocket::ConnectedState==socket.state()) { DBUG << (void *)(&socket) << "Waiting for read data." << attempt; if (socket.waitForReadyRead(constSocketCommsTimeout)) { break; } if (++attempt>=constMaxReadAttempts) { DBUG << "ERROR: Timedout waiting for response"; socket.close(); return QByteArray(); } } data.append(socket.readAll()); if (data.endsWith("OK\n") || data.startsWith("OK") || data.startsWith("ACK")) { break; } } if (data.size()>256) { DBUG << (void *)(&socket) << "Read (bytes):" << data.size(); } else { DBUG << (void *)(&socket) << "Read:" << data; } return data; } MPDConnection::Response readReply(MpdSocket &socket) { QByteArray data = readFromSocket(socket); return MPDConnection::Response(data.endsWith("OK\n"), data); } MPDConnection::Response::Response(bool o, const QByteArray &d) : ok(o) , data(d) { } QString MPDConnection::Response::getError() { if (ok || data.isEmpty()) { return QString(); } if (!ok && data.size()>0) { int cmdEnd=data.indexOf("} "); if (-1!=cmdEnd) { cmdEnd+=2; data=data.mid(cmdEnd, data.length()-(data.endsWith('\n') ? cmdEnd+1 : cmdEnd)); } } return data; } MPDConnectionDetails::MPDConnectionDetails() : port(6600) , dynamizerPort(0) , dirReadable(false) { } QString MPDConnectionDetails::getName() const { return name.isEmpty() ? i18n("Default") : (name==MPDUser::constName ? MPDUser::translatedName() : name); } QString MPDConnectionDetails::description() const { if (hostname.startsWith('/')) { return i18nc("name (host)", "\"%1\"", getName()); } else { return i18nc("name (host:port)", "\"%1\" (%2:%3)", getName(), hostname, port); } } MPDConnection::MPDConnection() : thread(0) , ver(0) , sock(this) , idleSocket(this) , lastStatusPlayQueueVersion(0) , lastUpdatePlayQueueVersion(0) , state(State_Blank) , reconnectTimer(0) , reconnectStart(0) , stopAfterCurrent(false) , currentSongId(-1) , songPos(0) , unmuteVol(-1) { qRegisterMetaType("Song"); qRegisterMetaType("Output"); qRegisterMetaType("Playlist"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType("QAbstractSocket::SocketState"); qRegisterMetaType("MPDStatsValues"); qRegisterMetaType("MPDStatusValues"); qRegisterMetaType("MPDConnectionDetails"); } MPDConnection::~MPDConnection() { // 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()); moveToThread(thread); thread->start(); } } void MPDConnection::stop() { if (details.name==MPDUser::constName && Settings::self()->stopOnExit()) { MPDUser::self()->stop(); } if (thread) { thread->stop(); thread=0; } } 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("OK MPD")) { DBUG << (void *)(&socket) << "Received identification string"; } lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=0; playQueueIds.clear(); int min, maj, patch; if (3==sscanf(&(recvdata.constData()[7]), "%d.%d.%d", &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) { 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"; return Failed; } } // DBUG << "Already connected" << (enableIdle ? "(idle)" : "(std)"); return Success; } MPDConnection::ConnectionReturn MPDConnection::connectToMPD() { if (State_Connected==state && (QAbstractSocket::ConnectedState!=sock.state() || QAbstractSocket::ConnectedState!=idleSocket.state())) { DBUG << "Something has gone wrong with sockets, so disconnect"; disconnectFromMPD(); } ConnectionReturn status=Failed; if (Success==(status=connectToMPD(sock)) && Success==(status=connectToMPD(idleSocket, true))) { state=State_Connected; } else { disconnectFromMPD(); state=State_Disconnected; } return status; } void MPDConnection::disconnectFromMPD() { DBUG << "disconnectFromMPD"; 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; } // 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); switch (connectToMPD()) { case Success: getStatus(); getStats(false); getUrlHandlers(); 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(i18n("Connecting to %1", details.description())); } reconnectTimer->start(500); } else { emit stateChanged(false); emit error(i18n("Connection to %1 failed", details.description()), true); reconnectStart=0; } break; case IncorrectPassword: emit stateChanged(false); emit error(i18n("Connection to %1 failed - incorrect password", details.description()), true); reconnectStart=0; break; } } void MPDConnection::setDetails(const MPDConnectionDetails &d) { bool isUser=d.name==MPDUser::constName; const MPDConnectionDetails &det=isUser ? MPDUser::self()->details() : d; bool changedDir=det.dir!=details.dir; bool diffName=det.name!=details.name; bool diffDetails=det!=details; bool diffDynamicPort=det.dynamizerPort!=details.dynamizerPort; #ifdef ENABLE_HTTP_STREAM_PLAYBACK bool diffStreamUrl=det.streamUrl!=details.streamUrl; #endif details=det; if (diffDetails || State_Connected!=state) { DBUG << "setDetails" << details.hostname << details.port << (details.password.isEmpty() ? false : true); disconnectFromMPD(); DBUG << "call connectToMPD"; unmuteVol=-1; toggleStopAfterCurrent(false); if (isUser) { MPDUser::self()->start(); } switch (connectToMPD()) { case Success: getStatus(); getStats(false); getUrlHandlers(); emit stateChanged(true); break; case Failed: emit stateChanged(false); emit error(i18n("Connection to %1 failed", details.description()), true); break; case IncorrectPassword: emit stateChanged(false); emit error(i18n("Connection to %1 failed - incorrect password", details.description()), true); break; } } else if (diffName) { emit stateChanged(true); } if (diffDynamicPort) { if (details.dynamizerPort<=0) { dynamicUrl(QString()); } else { emit dynamicUrl("http://"+(details.isLocal() ? "127.0.0.1" : details.hostname)+":"+QString::number(details.dynamizerPort)); } } #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) { DBUG << (void *)(&sock) << "sendCommand:" << command << emitErrors << retry; if (!isConnected()) { emit error(i18n("Failed to send command to %1 - not connected", details.description()), true); return Response(false); } if (QAbstractSocket::ConnectedState!=sock.state()) { sock.close(); if (Success!=connectToMPD(sock)) { // Failed to connect, so close *both* sockets! disconnectFromMPD(); emit stateChanged(false); emit error(i18n("Connection to %1 failed", details.description()), true); return Response(false); } } Response response; if (-1==sock.write(command+"\n")) { // If we fail to write, dont wait for bytes to be written!! response=Response(false); sock.close(); } else { sock.waitForBytesWritten(constSocketCommsTimeout); response=readReply(sock); } if (!response.ok) { DBUG << command << "failed"; if (response.data.isEmpty() && retry && QAbstractSocket::ConnectedState!=sock.state()) { // Try one more time... // This scenario, where socket seems to be closed during/after 'write' seems to occiu more often // when dynamizer is running. However, simply reconnecting seems to resolve the issue. return sendCommand(command, emitErrors, false); } if (emitErrors) { if ((command.startsWith("add ") || command.startsWith("command_list_begin\nadd ")) && -1!=command.indexOf("\"file:///")) { if (details.isLocal() && response.data=="Permission denied") { emit error(i18n("Failed to load. Please check user \"mpd\" has read permission.")); } else if (!details.isLocal() && response.data=="Access denied") { emit error(i18n("Failed to load. MPD can only play local files if connected via a local socket.")); } else if (!response.getError().isEmpty()) { emit error(response.getError()); } else { disconnectFromMPD(); emit stateChanged(false); emit error(i18n("Failed to send command. Disconnected from %1", details.description()), true); } } else if (!response.getError().isEmpty()) { emit error(response.getError()); } else { disconnectFromMPD(); emit stateChanged(false); emit error(i18n("Failed to send command. Disconnected from %1", details.description()), true); } } } DBUG << (void *)(&sock) << "sendCommand - sent"; return response; } /* * Playlist commands */ bool MPDConnection::isPlaylist(const QString &file) { return file.endsWith("asx", Qt::CaseInsensitive) || file.endsWith("cue", Qt::CaseInsensitive) || file.endsWith("m3u", Qt::CaseInsensitive) || file.endsWith("pls", Qt::CaseInsensitive) || file.endsWith("xspf", Qt::CaseInsensitive); } void MPDConnection::add(const QStringList &files, bool replace, quint8 priority) { add(files, 0, 0, replace, priority); } void MPDConnection::add(const QStringList &files, quint32 pos, quint32 size, bool replace, quint8 priority) { toggleStopAfterCurrent(false); if (replace) { clear(); getStatus(); } QByteArray send = "command_list_begin\n"; int curSize = size; int curPos = pos; // bool addedFile=false; bool havePlaylist=false; bool usePrio=priority>0 && canUsePriority(); for (int i = 0; i < files.size(); i++) { if (CueFile::isCue(files.at(i))) { send += "load "+CueFile::getLoadLine(files.at(i))+"\n"; } else { if (isPlaylist(files.at(i))) { send+="load "; havePlaylist=true; } else { // addedFile=true; send += "add "; } send += encodeName(files.at(i))+"\n"; } if (!havePlaylist) { if (0!=size) { send += "move "+QByteArray::number(curSize)+" "+QByteArray::number(curPos)+"\n"; } if (usePrio && !havePlaylist) { send += "prio "+QByteArray::number(priority)+" "+QByteArray::number(curPos)+" "+QByteArray::number(curPos)+"\n"; } } curSize++; curPos++; } send += "command_list_end"; if (sendCommand(send).ok) { if (replace /*&& addedFile */&& !files.isEmpty()) { // Dont emit error if fail plays, might be that playlist was not loaded... sendCommand("play "+QByteArray::number(0), false); } emit added(files); } } 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 "+QByteArray::number(sv.playlistLength)+"\n"; send+="command_list_end"; sendCommand(send); } } void MPDConnection::clear() { toggleStopAfterCurrent(false); if (sendCommand("clear").ok) { lastUpdatePlayQueueVersion=0; playQueueIds.clear(); } } void MPDConnection::removeSongs(const QList &items) { toggleStopAfterCurrent(false); QByteArray send = "command_list_begin\n"; foreach (qint32 i, items) { send += "deleteid "; send += QByteArray::number(i); send += "\n"; } send += "command_list_end"; sendCommand(send); } void MPDConnection::move(quint32 from, quint32 to) { toggleStopAfterCurrent(false); sendCommand("move "+QByteArray::number(from)+' '+QByteArray::number(to)); } void MPDConnection::move(const QList &items, quint32 pos, quint32 size) { doMoveInPlaylist(QString(), items, pos, size); #if 0 QByteArray send = "command_list_begin\n"; QList 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 += QByteArray::number(moveItems.at(i)); send += " "; send += QByteArray::number(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 += QByteArray::number(size - 1 - i); send += " "; send += QByteArray::number(pos - posOffset); send += "\n"; } send += "command_list_end"; sendCommand(send); #endif } void MPDConnection::shuffle() { toggleStopAfterCurrent(false); sendCommand("shuffle"); } void MPDConnection::shuffle(quint32 from, quint32 to) { toggleStopAfterCurrent(false); sendCommand("shuffle "+QByteArray::number(from)+':'+QByteArray::number(to+1)); } void MPDConnection::currentSong() { Response response=sendCommand("currentsong"); if (response.ok) { emit currentSongUpdated(MPDParseUtils::parseSong(response.data, true)); } } /* * 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 " 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() { if (0==lastUpdatePlayQueueVersion || 0==playQueueIds.size()) { playListInfo(); return; } QByteArray data = "plchangesposid "; data += QByteArray::number(lastUpdatePlayQueueVersion); Response status=sendCommand("status"); // We need an updated status so as to detect deletes at end of list... Response response=sendCommand(data); if (response.ok && status.ok) { MPDStatusValues sv=MPDParseUtils::parseStatus(status.data); lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist; emit statusUpdated(sv); QList changes=MPDParseUtils::parseChanges(response.data); if (!changes.isEmpty()) { bool first=true; quint32 firstPos=0; QList songs; QList ids; QSet prevIds=playQueueIds.toSet(); QSet strmIds; foreach (const MPDParseUtils::IdPos &idp, changes) { if (first) { first=false; firstPos=idp.pos; if (idp.pos!=0) { for (quint32 i=0; i songs=MPDParseUtils::parseSongs(response.data); playQueueIds.clear(); streamIds.clear(); foreach (const Song &s, songs) { playQueueIds.append(s.id); if (s.isStream() && !s.isCantataStream()) { streamIds.insert(s.id); } } emit playlistUpdated(songs); } } /* * Playback commands */ void MPDConnection::setCrossFade(int secs) { sendCommand("crossfade "+QByteArray::number(secs)); } void MPDConnection::setReplayGain(const QString &v) { sendCommand("replay_gain_mode "+v.toLatin1()); } void MPDConnection::getReplayGain() { 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); sendCommand("next"); } static inline QByteArray value(bool b) { return b ? "1" : "0"; } void MPDConnection::setPause(bool toggle) { toggleStopAfterCurrent(false); sendCommand("pause "+value(toggle)); } void MPDConnection::startPlayingSong(quint32 song) { toggleStopAfterCurrent(false); sendCommand("play "+QByteArray::number(song)); } void MPDConnection::startPlayingSongId(qint32 songId) { toggleStopAfterCurrent(false); sendCommand("playid "+QByteArray::number(songId)); } void MPDConnection::goToPrevious() { toggleStopAfterCurrent(false); 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) { toggleStopAfterCurrent(false); sendCommand("seek "+QByteArray::number(song)+' '+QByteArray::number(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 "+QByteArray::number(songId)+' '+QByteArray::number(time)).ok) { if (stopAfterCurrent && songId==currentSongId && songPos>time) { songPos=time; } } } void MPDConnection::setVolume(int vol) { if (vol>=0) { unmuteVol=-1; sendCommand("setvol "+QByteArray::number(vol), false); } } void MPDConnection::toggleMute() { if (unmuteVol>0) { sendCommand("setvol "+QByteArray::number(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 "+QByteArray::number(0), false); } } } } void MPDConnection::stopPlaying(bool afterCurrent) { toggleStopAfterCurrent(afterCurrent); if (!stopAfterCurrent) { sendCommand("stop"); } } void MPDConnection::clearStopAfter() { toggleStopAfterCurrent(false); } void MPDConnection::getStats(bool andUpdate) { Response response=sendCommand(andUpdate ? "command_list_begin\nupdate\nstats\ncommand_list_end" : "stats"); if (response.ok) { MPDStatsValues stats=MPDParseUtils::parseStats(response.data); dbUpdate=stats.dbUpdate; emit statsUpdated(stats); } } void MPDConnection::getStatus() { Response response=sendCommand("status"); if (response.ok) { MPDStatusValues sv=MPDParseUtils::parseStatus(response.data); lastStatusPlayQueueVersion=sv.playlist; if (stopAfterCurrent && (currentSongId!=sv.songId || (songPos>0 && sv.timeElapsed<(qint32)songPos))) { if (sendCommand("stop").ok) { sv.state=MPDState_Stopped; } toggleStopAfterCurrent(false); } currentSongId=sv.songId; emit statusUpdated(sv); } } void MPDConnection::getUrlHandlers() { Response response=sendCommand("urlhandlers"); if (response.ok) { handlers=MPDParseUtils::parseUrlHandlers(response.data).toSet(); } } /* * Data is written during idle. * Retrieve it and parse it */ void MPDConnection::idleDataReady() { DBUG << "idleDataReady"; if (idleSocket.bytesAvailable() == 0) { 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(); if (wasConnected && Success!=connectToMPD(idleSocket, true)) { // Failed to connect idle socket - so close *both* disconnectFromMPD(); emit stateChanged(false); emit error(i18n("Connection to %1 failed", details.description()), true); } if (QAbstractSocket::ConnectedState==idleSocket.state()) { connect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState))); } } } /* * Parse data returned by the mpd deamon on an idle commond. */ void MPDConnection::parseIdleReturn(const QByteArray &data) { DBUG << "parseIdleReturn:" << data; Response response(data.endsWith("OK\n"), data); if (response.ok) { DBUG << (void *)(&idleSocket) << "write idle"; idleSocket.write("idle\n"); idleSocket.waitForBytesWritten(); } else { DBUG << "idle failed? reconnect"; idleSocket.close(); if (Success!=connectToMPD(idleSocket, true)) { // Failed to connect idle socket - so close *both* disconnectFromMPD(); emit stateChanged(false); emit error(i18n("Connection to %1 failed", details.description()), true); } return; } QList 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 == "changed: database") { getStats(false); playListInfo(); playListUpdated=true; } else if (line == "changed: update") { emit databaseUpdated(); } else if (line == "changed: stored_playlist") { emit storedPlayListUpdated(); } else if (line == "changed: playlist") { if (!playListUpdated) { playListChanges(); } } else if (!statusUpdated && (line == "changed: player" || line == "changed: mixer" || line == "changed: options")) { getStatus(); getReplayGain(); statusUpdated=true; } else if (line == "changed: output") { outputs(); } else if (line == "OK" || line.startsWith("OK MPD ") || line.isEmpty()) { ; } else { DBUG << "Unknown command in idle return: " << line; } } } void MPDConnection::outputs() { Response response=sendCommand("outputs"); if (response.ok) { emit outputsUpdated(MPDParseUtils::parseOuputs(response.data)); } } void MPDConnection::enableOutput(int id, bool enable) { sendCommand((enable ? "enableoutput " : "disableoutput ")+QByteArray::number(id)); } /* * Admin commands */ void MPDConnection::update() { sendCommand("update"); } /* * Database commands */ /** * Get all files in the playlist with detailed info (artist, album, * title, time etc). */ void MPDConnection::loadLibrary() { emit updatingLibrary(); Response response=sendCommand("listallinfo"); if (response.ok) { emit musicLibraryUpdated(MPDParseUtils::parseLibraryItems(response.data, details.dir, ver), dbUpdate); } emit updatedLibrary(); } /** * Get all the files and dir in the mpdmusic dir. * */ void MPDConnection::loadFolders() { emit updatingFileList(); Response response=sendCommand("listall"); if (response.ok) { emit dirViewUpdated(MPDParseUtils::parseDirViewItems(response.data)); } emit updatedFileList(); } /* * 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) { emit playlistsRetrieved(MPDParseUtils::parsePlaylists(response.data)); } } void MPDConnection::playlistInfo(const QString &name) { Response response=sendCommand("listplaylistinfo "+encodeName(name)); if (response.ok) { emit playlistInfoRetrieved(name, MPDParseUtils::parseSongs(response.data)); } } void MPDConnection::loadPlaylist(const QString &name, bool replace) { if (replace) { clear(); getStatus(); } if (sendCommand("load "+encodeName(name)).ok) { 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(i18n("Failed to rename %1 to %2", oldName, 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(i18n("Failed to save %1", 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(i18n("You cannot add parts of a cue sheet to a playlist!")); return; } else if (isPlaylist(song)) { emit error(i18n("You cannot add a playlist to another playlist!")); return; } } } QByteArray encodedName=encodeName(name); QByteArray send = "command_list_begin\n"; foreach (const QString &s, songs) { send += "playlistadd "+encodedName+" "+encodeName(s)+"\n"; } send += "command_list_end"; if (sendCommand(send).ok) { if (size>0) { QList items; for(int i=0; i &positions) { if (positions.isEmpty()) { return; } QByteArray encodedName=encodeName(name); QList sorted=positions; QList removed; qSort(sorted); for (int i=sorted.count()-1; i>=0; --i) { quint32 idx=sorted.at(i); QByteArray data = "playlistdelete "; data += encodedName; data += " "; data += QByteArray::number(idx); if (sendCommand(data).ok) { removed.prepend(idx); } else { break; } } if (removed.count()) { emit removedFromPlaylist(name, removed); } // playlistInfo(name); } void MPDConnection::setPriority(const QList &ids, quint8 priority) { if (canUsePriority()) { QByteArray send = "command_list_begin\n"; foreach (quint32 id, ids) { send += "prioid "+QByteArray::number(priority)+" "+QByteArray::number(id)+"\n"; } send += "command_list_end"; if (sendCommand(send).ok) { emit prioritySet(ids, priority); } } } void MPDConnection::moveInPlaylist(const QString &name, const QList &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 &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 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 destinatino row, manipulate destination row posOffset++; } send += cmd; send += QByteArray::number(moveItems.at(i)); send += " "; send += QByteArray::number(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 += QByteArray::number(size - 1 - i); send += " "; send += QByteArray::number(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); } } 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('/')) { 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(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; } }