[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);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.