/* * Cantata * * Copyright (c) 2011-2014 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 "models/musiclibraryitemroot.h" #include "models/streamsmodel.h" #ifndef ENABLE_UBUNTU #include "mpduser.h" #endif #include "support/localize.h" #include "support/utils.h" #include "support/globalstatic.h" #include #include #include #include #include #include "support/thread.h" #include "gui/settings.h" #include "cuefile.h" #ifdef QT_QTDBUS_FOUND #include "dbus/powermanagement.h" #endif #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; static int maxFilesPerAddCommand=10000; static bool alwaysUseLsInfo=false; 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"); #ifdef ENABLE_DYNAMIC static const QByteArray constDynamicIn("cantata-dynamic-in"); static const QByteArray constDynamicOut("cantata-dynamic-out"); #endif static inline int socketTimeout(int dataSize) { static const int constDataBlock=100000; 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) 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) { 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" << attempt; if (socket.waitForReadyRead()) { 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) { QByteArray data = readFromSocket(socket); 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 i18n("Uknown")+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; } return rv; } } return data; } MPDConnectionDetails::MPDConnectionDetails() : port(6600) , dirReadable(false) { } QString MPDConnectionDetails::getName() const { #ifdef ENABLE_UBUNTU return name.isEmpty() ? i18n("Default") : name; #else return name.isEmpty() ? i18n("Default") : (name==MPDUser::constName ? MPDUser::translatedName() : name); #endif } QString MPDConnectionDetails::description() const { if (hostname.startsWith('/')) { return i18nc("name (host)", "\"%1\"", getName()); } else { return i18nc("name (host:port)", "\"%1\" (%2:%3)", getName(), hostname, QString::number(port)); // Use QString::number to prevent KDE's i18n converting 6600 to 6,600! } } void MPDConnectionDetails::setDirReadable() { dirReadable=Utils::isDirReadable(dir); } 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) , mopidy(false) , isUpdatingDb(false) { qRegisterMetaType("Song"); qRegisterMetaType("Output"); qRegisterMetaType("Playlist"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); qRegisterMetaType >("QSet"); qRegisterMetaType >("QSet"); qRegisterMetaType("QAbstractSocket::SocketState"); qRegisterMetaType("MPDStatsValues"); qRegisterMetaType("MPDStatusValues"); qRegisterMetaType("MPDConnectionDetails"); qRegisterMetaType("Stream"); qRegisterMetaType >("QList"); #ifdef QT_QTDBUS_FOUND connect(PowerManagement::self(), SIGNAL(resuming()), this, SLOT(reconnect())); #endif #ifndef ENABLE_UBUNTU maxFilesPerAddCommand=Settings::self()->mpdListSize(); alwaysUseLsInfo=Settings::self()->alwaysUseLsInfo(); #endif } 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() { #ifndef ENABLE_UBUNTU if (details.name==MPDUser::constName && Settings::self()->stopOnExit()) { MPDUser::self()->stop(); } #endif 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(constOkMpdValue)) { DBUG << (void *)(&socket) << "Received identification string"; } lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=0; playQueueIds.clear(); emit cantataStreams(QList(), 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) { #ifdef ENABLE_DYNAMIC dynamicId.clear(); setupRemoteDynamic(); #endif 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 i18n("Connection to %1 failed", details.description()); case ProxyError: return i18n("Connection to %1 failed - please check your proxy settings", details.description()); case IncorrectPassword: return i18n("Connection to %1 failed - incorrect password", details.description()); } } 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; emit socketAddress(sock.address()); } 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; 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(); 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(errorString(Failed), true); reconnectStart=0; } break; default: emit stateChanged(false); emit error(errorString(status), true); reconnectStart=0; break; } } void MPDConnection::setDetails(const MPDConnectionDetails &d) { #ifdef ENABLE_UBUNTU const MPDConnectionDetails &det=d; #else bool isUser=d.name==MPDUser::constName; const MPDConnectionDetails &det=isUser ? MPDUser::self()->details() : 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) { DBUG << "setDetails" << details.hostname << details.port << (details.password.isEmpty() ? false : true); disconnectFromMPD(); DBUG << "call connectToMPD"; unmuteVol=-1; toggleStopAfterCurrent(false); mopidy=false; #ifndef ENABLE_UBUNTU if (isUser) { MPDUser::self()->start(); } #endif if (isUpdatingDb) { isUpdatingDb=false; emit updatingDatabase(); // Stop any spinners... } ConnectionReturn status=connectToMPD(); switch (status) { case Success: getStatus(); getStats(); getUrlHandlers(); getTagTypes(); 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) { DBUG << (void *)(&sock) << "sendCommand:" << log(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()) { DBUG << (void *)(&sock) << "Socket (state:" << sock.state() << ") need to reconnect"; sock.close(); ConnectionReturn status=connectToMPD(sock); if (Success!=status) { // Failed to connect, so close *both* sockets! disconnectFromMPD(); emit stateChanged(false); emit error(errorString(status), true); return Response(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); } if (!response.ok) { DBUG << log(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 occur more often // when dynamizer is running. However, simply reconnecting seems to resolve the issue. return sendCommand(command, emitErrors, false); } 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 ((command.startsWith("crossfade ") || command.startsWith("replay_gain_mode ")) && "incorrect arguments"==response.getError(command)) { 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(i18n("Failed to load. Please check user \"mpd\" has read permission.")); } else if (!details.isLocal() && -1!=response.data.indexOf("Access denied")) { emit error(i18n("Failed to load. MPD can only play local files if connected via a local socket.")); } else if (!response.getError(command).isEmpty()) { emit error(i18n("MPD reported the following error: %1", response.getError(command))); } else { disconnectFromMPD(); emit stateChanged(false); emit error(i18n("Failed to send command. Disconnected from %1", details.description()), true); } } else if (!response.getError(command).isEmpty()) { emit error(i18n("MPD reported the following error: %1", response.getError(command))); } /*else if ("listallinfo"==command && ver>=MPD_MAKE_VERSION(0,18,0)) { disconnectFromMPD(); emit stateChanged(false); emit error(i18n("Failed to load library. Please increase \"max_output_buffer_size\" in MPD's config file.")); } */ 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(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, bool replace, quint8 priority) { add(files, 0, 0, replace ? AddReplaceAndPlay : AddToEnd, priority); } void MPDConnection::add(const QStringList &files, quint32 pos, quint32 size, int action, quint8 priority) { QList 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 &priority) { toggleStopAfterCurrent(false); if (AddToEnd!=action) { clear(); getStatus(); } QList fileLists; if (priority.count()<=1 && origList.count()>maxFilesPerAddCommand) { int numChunks=(origList.count()/maxFilesPerAddCommand)+(origList.count()%maxFilesPerAddCommand ? 1 : 0); for (int i=0; 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 (AddReplaceAndPlay==action /*&& addedFile */&& !origList.isEmpty()) { // Dont emit error if fail plays, might be that playlist was not loaded... sendCommand("play "+quote(0), false); } emit added(origList); } } void MPDConnection::populate(const QStringList &files, const QList &priority) { add(files, 0, 0, AddAndReplace, 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(), false); } } void MPDConnection::removeSongs(const QList &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 &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 += 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::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 " 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 << 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; emit statusUpdated(sv); QList changes=MPDParseUtils::parseChanges(response.data); if (!changes.isEmpty()) { bool first=true; quint32 firstPos=0; QList songs; QList newCantataStreams; 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=(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 removed=prevIds-playQueueIds.toSet(); if (!removed.isEmpty()) { emit removedIds(removed); } emit playlistUpdated(songs); return; } } playListInfo(); } void MPDConnection::playListInfo() { Response response=sendCommand("playlistinfo"); if (response.ok) { lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion; QList songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_PlayQueue); playQueueIds.clear(); streamIds.clear(); QList 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); emit playlistUpdated(songs); Response status=sendCommand("status"); if (status.ok) { MPDStatusValues sv=MPDParseUtils::parseStatus(status.data); lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist; emit statusUpdated(sv);; } } } /* * Playback commands */ void MPDConnection::setCrossFade(int secs) { sendCommand("crossfade "+quote(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 MPDConnection::quote(b ? 1 : 0); } void MPDConnection::setPause(bool toggle) { toggleStopAfterCurrent(false); sendCommand("pause "+value(toggle)); } void MPDConnection::play() { toggleStopAfterCurrent(false); sendCommand("play"); } //void MPDConnection::startPlayingSong(quint32 song) //{ // toggleStopAfterCurrent(false); // sendCommand("play "+quote(song)); //} void MPDConnection::startPlayingSongId(qint32 songId) { toggleStopAfterCurrent(false); sendCommand("playid "+quote(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 "+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 (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) { 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.toTime_t(); 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; if (!isUpdatingDb && -1!=sv.updatingDb) { isUpdatingDb=true; emit updatingDatabase(); } else if (isUpdatingDb && -1==sv.updatingDb) { isUpdatingDb=false; emit updatedDatabase(); } emit statusUpdated(sv); } } void MPDConnection::getUrlHandlers() { Response response=sendCommand("urlhandlers"); if (response.ok) { handlers=MPDParseUtils::parseList(response.data, QByteArray("handler: ")).toSet(); } } 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))); } } } /* * 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"; 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 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) { getStatus(); } else if (constIdleStoredPlaylistValue==value) { listPlaylists(); } 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(); } #ifdef ENABLE_DYNAMIC else if (constIdleSubscriptionValue==value) { //if (dynamicId.isEmpty()) { setupRemoteDynamic(); //} } else if (constIdleMessageValue==value) { readRemoteDynamicMessages(); } #endif } } 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) { sendCommand((enable ? "enableoutput " : "disableoutput ")+quote(id)); } /* * Admin commands */ void MPDConnection::update() { if (sendCommand("update").ok) { if (isMopdidy()) { // Mopidy does not support MPD's update command. So, when user presses update DB, what we // do instead is clear library/dir caches, then when response to getStats is received, // library/dir should get refreshed... getStats(); } } } /* * Database commands */ void MPDConnection::loadLibrary() { emit updatingLibrary(); Response response=alwaysUseLsInfo ? Response(false) : sendCommand("listallinfo", false); MusicLibraryItemRoot *root=0; if (response.ok) { root = new MusicLibraryItemRoot; MPDParseUtils::parseLibraryItems(response.data, details.dir, ver, mopidy, root); } else { // MPD >=0.18 can fail listallinfo for large DBs, so get info dir by dir... root = new MusicLibraryItemRoot; if (!listDirInfo("/", root)) { delete root; root=0; } } if (root) { root->applyGrouping(); emit musicLibraryUpdated(root, dbUpdate); } emit updatedLibrary(); } void MPDConnection::loadFolders() { emit updatingFileList(); Response response=sendCommand("listall"); if (response.ok) { emit dirViewUpdated(MPDParseUtils::parseDirViewItems(response.data, mopidy), dbUpdate); } 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, MPDParseUtils::Loc_Playlists)); } } 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!")+QLatin1String(" (")+song+QLatin1Char(')')); return; } else if (isPlaylist(song)) { emit error(i18n("You cannot add a playlist to another playlist!")+QLatin1String(" (")+song+QLatin1Char(')')); 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 += 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 &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 songs; Response response=sendCommand("search "+field.toLatin1()+" "+encodeName(value)); if (response.ok) { songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Library); qSort(songs); } emit searchResponse(id, songs); } void MPDConnection::listStreams() { Response response=sendCommand("listplaylistinfo "+encodeName(StreamsModel::constPlayListName)); if (response.ok) { QList songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Streams); QList streams; foreach (const Song &song, songs) { streams.append(Stream(song.file, song.name)); } emit streamList(streams); } } void MPDConnection::saveStream(const QString &url, const QString &name) { if (sendCommand("playlistadd "+encodeName(StreamsModel::constPlayListName)+" "+encodeName(MPDParseUtils::addStreamName(url, name))).ok) { emit savedStream(url, name); } } void MPDConnection::removeStreams(const QList &positions) { if (positions.isEmpty()) { return; } QByteArray encodedName=encodeName(StreamsModel::constPlayListName); 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 += quote(idx); if (sendCommand(data).ok) { removed.prepend(idx); } else { break; } } if (removed.count()) { emit removedStreams(removed); } } void MPDConnection::editStream(const QString &url, const QString &name, quint32 position) { QByteArray encodedName=encodeName(StreamsModel::constPlayListName); 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(i18n("Failed to send '%1' to %2. Please check %2 is registered with MPD.", msg, clientName.isEmpty() ? channel : clientName)); emit clientMessageFailed(channel, msg); } } void MPDConnection::sendDynamicMessage(const QStringList &msg) { #ifdef ENABLE_DYNAMIC // 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); } #else Q_UNUSED(msg) #endif } 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 += 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::listDirInfo(const QString &dir, MusicLibraryItemRoot *root) { bool topLevel="/"==dir; Response response=sendCommand(topLevel ? "lsinfo" : ("lsinfo "+encodeName(dir))); if (response.ok) { QSet childDirs; MPDParseUtils::parseLibraryItems(response.data, details.dir, ver, mopidy, root, !topLevel, &childDirs); foreach (const QString &child, childDirs) { if (!listDirInfo(child, root)) { return false; } } return true; } else { return false; } } #ifdef ENABLE_DYNAMIC bool MPDConnection::checkRemoteDynamicSupport() { if (ver>=MPD_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 channels=QList() << constDynamicOut << constDynamicOut+'-'+dynamicId; foreach (const QByteArray &channel, channels) { if (messages.contains(channel)) { foreach (const QString &m, messages[channel]) { if (!m.isEmpty()) { DBUG << "Recieved 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); } } } } } } } } #endif 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; } }