You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
389 lines
14 KiB
389 lines
14 KiB
/* |
|
* Copyright 2012 Alex Merry <alex.merry@kdemail.net> |
|
* |
|
* This library is free software; you can redistribute it and/or |
|
* modify it under the terms of the GNU Lesser General Public |
|
* License as published by the Free Software Foundation; either |
|
* version 2.1 of the License, or (at your option) any later version. |
|
* |
|
* This library 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 |
|
* Lesser General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Lesser General Public |
|
* License along with this library; if not, write to the Free Software |
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, |
|
* Boston, MA 02110-1301 USA |
|
*/ |
|
|
|
#include "playercontainer.h" |
|
|
|
#include <dbusproperties.h> |
|
#include <mprisplayer.h> |
|
#include <mprisroot.h> |
|
|
|
#define MPRIS2_PATH "/org/mpris/MediaPlayer2" |
|
#define POS_UPD_STRING "Position last updated (UTC)" |
|
|
|
#include <KDesktopFile> |
|
|
|
#include <QtDBus> |
|
#include <QDateTime> |
|
|
|
#include "debug.h" |
|
|
|
static QVariant::Type expPropType(const QString& propName) |
|
{ |
|
if (propName == QLatin1String("Identity")) |
|
return QVariant::String; |
|
else if (propName == QLatin1String("DesktopEntry")) |
|
return QVariant::String; |
|
else if (propName == QLatin1String("SupportedUriSchemes")) |
|
return QVariant::StringList; |
|
else if (propName == QLatin1String("SupportedMimeTypes")) |
|
return QVariant::StringList; |
|
else if (propName == QLatin1String("Fullscreen")) |
|
return QVariant::Bool; |
|
else if (propName == QLatin1String("PlaybackStatus")) |
|
return QVariant::String; |
|
else if (propName == QLatin1String("LoopStatus")) |
|
return QVariant::String; |
|
else if (propName == QLatin1String("Shuffle")) |
|
return QVariant::Bool; |
|
else if (propName == QLatin1String("Rate")) |
|
return QVariant::Double; |
|
else if (propName == QLatin1String("MinimumRate")) |
|
return QVariant::Double; |
|
else if (propName == QLatin1String("MaximumRate")) |
|
return QVariant::Double; |
|
else if (propName == QLatin1String("Volume")) |
|
return QVariant::Double; |
|
else if (propName == QLatin1String("Position")) |
|
return QVariant::LongLong; |
|
else if (propName == QLatin1String("Metadata")) |
|
return QVariant::Map; |
|
// we give out CanControl, as this may completely |
|
// change the UI of the widget |
|
else if (propName == QLatin1String("CanControl")) |
|
return QVariant::Bool; |
|
return QVariant::Invalid; |
|
} |
|
|
|
static PlayerContainer::Cap capFromName(const QString& capName) |
|
{ |
|
if (capName == QLatin1String("CanQuit")) |
|
return PlayerContainer::CanQuit; |
|
else if (capName == QLatin1String("CanRaise")) |
|
return PlayerContainer::CanRaise; |
|
else if (capName == QLatin1String("CanSetFullscreen")) |
|
return PlayerContainer::CanSetFullscreen; |
|
else if (capName == QLatin1String("CanControl")) |
|
return PlayerContainer::CanControl; |
|
else if (capName == QLatin1String("CanPlay")) |
|
return PlayerContainer::CanPlay; |
|
else if (capName == QLatin1String("CanPause")) |
|
return PlayerContainer::CanPause; |
|
else if (capName == QLatin1String("CanSeek")) |
|
return PlayerContainer::CanSeek; |
|
else if (capName == QLatin1String("CanGoNext")) |
|
return PlayerContainer::CanGoNext; |
|
else if (capName == QLatin1String("CanGoPrevious")) |
|
return PlayerContainer::CanGoPrevious; |
|
return PlayerContainer::NoCaps; |
|
} |
|
|
|
PlayerContainer::PlayerContainer(const QString& busAddress, QObject* parent) |
|
: DataContainer(parent) |
|
, m_caps(NoCaps) |
|
, m_fetchesPending(0) |
|
, m_dbusAddress(busAddress) |
|
, m_currentRate(0.0) |
|
{ |
|
Q_ASSERT(!busAddress.isEmpty()); |
|
Q_ASSERT(busAddress.startsWith(QLatin1String("org.mpris.MediaPlayer2."))); |
|
|
|
m_propsIface = new OrgFreedesktopDBusPropertiesInterface( |
|
busAddress, MPRIS2_PATH, |
|
QDBusConnection::sessionBus(), this); |
|
|
|
m_playerIface = new OrgMprisMediaPlayer2PlayerInterface( |
|
busAddress, MPRIS2_PATH, |
|
QDBusConnection::sessionBus(), this); |
|
|
|
m_rootIface = new OrgMprisMediaPlayer2Interface( |
|
busAddress, MPRIS2_PATH, |
|
QDBusConnection::sessionBus(), this); |
|
|
|
connect(m_propsIface, SIGNAL(PropertiesChanged(QString,QVariantMap,QStringList)), |
|
this, SLOT(propertiesChanged(QString,QVariantMap,QStringList))); |
|
|
|
connect(m_playerIface, SIGNAL(Seeked(qlonglong)), |
|
this, SLOT(seeked(qlonglong))); |
|
|
|
refresh(); |
|
} |
|
|
|
void PlayerContainer::refresh() |
|
{ |
|
// despite these calls being async, we should never update values in the |
|
// wrong order (eg: a stale GetAll response overwriting a more recent value |
|
// from a PropertiesChanged signal) due to D-Bus message ordering guarantees. |
|
|
|
QDBusPendingCall async = m_propsIface->GetAll(OrgMprisMediaPlayer2Interface::staticInterfaceName()); |
|
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); |
|
connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), |
|
this, SLOT(getPropsFinished(QDBusPendingCallWatcher*))); |
|
++m_fetchesPending; |
|
|
|
async = m_propsIface->GetAll(OrgMprisMediaPlayer2PlayerInterface::staticInterfaceName()); |
|
watcher = new QDBusPendingCallWatcher(async, this); |
|
connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), |
|
this, SLOT(getPropsFinished(QDBusPendingCallWatcher*))); |
|
++m_fetchesPending; |
|
} |
|
|
|
static bool decodeUri(QVariantMap &map, const QString& entry) { |
|
if (map.contains(entry)) { |
|
QString urlString = map.value(entry).toString(); |
|
QUrl url = QUrl::fromEncoded(urlString.toAscii()); |
|
if (!url.isValid()) { |
|
// try to be lenient |
|
url = QUrl(urlString); |
|
} |
|
if (url.isValid()) { |
|
map.insert(entry, QVariant(url)); |
|
return true; |
|
} else { |
|
map.remove(entry); |
|
return false; |
|
} |
|
} |
|
// count it as a success if it doesn't exist |
|
return true; |
|
} |
|
|
|
void PlayerContainer::copyProperty(const QString& propName, const QVariant& _value, QVariant::Type expType, UpdateType updType) |
|
{ |
|
QVariant value = _value; |
|
// we protect our users from bogus values |
|
if (value.userType() == qMetaTypeId<QDBusArgument>()) { |
|
if (expType == QVariant::Map) { |
|
QDBusArgument arg = value.value<QDBusArgument>(); |
|
if (arg.currentType() != QDBusArgument::MapType) { |
|
qCWarning(MPRIS2) << m_dbusAddress << "exports" << propName |
|
<< "with the wrong type; it should be D-Bus type \"a{sv}\""; |
|
return; |
|
} |
|
QVariantMap map; |
|
arg >> map; |
|
if (propName == QLatin1String("Metadata")) { |
|
if (!decodeUri(map, QLatin1String("mpris:artUrl"))) { |
|
qCWarning(MPRIS2) << m_dbusAddress << "has an invalid URL for the mpris:artUrl entry of the \"Metadata\" property"; |
|
} |
|
if (!decodeUri(map, QLatin1String("xesam:url"))) { |
|
qCWarning(MPRIS2) << m_dbusAddress << "has an invalid URL for the xesam:url entry of the \"Metadata\" property"; |
|
} |
|
} |
|
value = QVariant(map); |
|
} |
|
} |
|
if (value.type() != expType) { |
|
const char * gotTypeCh = QDBusMetaType::typeToSignature(value.userType()); |
|
QString gotType = gotTypeCh ? QString::fromAscii(gotTypeCh) : "<unknown>"; |
|
const char * expTypeCh = QDBusMetaType::typeToSignature(expType); |
|
QString expType = expTypeCh ? QString::fromAscii(expTypeCh) : "<unknown>"; |
|
|
|
qCWarning(MPRIS2) << m_dbusAddress << "exports" << propName |
|
<< "as D-Bus type" << gotType |
|
<< "but it should be D-Bus type" << expType; |
|
} |
|
if (value.convert(expType)) { |
|
if (propName == QLatin1String("Position")) { |
|
|
|
setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); |
|
|
|
} else if (propName == QLatin1String("Metadata")) { |
|
|
|
if (updType == UpdatedSignal) { |
|
QDBusObjectPath oldTrackId(data().value("Metadata").toMap().value("mpris:trackid").toString()); |
|
QDBusObjectPath newTrackId(value.toMap().value("mpris:trackid").toString()); |
|
if (oldTrackId != newTrackId) { |
|
setData("Position", static_cast<qlonglong>(0)); |
|
setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); |
|
} |
|
} |
|
|
|
if (value.toMap().value("mpris:length").toLongLong() <= 0) { |
|
QMap<QString, QVariant> metadataMap = value.toMap(); |
|
metadataMap.remove("mpris:length"); |
|
value = QVariant(metadataMap); |
|
} |
|
|
|
} else if (propName == QLatin1String("Rate") && |
|
data().value("PlaybackStatus").toString() == QLatin1String("Playing")) { |
|
|
|
if (data().contains("Position")) |
|
recalculatePosition(); |
|
m_currentRate = value.toDouble(); |
|
|
|
} else if (propName == QLatin1String("PlaybackStatus")) { |
|
|
|
if (data().contains("Position") && data().contains("PlaybackStatus")) { |
|
updatePosition(); |
|
} |
|
|
|
// update the effective rate |
|
if (data().contains("Rate")) { |
|
if (value.toString() == QLatin1String("Playing")) |
|
m_currentRate = data().value("Rate").toDouble(); |
|
else |
|
m_currentRate = 0.0; |
|
} |
|
if (value.toString() == QLatin1String("Stopped")) { |
|
// assume the position has reset to 0, since this is really the |
|
// only sensible value for a stopped track |
|
setData("Position", static_cast<qint64>(0)); |
|
setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); |
|
} |
|
} else if (propName == QLatin1String("DesktopEntry")) { |
|
QString filename = value.toString() + QLatin1String(".desktop"); |
|
KDesktopFile desktopFile(filename); |
|
QString iconName = desktopFile.readIcon(); |
|
if (!iconName.isEmpty()) { |
|
setData("Desktop Icon Name", iconName); |
|
} |
|
} |
|
setData(propName, value); |
|
} |
|
} |
|
|
|
void PlayerContainer::updateFromMap(const QVariantMap& map, UpdateType updType) |
|
{ |
|
QMap<QString, QVariant>::const_iterator i = map.constBegin(); |
|
while (i != map.constEnd()) { |
|
QVariant::Type type = expPropType(i.key()); |
|
if (type != QVariant::Invalid) { |
|
copyProperty(i.key(), i.value(), type, updType); |
|
} |
|
|
|
Cap cap = capFromName(i.key()); |
|
if (cap != NoCaps) { |
|
if (i.value().type() == QVariant::Bool) { |
|
if (i.value().toBool()) { |
|
m_caps |= cap; |
|
} else { |
|
m_caps &= ~cap; |
|
} |
|
} else { |
|
const char * gotTypeCh = QDBusMetaType::typeToSignature(i.value().userType()); |
|
QString gotType = gotTypeCh ? QString::fromAscii(gotTypeCh) : "<unknown>"; |
|
|
|
qCWarning(MPRIS2) << m_dbusAddress << "exports" << i.key() |
|
<< "as D-Bus type" << gotType |
|
<< "but it should be D-Bus type \"b\""; |
|
} |
|
} |
|
// fake the CanStop capability |
|
if (cap == CanControl || i.key() == QLatin1String("PlaybackStatus")) { |
|
if ((m_caps & CanControl) && i.value().toString() != QLatin1String("Stopped")) { |
|
qCDebug(MPRIS2) << "Enabling stop action"; |
|
m_caps |= CanStop; |
|
} else { |
|
qCDebug(MPRIS2) << "Disabling stop action"; |
|
m_caps &= ~CanStop; |
|
} |
|
} |
|
++i; |
|
} |
|
} |
|
|
|
void PlayerContainer::getPropsFinished(QDBusPendingCallWatcher* watcher) |
|
{ |
|
QDBusPendingReply<QVariantMap> propsReply = *watcher; |
|
watcher->deleteLater(); |
|
|
|
if (m_fetchesPending < 1) { |
|
// we already failed |
|
return; |
|
} |
|
|
|
if (propsReply.isError()) { |
|
qCWarning(MPRIS2) << m_dbusAddress << "does not implement" |
|
<< OrgFreedesktopDBusPropertiesInterface::staticInterfaceName() |
|
<< "correctly"; |
|
qCDebug(MPRIS2) << "Error message was" << propsReply.error().name() << propsReply.error().message(); |
|
m_fetchesPending = 0; |
|
emit initialFetchFailed(this); |
|
return; |
|
} |
|
|
|
updateFromMap(propsReply.value(), FetchAll); |
|
checkForUpdate(); |
|
|
|
--m_fetchesPending; |
|
if (m_fetchesPending == 0) { |
|
emit initialFetchFinished(this); |
|
} |
|
} |
|
|
|
void PlayerContainer::updatePosition() |
|
{ |
|
QDBusPendingCall async = m_propsIface->Get(OrgMprisMediaPlayer2PlayerInterface::staticInterfaceName(), "Position"); |
|
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); |
|
connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), |
|
this, SLOT(getPositionFinished(QDBusPendingCallWatcher*))); |
|
} |
|
|
|
void PlayerContainer::getPositionFinished(QDBusPendingCallWatcher* watcher) |
|
{ |
|
QDBusPendingReply<QVariant> propsReply = *watcher; |
|
watcher->deleteLater(); |
|
|
|
if (propsReply.isError()) { |
|
qCWarning(MPRIS2) << m_dbusAddress << "does not implement" |
|
<< OrgFreedesktopDBusPropertiesInterface::staticInterfaceName() |
|
<< "correctly"; |
|
qCDebug(MPRIS2) << "Error message was" << propsReply.error().name() << propsReply.error().message(); |
|
return; |
|
} |
|
|
|
setData("Position", propsReply.value().toLongLong()); |
|
setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); |
|
checkForUpdate(); |
|
} |
|
|
|
void PlayerContainer::propertiesChanged( |
|
const QString& interface, |
|
const QVariantMap& changedProperties, |
|
const QStringList& invalidatedProperties) |
|
{ |
|
Q_UNUSED(interface) |
|
|
|
updateFromMap(changedProperties, UpdatedSignal); |
|
if (!invalidatedProperties.isEmpty()) { |
|
refresh(); |
|
} |
|
checkForUpdate(); |
|
} |
|
|
|
void PlayerContainer::seeked(qlonglong position) |
|
{ |
|
setData("Position", position); |
|
setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); |
|
checkForUpdate(); |
|
} |
|
|
|
void PlayerContainer::recalculatePosition() |
|
{ |
|
Q_ASSERT(data().contains("Position")); |
|
|
|
qint64 pos = data().value("Position").toLongLong(); |
|
QDateTime lastUpdated = data().value(POS_UPD_STRING).toDateTime(); |
|
QDateTime now = QDateTime::currentDateTimeUtc(); |
|
qint64 diff = lastUpdated.msecsTo(now) * 1000; |
|
qint64 newPos = pos + static_cast<qint64>(diff * m_currentRate); |
|
setData("Position", newPos); |
|
setData(POS_UPD_STRING, now); |
|
} |
|
|
|
|