[multimedia/haruna] src: mpris: refactor into a library
George Florea Bănuș <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit d231ebd29a8fb2780101bf1ce7b15156509ab5e1 by George Florea Bănuș.
Committed on 25/07/2026 at 22:50.
Pushed by georgefb into branch 'master'.
mpris: refactor into a library
M +5 -5 src/CMakeLists.txt
A +17 -0 src/mpris/CMakeLists.txt
A +497 -0 src/mpris/mpriscontroller.cpp [License: GPL(v3.0+)]
A +214 -0 src/mpris/mpriscontroller.h [License: GPL(v3.0+)]
A +182 -0 src/mpris/playeradaptor.cpp [License: GPL(v3.0+)]
A +97 -0 src/mpris/playeradaptor.h [License: GPL(v3.0+)]
A +95 -0 src/mpris/rootadaptor.cpp [License: GPL(v3.0+)]
A +63 -0 src/mpris/rootadaptor.h [License: GPL(v3.0+)]
D +0 -65 src/mpris2/mediaplayer2.cpp
D +0 -47 src/mpris2/mediaplayer2.h
D +0 -336 src/mpris2/mediaplayer2player.cpp
D +0 -92 src/mpris2/mediaplayer2player.h
A +417 -0 src/mprisitem.cpp [License: GPL(v3.0+)]
A +55 -0 src/mprisitem.h [License: GPL(v3.0+)]
M +19 -15 src/mpv/mpvitem.cpp
M +6 -1 src/mpv/mpvitem.h
M +4 -4 src/qml/Actions.qml
M +13 -0 src/qml/Main.qml
M +0 -4 src/qml/MpvVideo.qml
M +2 -2 src/utilities/miscutils.cpp
M +4 -1 src/utilities/miscutils.h
https://invent.kde.org/multimedia/haruna/-/commit/d231ebd29a8fb2780101bf1ce7b15156509ab5e1
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index cc0e17a8..63b9b91d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -28,6 +28,7 @@ target_sources(haruna
mpv/mpvpreview.h mpv/mpvpreview.cpp
mpv/mpvproperties.h
mpv/seekablerangesmodel.h mpv/seekablerangesmodel.cpp
+ mprisitem.h mprisitem.cpp
thumbnailimageprovider.h thumbnailimageprovider.cpp
worker.h worker.cpp
${ICONS_SRCS}
@@ -58,13 +59,10 @@ if (CMAKE_SYSTEM_NAME IN_LIST DBUS_PLATFORMS)
qt_add_dbus_interface(haruna_screensaver_inhibit_SRCS ../org.freedesktop.ScreenSaver.xml screensaverdbusinterface)
target_sources(haruna
PRIVATE
- mpris2/mediaplayer2.h mpris2/mediaplayer2.cpp
- mpris2/mediaplayer2player.h mpris2/mediaplayer2player.cpp
${haruna_screensaver_inhibit_SRCS}
)
target_include_directories(haruna
PRIVATE
- mpris2
${CMAKE_CURRENT_BINARY_DIR}
)
endif()
@@ -190,8 +188,10 @@ target_link_libraries(haruna PRIVATE
)
if (CMAKE_SYSTEM_NAME IN_LIST DBUS_PLATFORMS)
- target_link_libraries(haruna PRIVATE
- Qt6::DBus
+ add_subdirectory(mpris)
+ target_link_libraries(haruna
+ PUBLIC
+ mpris
)
endif()
install(TARGETS haruna DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/src/mpris/CMakeLists.txt b/src/mpris/CMakeLists.txt
new file mode 100755
index 00000000..7952dbc1
--- /dev/null
+++ b/src/mpris/CMakeLists.txt
@@ -0,0 +1,17 @@
+#
+# SPDX-FileCopyrightText: 2026 George Florea Bănuș <[email protected]>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+qt_add_library(mpris STATIC)
+target_sources(mpris
+ PRIVATE
+ mpriscontroller.h mpriscontroller.cpp
+ playeradaptor.h playeradaptor.cpp
+ rootadaptor.h rootadaptor.cpp
+)
+
+target_link_libraries(mpris PRIVATE
+ Qt6::DBus
+)
diff --git a/src/mpris/mpriscontroller.cpp b/src/mpris/mpriscontroller.cpp
new file mode 100644
index 00000000..43784498
--- /dev/null
+++ b/src/mpris/mpriscontroller.cpp
@@ -0,0 +1,497 @@
+/*
+ * SPDX-FileCopyrightText: 2026 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "mpriscontroller.h"
+
+#include <QDBusConnection>
+#include <QTimer>
+
+#include "playeradaptor.h"
+#include "rootadaptor.h"
+
+using namespace Qt::StringLiterals;
+
+class MprisControllerPrivate
+{
+ Q_DISABLE_COPY(MprisControllerPrivate);
+
+public:
+ MprisControllerPrivate(MprisController *q)
+ : q_ptr(q)
+ , rootAdaptor(*q)
+ , playerAdaptor(*q)
+ {
+ }
+ MprisController *const q_ptr;
+
+ QTimer notifyTimer;
+
+ // Interface MediaPlayer2
+ RootAdaptor rootAdaptor;
+ bool canQuit = false;
+ bool fullscreen = false;
+ bool canSetFullscreen = false;
+ bool canRaise = false;
+ bool hasTrackList = false;
+ QString identity;
+ QString desktopEntry;
+ QStringList supportedUriSchemes;
+ QStringList supportedMimeTypes;
+
+ // Interface MediaPlayer2.Player
+ PlayerAdaptor playerAdaptor;
+ QString playbackStatus;
+ QString loopStatus;
+ double rate = 1.0;
+ bool shuffle = false;
+ QVariantMap metadata;
+ double volume = 1.0;
+ qlonglong position = 0;
+ double minimumRate = 0.0;
+ double maximumRate = 1.0;
+ bool canGoNext = false;
+ bool canGoPrevious = false;
+ bool canPlay = false;
+ bool canPause = false;
+ bool canSeek = false;
+ bool canControl = false;
+};
+
+MprisController::MprisController(QObject *parent)
+ : QObject{parent}
+ , d_ptr{std::make_unique<MprisControllerPrivate>(this)}
+{
+ d_ptr->notifyTimer.setSingleShot(true);
+ d_ptr->notifyTimer.setInterval(100);
+ d_ptr->notifyTimer.callOnTimeout([this]() {
+ d_ptr->playerAdaptor.notify(u"Metadata"_s, d_ptr->metadata);
+ });
+
+ // register mpris dbus service
+ QDBusConnection::sessionBus().registerService(u"org.mpris.MediaPlayer2.haruna"_s);
+ QDBusConnection::sessionBus().registerObject(u"/org/mpris/MediaPlayer2"_s, this, QDBusConnection::ExportAdaptors);
+}
+
+MprisController::~MprisController() = default;
+
+void MprisController::raise()
+{
+ Q_EMIT raiseRequested();
+}
+
+void MprisController::quit()
+{
+ Q_EMIT quitRequested();
+}
+
+void MprisController::next()
+{
+ Q_EMIT nextRequested();
+}
+
+void MprisController::previous()
+{
+ Q_EMIT previousRequested();
+}
+
+void MprisController::pause()
+{
+ Q_EMIT pauseRequested();
+}
+
+void MprisController::playpause()
+{
+ Q_EMIT playpauseRequested();
+}
+
+void MprisController::stop()
+{
+ Q_EMIT stopRequested();
+}
+
+void MprisController::play()
+{
+ Q_EMIT playRequested();
+}
+
+void MprisController::seek(qlonglong offset)
+{
+ Q_EMIT seekRequested(offset);
+}
+
+void MprisController::seekTo(qlonglong position)
+{
+ Q_EMIT seekToRequested(position);
+}
+
+void MprisController::openUri(QString uri)
+{
+ Q_EMIT openUriRequested(uri);
+}
+
+bool MprisController::canQuit() const
+{
+ return d_ptr->canQuit;
+}
+
+void MprisController::setCanQuit(bool newCanQuit)
+{
+ if (d_ptr->canQuit == newCanQuit) {
+ return;
+ }
+ d_ptr->canQuit = newCanQuit;
+ d_ptr->rootAdaptor.notify(u"CanQuit"_s, d_ptr->canQuit);
+}
+
+bool MprisController::fullscreen() const
+{
+ return d_ptr->fullscreen;
+}
+
+void MprisController::setFullscreen(bool newFullscreen)
+{
+ if (d_ptr->fullscreen == newFullscreen) {
+ return;
+ }
+ d_ptr->fullscreen = newFullscreen;
+ d_ptr->rootAdaptor.notify(u"Fullscreen"_s, d_ptr->fullscreen);
+}
+
+void MprisController::requestFullscreenChange(bool newFullscreen)
+{
+ Q_EMIT fullscreenChangeRequested(newFullscreen);
+}
+
+bool MprisController::canSetFullscreen() const
+{
+ return d_ptr->canSetFullscreen;
+}
+
+void MprisController::setCanSetFullscreen(bool newCanSetFullscreen)
+{
+ if (d_ptr->canSetFullscreen == newCanSetFullscreen) {
+ return;
+ }
+ d_ptr->canSetFullscreen = newCanSetFullscreen;
+ d_ptr->rootAdaptor.notify(u"CanSetFullscreen"_s, d_ptr->canSetFullscreen);
+}
+
+bool MprisController::canRaise() const
+{
+ return d_ptr->canRaise;
+}
+
+void MprisController::setCanRaise(bool newCanRaise)
+{
+ if (d_ptr->canRaise == newCanRaise) {
+ return;
+ }
+ d_ptr->canRaise = newCanRaise;
+ d_ptr->rootAdaptor.notify(u"CanRaise"_s, d_ptr->canRaise);
+}
+
+bool MprisController::hasTrackList() const
+{
+ return d_ptr->hasTrackList;
+}
+
+void MprisController::setHasTrackList(bool newHasTrackList)
+{
+ if (d_ptr->hasTrackList == newHasTrackList) {
+ return;
+ }
+ d_ptr->hasTrackList = newHasTrackList;
+ d_ptr->rootAdaptor.notify(u"HasTrackList"_s, d_ptr->hasTrackList);
+}
+
+QString MprisController::identity() const
+{
+ return d_ptr->identity;
+}
+
+void MprisController::setIdentity(const QString &newIdentity)
+{
+ if (d_ptr->identity == newIdentity) {
+ return;
+ }
+ d_ptr->identity = newIdentity;
+ d_ptr->rootAdaptor.notify(u"Identity"_s, d_ptr->identity);
+}
+
+QString MprisController::desktopEntry() const
+{
+ return d_ptr->desktopEntry;
+}
+
+void MprisController::setDesktopEntry(const QString &newDesktopEntry)
+{
+ if (d_ptr->desktopEntry == newDesktopEntry) {
+ return;
+ }
+ d_ptr->desktopEntry = newDesktopEntry;
+ d_ptr->rootAdaptor.notify(u"DesktopEntry"_s, d_ptr->desktopEntry);
+}
+
+QStringList MprisController::supportedUriSchemes() const
+{
+ return d_ptr->supportedUriSchemes;
+}
+
+void MprisController::setSupportedUriSchemes(const QStringList &newSupportedUriSchemes)
+{
+ if (d_ptr->supportedUriSchemes == newSupportedUriSchemes) {
+ return;
+ }
+ d_ptr->supportedUriSchemes = newSupportedUriSchemes;
+ d_ptr->rootAdaptor.notify(u"SupportedUriSchemes"_s, d_ptr->supportedUriSchemes);
+}
+
+QStringList MprisController::supportedMimeTypes() const
+{
+ return d_ptr->supportedMimeTypes;
+}
+
+void MprisController::setSupportedMimeTypes(const QStringList &newSupportedMimeTypes)
+{
+ if (d_ptr->supportedMimeTypes == newSupportedMimeTypes) {
+ return;
+ }
+ d_ptr->supportedMimeTypes = newSupportedMimeTypes;
+ d_ptr->rootAdaptor.notify(u"SupportedMimeTypes"_s, d_ptr->supportedMimeTypes);
+}
+
+QString MprisController::playbackStatus() const
+{
+ return d_ptr->playbackStatus;
+}
+
+void MprisController::setPlaybackStatus(const QString &newStatus)
+{
+ if (d_ptr->playbackStatus == newStatus) {
+ return;
+ }
+ d_ptr->playbackStatus = newStatus;
+ d_ptr->playerAdaptor.notify(u"PlaybackStatus"_s, d_ptr->playbackStatus);
+}
+
+QString MprisController::loopStatus() const
+{
+ return d_ptr->loopStatus;
+}
+
+void MprisController::requestLoopStatusChange(const QString &newLoopStatus)
+{
+ Q_EMIT loopStatusChangeRequested(newLoopStatus);
+}
+
+double MprisController::rate() const
+{
+ return d_ptr->rate;
+}
+
+void MprisController::setRate(const double newRate)
+{
+ if (d_ptr->rate == newRate) {
+ return;
+ }
+ d_ptr->rate = newRate;
+ d_ptr->playerAdaptor.notify(u"Rate"_s, d_ptr->rate);
+}
+
+void MprisController::requestRateChange(double newRate)
+{
+ Q_EMIT rateChangeRequested(newRate);
+}
+
+void MprisController::setLoopStatus(const QString &newLoopStatus)
+{
+ if (d_ptr->loopStatus == newLoopStatus) {
+ return;
+ }
+ d_ptr->loopStatus = newLoopStatus;
+ d_ptr->playerAdaptor.notify(u"LoopStatus"_s, d_ptr->loopStatus);
+}
+
+bool MprisController::shuffle() const
+{
+ return d_ptr->shuffle;
+}
+
+void MprisController::setShuffle(const bool newShuffle)
+{
+ if (d_ptr->shuffle == newShuffle) {
+ return;
+ }
+ d_ptr->shuffle = newShuffle;
+ d_ptr->playerAdaptor.notify(u"Shuffle"_s, d_ptr->shuffle);
+}
+
+void MprisController::requestShuffleChange(bool newShuffle)
+{
+ Q_EMIT shuffleChangeRequested(newShuffle);
+}
+
+QVariantMap MprisController::metadata() const
+{
+ return d_ptr->metadata;
+}
+
+void MprisController::setMetadata(const QString &field, const QVariant value)
+{
+ d_ptr->metadata.insert(field, value);
+ d_ptr->notifyTimer.start();
+}
+
+double MprisController::volume() const
+{
+ return d_ptr->volume;
+}
+
+void MprisController::setVolume(const double newVolume)
+{
+ if (d_ptr->volume == newVolume) {
+ return;
+ }
+ d_ptr->volume = newVolume;
+ d_ptr->playerAdaptor.notify(u"Volume"_s, d_ptr->volume);
+}
+
+void MprisController::requestVolumeChange(const double &newVolume)
+{
+ Q_EMIT volumeChangeRequested(newVolume);
+}
+
+qlonglong MprisController::position() const
+{
+ return d_ptr->position;
+}
+
+void MprisController::setPosition(const qlonglong newPosition)
+{
+ if (d_ptr->position == newPosition) {
+ return;
+ }
+ d_ptr->position = newPosition;
+}
+
+double MprisController::minimumRate() const
+{
+ return d_ptr->minimumRate;
+}
+
+void MprisController::setMinimumRate(const double newMinRate)
+{
+ if (d_ptr->minimumRate == newMinRate) {
+ return;
+ }
+ d_ptr->minimumRate = newMinRate;
+ d_ptr->playerAdaptor.notify(u"MinimumRate"_s, d_ptr->minimumRate);
+}
+
+double MprisController::maximumRate() const
+{
+ return d_ptr->maximumRate;
+}
+
+void MprisController::setMaximumRate(const double newMaxRate)
+{
+ if (d_ptr->maximumRate == newMaxRate) {
+ return;
+ }
+ d_ptr->maximumRate = newMaxRate;
+ d_ptr->playerAdaptor.notify(u"MaximumRate"_s, d_ptr->maximumRate);
+}
+
+bool MprisController::canGoNext() const
+{
+ return d_ptr->canGoNext;
+}
+
+void MprisController::setCanGoNext(const bool newCanGoNext)
+{
+ if (d_ptr->canGoNext == newCanGoNext) {
+ return;
+ }
+ d_ptr->canGoNext = newCanGoNext;
+ d_ptr->playerAdaptor.notify(u"CanGoNext"_s, d_ptr->canGoNext);
+}
+
+bool MprisController::canGoPrevious() const
+{
+ return d_ptr->canGoPrevious;
+}
+
+void MprisController::setCanGoPrevious(const bool newCanGoPrevious)
+{
+ if (d_ptr->canGoPrevious == newCanGoPrevious) {
+ return;
+ }
+ d_ptr->canGoPrevious = newCanGoPrevious;
+ d_ptr->playerAdaptor.notify(u"CanGoPrevious"_s, d_ptr->canGoPrevious);
+}
+
+bool MprisController::canPlay() const
+{
+ return d_ptr->canPlay;
+}
+
+void MprisController::setCanPlay(const bool newCanPlay)
+{
+ if (d_ptr->canPlay == newCanPlay) {
+ return;
+ }
+ d_ptr->canPlay = newCanPlay;
+ d_ptr->playerAdaptor.notify(u"CanPlay"_s, d_ptr->canPlay);
+}
+
+bool MprisController::canPause() const
+{
+ return d_ptr->canPause;
+}
+
+void MprisController::setCanPause(const bool newCanPause)
+{
+ if (d_ptr->canPause == newCanPause) {
+ return;
+ }
+ d_ptr->canPause = newCanPause;
+ d_ptr->playerAdaptor.notify(u"CanPause"_s, d_ptr->canPause);
+}
+
+bool MprisController::canSeek() const
+{
+ return d_ptr->canSeek;
+}
+
+void MprisController::setCanSeek(const bool newCanSeek)
+{
+ if (d_ptr->canSeek == newCanSeek) {
+ return;
+ }
+ d_ptr->canSeek = newCanSeek;
+ d_ptr->playerAdaptor.notify(u"CanSeek"_s, d_ptr->canSeek);
+}
+
+bool MprisController::canControl() const
+{
+ return d_ptr->canControl;
+}
+
+void MprisController::setCanControl(const bool newCanControl)
+{
+ if (d_ptr->canControl == newCanControl) {
+ return;
+ }
+ d_ptr->canControl = newCanControl;
+ d_ptr->playerAdaptor.notify(u"CanControl"_s, d_ptr->canControl);
+}
+
+void MprisController::seeked(qlonglong position)
+{
+ d_ptr->playerAdaptor.seeked(position);
+}
+
+#include "moc_mpriscontroller.cpp"
\ No newline at end of file
diff --git a/src/mpris/mpriscontroller.h b/src/mpris/mpriscontroller.h
new file mode 100644
index 00000000..eabd552e
--- /dev/null
+++ b/src/mpris/mpriscontroller.h
@@ -0,0 +1,214 @@
+/*
+ * SPDX-FileCopyrightText: 2026 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef MPRISCONTROLLER_H
+#define MPRISCONTROLLER_H
+
+#include <QObject>
+
+class MprisControllerPrivate;
+
+/*!
+ * \class MprisController
+ * \inmodule MPRIS2
+ * \brief Controls communication between application and MPRIS clients.
+ *
+ * MprisController receives requests from the MPRIS D-Bus adaptors and
+ * synchronizes state between the application and connected MPRIS clients.
+ */
+class MprisController : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit MprisController(QObject *parent = nullptr);
+ ~MprisController();
+
+ bool canQuit() const;
+ void setCanQuit(bool newCanQuit);
+
+ bool fullscreen() const;
+ void setFullscreen(bool newFullscreen);
+ void requestFullscreenChange(bool newFullscreen);
+
+ bool canSetFullscreen() const;
+ void setCanSetFullscreen(bool newCanSetFullscreen);
+
+ bool canRaise() const;
+ void setCanRaise(bool newCanRaise);
+
+ bool hasTrackList() const;
+ void setHasTrackList(bool newHasTrackList);
+
+ QString identity() const;
+ void setIdentity(const QString &newIdentity);
+
+ QString desktopEntry() const;
+ void setDesktopEntry(const QString &newDesktopEntry);
+
+ QStringList supportedUriSchemes() const;
+ void setSupportedUriSchemes(const QStringList &newSupportedUriSchemes);
+
+ QStringList supportedMimeTypes() const;
+ void setSupportedMimeTypes(const QStringList &newSupportedMimeTypes);
+
+ void raise();
+ void quit();
+
+ void next();
+ void previous();
+ void pause();
+ void playpause();
+ void stop();
+ void play();
+ /*!
+ * \a offset in microseconds to seek by
+ */
+ void seek(qlonglong offset);
+ /*!
+ * \a position to seek to in microseconds
+ */
+ void seekTo(qlonglong position);
+ void openUri(QString uri);
+
+ QString playbackStatus() const;
+ void setPlaybackStatus(const QString &newStatus);
+
+ QString loopStatus() const;
+ /*!
+ * Set the internal loop status property to \c newLoopStatus and notify MPRIS clients.
+ *
+ * Should be called when loop status changes in the application.
+ */
+ void setLoopStatus(const QString &newLoopStatus);
+ /*!
+ * Request the application to change the loop status property to \c newLoopStatus
+ */
+ void requestLoopStatusChange(const QString &newLoopStatus);
+
+ double rate() const;
+ /*!
+ * Set the internal rate property to \c newRate and notify MPRIS clients.
+ *
+ * Should be called when rate changes in the application.
+ */
+ void setRate(const double newRate);
+ /*!
+ * Request the application to change the rate property to \c newLoopStatus
+ */
+ void requestRateChange(double newRate);
+
+ bool shuffle() const;
+ /*!
+ * Set the internal shuffle property to \c newShuffle and notify MPRIS clients.
+ *
+ * Should be called when shuffle changes in the application.
+ */
+ void setShuffle(const bool newShuffle);
+ /*!
+ * Request the application to change the shuffle property to \c newShuffle
+ */
+ void requestShuffleChange(bool newShuffle);
+
+ QVariantMap metadata() const;
+ void setMetadata(const QString &field, const QVariant value);
+
+ double volume() const;
+ /*!
+ * Set the internal volume property to \c newVolume and notify MPRIS clients.
+ *
+ * Should be called when volume changes in the application.
+ */
+ void setVolume(const double newVolume);
+ /*!
+ * Request the application to change the volume property to \c newVolume
+ */
+ void requestVolumeChange(const double &newVolume);
+
+ /*!
+ * Returns \c position in microseconds
+ */
+ qlonglong position() const;
+ /*!
+ * Set position to /c newPosition in microseconds
+ */
+ void setPosition(const qlonglong newPosition);
+
+ double minimumRate() const;
+ void setMinimumRate(const double newMinRate);
+
+ double maximumRate() const;
+ void setMaximumRate(const double newMaxRate);
+
+ bool canGoNext() const;
+ void setCanGoNext(const bool newCanGoNext);
+ bool canGoPrevious() const;
+ void setCanGoPrevious(const bool newCanGoPrevious);
+ bool canPlay() const;
+ void setCanPlay(const bool newCanPlay);
+ bool canPause() const;
+ void setCanPause(const bool newCanPause);
+ bool canSeek() const;
+ void setCanSeek(const bool newCanSeek);
+ bool canControl() const;
+ void setCanControl(const bool newCanControl);
+
+ /*!
+ * Indicates that position has changed on the application side.
+ * Causes the Seeked signal of the M ediaPlayer2.Player interface to be emitted
+ */
+ void seeked(qlonglong position);
+
+Q_SIGNALS:
+ void raiseRequested();
+ void quitRequested();
+ void nextRequested();
+ void previousRequested();
+ void pauseRequested();
+ void playpauseRequested();
+ void stopRequested();
+ void playRequested();
+
+ /*!
+ * \a offset in microseconds to seek by
+ */
+ void seekRequested(qlonglong offset);
+ /*!
+ * \a position to seek to in microseconds
+ */
+ void seekToRequested(qlonglong position);
+ void openUriRequested(QString uri);
+
+ /*!
+ * Emitted when fullscreen is changed by MPRIS client
+ */
+ void fullscreenChangeRequested(bool fullscreen);
+
+ /*!
+ * Emitted when volume is changed by MPRIS client
+ */
+ void volumeChangeRequested(double volume);
+
+ /*!
+ * Emitted when loop status is changed by MPRIS client
+ */
+ void loopStatusChangeRequested(const QString &loopStatus);
+
+ /*!
+ * Emitted when shuffle is changed by MPRIS client
+ */
+ void shuffleChangeRequested(bool shuffle);
+
+ /*!
+ * Emitted when rate is changed by MPRIS client
+ */
+ void rateChangeRequested(double rate);
+
+private:
+ std::unique_ptr<MprisControllerPrivate> d_ptr;
+};
+
+#endif // MPRISCONTROLLER_H
diff --git a/src/mpris/playeradaptor.cpp b/src/mpris/playeradaptor.cpp
new file mode 100644
index 00000000..ffba53e8
--- /dev/null
+++ b/src/mpris/playeradaptor.cpp
@@ -0,0 +1,182 @@
+/*
+ * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "playeradaptor.h"
+
+#include <QDBusConnection>
+#include <QDBusMessage>
+#include <QDBusObjectPath>
+
+#include "mpriscontroller.h"
+
+using namespace Qt::StringLiterals;
+
+PlayerAdaptor::PlayerAdaptor(MprisController &parent)
+ : QDBusAbstractAdaptor(&parent)
+ , m_mprisController(parent)
+{
+}
+
+void PlayerAdaptor::notify(const QString &property, const QVariant &value)
+{
+ const auto path = u"/org/mpris/MediaPlayer2"_s;
+ const auto propertiesInterface = u"org.freedesktop.DBus.Properties"_s;
+ const auto playerInterface = u"org.mpris.MediaPlayer2.Player"_s;
+ const auto properties = QVariantMap{{property, value}};
+
+ QDBusMessage msg = QDBusMessage::createSignal(path, propertiesInterface, u"PropertiesChanged"_s);
+ msg << playerInterface << properties << QStringList{};
+
+ QDBusConnection::sessionBus().send(msg);
+}
+
+void PlayerAdaptor::seeked(qlonglong position)
+{
+ Q_EMIT Seeked(position);
+}
+
+QString PlayerAdaptor::playbackStatus()
+{
+ return m_mprisController.playbackStatus();
+}
+
+QString PlayerAdaptor::loopStatus()
+{
+ return m_mprisController.loopStatus();
+}
+
+void PlayerAdaptor::setLoopStatus(const QString &newLoopStatus)
+{
+ m_mprisController.requestLoopStatusChange(newLoopStatus);
+}
+
+double PlayerAdaptor::rate() const
+{
+ return m_mprisController.rate();
+}
+
+void PlayerAdaptor::setRate(double newRate)
+{
+ m_mprisController.requestRateChange(newRate);
+}
+
+bool PlayerAdaptor::shuffle()
+{
+ return m_mprisController.shuffle();
+}
+
+void PlayerAdaptor::setShuffle(bool newShuffle)
+{
+ m_mprisController.requestShuffleChange(newShuffle);
+}
+
+QVariantMap PlayerAdaptor::metadata()
+{
+ return m_mprisController.metadata();
+}
+
+double PlayerAdaptor::volume()
+{
+ return m_mprisController.volume();
+}
+
+void PlayerAdaptor::setVolume(double newVolume)
+{
+ m_mprisController.requestVolumeChange(newVolume);
+}
+
+qlonglong PlayerAdaptor::position()
+{
+ return m_mprisController.position();
+}
+
+double PlayerAdaptor::minimumRate() const
+{
+ return m_mprisController.minimumRate();
+}
+
+double PlayerAdaptor::maximumRate() const
+{
+ return m_mprisController.maximumRate();
+}
+
+bool PlayerAdaptor::canGoNext()
+{
+ return m_mprisController.canGoNext();
+}
+
+bool PlayerAdaptor::canGoPrevious()
+{
+ return m_mprisController.canGoPrevious();
+}
+
+bool PlayerAdaptor::canPlay()
+{
+ return m_mprisController.canPlay();
+}
+
+bool PlayerAdaptor::canPause()
+{
+ return m_mprisController.canPause();
+}
+
+bool PlayerAdaptor::canSeek()
+{
+ return m_mprisController.canSeek();
+}
+
+bool PlayerAdaptor::canControl()
+{
+ return m_mprisController.canControl();
+}
+
+void PlayerAdaptor::Next()
+{
+ m_mprisController.next();
+}
+
+void PlayerAdaptor::Previous()
+{
+ m_mprisController.previous();
+}
+
+void PlayerAdaptor::Pause()
+{
+ m_mprisController.pause();
+}
+
+void PlayerAdaptor::PlayPause()
+{
+ m_mprisController.playpause();
+}
+
+void PlayerAdaptor::Stop()
+{
+ m_mprisController.stop();
+}
+
+void PlayerAdaptor::Play()
+{
+ m_mprisController.play();
+}
+
+void PlayerAdaptor::Seek(qlonglong offset)
+{
+ m_mprisController.seek(offset);
+}
+
+void PlayerAdaptor::SetPosition(const QDBusObjectPath &trackId, qlonglong position)
+{
+ Q_UNUSED(trackId)
+ m_mprisController.seekTo(position);
+}
+
+void PlayerAdaptor::OpenUri(const QString &uri)
+{
+ m_mprisController.openUri(uri);
+}
+
+#include "moc_playeradaptor.cpp"
diff --git a/src/mpris/playeradaptor.h b/src/mpris/playeradaptor.h
new file mode 100644
index 00000000..dd1cf386
--- /dev/null
+++ b/src/mpris/playeradaptor.h
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef MEDIAPLAYER2PLAYER_H
+#define MEDIAPLAYER2PLAYER_H
+
+#include <QDBusAbstractAdaptor>
+
+class MprisController;
+class MpvItem;
+class QDBusObjectPath;
+class QImage;
+
+// https://specifications.freedesktop.org/mpris/latest/Player_Interface.html
+class PlayerAdaptor : public QDBusAbstractAdaptor
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player")
+
+public:
+ explicit PlayerAdaptor(MprisController &parent);
+ ~PlayerAdaptor() = default;
+
+ Q_PROPERTY(QString PlaybackStatus READ playbackStatus CONSTANT)
+ QString playbackStatus();
+
+ Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus CONSTANT)
+ QString loopStatus();
+ void setLoopStatus(const QString &newLoopStatus);
+
+ Q_PROPERTY(double Rate READ rate WRITE setRate CONSTANT)
+ double rate() const;
+ void setRate(double newRate);
+
+ Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle CONSTANT)
+ bool shuffle();
+ void setShuffle(bool newShuffle);
+
+ Q_PROPERTY(QVariantMap Metadata READ metadata CONSTANT)
+ QVariantMap metadata();
+
+ Q_PROPERTY(double Volume READ volume WRITE setVolume CONSTANT)
+ double volume();
+ void setVolume(double newVolume);
+
+ Q_PROPERTY(qlonglong Position READ position CONSTANT)
+ qlonglong position();
+
+ Q_PROPERTY(double MinimumRate READ minimumRate CONSTANT)
+ double minimumRate() const;
+
+ Q_PROPERTY(double MaximumRate READ maximumRate CONSTANT)
+ double maximumRate() const;
+
+ Q_PROPERTY(bool CanGoNext READ canGoNext CONSTANT)
+ bool canGoNext();
+
+ Q_PROPERTY(bool CanGoPrevious READ canGoPrevious CONSTANT)
+ bool canGoPrevious();
+
+ Q_PROPERTY(bool CanPlay READ canPlay CONSTANT)
+ bool canPlay();
+
+ Q_PROPERTY(bool CanPause READ canPause CONSTANT)
+ bool canPause();
+
+ Q_PROPERTY(bool CanSeek READ canSeek CONSTANT)
+ bool canSeek();
+
+ Q_PROPERTY(bool CanControl READ canControl CONSTANT)
+ bool canControl();
+
+ void notify(const QString &property, const QVariant &value);
+ void seeked(qlonglong position);
+
+public Q_SLOTS:
+ void Next();
+ void Previous();
+ void Pause();
+ void PlayPause();
+ void Stop();
+ void Play();
+ void Seek(qlonglong offset);
+ void SetPosition(const QDBusObjectPath &trackId, qlonglong position);
+ void OpenUri(const QString &uri);
+
+Q_SIGNALS:
+ void Seeked(qlonglong position);
+
+private:
+ MprisController &m_mprisController;
+};
+
+#endif // MEDIAPLAYER2PLAYER_H
diff --git a/src/mpris/rootadaptor.cpp b/src/mpris/rootadaptor.cpp
new file mode 100644
index 00000000..e7da28ef
--- /dev/null
+++ b/src/mpris/rootadaptor.cpp
@@ -0,0 +1,95 @@
+/*
+ * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "rootadaptor.h"
+
+#include <QDBusConnection>
+#include <QDBusMessage>
+
+#include "mpriscontroller.h"
+
+using namespace Qt::StringLiterals;
+
+RootAdaptor::RootAdaptor(MprisController &parent)
+ : QDBusAbstractAdaptor(&parent)
+ , m_mprisController(parent)
+{
+}
+
+void RootAdaptor::notify(const QString &property, const QVariant &value)
+{
+ const auto path = u"/org/mpris/MediaPlayer2"_s;
+ const auto propertiesInterface = u"org.freedesktop.DBus.Properties"_s;
+ const auto rootInterface = u"org.mpris.MediaPlayer2"_s;
+ const auto properties = QVariantMap{{property, value}};
+
+ QDBusMessage msg = QDBusMessage::createSignal(path, propertiesInterface, u"PropertiesChanged"_s);
+ msg << rootInterface << properties << QStringList{};
+
+ QDBusConnection::sessionBus().send(msg);
+}
+
+bool RootAdaptor::canQuit() const
+{
+ return true;
+}
+
+bool RootAdaptor::fullscreen() const
+{
+ return m_mprisController.fullscreen();
+}
+
+void RootAdaptor::setFullscreen(bool newFullscreen)
+{
+ m_mprisController.requestFullscreenChange(newFullscreen);
+}
+
+bool RootAdaptor::canSetFullscreen() const
+{
+ return m_mprisController.canSetFullscreen();
+}
+
+bool RootAdaptor::canRaise() const
+{
+ return m_mprisController.canRaise();
+}
+
+bool RootAdaptor::hasTrackList() const
+{
+ return m_mprisController.hasTrackList();
+}
+
+QString RootAdaptor::identity() const
+{
+ return m_mprisController.identity();
+}
+
+QString RootAdaptor::desktopEntry() const
+{
+ return m_mprisController.desktopEntry();
+}
+
+QStringList RootAdaptor::supportedUriSchemes() const
+{
+ return m_mprisController.supportedUriSchemes();
+}
+
+QStringList RootAdaptor::supportedMimeTypes() const
+{
+ return m_mprisController.supportedMimeTypes();
+}
+
+void RootAdaptor::Raise()
+{
+ m_mprisController.raise();
+}
+
+void RootAdaptor::Quit()
+{
+ m_mprisController.quit();
+}
+
+#include "moc_rootadaptor.cpp"
diff --git a/src/mpris/rootadaptor.h b/src/mpris/rootadaptor.h
new file mode 100644
index 00000000..e6a581be
--- /dev/null
+++ b/src/mpris/rootadaptor.h
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef MEDIAPLAYER2_H
+#define MEDIAPLAYER2_H
+
+#include <QDBusAbstractAdaptor>
+
+class MprisController;
+class QDBusObjectPath;
+
+// https://specifications.freedesktop.org/mpris/latest/Media_Player.html
+class RootAdaptor : public QDBusAbstractAdaptor
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
+
+public:
+ explicit RootAdaptor(MprisController &parent);
+ ~RootAdaptor() = default;
+
+ Q_PROPERTY(bool CanQuit READ canQuit CONSTANT)
+ bool canQuit() const;
+
+ Q_PROPERTY(bool Fullscreen READ fullscreen WRITE setFullscreen CONSTANT)
+ bool fullscreen() const;
+ void setFullscreen(bool newFullscreen);
+
+ Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen CONSTANT)
+ bool canSetFullscreen() const;
+
+ Q_PROPERTY(bool CanRaise READ canRaise CONSTANT)
+ bool canRaise() const;
+
+ Q_PROPERTY(bool HasTrackList READ hasTrackList CONSTANT)
+ bool hasTrackList() const;
+
+ Q_PROPERTY(QString Identity READ identity CONSTANT)
+ QString identity() const;
+
+ Q_PROPERTY(QString DesktopEntry READ desktopEntry CONSTANT)
+ QString desktopEntry() const;
+
+ Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes CONSTANT)
+ QStringList supportedUriSchemes() const;
+
+ Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes CONSTANT)
+ QStringList supportedMimeTypes() const;
+
+ void notify(const QString &property, const QVariant &value);
+
+public Q_SLOTS:
+ void Raise();
+ void Quit();
+
+private:
+ MprisController &m_mprisController;
+};
+
+#endif // MEDIAPLAYER2_H
diff --git a/src/mpris2/mediaplayer2.cpp b/src/mpris2/mediaplayer2.cpp
deleted file mode 100644
index 53eaa1fe..00000000
--- a/src/mpris2/mediaplayer2.cpp
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#include "mediaplayer2.h"
-
-#include <QApplication>
-
-#include <KAboutData>
-
-using namespace Qt::StringLiterals;
-
-MediaPlayer2::MediaPlayer2(QObject *obj)
- : QDBusAbstractAdaptor(obj)
-{
-}
-
-void MediaPlayer2::Raise()
-{
- Q_EMIT raise();
-}
-
-void MediaPlayer2::Quit()
-{
- qApp->quit();
-}
-
-bool MediaPlayer2::CanRaise() const
-{
- return true;
-}
-
-bool MediaPlayer2::CanQuit() const
-{
- return true;
-}
-
-bool MediaPlayer2::HasTrackList() const
-{
- return false;
-}
-
-QString MediaPlayer2::Identity() const
-{
- return u"Haruna"_s;
-}
-
-QString MediaPlayer2::DesktopEntry() const
-{
- return KAboutData::applicationData().desktopFileName();
-}
-
-QStringList MediaPlayer2::SupportedUriSchemes() const
-{
- return QStringList() << u"file"_s << u"http"_s << u"https"_s;
-}
-
-QStringList MediaPlayer2::SupportedMimeTypes() const
-{
- return QStringList() << u"video/*"_s << u"audio/*"_s;
-}
-
-#include "moc_mediaplayer2.cpp"
diff --git a/src/mpris2/mediaplayer2.h b/src/mpris2/mediaplayer2.h
deleted file mode 100644
index ceb852dc..00000000
--- a/src/mpris2/mediaplayer2.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#ifndef MEDIAPLAYER2_H
-#define MEDIAPLAYER2_H
-
-#include <QDBusAbstractAdaptor>
-
-class QDBusObjectPath;
-
-class MediaPlayer2 : public QDBusAbstractAdaptor
-{
- Q_OBJECT
- Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
-
- Q_PROPERTY(bool CanRaise READ CanRaise CONSTANT)
- Q_PROPERTY(bool CanQuit READ CanQuit CONSTANT)
- Q_PROPERTY(bool HasTrackList READ HasTrackList CONSTANT)
- Q_PROPERTY(QString Identity READ Identity CONSTANT)
- Q_PROPERTY(QString DesktopEntry READ DesktopEntry CONSTANT)
- Q_PROPERTY(QStringList SupportedUriSchemes READ SupportedUriSchemes CONSTANT)
- Q_PROPERTY(QStringList SupportedMimeTypes READ SupportedMimeTypes CONSTANT)
-
-public:
- explicit MediaPlayer2(QObject *obj);
- ~MediaPlayer2() = default;
-
- bool CanRaise() const;
- bool CanQuit() const;
- bool HasTrackList() const;
- QString Identity() const;
- QString DesktopEntry() const;
- QStringList SupportedUriSchemes() const;
- QStringList SupportedMimeTypes() const;
-
-public Q_SLOTS:
- void Raise();
- void Quit();
-
-Q_SIGNALS:
- void raise();
-};
-
-#endif // MEDIAPLAYER2_H
diff --git a/src/mpris2/mediaplayer2player.cpp b/src/mpris2/mediaplayer2player.cpp
deleted file mode 100644
index 4ba6a3b5..00000000
--- a/src/mpris2/mediaplayer2player.cpp
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#include "mediaplayer2player.h"
-
-#include <QDBusConnection>
-#include <QDBusMessage>
-#include <QDBusObjectPath>
-#include <QFileInfo>
-#include <QImage>
-#include <QThreadPool>
-
-#include <KFileMetaData/EmbeddedImageData>
-
-#include <filesystem>
-
-#include "miscutils.h"
-#include "mpvitem.h"
-#include "mpvproperties.h"
-#include "playlistsettings.h"
-#include "videosettings.h"
-
-using namespace Qt::StringLiterals;
-
-MediaPlayer2Player::MediaPlayer2Player(MpvItem *parent)
- : QDBusAbstractAdaptor(parent)
- , m_mpv{parent}
-{
- connect(this, &MediaPlayer2Player::findAudioCoverFinished, this, &MediaPlayer2Player::onFindAudioCoverFinished, Qt::QueuedConnection);
-
- connect(m_mpv, &MpvItem::mediaTitleChanged, this, [this]() {
- auto title = m_mpv->mediaTitle();
- if (title.isEmpty() || title.startsWith(u"watch"_s)) {
- return;
- }
- propertiesChanged(u"Metadata"_s, Metadata());
- Q_EMIT metadataChanged();
- });
-
- connect(m_mpv, &MpvItem::durationChanged, this, [this]() {
- m_metadata.insert(u"mpris:length"_s, static_cast<qint64>(m_mpv->duration() * 1000 * 1000));
- propertiesChanged(u"Metadata"_s, m_metadata);
- });
-
- connect(m_mpv, &MpvItem::pauseChanged, this, [this]() {
- propertiesChanged(u"PlaybackStatus"_s, PlaybackStatus());
- Q_EMIT playbackStatusChanged();
- });
-
- connect(m_mpv, &MpvItem::volumeChanged, this, [this]() {
- propertiesChanged(u"Volume"_s, Volume());
- Q_EMIT volumeChanged();
- });
-
- connect(this, &MediaPlayer2Player::playpause, this, [this]() {
- m_mpv->setPause(!m_mpv->pause());
- });
- connect(this, &MediaPlayer2Player::play, this, [this]() {
- m_mpv->setPause(false);
- });
- connect(this, &MediaPlayer2Player::pause, this, [this]() {
- m_mpv->setPause(true);
- });
- connect(this, &MediaPlayer2Player::stop, this, [this]() {
- m_mpv->setPosition(0);
- m_mpv->setPause(true);
- m_mpv->stop();
- });
- connect(this, &MediaPlayer2Player::next, m_mpv, &MpvItem::playNext);
- connect(this, &MediaPlayer2Player::previous, m_mpv, &MpvItem::playPrevious);
- connect(this, &MediaPlayer2Player::seek, this, [this](int offset) {
- m_mpv->command(QStringList() << u"add"_s << u"time-pos"_s << QString::number(offset));
- });
- connect(this, &MediaPlayer2Player::openUri, m_mpv, &MpvItem::openUri);
-
- connect(PlaylistSettings::self(), &PlaylistSettings::PlaybackBehaviorChanged, this, [this]() {
- propertiesChanged(u"LoopStatus"_s, LoopStatus());
- });
- connect(PlaylistSettings::self(), &PlaylistSettings::RandomPlaybackChanged, this, [this]() {
- propertiesChanged(u"Shuffle"_s, Shuffle());
- });
-}
-
-void MediaPlayer2Player::propertiesChanged(const QString &property, const QVariant &value)
-{
- QVariantMap properties;
- properties[property] = value;
-
- QDBusMessage msg = QDBusMessage::createSignal(u"/org/mpris/MediaPlayer2"_s, u"org.freedesktop.DBus.Properties"_s, u"PropertiesChanged"_s);
- msg << u"org.mpris.MediaPlayer2.Player"_s << properties << QStringList();
-
- QDBusConnection::sessionBus().send(msg);
-}
-
-QVariantMap MediaPlayer2Player::Metadata()
-{
- auto url = m_mpv->currentUrl();
- QString mimeType = MiscUtils::mimeType(url);
-
- m_metadata.insert(u"xesam:title"_s, m_mpv->mediaTitle());
- m_metadata.insert(u"mpris:trackid"_s, QVariant::fromValue<QDBusObjectPath>(QDBusObjectPath(u"/org/kde/haruna"_s)));
- if (mimeType.startsWith(u"audio"_s)) {
- m_metadata.insert(u"mpris:artUrl"_s, getThumbnail(url.toLocalFile()));
- } else {
- m_metadata.insert(u"mpris:artUrl"_s, QString{});
- }
- m_metadata.insert(u"xesam:url"_s, url.toString());
-
- return m_metadata;
-}
-
-QString MediaPlayer2Player::getThumbnail(const QString &path)
-{
- auto url = QUrl::fromUserInput(path);
- if (!url.isLocalFile()) {
- return {};
- }
-
- const auto metadata = MiscUtils::metadata(url);
- if (metadata.has_value()) {
- QString base64 = u"data:image/png;base64,"_s;
- auto imageData = metadata.value().imageData;
- QByteArray data;
- if (!imageData.isEmpty()) {
- // look for image inside the file
- using namespace KFileMetaData;
- if (imageData.contains(EmbeddedImageData::MovieScreenCapture)) {
- data = imageData.value(EmbeddedImageData::MovieScreenCapture);
- } else if (imageData.contains(EmbeddedImageData::FrontCover)) {
- data = imageData.value(EmbeddedImageData::FrontCover);
- } else if (imageData.contains(EmbeddedImageData::BackCover)) {
- data = imageData.value(EmbeddedImageData::BackCover);
- } else if (imageData.contains(EmbeddedImageData::Other)) {
- data = imageData.value(EmbeddedImageData::Other);
- } else {
- data = imageData.value(EmbeddedImageData::Unknown);
- }
- return base64.append(QString::fromUtf8(data.toBase64()));
- }
-
- QPointer self(this);
- QThreadPool::globalInstance()->start([self, path]() {
- QFileInfo fileInfo(path);
- if (fileInfo.isDir()) {
- return;
- }
-
- QString image = self->findAudioCover(fileInfo.absolutePath());
- if (!self) {
- return;
- }
- QMetaObject::invokeMethod(self, &MediaPlayer2Player::findAudioCoverFinished, image);
- });
- }
- return {};
-}
-
-QString MediaPlayer2Player::findAudioCover(const QString &filePath)
-{
- static const QStringList coverNames = {u"cover"_s, u"front"_s, u"folder"_s};
- static const QStringList extensions = {u"png"_s, u"jpeg"_s, u"jpg"_s, u"webp"_s};
-
- for (const auto &name : coverNames) {
- for (const auto &ext : extensions) {
- const auto path = std::filesystem::path(filePath.toStdString());
- const auto filePath = path / QString(name % u"."_s % ext).toStdString();
-
- if (QFileInfo::exists(QString::fromStdString(filePath))) {
- return QString::fromStdString(filePath);
- }
- }
- }
-
- return {};
-}
-
-void MediaPlayer2Player::onFindAudioCoverFinished(const QString &image)
-{
- QImage img(image);
- if (img.isNull()) {
- m_metadata.insert(u"mpris:artUrl"_s, VideoSettings::defaultCover());
- propertiesChanged(u"Metadata"_s, m_metadata);
- return;
- }
-
- m_metadata.insert(u"mpris:artUrl"_s, {u"file://"_s % image});
- propertiesChanged(u"Metadata"_s, m_metadata);
-}
-
-void MediaPlayer2Player::Next()
-{
- Q_EMIT next();
-}
-
-void MediaPlayer2Player::Previous()
-{
- Q_EMIT previous();
-}
-
-void MediaPlayer2Player::Pause()
-{
- Q_EMIT pause();
-}
-
-void MediaPlayer2Player::PlayPause()
-{
- Q_EMIT playpause();
-}
-
-void MediaPlayer2Player::Stop()
-{
- Q_EMIT stop();
-}
-
-void MediaPlayer2Player::Play()
-{
- Q_EMIT play();
-}
-
-void MediaPlayer2Player::Seek(qlonglong offset)
-{
- Q_EMIT seek(offset / 1000 / 1000);
-}
-
-void MediaPlayer2Player::SetPosition(const QDBusObjectPath &trackId, qlonglong pos)
-{
- Q_UNUSED(trackId)
- Q_EMIT m_mpv->setProperty(MpvProperties::self()->Position, pos / 1000 / 1000);
-}
-
-void MediaPlayer2Player::OpenUri(const QString &uri)
-{
- Q_EMIT openUri(uri);
-}
-
-QString MediaPlayer2Player::PlaybackStatus()
-{
- bool isPaused = m_mpv->pause();
- double position = m_mpv->position();
-
- return isPaused && position == 0 ? u"Stopped"_s : (isPaused ? u"Paused"_s : u"Playing"_s);
-}
-
-double MediaPlayer2Player::Volume()
-{
- return m_mpv->getProperty(MpvProperties::self()->Volume).toDouble() / 100;
-}
-
-void MediaPlayer2Player::setVolume(double vol)
-{
- Q_EMIT m_mpv->setProperty(MpvProperties::self()->Volume, vol * 100);
-}
-
-void MediaPlayer2Player::setShuffle(bool shuffle)
-{
- PlaylistSettings::setRandomPlayback(shuffle);
- PlaylistSettings::self()->save();
- Q_EMIT PlaylistSettings::self()->RandomPlaybackChanged();
-}
-
-void MediaPlayer2Player::setLoopStatus(QString loop)
-{
- if (loop == u"None"_s) {
- loop = u"StopAfterLast"_s;
- } else if (loop == u"Track"_s) {
- loop = u"RepeatItem"_s;
- } else if (loop == u"Playlist"_s) {
- loop = u"RepeatPlaylist"_s;
- }
-
- PlaylistSettings::setPlaybackBehavior(loop);
- PlaylistSettings::self()->save();
- Q_EMIT PlaylistSettings::self()->PlaybackBehaviorChanged();
-}
-
-qlonglong MediaPlayer2Player::Position()
-{
- return m_mpv->position() * 1000 * 1000;
-}
-
-void MediaPlayer2Player::setPosition(int pos)
-{
- Q_EMIT m_mpv->setProperty(MpvProperties::self()->Position, pos);
-}
-
-bool MediaPlayer2Player::CanGoNext()
-{
- return true;
-}
-
-bool MediaPlayer2Player::CanGoPrevious()
-{
- return true;
-}
-
-bool MediaPlayer2Player::CanPlay()
-{
- return true;
-}
-
-bool MediaPlayer2Player::CanPause()
-{
- return true;
-}
-
-bool MediaPlayer2Player::CanSeek()
-{
- return true;
-}
-
-bool MediaPlayer2Player::CanControl()
-{
- return true;
-}
-
-bool MediaPlayer2Player::Shuffle()
-{
- return PlaylistSettings::randomPlayback();
-}
-
-QString MediaPlayer2Player::LoopStatus()
-{
- if (PlaylistSettings::playbackBehavior() == u"RepeatItem"_s) {
- return u"Track"_s;
- }
- if (PlaylistSettings::playbackBehavior() == u"RepeatPlaylist"_s) {
- return u"Playlist"_s;
- }
- // StopAfterLast or StopAfterItem
- return u"None"_s;
-}
-
-#include "moc_mediaplayer2player.cpp"
diff --git a/src/mpris2/mediaplayer2player.h b/src/mpris2/mediaplayer2player.h
deleted file mode 100644
index 379554d1..00000000
--- a/src/mpris2/mediaplayer2player.h
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 George Florea Bănuș <[email protected]>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#ifndef MEDIAPLAYER2PLAYER_H
-#define MEDIAPLAYER2PLAYER_H
-
-#include <QDBusAbstractAdaptor>
-
-class QDBusObjectPath;
-class QImage;
-class MpvItem;
-
-class MediaPlayer2Player : public QDBusAbstractAdaptor
-{
- Q_OBJECT
- Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player")
-
-public:
- explicit MediaPlayer2Player(MpvItem *parent = nullptr);
- ~MediaPlayer2Player() = default;
-
- Q_PROPERTY(QString PlaybackStatus READ PlaybackStatus NOTIFY playbackStatusChanged)
- Q_PROPERTY(QVariantMap Metadata READ Metadata NOTIFY metadataChanged)
- Q_PROPERTY(double Volume READ Volume WRITE setVolume NOTIFY volumeChanged)
- Q_PROPERTY(qlonglong Position READ Position WRITE setPosition NOTIFY playbackStatusChanged)
- Q_PROPERTY(bool CanGoNext READ CanGoNext CONSTANT)
- Q_PROPERTY(bool CanGoPrevious READ CanGoPrevious CONSTANT)
- Q_PROPERTY(bool CanPlay READ CanPlay CONSTANT)
- Q_PROPERTY(bool CanPause READ CanPause CONSTANT)
- Q_PROPERTY(bool CanSeek READ CanSeek CONSTANT)
- Q_PROPERTY(bool CanControl READ CanControl CONSTANT)
- Q_PROPERTY(bool Shuffle READ Shuffle WRITE setShuffle NOTIFY shuffleChanged)
- Q_PROPERTY(QString LoopStatus READ LoopStatus WRITE setLoopStatus NOTIFY loopStatusChanged)
-
- void propertiesChanged(const QString &property, const QVariant &value);
-
-public Q_SLOTS:
- void Next();
- void Previous();
- void Pause();
- void PlayPause();
- void Stop();
- void Play();
- void Seek(qlonglong offset);
- void SetPosition(const QDBusObjectPath &trackId, qlonglong pos);
- void OpenUri(const QString &uri);
- QString PlaybackStatus();
- QVariantMap Metadata();
- double Volume();
- qlonglong Position();
- bool CanGoNext();
- bool CanGoPrevious();
- bool CanPlay();
- bool CanPause();
- bool CanSeek();
- bool CanControl();
- bool Shuffle();
- QString LoopStatus();
- void setPosition(int pos);
- void setVolume(double vol);
- void setShuffle(bool shuffle);
- void setLoopStatus(QString loop);
-
-Q_SIGNALS:
- void next();
- void previous();
- void pause();
- void playpause();
- void stop();
- void play();
- void seek(int offset);
- void openUri(QString uri);
- void playbackStatusChanged();
- void metadataChanged();
- void volumeChanged();
- // void requestMprisThumbnail(const QString &path, int width);
- void shuffleChanged(bool shuffle);
- void loopStatusChanged(QString loop);
- void findAudioCoverFinished(const QString &image);
-
-private:
- QString getThumbnail(const QString &path);
- QString findAudioCover(const QString &filePath);
- void onFindAudioCoverFinished(const QString &image);
- MpvItem *m_mpv;
- QVariantMap m_metadata;
-};
-
-#endif // MEDIAPLAYER2PLAYER_H
diff --git a/src/mprisitem.cpp b/src/mprisitem.cpp
new file mode 100644
index 00000000..5d22a9a1
--- /dev/null
+++ b/src/mprisitem.cpp
@@ -0,0 +1,417 @@
+/*
+ * SPDX-FileCopyrightText: 2026 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "mprisitem.h"
+
+#include <QApplication>
+#include <QBuffer>
+#include <QFileInfo>
+#include <QImageReader>
+#include <QPointer>
+#include <QThreadPool>
+#include <QWindow>
+
+#include "miscutils.h"
+#include "mpriscontroller.h"
+#include "mpvitem.h"
+#include "mpvproperties.h"
+#include "playlistfilterproxymodel.h"
+#include "playlistsettings.h"
+#include "videosettings.h"
+
+using namespace Qt::StringLiterals;
+using EmbeddedImageDataMap = QMap<KFileMetaData::EmbeddedImageData::ImageType, QByteArray>;
+
+class MprisItemPrivate
+{
+ Q_DISABLE_COPY(MprisItemPrivate);
+
+public:
+ MprisItemPrivate(MprisItem *q)
+ : q_ptr(q)
+ , mprisController(q)
+ {
+ }
+ MprisItem *const q_ptr;
+ MpvItem *mpv = nullptr;
+ PlaylistFilterProxyModel *activePlaylist = nullptr;
+ QWindow *window = nullptr;
+ MprisController mprisController;
+
+ QMetaObject::Connection playlistNextConnection;
+ QMetaObject::Connection playlistPreviousConnection;
+ QMetaObject::Connection playlistItemCountConnection;
+
+ QByteArray metadataImage(EmbeddedImageDataMap imageData);
+};
+
+QByteArray MprisItemPrivate::metadataImage(EmbeddedImageDataMap imageData)
+{
+ using namespace KFileMetaData;
+
+ QByteArray data;
+ if (imageData.contains(EmbeddedImageData::MovieScreenCapture)) {
+ data = imageData.value(EmbeddedImageData::MovieScreenCapture);
+
+ } else if (imageData.contains(EmbeddedImageData::FrontCover)) {
+ data = imageData.value(EmbeddedImageData::FrontCover);
+
+ } else if (imageData.contains(EmbeddedImageData::BackCover)) {
+ data = imageData.value(EmbeddedImageData::BackCover);
+
+ } else if (imageData.contains(EmbeddedImageData::Other)) {
+ data = imageData.value(EmbeddedImageData::Other);
+
+ } else if (imageData.contains(EmbeddedImageData::Unknown)) {
+ data = imageData.value(EmbeddedImageData::Unknown);
+
+ } else {
+ data = {};
+ }
+
+ return data;
+}
+
+MprisItem::MprisItem(QObject *parent)
+ : QObject(parent)
+ , d_ptr{std::make_unique<MprisItemPrivate>(this)}
+{
+ d_ptr->mprisController.setCanQuit(true);
+ d_ptr->mprisController.setCanRaise(true);
+ d_ptr->mprisController.setHasTrackList(false);
+ d_ptr->mprisController.setIdentity(QGuiApplication::applicationDisplayName());
+ d_ptr->mprisController.setDesktopEntry(QGuiApplication::desktopFileName());
+ d_ptr->mprisController.setSupportedUriSchemes({u"file"_s, u"http"_s, u"https"_s});
+ d_ptr->mprisController.setSupportedMimeTypes({u"video/*"_s, u"audio/*"_s});
+
+ d_ptr->mprisController.setShuffle(PlaylistSettings::randomPlayback());
+ d_ptr->mprisController.setMinimumRate(0.0);
+ d_ptr->mprisController.setMinimumRate(10.0);
+ d_ptr->mprisController.setCanControl(true);
+
+ auto setLoopStatus = [this]() {
+ QString loopStatus = u"None"_s;
+ if (PlaylistSettings::playbackBehavior() == u"RepeatItem"_s) {
+ loopStatus = u"Track"_s;
+ }
+ if (PlaylistSettings::playbackBehavior() == u"RepeatPlaylist"_s) {
+ loopStatus = u"Playlist"_s;
+ }
+ d_ptr->mprisController.setLoopStatus(loopStatus);
+ };
+
+ setLoopStatus();
+
+ connect(&d_ptr->mprisController, &MprisController::raiseRequested, this, &MprisItem::raise);
+ connect(&d_ptr->mprisController, &MprisController::openUriRequested, this, &MprisItem::openUri);
+ connect(PlaylistSettings::self(), &PlaylistSettings::PlaybackBehaviorChanged, this, setLoopStatus);
+ connect(PlaylistSettings::self(), &PlaylistSettings::RandomPlaybackChanged, this, [this]() {
+ d_ptr->mprisController.setShuffle(PlaylistSettings::randomPlayback());
+ });
+ connect(&d_ptr->mprisController, &MprisController::shuffleChangeRequested, this, [](bool shuffle) {
+ PlaylistSettings::setRandomPlayback(shuffle);
+ PlaylistSettings::self()->save();
+ });
+
+ connect(&d_ptr->mprisController, &MprisController::loopStatusChangeRequested, this, [](const QString &loopStatus) {
+ if (loopStatus == u"None"_s) {
+ PlaylistSettings::setPlaybackBehavior(u"StopAfterLast"_s);
+ } else if (loopStatus == u"Track"_s) {
+ PlaylistSettings::setPlaybackBehavior(u"RepeatItem"_s);
+ } else if (loopStatus == u"Playlist"_s) {
+ PlaylistSettings::setPlaybackBehavior(u"RepeatPlaylist"_s);
+ }
+
+ PlaylistSettings::self()->save();
+ });
+
+ connect(&d_ptr->mprisController, &MprisController::quitRequested, QApplication::instance(), &QApplication::quit);
+}
+
+MprisItem::~MprisItem() = default;
+
+MpvItem *MprisItem::mpv() const
+{
+ return d_ptr->mpv;
+}
+
+void MprisItem::setMpv(MpvItem *newMpv)
+{
+ if (d_ptr->mpv != nullptr) {
+ return;
+ }
+ d_ptr->mpv = newMpv;
+ Q_EMIT mpvChanged();
+
+ d_ptr->mprisController.setVolume(d_ptr->mpv->volume());
+
+ setupMpvConnections();
+}
+
+PlaylistFilterProxyModel *MprisItem::activePlaylist() const
+{
+ return d_ptr->activePlaylist;
+}
+
+void MprisItem::setActivePlaylist(PlaylistFilterProxyModel *newActivePlaylist)
+{
+ if (d_ptr->activePlaylist == newActivePlaylist) {
+ return;
+ }
+ d_ptr->activePlaylist = newActivePlaylist;
+ Q_EMIT activePlaylistChanged();
+
+ const auto playlist = d_ptr->activePlaylist;
+ d_ptr->mprisController.setCanGoNext(playlist->rowCount() > 1);
+ d_ptr->mprisController.setCanGoPrevious(playlist->rowCount() > 1);
+
+ disconnect(d_ptr->playlistNextConnection);
+ disconnect(d_ptr->playlistPreviousConnection);
+ disconnect(d_ptr->playlistItemCountConnection);
+
+ d_ptr->playlistNextConnection = connect(&d_ptr->mprisController, &MprisController::nextRequested, playlist, &PlaylistFilterProxyModel::playNext);
+
+ d_ptr->playlistPreviousConnection =
+ connect(&d_ptr->mprisController, &MprisController::previousRequested, playlist, &PlaylistFilterProxyModel::playPrevious);
+
+ d_ptr->playlistItemCountConnection = connect(playlist, &PlaylistFilterProxyModel::itemCountChanged, this, [this]() {
+ const auto playlist = d_ptr->activePlaylist;
+ d_ptr->mprisController.setCanGoNext(playlist->rowCount() > 1);
+ d_ptr->mprisController.setCanGoPrevious(playlist->rowCount() > 1);
+ });
+}
+
+QWindow *MprisItem::window() const
+{
+ return d_ptr->window;
+}
+
+void MprisItem::setWindow(QWindow *newWindow)
+{
+ if (d_ptr->window != nullptr) {
+ return;
+ }
+ d_ptr->window = newWindow;
+ Q_EMIT windowChanged();
+
+ const auto isFullScreen = d_ptr->window->windowStates() & Qt::WindowFullScreen;
+ d_ptr->mprisController.setCanSetFullscreen(!isFullScreen);
+
+ connect(d_ptr->window, &QWindow::windowStateChanged, this, [this]() {
+ const auto isFullScreen = d_ptr->window->windowStates() & Qt::WindowFullScreen;
+ d_ptr->mprisController.setCanSetFullscreen(!isFullScreen);
+ });
+ connect(&d_ptr->mprisController, &MprisController::fullscreenChangeRequested, this, [this](bool fullscreen) {
+ if (fullscreen) {
+ d_ptr->window->showFullScreen();
+ } else {
+ d_ptr->window->showNormal();
+ }
+ const auto isFullScreen = d_ptr->window->windowStates().testFlag(Qt::WindowFullScreen);
+ d_ptr->mprisController.setFullscreen(isFullScreen);
+ });
+}
+
+void MprisItem::setupMpvConnections()
+{
+ // TODO: handle mpv seek signal to mpris Seeked signal once a new MpvQt version is out
+ connect(&d_ptr->mprisController, &MprisController::volumeChangeRequested, d_ptr->mpv, [this](double volume) {
+ const auto maxVolume = d_ptr->mpv->getProperty(MpvProperties::self()->VolumeMax).toUInt();
+ d_ptr->mpv->setVolume(volume * maxVolume);
+ });
+ connect(&d_ptr->mprisController, &MprisController::seekToRequested, this, [this](qlonglong offset) {
+ d_ptr->mpv->setPosition(offset / 1'000'000.0);
+ });
+ connect(&d_ptr->mprisController, &MprisController::seekRequested, this, [this](qlonglong position) {
+ d_ptr->mpv->command(QStringList() << u"add"_s << u"time-pos"_s << QString::number(position / 1'000'000.0));
+ });
+ connect(&d_ptr->mprisController, &MprisController::stopRequested, d_ptr->mpv, &MpvItem::stop);
+ connect(&d_ptr->mprisController, &MprisController::playpauseRequested, this, [this]() {
+ d_ptr->mpv->setPause(!d_ptr->mpv->pause());
+ });
+ connect(&d_ptr->mprisController, &MprisController::playRequested, this, [this]() {
+ d_ptr->mpv->setPause(false);
+ });
+ connect(&d_ptr->mprisController, &MprisController::pauseRequested, this, [this]() {
+ d_ptr->mpv->setPause(true);
+ });
+ connect(&d_ptr->mprisController, &MprisController::rateChangeRequested, d_ptr->mpv, &MpvItem::setPlaybackSpeed);
+
+ connect(d_ptr->mpv, &MpvItem::volumeChanged, &d_ptr->mprisController, [this]() {
+ d_ptr->mprisController.setVolume(d_ptr->mpv->volume());
+ });
+
+ connect(d_ptr->mpv, &MpvItem::positionChanged, &d_ptr->mprisController, [this]() {
+ d_ptr->mprisController.setPosition(d_ptr->mpv->position() * 1'000'000.0);
+ });
+ connect(d_ptr->mpv, &MpvItem::playbackStateChanged, &d_ptr->mprisController, [this]() {
+ QString status = u"Stopped"_s;
+ switch (d_ptr->mpv->playbackState()) {
+ case MpvItem::PlaybackState::Stopped:
+ case MpvItem::PlaybackState::Loading:
+ status = u"Stopped"_s;
+ break;
+ case MpvItem::PlaybackState::Paused:
+ status = u"Paused"_s;
+ break;
+ case MpvItem::PlaybackState::Playing:
+ status = u"Playing"_s;
+ break;
+ default:
+ status = u"Stopped"_s;
+ break;
+ }
+ d_ptr->mprisController.setPlaybackStatus(status);
+ });
+
+ connect(d_ptr->mpv, &MpvItem::mediaTitleChanged, &d_ptr->mprisController, [this]() {
+ if (!d_ptr->mpv->mediaTitle().isEmpty()) {
+ d_ptr->mprisController.setMetadata(u"xesam:title"_s, d_ptr->mpv->mediaTitle());
+ }
+ });
+
+ connect(d_ptr->mpv, &MpvItem::durationChanged, &d_ptr->mprisController, [this]() {
+ const auto duration = d_ptr->mpv->duration() * 1'000'000.0;
+ if (duration > 0) {
+ d_ptr->mprisController.setMetadata(u"mpris:length"_s, duration);
+ }
+ });
+
+ connect(d_ptr->mpv, &MpvItem::playbackSpeedChanged, &d_ptr->mprisController, [this]() {
+ const auto speed = d_ptr->mpv->playbackSpeed();
+ if (speed > 0) {
+ d_ptr->mprisController.setRate(speed);
+ }
+ });
+
+ connect(d_ptr->mpv, &MpvItem::currentUrlChanged, &d_ptr->mprisController, [this]() {
+ const auto playlist = d_ptr->activePlaylist;
+ const auto playlistItemIndex = playlist->playlistModel()->playingItem();
+ const auto url = d_ptr->mpv->currentUrl();
+
+ d_ptr->mprisController.setCanPlay(true);
+ d_ptr->mprisController.setCanPause(true);
+ d_ptr->mprisController.setCanSeek(true);
+
+ const auto trackId = u"/org/kde/haruna/track/%1"_s.arg(playlistItemIndex);
+ d_ptr->mprisController.setMetadata(u"mpris:trackid"_s, trackId);
+ d_ptr->mprisController.setMetadata(u"xesam:url"_s, url.toString());
+
+ const auto mimeType = MiscUtils::mimeType(url);
+ if (mimeType.startsWith(u"audio"_s)) {
+ getAudioThumbnail(url);
+ } else {
+ // videos don't get a thumbnail so that the window preview
+ // in the task manager displays the live window preview rather than a static thumbnail
+ d_ptr->mprisController.setMetadata(u"mpris:artUrl"_s, QString{});
+ }
+ });
+}
+
+void MprisItem::getAudioThumbnail(const QUrl &url)
+{
+ if (!url.isLocalFile()) {
+ return;
+ }
+
+ QPointer self(this);
+ QThreadPool::globalInstance()->start([this, self, url]() {
+ if (!self) {
+ return;
+ }
+
+ QByteArray imageData = embeddedArtwork(url);
+ if (!imageData.isEmpty()) {
+ QBuffer buffer(&imageData);
+ if (!buffer.open(QBuffer::ReadOnly)) {
+ qDebug() << "MprisItem::getThumbnail : Failed to open buffer:" << buffer.errorString();
+ }
+
+ const auto format = QImageReader::imageFormat(&buffer);
+ auto base64 = u"data:image/%1;base64,%2"_s.arg(format).arg(QString::fromLatin1(imageData.toBase64()));
+
+ if (!self) {
+ return;
+ }
+ QMetaObject::invokeMethod(self, &MprisItem::setAudioThumbnail, base64);
+ return;
+ }
+
+ QFileInfo fileInfo(url.toLocalFile());
+ if (fileInfo.isDir()) {
+ return;
+ }
+
+ QString image = findAudioThumbnail(fileInfo.absolutePath());
+ if (!self) {
+ return;
+ }
+ QMetaObject::invokeMethod(self, &MprisItem::setAudioThumbnail, image);
+ });
+}
+
+QString MprisItem::findAudioThumbnail(const QString &filePath)
+{
+ static const QStringList coverNames = {u"cover"_s, u"front"_s, u"folder"_s};
+ static const QStringList extensions = {u"png"_s, u"jpeg"_s, u"jpg"_s, u"webp"_s};
+
+ for (const auto &name : coverNames) {
+ for (const auto &ext : extensions) {
+ const auto path = std::filesystem::path(filePath.toStdString());
+ const auto filePath = path / QString(name % u"."_s % ext).toStdString();
+
+ if (QFileInfo::exists(QString::fromStdString(filePath))) {
+ return QString::fromStdString(filePath);
+ }
+ }
+ }
+
+ return {};
+}
+
+void MprisItem::setAudioThumbnail(const QString &image)
+{
+ // embedded image encoded as base64
+ if (image.startsWith(u"data:image/"_s)) {
+ d_ptr->mprisController.setMetadata(u"mpris:artUrl"_s, image);
+ return;
+ }
+
+ // file path
+ auto mimeType = MiscUtils::mimeType(QUrl(image));
+ if (QFile::exists(image) && mimeType.startsWith(u"image"_s)) {
+ d_ptr->mprisController.setMetadata(u"mpris:artUrl"_s, {u"file://"_s % image});
+ return;
+ }
+
+ if (!VideoSettings::defaultCover().isEmpty()) {
+ d_ptr->mprisController.setMetadata(u"mpris:artUrl"_s, VideoSettings::defaultCover());
+ return;
+ }
+ d_ptr->mprisController.setMetadata(u"mpris:artUrl"_s, QString{});
+}
+
+QByteArray MprisItem::embeddedArtwork(const QUrl &url)
+{
+ const auto metadata = MiscUtils::metadata(url, KFileMetaData::ExtractionResult::ExtractImageData);
+ if (!metadata.has_value()) {
+ return {};
+ }
+
+ const auto imageData = metadata.value().imageData;
+ if (imageData.isEmpty()) {
+ return {};
+ }
+
+ const auto data = d_ptr->metadataImage(imageData);
+ if (data.isEmpty()) {
+ return {};
+ }
+
+ return data;
+}
+
+#include "moc_mprisitem.cpp"
\ No newline at end of file
diff --git a/src/mprisitem.h b/src/mprisitem.h
new file mode 100644
index 00000000..38773083
--- /dev/null
+++ b/src/mprisitem.h
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: 2026 George Florea Bănuș <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef MPRISITEM_H
+#define MPRISITEM_H
+
+#include <QObject>
+#include <qqmlintegration.h>
+
+class MprisItemPrivate;
+class MpvItem;
+class PlaylistFilterProxyModel;
+class QWindow;
+
+class MprisItem : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ explicit MprisItem(QObject *parent = nullptr);
+ ~MprisItem();
+
+ Q_PROPERTY(MpvItem *mpv READ mpv WRITE setMpv NOTIFY mpvChanged FINAL REQUIRED)
+ Q_SIGNAL void mpvChanged();
+ MpvItem *mpv() const;
+ void setMpv(MpvItem *newMpv);
+
+ Q_PROPERTY(PlaylistFilterProxyModel *activePlaylist READ activePlaylist WRITE setActivePlaylist NOTIFY activePlaylistChanged FINAL)
+ Q_SIGNAL void activePlaylistChanged();
+ PlaylistFilterProxyModel *activePlaylist() const;
+ void setActivePlaylist(PlaylistFilterProxyModel *newActivePlaylist);
+
+ Q_PROPERTY(QWindow *window READ window WRITE setWindow NOTIFY windowChanged FINAL REQUIRED)
+ Q_SIGNAL void windowChanged();
+ QWindow *window() const;
+ void setWindow(QWindow *newWindow);
+
+ void setupMpvConnections();
+
+ Q_SIGNAL void raise();
+ Q_SIGNAL void openUri();
+
+private:
+ void getAudioThumbnail(const QUrl &url);
+ QString findAudioThumbnail(const QString &filePath);
+ void setAudioThumbnail(const QString &image);
+ QByteArray embeddedArtwork(const QUrl &url);
+ std::unique_ptr<MprisItemPrivate> d_ptr;
+};
+
+#endif // MPRISITEM_H
diff --git a/src/mpv/mpvitem.cpp b/src/mpv/mpvitem.cpp
index 9815aaa8..c562bd25 100644
--- a/src/mpv/mpvitem.cpp
+++ b/src/mpv/mpvitem.cpp
@@ -40,9 +40,6 @@
#if HAVE_DBUS
#include <QDBusConnection>
#include <QDBusMessage>
-
-#include "mediaplayer2.h"
-#include "mediaplayer2player.h"
#endif
using namespace Qt::StringLiterals;
@@ -74,6 +71,7 @@ MpvItem::MpvItem(QQuickItem *parent)
observeProperty(MpvProperties::self()->SubtitleDelay, MPV_FORMAT_DOUBLE);
observeProperty(MpvProperties::self()->EofReached, MPV_FORMAT_FLAG);
observeProperty(MpvProperties::self()->VoConfigured, MPV_FORMAT_FLAG);
+ observeProperty(MpvProperties::self()->Speed, MPV_FORMAT_DOUBLE);
observeProperty(u"demuxer-cache-state"_s, MPV_FORMAT_NODE);
setupConnections();
@@ -261,18 +259,6 @@ void MpvItem::setupConnections()
connect(this, &MpvItem::chapterChanged,
this, &MpvItem::onChapterChanged);
-#if HAVE_DBUS
- // register mpris dbus service
- QString mspris2Name(u"org.mpris.MediaPlayer2.haruna"_s);
- QDBusConnection::sessionBus().registerService(mspris2Name);
- QDBusConnection::sessionBus().registerObject(u"/org/mpris/MediaPlayer2"_s, this, QDBusConnection::ExportAdaptors);
-
- // org.mpris.MediaPlayer2 mpris2 interface
- auto *mp2 = new MediaPlayer2(this);
- connect(mp2, &MediaPlayer2::raise, this, &MpvItem::raise);
- new MediaPlayer2Player(this);
-#endif
-
connect(this, &MpvItem::pauseChanged, this, [this]() {
static LockManager lockManager;
if (pause()) {
@@ -493,6 +479,11 @@ void MpvItem::onPropertyChanged(const QString &property, const QVariant &value)
if (!voConfigured) {
update();
}
+
+ } else if (property == MpvProperties::self()->Speed) {
+ m_playbackSpeed = value.toDouble();
+ Q_EMIT playbackSpeedChanged();
+
} else if (property == u"demuxer-cache-state"_s) {
const auto data = value.toMap().value(u"seekable-ranges"_s).toList();
m_seekableRangesModel->populate(data);
@@ -1170,6 +1161,19 @@ int MpvItem::videoHeight()
return m_videoHeight;
}
+double MpvItem::playbackSpeed() const
+{
+ return m_playbackSpeed;
+}
+
+void MpvItem::setPlaybackSpeed(double newSpeed)
+{
+ if (newSpeed == playbackSpeed()) {
+ return;
+ }
+ setProperty(MpvProperties::self()->Speed, newSpeed);
+}
+
bool MpvItem::isLocalFile() const
{
return m_isLocalFile;
diff --git a/src/mpv/mpvitem.h b/src/mpv/mpvitem.h
index 24cd70f9..3e4652d0 100644
--- a/src/mpv/mpvitem.h
+++ b/src/mpv/mpvitem.h
@@ -147,6 +147,10 @@ public:
Q_PROPERTY(int videoHeight READ videoHeight NOTIFY videoHeightChanged)
int videoHeight();
+ Q_PROPERTY(double playbackSpeed READ playbackSpeed WRITE setPlaybackSpeed NOTIFY playbackSpeedChanged FINAL)
+ double playbackSpeed() const;
+ Q_INVOKABLE void setPlaybackSpeed(double newSpeed);
+
/**
* Whether the file finished loading and it's safe to start certain events,
* like auto skipping chapters, which otherwise skips wrong chapter due to the multithreaded code
@@ -214,6 +218,7 @@ Q_SIGNALS:
void videoWidthChanged();
void videoHeightChanged();
void eofReached();
+ void playbackSpeedChanged();
void savePositionToDB(const QString &md5Hash, const QString &path, double position);
void eofPlayNext();
void osdMessage(const QString &text);
@@ -221,7 +226,6 @@ Q_SIGNALS:
void isVideoChanged();
// signals used for mpris
- void raise();
void playNext();
void playPrevious();
void openUri(const QString &uri);
@@ -265,6 +269,7 @@ private:
int m_secondarySubtitleId{0};
int m_videoWidth{0};
int m_videoHeight{0};
+ double m_playbackSpeed{1.0};
double m_watchLaterPosition{0.0};
bool m_isReady{false};
diff --git a/src/qml/Actions.qml b/src/qml/Actions.qml
index e82feacc..fcd37126 100644
--- a/src/qml/Actions.qml
+++ b/src/qml/Actions.qml
@@ -157,14 +157,14 @@ Item {
}
function onPlaybackSpeedIncreaseAction() {
- const speed = parseFloat(root.m_mpv.getProperty(MpvProperties.Speed) + 0.1)
- root.m_mpv.setProperty(MpvProperties.Speed, speed)
+ const speed = parseFloat(root.m_mpv.playbackSpeed + 0.1)
+ root.m_mpv.setPlaybackSpeed(speed)
root.m_osd.message(KI18n.i18nc("@info:tooltip", "Playback speed: %1", speed.toFixed(2)))
}
function onPlaybackSpeedDecreaseAction() {
- const speed = parseFloat(root.m_mpv.getProperty(MpvProperties.Speed) - 0.1)
- root.m_mpv.setProperty(MpvProperties.Speed, speed)
+ const speed = parseFloat(root.m_mpv.playbackSpeed - 0.1)
+ root.m_mpv.setPlaybackSpeed(speed)
root.m_osd.message(KI18n.i18nc("@info:tooltip", "Playback speed: %1", speed.toFixed(2)))
}
diff --git a/src/qml/Main.qml b/src/qml/Main.qml
index 45fbbbb2..41675e9d 100644
--- a/src/qml/Main.qml
+++ b/src/qml/Main.qml
@@ -40,6 +40,8 @@ ApplicationWindow {
}
}
+ objectName: "HarunaMainWindow"
+
visible: true
title: mpv.mediaTitle || KI18n.i18nc("@title:window", "Haruna")
width: 1000
@@ -204,6 +206,17 @@ ApplicationWindow {
Component.onCompleted: active = GeneralSettings.rememberWindowGeometry
}
+ MprisItem {
+ mpv: mpv
+ window: window
+ activePlaylist: playlist.manager.activePlaylist
+
+ onRaise: { HarunaApp.raiseWindow() }
+ onOpenUri: function(uri) {
+ window.openFile(uri, RecentFilesModel.OpenedFrom.ExternalApp)
+ }
+ }
+
MpvVideo {
id: mpv
diff --git a/src/qml/MpvVideo.qml b/src/qml/MpvVideo.qml
index 0db45301..950b700c 100644
--- a/src/qml/MpvVideo.qml
+++ b/src/qml/MpvVideo.qml
@@ -50,12 +50,8 @@ MpvItem {
}
}
- onRaise: { HarunaApp.raiseWindow() }
onPlayNext: { HarunaApp.actions.playNextAction.trigger() }
onPlayPrevious: { HarunaApp.actions.playPreviousAction.trigger() }
- onOpenUri: function(uri) {
- root.window.openFile(uri, RecentFilesModel.OpenedFrom.ExternalApp)
- }
Timer {
id: hideCursorTimer
diff --git a/src/utilities/miscutils.cpp b/src/utilities/miscutils.cpp
index f1c820e9..eee2a8d6 100644
--- a/src/utilities/miscutils.cpp
+++ b/src/utilities/miscutils.cpp
@@ -58,7 +58,7 @@ QString MiscUtils::md5(const QString &str)
return QString::fromUtf8(md5.toHex());
}
-std::optional<Metadata> MiscUtils::metadata(const QUrl &url)
+std::optional<Metadata> MiscUtils::metadata(const QUrl &url, const ExtractionResult::Flags &flags)
{
QString mimeType = MiscUtils::mimeType(url);
@@ -68,7 +68,7 @@ std::optional<Metadata> MiscUtils::metadata(const QUrl &url)
if (extractors.isEmpty()) {
return {};
}
- SimpleExtractionResult result(url.toLocalFile(), mimeType, ExtractionResult::ExtractMetaData);
+ SimpleExtractionResult result(url.toLocalFile(), mimeType, flags);
Extractor *ex = extractors.first();
ex->extract(&result);
diff --git a/src/utilities/miscutils.h b/src/utilities/miscutils.h
index f1a42503..2f95cffc 100644
--- a/src/utilities/miscutils.h
+++ b/src/utilities/miscutils.h
@@ -12,6 +12,8 @@
#include <KFileMetaData/SimpleExtractionResult>
+using KFileMetaData::ExtractionResult;
+
struct Metadata {
qint64 metadataId = 0;
QUrl url;
@@ -32,7 +34,8 @@ public:
Q_INVOKABLE static QString formatTime(const double time);
Q_INVOKABLE static QString mimeType(const QUrl &url);
Q_INVOKABLE static QString md5(const QString &str);
- Q_INVOKABLE static std::optional<Metadata> metadata(const QUrl &url);
+ Q_INVOKABLE static std::optional<Metadata>
+ metadata(const QUrl &url, const ExtractionResult::Flags &flags = ExtractionResult::ExtractPlainText | ExtractionResult::ExtractMetaData);
Q_SIGNALS:
void error(const QString &message);