Files
cantata/mpd/mpdconnection.cpp

761 lines
19 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011 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"
#ifdef ENABLE_KDE_SUPPORT
#include <KDE/KMessageBox>
#include <KDE/KLocale>
#include <KDE/KGlobal>
#else
#include <QtGui/QMessageBox>
#endif
#include <QtGui/QApplication>
//#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()
{
if (sock.state() == QAbstractSocket::ConnectedState) {
sock.disconnectFromHost();
}
if (idleSocket.state() == QAbstractSocket::ConnectedState) {
idleSocket.disconnectFromHost();
}
}
void MPDConnection::setDetails(const QString &host, const quint16 p, const QString &pass)
{
if (hostname!=host || port!=p || password!=pass) {
disconnectFromMPD();
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(false);
}
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<qint32> &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<quint32> items, const quint32 pos, const quint32 size)
{
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 += 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<QByteArray> 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 <i>%1</i> already exists!<br/>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
}
}