#include "player.hpp" #include <qcontainerfwd.h> #include <qdatetime.h> #include <qdbusconnection.h> #include <qdbusextratypes.h> #include <qlist.h> #include <qlogging.h> #include <qloggingcategory.h> #include <qobject.h> #include <qproperty.h> #include <qstring.h> #include <qtmetamacros.h> #include <qtypes.h> #include "../../dbus/properties.hpp" #include "dbus_player.h" #include "dbus_player_app.h" using namespace qs::dbus; namespace qs::service::mpris { namespace { Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); } QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) { switch (status) { case MprisPlaybackState::Stopped: return "Stopped"; case MprisPlaybackState::Playing: return "Playing"; case MprisPlaybackState::Paused: return "Paused"; default: return "Unknown Status"; } } QString MprisLoopState::toString(MprisLoopState::Enum status) { switch (status) { case MprisLoopState::None: return "None"; case MprisLoopState::Track: return "Track"; case MprisLoopState::Playlist: return "Playlist"; default: return "Unknown Status"; } } MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) { this->app = new DBusMprisPlayerApp( address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this ); this->player = new DBusMprisPlayer(address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this); if (!this->player->isValid() || !this->app->isValid()) { qCWarning(logMprisPlayer) << "Cannot create MprisPlayer for" << address; return; } this->bCanPlay.setBinding([this]() { return this->bCanControl && this->bpCanPlay; }); this->bCanPause.setBinding([this]() { return this->bCanControl && this->bpCanPause; }); this->bCanSeek.setBinding([this]() { return this->bCanControl && this->bpCanSeek; }); this->bCanGoNext.setBinding([this]() { return this->bCanControl && this->bpCanGoNext; }); this->bCanGoPrevious.setBinding([this]() { return this->bCanControl && this->bpCanGoPrevious; }); this->bCanTogglePlaying.setBinding([this]() { return this->bIsPlaying ? this->bCanPause.value() : this->bCanPlay.value(); }); this->bTrackTitle.setBinding([this]() { return this->bMetadata.value().value("xesam:title").toString(); }); this->bTrackAlbum.setBinding([this]() { return this->bMetadata.value().value("xesam:album").toString(); }); this->bTrackArtist.setBinding([this]() { const auto& artist = this->bMetadata.value().value("xesam:artist").value<QList<QString>>(); return artist.isEmpty() ? QString() : artist.join(", "); }); this->bTrackAlbumArtist.setBinding([this]() { return this->bMetadata.value().value("xesam:albumArtist").toString(); }); this->bTrackArtUrl.setBinding([this]() { return this->bMetadata.value().value("mpris:artUrl").toString(); }); this->bInternalLength.setBinding([this]() { auto variant = this->bMetadata.value().value("mpris:length"); if (variant.isValid() && variant.canConvert<qlonglong>()) { return variant.value<qlonglong>(); } else return static_cast<qlonglong>(-1); }); this->bPlaybackState.setBinding([this]() { const auto& status = this->bpPlaybackStatus.value(); if (status == "Playing") { return MprisPlaybackState::Playing; } else if (status == "Paused") { this->pausedTime = QDateTime::currentDateTimeUtc(); return MprisPlaybackState::Paused; } else if (status == "Stopped") { return MprisPlaybackState::Stopped; } else { qWarning() << "Received unexpected PlaybackStatus for" << this << status; return MprisPlaybackState::Stopped; } }); this->bIsPlaying.setBinding([this]() { return this->bPlaybackState == MprisPlaybackState::Playing; }); this->bLoopState.setBinding([this]() { const auto& status = this->bpLoopStatus.value(); if (status == "None") { return MprisLoopState::None; } else if (status == "Track") { return MprisLoopState::Track; } else if (status == "Playlist") { return MprisLoopState::Playlist; } else { qWarning() << "Received unexpected LoopStatus for" << this << status; return MprisLoopState::None; } }); // clang-format off QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek); QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); // Ensure user triggered position updates can update length. QObject::connect(this, &MprisPlayer::positionChanged, this, &MprisPlayer::onExportedPositionChanged); // clang-format on this->appProperties.setInterface(this->app); this->playerProperties.setInterface(this->player); this->appProperties.updateAllViaGetAll(); this->playerProperties.updateAllViaGetAll(); } void MprisPlayer::raise() { if (!this->canRaise()) { qWarning() << "Cannot call raise() on" << this << "because canRaise is false."; return; } this->app->Raise(); } void MprisPlayer::quit() { if (!this->canQuit()) { qWarning() << "Cannot call quit() on" << this << "because canQuit is false."; return; } this->app->Quit(); } void MprisPlayer::openUri(const QString& uri) { this->player->OpenUri(uri); } void MprisPlayer::next() { if (!this->canGoNext()) { qWarning() << "Cannot call next() on" << this << "because canGoNext is false."; return; } this->player->Next(); } void MprisPlayer::previous() { if (!this->canGoPrevious()) { qWarning() << "Cannot call previous() on" << this << "because canGoPrevious is false."; return; } this->player->Previous(); } void MprisPlayer::seek(qreal offset) { if (!this->canSeek()) { qWarning() << "Cannot call seek() on" << this << "because canSeek is false."; return; } auto target = static_cast<qlonglong>(offset * 1000) * 1000; this->player->Seek(target); } bool MprisPlayer::isValid() const { return this->player->isValid(); } QString MprisPlayer::address() const { return this->player->service(); } qlonglong MprisPlayer::positionMs() const { if (!this->positionSupported()) return 0; // unsupported if (this->bPlaybackState == MprisPlaybackState::Stopped) return 0; auto paused = this->bPlaybackState == MprisPlaybackState::Paused; auto time = paused ? this->pausedTime : QDateTime::currentDateTime(); auto offset = time - this->lastPositionTimestamp; auto rateMul = static_cast<qlonglong>(this->bRate.value() * 1000); offset = (offset * rateMul) / 1000; return (this->bpPosition.value() / 1000) + offset.count(); } qreal MprisPlayer::position() const { if (!this->positionSupported()) return 0; // unsupported if (this->bPlaybackState == MprisPlaybackState::Stopped) return 0; return static_cast<qreal>(this->positionMs()) / 1000.0; // NOLINT } bool MprisPlayer::positionSupported() const { return this->pPosition.exists(); } void MprisPlayer::setPosition(qreal position) { if (this->bpPosition.value() == -1) { qWarning() << "Cannot set position of" << this << "because position is not supported."; return; } if (!this->canSeek()) { qWarning() << "Cannot set position of" << this << "because canSeek is false."; return; } auto target = static_cast<qlonglong>(position * 1000) * 1000; if (!this->mTrackId.isEmpty()) { this->player->SetPosition(QDBusObjectPath(this->mTrackId), target); } else { auto pos = this->positionMs() * 1000; this->player->Seek(target - pos); } this->setPosition(target); } void MprisPlayer::onPositionUpdated() { const bool firstChange = !this->lastPositionTimestamp.isValid(); this->lastPositionTimestamp = QDateTime::currentDateTimeUtc(); this->pausedTime = this->lastPositionTimestamp; emit this->positionChanged(); if (firstChange) emit this->positionSupportedChanged(); } void MprisPlayer::setPosition(qlonglong position) { this->bpPosition = position; this->onPositionUpdated(); } void MprisPlayer::onExportedPositionChanged() { if (!this->lengthSupported()) emit this->lengthChanged(); } void MprisPlayer::onSeek(qlonglong time) { this->setPosition(time); } qreal MprisPlayer::length() const { if (this->bInternalLength == -1) { return this->position(); // unsupported } else { return static_cast<qreal>(this->bInternalLength / 1000) / 1000; // NOLINT } } bool MprisPlayer::lengthSupported() const { return this->bInternalLength != -1; } bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); } void MprisPlayer::setVolume(qreal volume) { if (!this->canControl()) { qWarning() << "Cannot set volume of" << this << "because canControl is false."; return; } if (!this->volumeSupported()) { qWarning() << "Cannot set volume of" << this << "because volume is not supported."; return; } this->bVolume = volume; this->pVolume.write(); } void MprisPlayer::onMetadataChanged() { auto trackChanged = false; QString trackId; auto trackidVariant = this->bpMetadata.value().value("mpris:trackid"); if (trackidVariant.isValid()) { if (trackidVariant.canConvert<QString>()) { trackId = trackidVariant.toString(); } else if (trackidVariant.canConvert<QDBusObjectPath>()) { trackId = trackidVariant.value<QDBusObjectPath>().path(); } } if (trackId != this->mTrackId) { this->mTrackId = trackId; trackChanged = true; } // Helps to catch players without trackid. auto urlVariant = this->bpMetadata.value().value("xesam:url"); if (urlVariant.isValid() && urlVariant.canConvert<QString>()) { auto url = urlVariant.toString(); if (url != this->mTrackUrl) { this->mTrackUrl = url; trackChanged = true; } } Qt::beginPropertyUpdateGroup(); if (trackChanged) { emit this->trackChanged(); this->bUniqueId = this->bUniqueId + 1; // Some players don't seem to send position updates or seeks on track change. this->pPosition.requestUpdate(); } this->bMetadata = this->bpMetadata.value(); Qt::endPropertyUpdateGroup(); if (trackChanged) emit this->postTrackChanged(); } void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { if (playbackState == this->bPlaybackState) return; switch (playbackState) { case MprisPlaybackState::Stopped: if (!this->canControl()) { qWarning() << "Cannot set playbackState of" << this << "to Stopped because canControl is false."; return; } this->player->Stop(); break; case MprisPlaybackState::Playing: if (!this->canPlay()) { qWarning() << "Cannot set playbackState of" << this << "to Playing because canPlay is false."; return; } this->player->Play(); break; case MprisPlaybackState::Paused: if (!this->canPause()) { qWarning() << "Cannot set playbackState of" << this << "to Paused because canPause is false."; return; } this->player->Pause(); break; default: qWarning() << "Cannot set playbackState of" << this << "to unknown value" << playbackState; return; } } void MprisPlayer::play() { this->setPlaybackState(MprisPlaybackState::Playing); } void MprisPlayer::pause() { this->setPlaybackState(MprisPlaybackState::Paused); } void MprisPlayer::stop() { this->setPlaybackState(MprisPlaybackState::Stopped); } void MprisPlayer::togglePlaying() { if (this->bIsPlaying) { this->pause(); } else { this->play(); } } void MprisPlayer::setPlaying(bool playing) { if (playing == this->bIsPlaying) return; this->togglePlaying(); } bool MprisPlayer::loopSupported() const { return this->pLoopStatus.exists(); } void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) { if (!this->canControl()) { qWarning() << "Cannot set loopState of" << this << "because canControl is false."; return; } if (!this->loopSupported()) { qWarning() << "Cannot set loopState of" << this << "because loop state is not supported."; return; } if (loopState == this->bLoopState) return; QString loopStatusStr; switch (loopState) { case MprisLoopState::None: loopStatusStr = "None"; break; case MprisLoopState::Track: loopStatusStr = "Track"; break; case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break; default: qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState; return; } this->bpLoopStatus = loopStatusStr; this->pLoopStatus.write(); } void MprisPlayer::setRate(qreal rate) { if (rate == this->bRate.value()) return; if (rate < this->bMinRate.value() || rate > this->bMaxRate.value()) { qWarning() << "Cannot set rate for" << this << "to" << rate << "which is outside of minRate and maxRate" << this->bMinRate.value() << this->bMaxRate.value(); return; } this->bRate = rate; this->pRate.write(); } bool MprisPlayer::shuffleSupported() const { return this->pShuffle.exists(); } void MprisPlayer::setShuffle(bool shuffle) { if (!this->shuffleSupported()) { qWarning() << "Cannot set shuffle for" << this << "because shuffle is not supported."; return; } if (!this->canControl()) { qWarning() << "Cannot set shuffle state of" << this << "because canControl is false."; return; } this->bShuffle = shuffle; this->pShuffle.write(); } void MprisPlayer::setFullscreen(bool fullscreen) { if (!this->canSetFullscreen()) { qWarning() << "Cannot set fullscreen for" << this << "because canSetFullscreen is false."; return; } this->bFullscreen = fullscreen; this->pFullscreen.write(); } void MprisPlayer::onGetAllFinished() { if (this->volumeSupported()) emit this->volumeSupportedChanged(); if (this->loopSupported()) emit this->loopSupportedChanged(); if (this->shuffleSupported()) emit this->shuffleSupportedChanged(); emit this->ready(); } } // namespace qs::service::mpris