1077 lines
31 KiB
C++
1077 lines
31 KiB
C++
/*
|
|
* Cantata
|
|
*
|
|
* Copyright (c) 2011-2013 Craig Drummond <craig.p.drummond@gmail.com>
|
|
*
|
|
* ----
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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 this program; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "dynamic.h"
|
|
#include "config.h"
|
|
#include "utils.h"
|
|
#include "mpdconnection.h"
|
|
#include "icons.h"
|
|
#include "itemview.h"
|
|
#include "networkaccessmanager.h"
|
|
#include "settings.h"
|
|
#include "localize.h"
|
|
#include "qtplural.h"
|
|
#include "actioncollection.h"
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
#include <KDE/KGlobal>
|
|
K_GLOBAL_STATIC(Dynamic, instance)
|
|
#endif
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include <QProcess>
|
|
#include <QTimer>
|
|
#include <QIcon>
|
|
#include <QUrl>
|
|
#include <QUdpSocket>
|
|
#include <QNetworkProxy>
|
|
#if QT_VERSION >= 0x050000
|
|
#include <QUrlQuery>
|
|
#endif
|
|
#include <signal.h>
|
|
|
|
#include <QDebug>
|
|
static bool debugEnabled=false;
|
|
#define DBUG if (debugEnabled) qWarning() << metaObject()->className() << __FUNCTION__
|
|
void Dynamic::enableDebug()
|
|
{
|
|
debugEnabled=true;
|
|
}
|
|
|
|
static const QString constDir=QLatin1String("dynamic");
|
|
static const QString constExtension=QLatin1String(".rules");
|
|
static const QString constActiveRules=QLatin1String("rules");
|
|
static const QString constLockFile=QLatin1String("lock");
|
|
|
|
static const QString constIdCmd=QLatin1String("id");
|
|
static const QString constListCmd=QLatin1String("list");
|
|
static const QString constStatusCmd=QLatin1String("status");
|
|
static const QString constSaveCmd=QLatin1String("save");
|
|
static const QString constDeleteCmd=QLatin1String("delete");
|
|
static const QString constSetActiveCmd=QLatin1String("setActive");
|
|
static const QString constControlCmd=QLatin1String("control");
|
|
static const QString constStatusTime=QLatin1String("statusTime");
|
|
static const QString constStatusTimeResp=QLatin1String("TIME:");
|
|
static const QString constStatusStateResp=QLatin1String("STATE:");
|
|
static const QString constStatusRuleResp=QLatin1String("RULES:");
|
|
static const QString constUpdateRequiredResp=QLatin1String("UPDATE_REQUIRED");
|
|
|
|
Dynamic::Command Dynamic::toCommand(const QString &cmd)
|
|
{
|
|
if (constIdCmd==cmd) {
|
|
return Id;
|
|
}
|
|
if (constListCmd==cmd) {
|
|
return List;
|
|
}
|
|
if (constStatusCmd==cmd) {
|
|
return Status;
|
|
}
|
|
if (constSaveCmd==cmd) {
|
|
return Save;
|
|
}
|
|
if (constDeleteCmd==cmd) {
|
|
return Del;
|
|
}
|
|
if (constSetActiveCmd==cmd) {
|
|
return SetActive;
|
|
}
|
|
if (constControlCmd==cmd) {
|
|
return Control;
|
|
}
|
|
return Unknown;
|
|
}
|
|
|
|
QString Dynamic::toString(Command cmd)
|
|
{
|
|
switch (cmd) {
|
|
case Unknown: return QString();
|
|
case Id: return constIdCmd;
|
|
case List: return constListCmd;
|
|
case Status: return constStatusCmd;
|
|
case Save: return constSaveCmd;
|
|
case Del: return constDeleteCmd;
|
|
case SetActive: return constSetActiveCmd;
|
|
case Control: return constControlCmd;
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
Dynamic * Dynamic::self()
|
|
{
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
return instance;
|
|
#else
|
|
static Dynamic *instance=0;
|
|
if(!instance) {
|
|
instance=new Dynamic;
|
|
}
|
|
return instance;
|
|
#endif
|
|
}
|
|
|
|
const QString Dynamic::constRuleKey=QLatin1String("Rule");
|
|
const QString Dynamic::constArtistKey=QLatin1String("Artist");
|
|
const QString Dynamic::constSimilarArtistsKey=QLatin1String("SimilarArtists");
|
|
const QString Dynamic::constAlbumArtistKey=QLatin1String("AlbumArtist");
|
|
const QString Dynamic::constAlbumKey=QLatin1String("Album");
|
|
const QString Dynamic::constTitleKey=QLatin1String("Title");
|
|
const QString Dynamic::constGenreKey=QLatin1String("Genre");
|
|
const QString Dynamic::constDateKey=QLatin1String("Date");
|
|
const QString Dynamic::constExactKey=QLatin1String("Exact");
|
|
const QString Dynamic::constExcludeKey=QLatin1String("Exclude");
|
|
|
|
static const char * constMulticastMsgHeader="{CANTATA/";
|
|
const QString constStatusMsg(QLatin1String("STATUS:"));
|
|
|
|
MulticastReceiver::MulticastReceiver(QObject *parent, const QString &i, const QString &group, quint16 port)
|
|
: QObject(parent)
|
|
, id(i)
|
|
{
|
|
socket = new QUdpSocket(this);
|
|
socket->setProxy(QNetworkProxy(QNetworkProxy::NoProxy));
|
|
#if QT_VERSION < 0x050000
|
|
socket->bind(QHostAddress::Any, port, QUdpSocket::ShareAddress);
|
|
#else
|
|
socket->bind(QHostAddress::AnyIPv4, port, QAbstractSocket::ShareAddress);
|
|
#endif
|
|
socket->joinMulticastGroup(QHostAddress(group));
|
|
connect(socket, SIGNAL(readyRead()), this, SLOT(processMessages()));
|
|
}
|
|
|
|
void MulticastReceiver::processMessages()
|
|
{
|
|
static int headerLen=strlen(constMulticastMsgHeader);
|
|
|
|
while (socket->hasPendingDatagrams()) {
|
|
QByteArray datagram;
|
|
datagram.resize(socket->pendingDatagramSize());
|
|
socket->readDatagram(datagram.data(), datagram.size());
|
|
if (datagram.length()>headerLen && datagram.startsWith(constMulticastMsgHeader)) {
|
|
QString header(constMulticastMsgHeader+id+"}");
|
|
QString message=QString::fromUtf8(&(datagram.constData()[header.length()]));
|
|
DBUG << message;
|
|
if (message.startsWith(constStatusMsg)) {
|
|
emit status(message.mid(constStatusMsg.length()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Dynamic::Dynamic()
|
|
: localTimer(0)
|
|
, remoteTimer(0)
|
|
, remotePollingEnabled(false)
|
|
, statusTime(1)
|
|
, currentJob(0)
|
|
, currentCommand(Unknown)
|
|
, receiver(0)
|
|
{
|
|
loadLocal();
|
|
connect(this, SIGNAL(clear()), MPDConnection::self(), SLOT(clear()));
|
|
connect(MPDConnection::self(), SIGNAL(dynamicUrl(const QString &)), this, SLOT(dynamicUrlChanged(const QString &)));
|
|
// connect(MPDConnection::self(), SIGNAL(statusUpdated(const MPDStatusValues &)), this, SLOT(updateRemoteStatus()));
|
|
QTimer::singleShot(500, this, SLOT(checkHelper()));
|
|
startAction = ActionCollection::get()->createAction("startdynamic", i18n("Start Dynamic Playlist"), "media-playback-start");
|
|
stopAction = ActionCollection::get()->createAction("stopdynamic", i18n("Stop Dynamic Mode"), "process-stop");
|
|
}
|
|
|
|
QVariant Dynamic::headerData(int, Qt::Orientation, int) const
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
int Dynamic::rowCount(const QModelIndex &parent) const
|
|
{
|
|
return parent.isValid() ? 0 : entryList.count();
|
|
}
|
|
|
|
bool Dynamic::hasChildren(const QModelIndex &parent) const
|
|
{
|
|
return !parent.isValid();
|
|
}
|
|
|
|
QModelIndex Dynamic::parent(const QModelIndex &) const
|
|
{
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex Dynamic::index(int row, int column, const QModelIndex &parent) const
|
|
{
|
|
if (parent.isValid() || !hasIndex(row, column, parent) || row>=entryList.count()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
return createIndex(row, column);
|
|
}
|
|
|
|
#define IS_ACTIVE(E) !currentEntry.isEmpty() && (E)==currentEntry && (!isRemote() || QLatin1String("IDLE")!=lastState)
|
|
|
|
QVariant Dynamic::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid() || index.parent().isValid() || index.row()>=entryList.count()) {
|
|
return QVariant();
|
|
}
|
|
|
|
switch (role) {
|
|
case Qt::DisplayRole:
|
|
case Qt::ToolTipRole:
|
|
return entryList.at(index.row()).name;
|
|
case Qt::DecorationRole:
|
|
return IS_ACTIVE(entryList.at(index.row()).name) ? QIcon::fromTheme("media-playback-start") : Icons::self()->dynamicRuleIcon;
|
|
case ItemView::Role_SubText: {
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
return i18np("1 Rule", "%1 Rules", entryList.at(index.row()).rules.count());
|
|
#else
|
|
return QTP_RULES_STR(entryList.at(index.row()).rules.count());
|
|
#endif
|
|
}
|
|
case ItemView::Role_Actions: {
|
|
QVariant v;
|
|
v.setValue<QList<Action *> >(QList<Action *>() << (IS_ACTIVE(entryList.at(index.row()).name) ? stopAction : startAction));
|
|
return v;
|
|
}
|
|
default:
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
Qt::ItemFlags Dynamic::flags(const QModelIndex &index) const
|
|
{
|
|
if (index.isValid())
|
|
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
|
|
else
|
|
return Qt::NoItemFlags;
|
|
}
|
|
|
|
Dynamic::Entry Dynamic::entry(const QString &e)
|
|
{
|
|
if (!e.isEmpty()) {
|
|
QList<Entry>::Iterator it=find(e);
|
|
if (it!=entryList.end()) {
|
|
return *it;
|
|
}
|
|
}
|
|
|
|
return Entry();
|
|
}
|
|
|
|
bool Dynamic::save(const Entry &e)
|
|
{
|
|
if (e.name.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
QString string;
|
|
QTextStream str(&string);
|
|
foreach (const Rule &rule, e.rules) {
|
|
if (!rule.isEmpty()) {
|
|
str << constRuleKey << '\n';
|
|
Rule::ConstIterator it(rule.constBegin());
|
|
Rule::ConstIterator end(rule.constEnd());
|
|
for (; it!=end; ++it) {
|
|
str << it.key() << ':' << it.value() << '\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isRemote()) {
|
|
if (currentCommand!=Status && (currentCommand!=Unknown || !currentArgs.isEmpty())) {
|
|
return false;
|
|
}
|
|
|
|
#if QT_VERSION < 0x050000
|
|
QUrl url(dynamicUrl+"/"+constSaveCmd);
|
|
url.addQueryItem("name", e.name);
|
|
url.addQueryItem(constStatusTime, QString::number(statusTime));
|
|
#else
|
|
QUrl url(dynamicUrl+"/"+constSaveCmd);
|
|
QUrlQuery query;
|
|
query.addQueryItem("name", e.name);
|
|
query.addQueryItem(constStatusTime, QString::number(statusTime));
|
|
url.setQuery(query);
|
|
#endif
|
|
|
|
QNetworkRequest req(url);
|
|
req.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain");
|
|
currentJob=NetworkAccessManager::self()->post(req, string.toUtf8());
|
|
connect(currentJob, SIGNAL(finished()), this, SLOT(remoteJobFinished()));
|
|
currentCommand=Save;
|
|
currentArgs.clear();
|
|
currentSave=e;
|
|
return true;
|
|
}
|
|
|
|
QFile f(Utils::configDir(constDir, true)+e.name+constExtension);
|
|
if (f.open(QIODevice::WriteOnly|QIODevice::Text)) {
|
|
QTextStream out(&f);
|
|
out.setCodec("UTF-8");
|
|
out << string;
|
|
updateEntry(e);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Dynamic::updateEntry(const Entry &e)
|
|
{
|
|
QList<Dynamic::Entry>::Iterator it=find(e.name);
|
|
if (it!=entryList.end()) {
|
|
entryList.replace(it-entryList.begin(), e);
|
|
QModelIndex idx=index(it-entryList.begin(), 0, QModelIndex());
|
|
emit dataChanged(idx, idx);
|
|
} else {
|
|
beginInsertRows(QModelIndex(), entryList.count(), entryList.count());
|
|
entryList.append(e);
|
|
endInsertRows();
|
|
}
|
|
}
|
|
|
|
void Dynamic::del(const QString &name)
|
|
{
|
|
if (isRemote()) {
|
|
sendCommand(Del, QStringList() << name);
|
|
return;
|
|
}
|
|
|
|
QList<Dynamic::Entry>::Iterator it=find(name);
|
|
if (it==entryList.end()) {
|
|
return;
|
|
}
|
|
QString fName(Utils::configDir(constDir, false)+name+constExtension);
|
|
bool isCurrent=currentEntry==name;
|
|
|
|
if (!QFile::exists(fName) || QFile::remove(fName)) {
|
|
if (isCurrent) {
|
|
stop();
|
|
}
|
|
beginRemoveRows(QModelIndex(), it-entryList.begin(), it-entryList.begin());
|
|
entryList.erase(it);
|
|
endRemoveRows();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Dynamic::start(const QString &name)
|
|
{
|
|
if (isRemote()) {
|
|
sendCommand(SetActive, QStringList() << "name" << name << "start" << "1");
|
|
return;
|
|
}
|
|
|
|
if (Utils::findExe("perl").isEmpty()) {
|
|
emit error(i18n("You need to install \"perl\" on your system in order for Cantata's dynamic mode to function."));
|
|
return;
|
|
}
|
|
|
|
QString fName(Utils::configDir(constDir, false)+name+constExtension);
|
|
|
|
if (!QFile::exists(fName)) {
|
|
emit error(i18n("Failed to locate rules file - %1", fName));
|
|
return;
|
|
}
|
|
|
|
QString rules(Utils::cacheDir(constDir, true)+constActiveRules);
|
|
|
|
QFile::remove(rules);
|
|
if (QFile::exists(rules)) {
|
|
emit error(i18n("Failed to remove previous rules file - %1", rules));
|
|
return;
|
|
}
|
|
|
|
if (!QFile::link(fName, rules)) {
|
|
emit error(i18n("Failed to install rules file - %1 -> %2", fName, rules));
|
|
return;
|
|
}
|
|
|
|
int i=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry);
|
|
QModelIndex idx=index(i, 0, QModelIndex());
|
|
|
|
currentEntry=name;
|
|
|
|
if (idx.isValid()) {
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
|
|
i=entryList.indexOf(currentEntry);
|
|
idx=index(i, 0, QModelIndex());
|
|
if (idx.isValid()) {
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
|
|
if (isRunning()) {
|
|
emit clear();
|
|
return;
|
|
}
|
|
if (controlApp(true)) {
|
|
emit running(isRunning());
|
|
emit clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Dynamic::stop()
|
|
{
|
|
if (isRemote()) {
|
|
sendCommand(Control, QStringList() << "state" << "stop");
|
|
return;
|
|
}
|
|
|
|
#if !defined Q_OS_WIN
|
|
int i=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry);
|
|
QModelIndex idx=index(i, 0, QModelIndex());
|
|
int pid=getPid();
|
|
|
|
if (!pid) {
|
|
currentEntry=QString();
|
|
emit running(false);
|
|
if (idx.isValid()) {
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (0!=::kill(pid, 0)) {
|
|
currentEntry=QString();
|
|
emit running(false);
|
|
if (idx.isValid()) {
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (controlApp(false)) {
|
|
currentEntry=QString();
|
|
emit running(isRunning());
|
|
if (idx.isValid()) {
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Dynamic::toggle(const QString &name)
|
|
{
|
|
if(name==currentEntry) {
|
|
stop();
|
|
} else {
|
|
start(name);
|
|
}
|
|
}
|
|
|
|
bool Dynamic::isRunning()
|
|
{
|
|
#if defined Q_OS_WIN
|
|
return false;
|
|
#else
|
|
int pid=getPid();
|
|
return pid ? 0==::kill(pid, 0) : false;
|
|
#endif
|
|
}
|
|
|
|
void Dynamic::enableRemotePolling(bool e)
|
|
{
|
|
remotePollingEnabled=e;
|
|
if (remoteTimer && remoteTimer->isActive()) {
|
|
if (remotePollingEnabled) {
|
|
checkIfRemoteIsRunning();
|
|
}
|
|
remoteTimer->start(remotePollingEnabled ? 2000 : 30000);
|
|
}
|
|
}
|
|
|
|
int Dynamic::getPid() const
|
|
{
|
|
QFile pidFile(Utils::cacheDir(constDir, false)+constLockFile);
|
|
|
|
if (pidFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
|
|
QTextStream str(&pidFile);
|
|
int pid=0;
|
|
str >> pid;
|
|
return pid;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool Dynamic::controlApp(bool isStart)
|
|
{
|
|
QString cmd=QLatin1String(INSTALL_PREFIX"/share/cantata/scripts/cantata-dynamic");
|
|
QProcess process;
|
|
|
|
if (isStart) {
|
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
|
MPDConnectionDetails details=MPDConnection::self()->getDetails();
|
|
env.insert("MPD_HOST", details.password.isEmpty() ? details.hostname : (details.password+'@'+details.hostname));
|
|
env.insert("MPD_PORT", QString::number(details.port));
|
|
process.setProcessEnvironment(env);
|
|
}
|
|
process.start(cmd, QStringList() << QLatin1String(isStart ? "start" : "stop"), QIODevice::WriteOnly);
|
|
|
|
if (!localTimer) {
|
|
localTimer=new QTimer(this);
|
|
connect(localTimer, SIGNAL(timeout()), SLOT(checkHelper()));
|
|
}
|
|
bool rv=process.waitForFinished(1000);
|
|
localTimer->start(1000);
|
|
return rv;
|
|
}
|
|
|
|
QList<Dynamic::Entry>::Iterator Dynamic::find(const QString &e)
|
|
{
|
|
QList<Dynamic::Entry>::Iterator it(entryList.begin());
|
|
QList<Dynamic::Entry>::Iterator end(entryList.end());
|
|
|
|
for (; it!=end; ++it) {
|
|
if ((*it).name==e) {
|
|
break;
|
|
}
|
|
}
|
|
return it;
|
|
}
|
|
|
|
void Dynamic::loadLocal()
|
|
{
|
|
beginResetModel();
|
|
entryList.clear();
|
|
currentEntry=QString();
|
|
|
|
// Load all current enttries...
|
|
QString dirName=Utils::configDir(constDir);
|
|
QDir d(dirName);
|
|
if (d.exists()) {
|
|
QStringList rulesFiles=d.entryList(QStringList() << QChar('*')+constExtension);
|
|
foreach (const QString &rf, rulesFiles) {
|
|
QFile f(dirName+rf);
|
|
if (f.open(QIODevice::ReadOnly|QIODevice::Text)) {
|
|
QStringList keys=QStringList() << constArtistKey << constSimilarArtistsKey << constAlbumArtistKey << constDateKey << constExactKey
|
|
<< constAlbumKey << constTitleKey << constGenreKey << constExcludeKey;
|
|
|
|
Entry e;
|
|
e.name=rf.left(rf.length()-constExtension.length());
|
|
Rule r;
|
|
QTextStream in(&f);
|
|
in.setCodec("UTF-8");
|
|
QStringList lines = in.readAll().split('\n', QString::SkipEmptyParts);
|
|
foreach (const QString &line, lines) {
|
|
QString str=line.trimmed();
|
|
|
|
if (str.isEmpty() || str.startsWith('#')) {
|
|
continue;
|
|
}
|
|
|
|
if (str==constRuleKey) {
|
|
if (!r.isEmpty()) {
|
|
e.rules.append(r);
|
|
r.clear();
|
|
}
|
|
} else {
|
|
foreach (const QString &k, keys) {
|
|
if (str.startsWith(k+':')) {
|
|
r.insert(k, str.mid(k.length()+1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!r.isEmpty()) {
|
|
e.rules.append(r);
|
|
r.clear();
|
|
}
|
|
entryList.append(e);
|
|
}
|
|
}
|
|
}
|
|
endResetModel();
|
|
}
|
|
|
|
void Dynamic::loadRemote()
|
|
{
|
|
beginResetModel();
|
|
entryList.clear();
|
|
currentEntry=QString();
|
|
endResetModel();
|
|
sendCommand(Id);
|
|
}
|
|
|
|
void Dynamic::parseRemote(const QString &response)
|
|
{
|
|
beginResetModel();
|
|
entryList.clear();
|
|
currentEntry=QString();
|
|
QStringList keys=QStringList() << constArtistKey << constSimilarArtistsKey << constAlbumArtistKey << constDateKey << constExactKey
|
|
<< constAlbumKey << constTitleKey << constGenreKey << constExcludeKey;
|
|
QStringList list=response.split("\n");
|
|
Entry e;
|
|
Rule r;
|
|
|
|
foreach (const QString &s, list) {
|
|
QString str=s.trimmed();
|
|
if (str.isEmpty() || str.startsWith('#')) {
|
|
continue;
|
|
}
|
|
if (str.startsWith(QLatin1String("FILENAME:"))) {
|
|
if (!e.name.isEmpty()) {
|
|
entryList.append(e);
|
|
}
|
|
e.name=str.mid(9, str.length()-15); // Remove extension...
|
|
e.rules.clear();
|
|
r.clear();
|
|
} else if (str==constRuleKey) {
|
|
if (!r.isEmpty()) {
|
|
e.rules.append(r);
|
|
r.clear();
|
|
}
|
|
} else if (str.startsWith(constStatusTimeResp)) {
|
|
statusTime=str.mid(constStatusTimeResp.length()).toInt();
|
|
} else if (str.startsWith(constUpdateRequiredResp)) {
|
|
continue;
|
|
} else {
|
|
foreach (const QString &k, keys) {
|
|
if (str.startsWith(k+':')) {
|
|
r.insert(k, str.mid(k.length()+1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!e.name.isEmpty()) {
|
|
if (!r.isEmpty()) {
|
|
e.rules.append(r);
|
|
}
|
|
entryList.append(e);
|
|
}
|
|
endResetModel();
|
|
}
|
|
|
|
void Dynamic::parseStatus(const QString &response)
|
|
{
|
|
QStringList list=response.split("\n");
|
|
QString state;
|
|
QString prevEntry=currentEntry;
|
|
int st=-1;
|
|
foreach (const QString &str, list) {
|
|
if (str.startsWith(constStatusStateResp)) {
|
|
state=str.mid(constStatusStateResp.length());
|
|
} else if (str.startsWith(constStatusRuleResp)) {
|
|
currentEntry=str.mid(constStatusRuleResp.length());
|
|
} else if (str.startsWith(constStatusTimeResp)) {
|
|
st=str.mid(constStatusTimeResp.length()).toInt();
|
|
}
|
|
}
|
|
|
|
bool stateChanged=lastState!=state;
|
|
bool noSongs=QLatin1String("NO_SONGS")==state;
|
|
bool terminated=false;
|
|
|
|
if (stateChanged) {
|
|
lastState=state;
|
|
if (noSongs) {
|
|
bool sendError=!currentEntry.isEmpty();
|
|
emit running(true);
|
|
if (sendError) {
|
|
emit error(QLatin1String("NO_SONGS"));
|
|
}
|
|
} else if (QLatin1String("HAVE_SONGS")==state || QLatin1String("STARTING")==state) {
|
|
emit running(true);
|
|
} else if (QLatin1String("IDLE")==state) {
|
|
currentEntry.clear();
|
|
emit running(false);
|
|
} else if (QLatin1String("TERMINATED")==state) {
|
|
currentEntry.clear();
|
|
emit running(false);
|
|
emit error(i18n("Dynamizer has been terminated."));
|
|
pollRemoteHelper();
|
|
terminated=true;
|
|
currentEntry=QString();
|
|
}
|
|
}
|
|
|
|
if (prevEntry!=currentEntry) {
|
|
int prev=prevEntry.isEmpty() ? -1 : entryList.indexOf(prevEntry);
|
|
int cur=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry);
|
|
if (-1!=prev) {
|
|
QModelIndex idx=index(prev, 0, QModelIndex());
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
if (-1!=cur) {
|
|
QModelIndex idx=index(cur, 0, QModelIndex());
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
} else if (stateChanged) {
|
|
int row=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry);
|
|
if (-1!=row) {
|
|
QModelIndex idx=index(row, 0, QModelIndex());
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
}
|
|
|
|
if (st>statusTime && !terminated) {
|
|
if (Unknown==currentCommand && currentArgs.isEmpty()) {
|
|
statusTime=st;
|
|
sendCommand(List, QStringList() << "withDetails" << "1");
|
|
}
|
|
}
|
|
}
|
|
|
|
void Dynamic::parseId(const QString &response)
|
|
{
|
|
QStringList list=response.split("\n");
|
|
QString id;
|
|
quint16 port=0;
|
|
QString group;
|
|
|
|
foreach (const QString ¶m, list) {
|
|
if (param.startsWith(QLatin1String("ID:"))) {
|
|
id=param.mid(3);
|
|
} else if (param.startsWith(QLatin1String("GROUP:"))) {
|
|
group=param.mid(6);
|
|
} else if (param.startsWith(QLatin1String("PORT:"))) {
|
|
port=param.mid(5).toUInt();
|
|
}
|
|
}
|
|
|
|
if (!id.isEmpty() && !group.isEmpty() && 0!=port) {
|
|
startReceiver(id, group, port);
|
|
}
|
|
}
|
|
|
|
void Dynamic::checkResponse(const QString &response)
|
|
{
|
|
QStringList list=response.split("\n");
|
|
bool updateReq=false;
|
|
foreach (const QString &str, list) {
|
|
if (constUpdateRequiredResp==str) {
|
|
updateReq=true;
|
|
} else if (str.startsWith(constStatusTimeResp)) {
|
|
statusTime=str.mid(constStatusTimeResp.length()).toInt();
|
|
}
|
|
}
|
|
|
|
if (updateReq) {
|
|
sendCommand(List, QStringList() << "withDetails" << "1");
|
|
}
|
|
}
|
|
|
|
void Dynamic::sendCommand(Command cmd, const QStringList &args)
|
|
{
|
|
if (Status==currentCommand) {
|
|
if (cmd==Status) {
|
|
return;
|
|
}
|
|
currentCommand=Unknown;
|
|
currentArgs.clear();
|
|
if (currentJob) {
|
|
disconnect(currentJob, SIGNAL(finished()), this, SLOT(remoteJobFinished()));
|
|
}
|
|
}
|
|
if (Unknown!=currentCommand) {
|
|
if (Status!=cmd) {
|
|
QString cmdStr=i18n("Uknown");
|
|
switch(cmd) {
|
|
case List: cmdStr=i18n("Loading list of rules"); break;
|
|
case Save: cmdStr=i18n("Saving rule"); break;
|
|
case Del: cmdStr=i18n("Deleting rule"); break;
|
|
case SetActive: cmdStr=i18n("Setting active rule"); break;
|
|
case Control: cmdStr=i18n("Stopping dynamizer"); break;
|
|
case Id: cmdStr=i18n("Requesting ID details"); break;
|
|
default: break;
|
|
}
|
|
emit error(i18n("Awaiting response for previous command. (%1)", cmdStr));
|
|
}
|
|
return;
|
|
}
|
|
|
|
currentCommand=cmd;
|
|
currentArgs=args;
|
|
|
|
DBUG << toString(cmd) << args;
|
|
if (Del==cmd) {
|
|
if (!args.isEmpty()) {
|
|
#if QT_VERSION < 0x050000
|
|
QUrl url(dynamicUrl+"/"+args.at(0));
|
|
url.addQueryItem(constStatusTime, QString::number(statusTime));
|
|
#else
|
|
QUrl url(dynamicUrl+"/"+args.at(0));
|
|
QUrlQuery query;
|
|
query.addQueryItem(constStatusTime, QString::number(statusTime));
|
|
url.setQuery(query);
|
|
#endif
|
|
|
|
currentJob=NetworkAccessManager::self()->deleteResource(QNetworkRequest(url));
|
|
} else {
|
|
currentCommand=Unknown;
|
|
currentArgs.clear();
|
|
}
|
|
} else {
|
|
QUrl url(dynamicUrl+"/"+toString(currentCommand));
|
|
#if QT_VERSION < 0x050000
|
|
QUrl &query=url;
|
|
#else
|
|
QUrlQuery query;
|
|
#endif
|
|
if (!args.isEmpty() && 0==args.size()%2) {
|
|
for (int i=0; i<args.size(); i+=2) {
|
|
query.addQueryItem(args.at(i), args.at(i+1));
|
|
}
|
|
}
|
|
query.addQueryItem(constStatusTime, QString::number(statusTime));
|
|
#if QT_VERSION >= 0x050000
|
|
url.setQuery(query);
|
|
#endif
|
|
if (SetActive==cmd || Control==cmd) {
|
|
QNetworkRequest req(url);
|
|
req.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain");
|
|
currentJob=NetworkAccessManager::self()->post(req, QByteArray());
|
|
} else {
|
|
currentJob=NetworkAccessManager::self()->get(QNetworkRequest(url));
|
|
}
|
|
}
|
|
if (List==cmd) {
|
|
emit loadingList();
|
|
}
|
|
connect(currentJob, SIGNAL(finished()), this, SLOT(remoteJobFinished()));
|
|
}
|
|
|
|
void Dynamic::refreshList()
|
|
{
|
|
if (isRemote()) {
|
|
sendCommand(List, QStringList() << "withDetails" << "1");
|
|
}
|
|
}
|
|
|
|
void Dynamic::checkHelper()
|
|
{
|
|
if (isRemote()) {
|
|
return;
|
|
}
|
|
|
|
if (!isRunning()) {
|
|
emit running(false);
|
|
int i=currentEntry.isEmpty() ? -1 : entryList.indexOf(currentEntry);
|
|
currentEntry=QString();
|
|
if (i>-1) {
|
|
QModelIndex idx=index(i, 0, QModelIndex());
|
|
emit dataChanged(idx, idx);
|
|
}
|
|
if (localTimer) {
|
|
localTimer->stop();
|
|
}
|
|
} else {
|
|
if (localTimer && localTimer->isActive()) {
|
|
static const int constAppCheck=15*1000;
|
|
if (localTimer->interval()<constAppCheck) {
|
|
localTimer->start(constAppCheck);
|
|
}
|
|
} else { // No timer => app startup!
|
|
// Attempt to read current name...
|
|
QFileInfo inf(Utils::cacheDir(constDir, false)+constActiveRules);
|
|
|
|
if (inf.exists() && inf.isSymLink()) {
|
|
QString link=inf.readLink();
|
|
if (!link.isEmpty()) {
|
|
QString fname=QFileInfo(link).fileName();
|
|
if (fname.endsWith(constExtension)) {
|
|
currentEntry=fname.left(fname.length()-constExtension.length());
|
|
}
|
|
}
|
|
}
|
|
emit running(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Dynamic::pollRemoteHelper()
|
|
{
|
|
if (!remoteTimer) {
|
|
remoteTimer=new QTimer(this);
|
|
connect(remoteTimer, SIGNAL(timeout()), SLOT(checkIfRemoteIsRunning()));
|
|
}
|
|
beginResetModel();
|
|
entryList.clear();
|
|
currentEntry=QString();
|
|
endResetModel();
|
|
remoteTimer->start(remotePollingEnabled ? 2000 : 30000);
|
|
emit remoteRunning(false);
|
|
}
|
|
|
|
void Dynamic::checkIfRemoteIsRunning()
|
|
{
|
|
if (isRemote() && entryList.isEmpty()) {
|
|
if (remotePollingEnabled) {
|
|
sendCommand(Id);
|
|
}
|
|
} else if (remoteTimer) {
|
|
remoteTimer->stop();
|
|
}
|
|
}
|
|
|
|
void Dynamic::updateRemoteStatus()
|
|
{
|
|
if (isRemote()) {
|
|
sendCommand(Status);
|
|
}
|
|
}
|
|
|
|
void Dynamic::remoteJobFinished()
|
|
{
|
|
QNetworkReply *reply=qobject_cast<QNetworkReply *>(sender());
|
|
if (!reply || reply!=currentJob) {
|
|
return;
|
|
}
|
|
|
|
currentJob=0;
|
|
reply->deleteLater();
|
|
|
|
int httpResponse=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
bool cmdOk=QNetworkReply::NoError==reply->error() && (200==httpResponse || 201==httpResponse);
|
|
DBUG << toString(currentCommand) << currentArgs << cmdOk;
|
|
QString response;
|
|
if (cmdOk) {
|
|
reply->open(QIODevice::ReadOnly | QIODevice::Text);
|
|
response=QString::fromUtf8(reply->readAll());
|
|
reply->close();
|
|
} else {
|
|
response=0==httpResponse ? i18n("Dynamizer is not active") : QString::number(httpResponse);
|
|
}
|
|
|
|
switch (currentCommand) {
|
|
case List:
|
|
if (cmdOk) {
|
|
parseRemote(response);
|
|
} else {
|
|
emit error(i18n("Failed to retrieve list of dynamic rules. (%1)", response));
|
|
}
|
|
emit loadedList();
|
|
break;
|
|
case Status:
|
|
if (cmdOk) {
|
|
parseStatus(response);
|
|
} else {
|
|
emit running(false);
|
|
}
|
|
break;
|
|
case Save:
|
|
if (cmdOk) {
|
|
updateEntry(currentSave);
|
|
currentSave.name=QString();
|
|
currentSave.rules.clear();
|
|
}
|
|
checkResponse(response);
|
|
emit saved(cmdOk);
|
|
break;
|
|
case Del:
|
|
if (cmdOk) {
|
|
QString name=currentArgs.isEmpty() ? QString() : currentArgs.at(0);
|
|
QList<Dynamic::Entry>::Iterator it=find(name);
|
|
if (it!=entryList.end()) {
|
|
beginRemoveRows(QModelIndex(), it-entryList.begin(), it-entryList.begin());
|
|
entryList.erase(it);
|
|
endRemoveRows();
|
|
}
|
|
checkResponse(response);
|
|
} else {
|
|
emit error(i18n("Failed to delete rules file. (%1)", response));
|
|
}
|
|
break;
|
|
case Control:
|
|
if (cmdOk) {
|
|
checkResponse(response);
|
|
} else {
|
|
emit error(i18n("Failed to control dynamizer state. (%1)", response));
|
|
}
|
|
lastState.clear();
|
|
break;
|
|
case SetActive:
|
|
if (cmdOk) {
|
|
checkResponse(response);
|
|
QTimer::singleShot(1000, this, SLOT(updateRemoteStatus()));
|
|
} else {
|
|
emit error(i18n("Failed to set the current dynamic rules. (%1)", response));
|
|
}
|
|
lastState.clear();
|
|
break;
|
|
case Id:
|
|
if (cmdOk) {
|
|
parseId(response);
|
|
currentCommand=Unknown;
|
|
currentArgs.clear();
|
|
sendCommand(List, QStringList() << "withDetails" << "1");
|
|
emit remoteRunning(true);
|
|
return;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
Command prevCommand=currentCommand;
|
|
currentCommand=Unknown;
|
|
currentArgs.clear();
|
|
|
|
if (cmdOk) {
|
|
if (remoteTimer) {
|
|
remoteTimer->stop();
|
|
}
|
|
if (Status!=prevCommand) {
|
|
sendCommand(Status);
|
|
}
|
|
} else if (0==httpResponse) {
|
|
pollRemoteHelper();
|
|
}
|
|
}
|
|
|
|
void Dynamic::dynamicUrlChanged(const QString &url)
|
|
{
|
|
if (url!=dynamicUrl) {
|
|
dynamicUrl=url;
|
|
stopReceiver();
|
|
if (isRemote()) {
|
|
if (localTimer) {
|
|
localTimer->stop();
|
|
}
|
|
loadRemote();
|
|
} else {
|
|
if (remoteTimer) {
|
|
remoteTimer->stop();
|
|
}
|
|
loadLocal();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Dynamic::stopReceiver()
|
|
{
|
|
if (receiver) {
|
|
disconnect(receiver, SIGNAL(status(QString)), this, SLOT(parseStatus(QString)));
|
|
receiver->deleteLater();
|
|
receiver=0;
|
|
}
|
|
}
|
|
|
|
void Dynamic::startReceiver(const QString &id, const QString &group, quint16 port)
|
|
{
|
|
stopReceiver();
|
|
receiver=new MulticastReceiver(this, id, group, port);
|
|
connect(receiver, SIGNAL(status(QString)), this, SLOT(parseStatus(QString)));
|
|
}
|