/* * 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" #ifdef ENABLE_KDE_SUPPORT #include #include #include #else #include #endif #include //#undef qDebug //#define qDebug qWarning #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 } static QByteArray readFromSocket(QTcpSocket &socket) { QByteArray data; while (socket.state() == QAbstractSocket::ConnectedState) { while (socket.bytesAvailable() == 0 && socket.state() == QAbstractSocket::ConnectedState) { qDebug("MPDConnection: waiting for read data."); if (socket.waitForReadyRead(5000)) { break; } } data.append(socket.readAll()); if (data.endsWith("OK\n") || data.startsWith("OK") || data.startsWith("ACK")) { break; } } if(data.size()>128) { qDebug() << "Read (bytes):" << data.size(); } else { qDebug() << "Read:" << data; } return data; } MPDConnection::Response readReply(QTcpSocket &socket) { QByteArray data = readFromSocket(socket); return MPDConnection::Response(data.endsWith("OK\n"), data); } MPDConnection::MPDConnection() { connect(&sock, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState))); connect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onIdleSocketStateChanged(QAbstractSocket::SocketState))); } MPDConnection::~MPDConnection() { disconnect(&sock, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState))); disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onIdleSocketStateChanged(QAbstractSocket::SocketState))); sock.disconnectFromHost(); idleSocket.disconnectFromHost(); quit(); while (isRunning()) msleep(100); } bool MPDConnection::connectToMPD(QTcpSocket &socket, bool enableIdle) { if (socket.state() != QAbstractSocket::ConnectedState) { qDebug() << "Connecting" << enableIdle; if (hostname.isEmpty() || port == 0) { qDebug("MPDConnection: no hostname and/or port supplied."); return false; } socket.connectToHost(hostname, port); if (socket.waitForConnected(5000)) { qDebug("MPDConnectionBase established"); QByteArray recvdata = readFromSocket(socket); if (recvdata.startsWith("OK MPD")) { qDebug("Received identification string"); } recvdata.clear(); if (!password.isEmpty()) { qWarning("MPDConnection: setting password..."); QByteArray senddata = "password "; senddata += password.toUtf8(); senddata += "\n"; socket.write(senddata); socket.waitForBytesWritten(5000); if (readReply(socket).ok) { qDebug("MPDConnection: password accepted"); } else { qDebug("MPDConnection: password rejected"); } } if(enableIdle) { connect(&socket, SIGNAL(readyRead()), this, SLOT(idleDataReady())); qDebug() << "Enabling idle"; socket.write("idle\n"); socket.waitForBytesWritten(); } return true; } else { qDebug("Couldn't connect"); return false; } } qDebug() << "Already connected" << enableIdle; return true; } bool MPDConnection::connectToMPD() { return connectToMPD(sock) && connectToMPD(idleSocket, true); } void MPDConnection::disconnectFromMPD() { sock.disconnectFromHost(); idleSocket.disconnectFromHost(); } void MPDConnection::setDetails(const QString &host, const quint16 p, const QString &pass) { // TODO: If diff, then reconnect! hostname=host; port=p; password=pass; } bool MPDConnection::isConnected() { return (sock.state() == QAbstractSocket::ConnectedState); } MPDConnection::Response MPDConnection::sendCommand(const QByteArray &command) { if (!connectToMPD()) { return Response(); } mutex.lock(); qDebug() << "command: " << command; sock.write(command); sock.write("\n"); sock.waitForBytesWritten(5000); Response response=readReply(sock); mutex.unlock(); return response; } /* * Playlist commands */ void MPDConnection::add(const QStringList &files) { QByteArray send = "command_list_begin\n"; for (int i = 0; i < files.size(); i++) { send += "add \""; send += files.at(i).toUtf8(); send += "\"\n"; } send += "command_list_end"; if (!sendCommand(send).ok){ qDebug("Couldn't add song(s) to playlist"); } } /** * Add all the files in the QStringList to the playlist at * postition post. * * NOTE: addid is not fully supported in < 0.14 So add everything * and then move everything to the right spot. * * @param files A QStringList with all the files to add * @param pos Position to add the files * @param size The size of the current playlist */ void MPDConnection::addid(const QStringList &files, const quint32 pos, const quint32 size) { QByteArray send = "command_list_begin\n"; int cur_size = size; for (int i = 0; i < files.size(); i++) { send += "add \""; send += files.at(i).toUtf8(); send += "\""; send += "\n"; send += "move "; send += QByteArray::number(cur_size); send += " "; send += QByteArray::number(pos); send += "\n"; cur_size++; } send += "command_list_end"; if (!sendCommand(send).ok) { qDebug("Couldn't add song(s) to playlist"); } } void MPDConnection::clear() { if(sendCommand("clear").ok) { qDebug("Couldn't clear playlist"); } } void MPDConnection::removeSongs(const QList &items) { QByteArray send = "command_list_begin\n"; for (int i = 0; i < items.size(); i++) { send += "deleteid "; send += QByteArray::number(items.at(i)); send += "\n"; } send += "command_list_end"; if (!sendCommand(send).ok) { qDebug("Couldn't remove songs from playlist"); } } void MPDConnection::move(const quint32 from, const quint32 to) { QByteArray send = "move " + QByteArray::number(from) + " " + QByteArray::number(to); qWarning() << send; if (!sendCommand(send).ok) { qDebug("Couldn't move files around in playlist"); } } void MPDConnection::move(const QList items, const quint32 pos, const quint32 size) { 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"; if (!sendCommand(send).ok) { qDebug("Couldn't move songs around in playlist"); } } void MPDConnection::shuffle() { if (!sendCommand("shuffle").ok) { qDebug("Couldn't shuffle playlist"); } } void MPDConnection::shuffle(const quint32 from, const quint32 to) { QByteArray command = "shuffle "; command += QByteArray::number(from); command += ":"; command += QByteArray::number(to + 1); if (!sendCommand(command).ok) { qDebug("Couldn't shuffle playlist"); } } void MPDConnection::currentSong() { Response response=sendCommand("currentsong"); if(response.ok) { emit currentSongUpdated(MPDParseUtils::parseSong(response.data)); } } void MPDConnection::playListInfo() { Response response=sendCommand("playlistinfo"); if(response.ok) { emit playlistUpdated(MPDParseUtils::parseSongs(response.data)); } } /* * Playback commands */ void MPDConnection::setCrossFade(const quint8 secs) { QByteArray data = "crossfade "; data += QByteArray::number(secs); if (!sendCommand(data).ok) { qDebug("Couldn't set xfade"); } } void MPDConnection::setReplayGain(const QString &v) { QByteArray data = "replay_gain_mode "; data += v.toLatin1(); if (!sendCommand(data).ok) { qDebug("Couldn't set replay_gain_mode"); } } QString 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: "))) { return lines[0].mid(18); } return QString(); } void MPDConnection::goToNext() { if (!sendCommand("next").ok) { qDebug("Couldn't go to next track"); } } void MPDConnection::setPause(const bool toggle) { QByteArray data = "pause "; if (toggle == true) data += "1"; else data += "0"; if (!sendCommand(data).ok) { qDebug("Couldn't set pause"); } } void MPDConnection::startPlayingSong(const quint32 song) { QByteArray data = "play "; data += QByteArray::number(song); if (!sendCommand(data).ok) { qDebug("Couldn't start playing song"); } } void MPDConnection::startPlayingSongId(const quint32 song_id) { QByteArray data = "playid "; data += QByteArray::number(song_id); if (!sendCommand(data).ok) { qDebug("Couldn't start playing song id"); } } void MPDConnection::goToPrevious() { if (!sendCommand("previous").ok) { qDebug("Couldn't go to previous track"); } } void MPDConnection::setConsume(const bool toggle) { QByteArray data = "consume "; if (toggle == true) data += "1"; else data += "0"; if (!sendCommand(data).ok) { qDebug("Couldn't toggle consume"); } } void MPDConnection::setRandom(const bool toggle) { QByteArray data = "random "; if (toggle == true) data += "1"; else data += "0"; if (!sendCommand(data).ok) { qDebug("Couldn't toggle random"); } } void MPDConnection::setRepeat(const bool toggle) { QByteArray data = "repeat "; if (toggle == true) data += "1"; else data += "0"; if (!sendCommand(data).ok) { qDebug("Couldn't toggle repeat"); } } void MPDConnection::setSeek(const quint32 song, const quint32 time) { QByteArray data = "seek "; data += QByteArray::number(song); data += " "; data += QByteArray::number(time); if (!sendCommand(data).ok) { qDebug("Couldn't set seek position"); } } void MPDConnection::setSeekId(const quint32 song_id, const quint32 time) { QByteArray data = "seekid "; data += QByteArray::number(song_id); data += " "; data += QByteArray::number(time); if (!sendCommand(data).ok) { qDebug("Couldn't set seek position"); } } void MPDConnection::setVolume(const quint8 vol) { QByteArray data = "setvol "; data += QByteArray::number(vol); if (!sendCommand(data).ok) { qDebug("Couldn't set volume"); } } void MPDConnection::stopPlaying() { if (!sendCommand("stop").ok) { qDebug("Couldn't stop playing"); } } void MPDConnection::getStats() { Response response=sendCommand("stats"); if(response.ok) { MPDParseUtils::parseStats(response.data); emit statsUpdated(); } } void MPDConnection::getStatus() { Response response=sendCommand("status"); if(response.ok) { MPDParseUtils::parseStatus(response.data); emit statusUpdated(); } } /* * Data is written during idle. * Retrieve it and parse it */ void MPDConnection::idleDataReady() { qDebug() << "idleDataReady"; QByteArray data; if (idleSocket.bytesAvailable() == 0) { return; } parseIdleReturn(readFromSocket(idleSocket)); idleSocket.write("idle\n"); idleSocket.waitForBytesWritten(); } /* * Socket state has changed, currently we only use this to gracefully * handle disconnects. */ void MPDConnection::onSocketStateChanged(QAbstractSocket::SocketState socketState) { if (socketState == QAbstractSocket::ClosingState){ // emit mpdConnectionDied(); sock.disconnectFromHost(); connectToMPD(sock); } } void MPDConnection::onIdleSocketStateChanged(QAbstractSocket::SocketState socketState) { if (socketState == QAbstractSocket::ClosingState){ idleSocket.disconnectFromHost(); connectToMPD(idleSocket); } } /* * Parse data returned by the mpd deamon on an idle commond. */ void MPDConnection::parseIdleReturn(const QByteArray &data) { qDebug() << "parseIdleReturn:" << data; QList lines = data.split('\n'); QByteArray line; /* * See http://www.musicpd.org/doc/protocol/ch02.html */ foreach(line, lines) { if (line == "changed: database") { /* * Temp solution */ getStats(); } else if (line == "changed: update") { emit databaseUpdated(); } else if (line == "changed: stored_playlist") { emit storedPlayListUpdated(); } else if (line == "changed: playlist") { playListInfo(); } else if (line == "changed: player") { getStatus(); } else if (line == "changed: mixer") { getStatus(); } else if (line == "changed: output") { outputs(); } else if (line == "changed: options") { getStatus(); } else if (line == "OK") { break; } else { qWarning() << "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(const quint8 id) { QByteArray data = "enableoutput "; data += QByteArray::number(id); if (!sendCommand(data).ok) { qDebug("Couldn't enable output"); } } void MPDConnection::disableOutput(const quint8 id) { QByteArray data = "disableoutput "; data += QByteArray::number(id); if (!sendCommand(data).ok) { qDebug("Couldn't disable output"); } } /* * Admin commands */ void MPDConnection::update() { sendCommand("update"); } /* * Database commands */ /** * Get all files in the playlist with detailed info (artist, album, * title, time etc). * * @param db_update The last update time of the library */ void MPDConnection::listAllInfo(QDateTime db_update) { Response response=sendCommand("listallinfo"); if(response.ok) { qDebug() << "parseLibraryItems:" << response.data; emit musicLibraryUpdated(MPDParseUtils::parseLibraryItems(response.data), db_update); } } /** * Get all the files and dir in the mpdmusic dir. * */ void MPDConnection::listAll() { Response response=sendCommand("listall"); if(response.ok) { emit dirViewUpdated(MPDParseUtils::parseDirViewItems(response.data)); } } /* * Playlists commands */ void MPDConnection::listPlaylist() { sendCommand("listplaylist"); } void MPDConnection::listPlaylists() { Response response=sendCommand("listplaylists"); if(response.ok) { emit playlistsRetrieved(MPDParseUtils::parsePlaylists(response.data)); } } void MPDConnection::load(QString name) { QByteArray data("load "); data += "\"" + name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"") + "\""; if (!sendCommand(data).ok) { qDebug("Couldn't load playlist"); } } void MPDConnection::rename(const QString oldName, const QString newName) { QByteArray data("rename "); data += "\"" + oldName.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"") + "\""; data += " "; data += "\"" + newName.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"") + "\""; if (!sendCommand(data).ok) { qDebug("Couldn't rename playlist"); } } void MPDConnection::rm(QString name) { QByteArray data("rm "); data += "\"" + name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"") + "\""; if (!sendCommand(data).ok) { qDebug("Couldn't remove playlist"); } } void MPDConnection::save(QString name) { QByteArray data("save "); data += "\"" + name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"") + "\""; Response response = sendCommand(data); if (!response.ok && response.data.endsWith("Playlist already exists\n")) { #ifdef ENABLE_KDE_SUPPORT if (KMessageBox::Yes==KMessageBox::warningYesNo(QApplication::activeWindow(), i18n("A Playlist named %1 already exists!
Overwrite?").arg(name))) { rm(name); response = sendCommand(data); if (!response.ok) { KMessageBox::error(QApplication::activeWindow(), i18n("Failed to save playlist.")); } } #else // TODO: Prompt user as per KDE version... QMessageBox::warning(QApplication::activeWindow(), "Warning", "Playlist couldn't be saved since there already exists a playlist with the same name."); #endif } }