diff --git a/CMakeLists.txt b/CMakeLists.txt index 6cf1c5e2..ca07e8ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,10 @@ if(WIN32) set(MPV_LIBRARY_mpv ${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/win32/mpv.lib) endif() +if(UNIX AND NOT APPLE) + list(APPEND SOURCES mpris.cpp) +endif() + if(APPLE) list(APPEND SOURCES images/stremio.icns) set_source_files_properties(images/stremio.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") diff --git a/main.cpp b/main.cpp index d571c4eb..8953ba1c 100644 --- a/main.cpp +++ b/main.cpp @@ -25,6 +25,10 @@ typedef QApplication Application; #include "screensaver.h" #include "qclipboardproxy.h" +#ifdef Q_OS_LINUX +#include "mpris.h" +#endif + #else #include #endif @@ -106,6 +110,20 @@ int main(int argc, char **argv) engine->load(QUrl(QStringLiteral("qrc:/main.qml"))); +#ifdef Q_OS_LINUX + { + QObject *root = engine->rootObjects().value(0); + if (root) { + MpvObject *mpv = root->findChild(); + if (mpv) { + mprisSetup(mpv); + } else { + qWarning("MPRIS: MpvObject not found in QML object tree"); + } + } + } +#endif + #ifndef Q_OS_MACOS QObject::connect( &app, &SingleApplication::receivedMessage, &app, &MainApp::processMessage ); #endif diff --git a/mpris.cpp b/mpris.cpp new file mode 100644 index 00000000..d3a2f70b --- /dev/null +++ b/mpris.cpp @@ -0,0 +1,191 @@ +#include "mpris.h" + +#include +#include +#include + +static const char *MPRIS_OBJECT_PATH = "/org/mpris/MediaPlayer2"; +static const char *PLAYER_IFACE = "org.mpris.MediaPlayer2.Player"; +static const char *SERVICE_NAME = "org.mpris.MediaPlayer2.stremio"; + +// ─── MprisRootAdaptor ───────────────────────────────────────────────────────── + +MprisRootAdaptor::MprisRootAdaptor(QObject *parent) + : QDBusAbstractAdaptor(parent) +{} + +void MprisRootAdaptor::Raise() +{ + QQuickItem *item = qobject_cast(parent()); + if (!item) return; + QQuickWindow *win = item->window(); + if (!win) return; + win->show(); + win->raise(); + win->requestActivate(); +} + +// ─── MprisPlayerAdaptor ─────────────────────────────────────────────────────── + +MprisPlayerAdaptor::MprisPlayerAdaptor(MpvObject *player) + : QDBusAbstractAdaptor(player), m_player(player) +{ + player->observeProperty("idle-active"); + player->observeProperty("pause"); + player->observeProperty("media-title"); + player->observeProperty("duration"); + player->observeProperty("volume"); + + connect(player, &MpvObject::mpvEvent, + this, &MprisPlayerAdaptor::onMpvEvent); +} + +QString MprisPlayerAdaptor::playbackStatus() const +{ + if (m_idle) return "Stopped"; + if (m_paused) return "Paused"; + return "Playing"; +} + +QVariantMap MprisPlayerAdaptor::metadata() const +{ + QVariantMap map; + map["mpris:trackid"] = QVariant::fromValue( + QDBusObjectPath(QString("/org/stremio/track/%1").arg(m_trackSeq)) + ); + if (!m_title.isEmpty()) + map["xesam:title"] = m_title; + if (m_duration > 0.0) + map["mpris:length"] = qlonglong(m_duration * 1e6); + return map; +} + +double MprisPlayerAdaptor::volume() const +{ + return m_player->getProperty("volume").toDouble() / 100.0; +} + +void MprisPlayerAdaptor::setVolume(double v) +{ + m_player->setProperty("volume", v * 100.0); + emitPropertiesChanged({{"Volume", v}}); +} + +qlonglong MprisPlayerAdaptor::position() const +{ + return qlonglong(m_player->getProperty("time-pos").toDouble() * 1e6); +} + +void MprisPlayerAdaptor::PlayPause() +{ + m_player->command(QVariantList() << "cycle" << "pause"); +} + +void MprisPlayerAdaptor::Play() +{ + m_player->setProperty("pause", false); +} + +void MprisPlayerAdaptor::Pause() +{ + m_player->setProperty("pause", true); +} + +void MprisPlayerAdaptor::Stop() +{ + m_player->command(QVariantList() << "stop"); +} + +void MprisPlayerAdaptor::Seek(qlonglong offsetUs) +{ + m_player->command(QVariantList() << "seek" << double(offsetUs) / 1e6 << "relative"); + emit Seeked(position()); +} + +void MprisPlayerAdaptor::SetPosition(const QDBusObjectPath &trackId, qlonglong positionUs) +{ + // Ignore stale requests targeting a previous track + QString expected = QString("/org/stremio/track/%1").arg(m_trackSeq); + if (trackId.path() != expected) return; + m_player->setProperty("time-pos", double(positionUs) / 1e6); + emit Seeked(positionUs); +} + +void MprisPlayerAdaptor::onMpvEvent(const QString &event, const QVariant &value) +{ + if (event == "mpv-prop-change") { + QJsonObject obj = value.toJsonObject(); + QString name = obj["name"].toString(); + QVariant data = obj["data"].toVariant(); + + if (name == "idle-active") { + bool idle = data.toBool(); + if (idle != m_idle) { + m_idle = idle; + if (m_idle) { + m_title.clear(); + m_duration = 0.0; + emitPropertiesChanged({ + {"PlaybackStatus", playbackStatus()}, + {"Metadata", QVariant::fromValue(metadata())} + }); + } else { + emitPropertiesChanged({{"PlaybackStatus", playbackStatus()}}); + } + } + } else if (name == "pause") { + bool paused = data.toBool(); + if (paused != m_paused) { + m_paused = paused; + emitPropertiesChanged({{"PlaybackStatus", playbackStatus()}}); + } + } else if (name == "media-title") { + m_title = data.toString(); + m_trackSeq++; + emitPropertiesChanged({{"Metadata", QVariant::fromValue(metadata())}}); + } else if (name == "duration") { + m_duration = data.toDouble(); + emitPropertiesChanged({{"Metadata", QVariant::fromValue(metadata())}}); + } else if (name == "volume") { + emitPropertiesChanged({{"Volume", data.toDouble() / 100.0}}); + } + } else if (event == "mpv-event-ended") { + m_idle = true; + m_title.clear(); + m_duration = 0.0; + emitPropertiesChanged({ + {"PlaybackStatus", QString("Stopped")}, + {"Metadata", QVariant::fromValue(metadata())} + }); + } +} + +void MprisPlayerAdaptor::emitPropertiesChanged(const QVariantMap &changed) +{ + auto msg = QDBusMessage::createSignal( + MPRIS_OBJECT_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged" + ); + msg << QString(PLAYER_IFACE) << changed << QStringList(); + QDBusConnection::sessionBus().send(msg); +} + +// ─── Setup ──────────────────────────────────────────────────────────────────── + +void mprisSetup(MpvObject *player) +{ + new MprisRootAdaptor(player); + new MprisPlayerAdaptor(player); + + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.registerObject(MPRIS_OBJECT_PATH, player)) { + qWarning("MPRIS: failed to register D-Bus object at %s", MPRIS_OBJECT_PATH); + return; + } + if (!bus.registerService(SERVICE_NAME)) { + qWarning("MPRIS: failed to register service %s", SERVICE_NAME); + return; + } + qDebug("MPRIS: registered successfully — media controllers can now control Stremio"); +} diff --git a/mpris.h b/mpris.h new file mode 100644 index 00000000..4efd7224 --- /dev/null +++ b/mpris.h @@ -0,0 +1,110 @@ +#ifndef MPRIS_H +#define MPRIS_H + +#include +#include +#include +#include +#include + +#include "mpv.h" + +// org.mpris.MediaPlayer2 — interface raiz (identidade do player) +class MprisRootAdaptor : public QDBusAbstractAdaptor { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2") + Q_PROPERTY(bool CanQuit READ canQuit) + Q_PROPERTY(bool CanRaise READ canRaise) + Q_PROPERTY(bool HasTrackList READ hasTrackList) + Q_PROPERTY(QString Identity READ identity) + Q_PROPERTY(QString DesktopEntry READ desktopEntry) + Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes) + Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes) + +public: + explicit MprisRootAdaptor(QObject *parent); + + bool canQuit() const { return false; } + bool canRaise() const { return true; } + bool hasTrackList() const { return false; } + QString identity() const { return "Stremio"; } + QString desktopEntry() const { return "smartcode-stremio"; } + QStringList supportedUriSchemes() const { return {}; } + QStringList supportedMimeTypes() const { return {}; } + +public slots: + void Raise(); + void Quit() {} +}; + +// org.mpris.MediaPlayer2.Player — controle de playback +class MprisPlayerAdaptor : public QDBusAbstractAdaptor { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player") + Q_PROPERTY(QString PlaybackStatus READ playbackStatus) + Q_PROPERTY(QString LoopStatus READ loopStatus) + Q_PROPERTY(double Rate READ rate) + Q_PROPERTY(bool Shuffle READ shuffle) + Q_PROPERTY(QVariantMap Metadata READ metadata) + Q_PROPERTY(double Volume READ volume WRITE setVolume) + Q_PROPERTY(qlonglong Position READ position) + Q_PROPERTY(double MinimumRate READ minimumRate) + Q_PROPERTY(double MaximumRate READ maximumRate) + Q_PROPERTY(bool CanGoNext READ canGoNext) + Q_PROPERTY(bool CanGoPrevious READ canGoPrevious) + Q_PROPERTY(bool CanPlay READ canPlay) + Q_PROPERTY(bool CanPause READ canPause) + Q_PROPERTY(bool CanSeek READ canSeek) + Q_PROPERTY(bool CanControl READ canControl) + +public: + explicit MprisPlayerAdaptor(MpvObject *player); + + QString playbackStatus() const; + QString loopStatus() const { return "None"; } + double rate() const { return 1.0; } + bool shuffle() const { return false; } + QVariantMap metadata() const; + double volume() const; + void setVolume(double v); + qlonglong position() const; + double minimumRate() const { return 1.0; } + double maximumRate() const { return 1.0; } + bool canGoNext() const { return false; } + bool canGoPrevious() const { return false; } + bool canPlay() const { return true; } + bool canPause() const { return true; } + bool canSeek() const { return true; } + bool canControl() const { return true; } + +public slots: + void PlayPause(); + void Play(); + void Pause(); + void Stop(); + void Next() {} + void Previous() {} + void Seek(qlonglong offsetUs); + void SetPosition(const QDBusObjectPath &trackId, qlonglong positionUs); + void OpenUri(const QString &uri) { Q_UNUSED(uri) } + +signals: + void Seeked(qlonglong positionUs); + +private slots: + void onMpvEvent(const QString &event, const QVariant &value); + +private: + void emitPropertiesChanged(const QVariantMap &changed); + + MpvObject *m_player; + bool m_idle = true; + bool m_paused = false; + QString m_title; + double m_duration = 0.0; + int m_trackSeq = 0; +}; + +void mprisSetup(MpvObject *player); + +#endif // MPRIS_H diff --git a/stremio.pro b/stremio.pro index 5a2f36a6..69f249b8 100644 --- a/stremio.pro +++ b/stremio.pro @@ -70,6 +70,11 @@ SOURCES += main.cpp \ qclipboardproxy.cpp \ verifysig.c +unix:!mac { + SOURCES += mpris.cpp + HEADERS += mpris.h +} + RESOURCES += qml.qrc # Additional import path used to resolve QML modules in Qt Creator's code model