| 1 | ;;; emms-mpris.el --- Mpris interface for EMMS -*- lexical-binding: t; -*- |
| 2 | |
| 3 | ;; Copyright (C) 2022, 2023 Free Software Foundation, Inc. |
| 4 | |
| 5 | ;; Author: Fran Burstall <fran.burstall@gmail.com> |
| 6 | ;; Keywords: multimedia |
| 7 | |
| 8 | ;; This program is free software; you can redistribute it and/or modify |
| 9 | ;; it under the terms of the GNU General Public License as published by |
| 10 | ;; the Free Software Foundation, either version 3 of the License, or |
| 11 | ;; (at your option) any later version. |
| 12 | |
| 13 | ;; This program is distributed in the hope that it will be useful, |
| 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | ;; GNU General Public License for more details. |
| 17 | |
| 18 | ;; You should have received a copy of the GNU General Public License |
| 19 | ;; along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 20 | |
| 21 | ;;; Commentary: |
| 22 | ;; This package provides a dbus interface to EMMS. |
| 23 | |
| 24 | ;; Usage: |
| 25 | ;; (require 'emms-mpris) |
| 26 | ;; (emms-mpris-enable) |
| 27 | |
| 28 | ;; Switch off with |
| 29 | ;; (emms-mpris-disable) |
| 30 | |
| 31 | ;; Caveats: this is not quite a complete implementation of the |
| 32 | ;; org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player interfaces |
| 33 | ;; (see |
| 34 | ;; https://specifications.freedesktop.org/mpris-spec/latest/index.html). |
| 35 | ;; What is missing: |
| 36 | ;; - Volume: this should be easy but there seems to be no way to get a |
| 37 | ;; simple percentage to report the volume---every emms-volume |
| 38 | ;; controller returns a string in a different format, sigh. |
| 39 | |
| 40 | |
| 41 | |
| 42 | ;;; Code: |
| 43 | |
| 44 | ;;* What we need |
| 45 | (require 'dbus) |
| 46 | (require 'url-parse) |
| 47 | (require 'emms) |
| 48 | (require 'emms-browser) |
| 49 | (require 'emms-playing-time) |
| 50 | (require 'cl-lib) |
| 51 | (require 'pcase) |
| 52 | (require 'subr-x) |
| 53 | (require 'seq) |
| 54 | |
| 55 | ;;* Dbus components |
| 56 | (defconst emms-mpris-service "org.mpris.MediaPlayer2.emms" |
| 57 | "The service we expose.") |
| 58 | |
| 59 | (defconst emms-mpris-path "/org/mpris/MediaPlayer2" |
| 60 | "Our object path.") |
| 61 | |
| 62 | ;;* Register and update |
| 63 | (defun emms-mpris-register-method (iface method handler) |
| 64 | "Register METHOD with HANDLER on interface IFACE." |
| 65 | (dbus-register-method :session |
| 66 | emms-mpris-service |
| 67 | emms-mpris-path |
| 68 | iface |
| 69 | method |
| 70 | handler |
| 71 | t)) |
| 72 | |
| 73 | (defun emms-mpris-register-property (iface property access value) |
| 74 | "Register PROPERTY on interface IFACE. |
| 75 | |
| 76 | VALUE is the initial value, ACCESS the access mode." |
| 77 | (let ((val (cond ((functionp value) (funcall value)) |
| 78 | ((and (symbolp value) (boundp value)) (symbol-value value)) |
| 79 | (t value)))) |
| 80 | (dbus-register-property :session |
| 81 | emms-mpris-service |
| 82 | emms-mpris-path |
| 83 | iface |
| 84 | property |
| 85 | access |
| 86 | val |
| 87 | ;; emit signal when readwrite properties change |
| 88 | (equal access :readwrite) |
| 89 | t))) |
| 90 | |
| 91 | (defun emms-mpris-register-iface (spec) |
| 92 | "Register an interface with spec SPEC on the EMMS service. |
| 93 | |
| 94 | The spec is a list of the form (IFACE METHODS PROPS). |
| 95 | |
| 96 | IFACE is a string naming the interface being registered. |
| 97 | |
| 98 | METHODS is a list of methods to register on the interface. |
| 99 | Each method is a list (NAME FN) with NAME a string and FN the |
| 100 | function the method calls. |
| 101 | |
| 102 | PROPS is a list of properties to register on the interface. |
| 103 | Each property is a list of the form (NAME ACCESS VAL) with |
| 104 | NAME a string, ACCESS a keyword and VAL either a function |
| 105 | that returns the default value of the property, a variable |
| 106 | which evaluates to that value or the value itself." |
| 107 | (cl-destructuring-bind (iface methods props) spec |
| 108 | (dolist (method methods) |
| 109 | (apply #'emms-mpris-register-method iface method)) |
| 110 | (dolist (prop props) |
| 111 | (apply #'emms-mpris-register-property iface prop)))) |
| 112 | |
| 113 | |
| 114 | ;;* Interfaces |
| 115 | |
| 116 | ;;** MediaPlayer2 interface |
| 117 | |
| 118 | (defvar emms-mpris-mediaplayer-iface-spec |
| 119 | '("org.mpris.MediaPlayer2" |
| 120 | (("Raise" ignore) |
| 121 | ("Quit" ignore)) |
| 122 | (("CanQuit" :read nil) |
| 123 | ("CanRaise" :read nil) |
| 124 | ("HasTrackList" :read nil) |
| 125 | ("Identity" :read "EMMS media player") |
| 126 | ("SupportedUriSchemes" :read (:array "file")) |
| 127 | ("SupportedMimeTypes" :read (:array "audio/mpeg" "application/ogg")))) |
| 128 | "Interface spec for MediaPlayer2.") |
| 129 | |
| 130 | ;;** MediaPlayer2.Player interface |
| 131 | |
| 132 | (defvar emms-mpris-player-iface-spec |
| 133 | '("org.mpris.MediaPlayer2.Player" |
| 134 | ;; Methods: |
| 135 | (("OpenUri" emms-mpris-open-uri) |
| 136 | ("Next" (lambda () (ignore-errors (emms-next)) :ignore)) |
| 137 | ("Previous" (lambda () (ignore-errors (emms-previous)) :ignore)) |
| 138 | ("Pause" (lambda () (emms-pause) :ignore)) |
| 139 | ("PlayPause" (lambda () (emms-pause) :ignore)) |
| 140 | ("Stop" (lambda () (emms-stop) :ignore)) |
| 141 | ("Play" (lambda () (emms-pause) :ignore)) |
| 142 | ("Seek" emms-mpris-seek) |
| 143 | ("SetPosition" emms-mpris-set-position)) |
| 144 | ;; Properties: Shuffle, LoopStatus, Volume not supported (yet) |
| 145 | (("LoopStatus" :readwrite emms-mpris-loop-status) |
| 146 | ("Shuffle" :readwrite emms-random-playlist) |
| 147 | ("PlaybackStatus" :read emms-mpris-status) |
| 148 | ("Rate" :readwrite 1.0) |
| 149 | ("MinimumRate" :read 1.0) |
| 150 | ("MaximumRate" :read 1.0) |
| 151 | ("Position" :read (:int64 0)) ;think more about this |
| 152 | ("CanGoNext" :read t) |
| 153 | ("CanGoPrevious" :read t) |
| 154 | ("CanPlay" :read t) |
| 155 | ("CanPause" :read t) |
| 156 | ("CanPause" :read t) |
| 157 | ("CanControl" :read t) |
| 158 | ("CanSeek" :read t) |
| 159 | ("Metadata" :read emms-mpris-current-metadata))) |
| 160 | "Interface spec for MediaPlayer2.Player.") |
| 161 | ;;** Introspection interface |
| 162 | |
| 163 | (defvar emms-mpris-xml |
| 164 | "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\" |
| 165 | \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\"> |
| 166 | <!-- GDBus 2.66.8 --> |
| 167 | <node> |
| 168 | <interface name=\"org.freedesktop.DBus.Properties\"> |
| 169 | <method name=\"Get\"> |
| 170 | <arg type=\"s\" name=\"interface_name\" direction=\"in\"/> |
| 171 | <arg type=\"s\" name=\"property_name\" direction=\"in\"/> |
| 172 | <arg type=\"v\" name=\"value\" direction=\"out\"/> |
| 173 | </method> |
| 174 | <method name=\"GetAll\"> |
| 175 | <arg type=\"s\" name=\"interface_name\" direction=\"in\"/> |
| 176 | <arg type=\"a{sv}\" name=\"properties\" direction=\"out\"/> |
| 177 | </method> |
| 178 | <method name=\"Set\"> |
| 179 | <arg type=\"s\" name=\"interface_name\" direction=\"in\"/> |
| 180 | <arg type=\"s\" name=\"property_name\" direction=\"in\"/> |
| 181 | <arg type=\"v\" name=\"value\" direction=\"in\"/> |
| 182 | </method> |
| 183 | <signal name=\"PropertiesChanged\"> |
| 184 | <arg type=\"s\" name=\"interface_name\"/> |
| 185 | <arg type=\"a{sv}\" name=\"changed_properties\"/> |
| 186 | <arg type=\"as\" name=\"invalidated_properties\"/> |
| 187 | </signal> |
| 188 | </interface> |
| 189 | <interface name=\"org.freedesktop.DBus.Introspectable\"> |
| 190 | <method name=\"Introspect\"> |
| 191 | <arg type=\"s\" name=\"xml_data\" direction=\"out\"/> |
| 192 | </method> |
| 193 | </interface> |
| 194 | <interface name=\"org.freedesktop.DBus.Peer\"> |
| 195 | <method name=\"Ping\"/> |
| 196 | <method name=\"GetMachineId\"> |
| 197 | <arg type=\"s\" name=\"machine_uuid\" direction=\"out\"/> |
| 198 | </method> |
| 199 | </interface> |
| 200 | <interface name=\"org.mpris.MediaPlayer2\"> |
| 201 | <method name=\"Raise\"/> |
| 202 | <method name=\"Quit\"/> |
| 203 | <property type=\"b\" name=\"CanQuit\" access=\"read\"/> |
| 204 | <property type=\"b\" name=\"CanRaise\" access=\"read\"/> |
| 205 | <property type=\"b\" name=\"HasTrackList\" access=\"read\"/> |
| 206 | <property type=\"s\" name=\"Identity\" access=\"read\"/> |
| 207 | <property type=\"s\" name=\"DesktopEntry\" access=\"read\"/> |
| 208 | <property type=\"as\" name=\"SupportedUriSchemes\" access=\"read\"/> |
| 209 | <property type=\"as\" name=\"SupportedMimeTypes\" access=\"read\"/> |
| 210 | </interface> |
| 211 | <interface name=\"org.mpris.MediaPlayer2.Player\"> |
| 212 | <method name=\"Next\"/> |
| 213 | <method name=\"Previous\"/> |
| 214 | <method name=\"Pause\"/> |
| 215 | <method name=\"PlayPause\"/> |
| 216 | <method name=\"Stop\"/> |
| 217 | <method name=\"Play\"/> |
| 218 | <method name=\"Seek\"> |
| 219 | <arg type=\"x\" name=\"Offset\" direction=\"in\"/> |
| 220 | </method> |
| 221 | <method name=\"SetPosition\"> |
| 222 | <arg type=\"o\" name=\"TrackId\" direction=\"in\"/> |
| 223 | <arg type=\"x\" name=\"Position\" direction=\"in\"/> |
| 224 | </method> |
| 225 | <method name=\"OpenUri\"> |
| 226 | <arg type=\"s\" name=\"Uri\" direction=\"in\"/> |
| 227 | </method> |
| 228 | <signal name=\"Seeked\"> |
| 229 | <arg type=\"x\" name=\"Position\"/> |
| 230 | </signal> |
| 231 | <property type=\"s\" name=\"PlaybackStatus\" access=\"read\"/> |
| 232 | <property type=\"s\" name=\"LoopStatus\" access=\"readwrite\"/> |
| 233 | <property type=\"d\" name=\"Rate\" access=\"readwrite\"/> |
| 234 | <property type=\"b\" name=\"Shuffle\" access=\"readwrite\"/> |
| 235 | <property type=\"a{sv}\" name=\"Metadata\" access=\"read\"/> |
| 236 | <property type=\"d\" name=\"Volume\" access=\"readwrite\"/> |
| 237 | <property type=\"x\" name=\"Position\" access=\"read\"/> |
| 238 | <property type=\"d\" name=\"MinimumRate\" access=\"read\"/> |
| 239 | <property type=\"d\" name=\"MaximumRate\" access=\"read\"/> |
| 240 | <property type=\"b\" name=\"CanGoNext\" access=\"read\"/> |
| 241 | <property type=\"b\" name=\"CanGoPrevious\" access=\"read\"/> |
| 242 | <property type=\"b\" name=\"CanPlay\" access=\"read\"/> |
| 243 | <property type=\"b\" name=\"CanPause\" access=\"read\"/> |
| 244 | <property type=\"b\" name=\"CanSeek\" access=\"read\"/> |
| 245 | <property type=\"b\" name=\"CanControl\" access=\"read\"/> |
| 246 | </interface> |
| 247 | </node> |
| 248 | " |
| 249 | "Mpris introspection data for emms.") |
| 250 | |
| 251 | (defun emms-mpris-introspect () |
| 252 | "Return dbus introspection data." |
| 253 | emms-mpris-xml) |
| 254 | |
| 255 | (defvar emms-mpris-introspectable-iface-spec |
| 256 | '("org.freedesktop.DBus.Introspectable" |
| 257 | (("Introspect" emms-mpris-introspect)) |
| 258 | nil) |
| 259 | "Introspectable interface spec for dbus.") |
| 260 | |
| 261 | ;;** Properties interface |
| 262 | |
| 263 | ;; We re-implement the "Get" and "GetAll" methods of the |
| 264 | ;; dbus.properties interface. For why? Well, the default handler |
| 265 | ;; looks up the value of a property in a hash table which works fine |
| 266 | ;; unless we want the "Position" property of the Player interface |
| 267 | ;; which changes all the time (and we don't want to update the table |
| 268 | ;; every second!). So we wrap the default handler to update the |
| 269 | ;; Position entry in the table before delegating to the default |
| 270 | ;; handler. This is a bit of a hack in that we go rather beyond the |
| 271 | ;; API of dbus.el and hope that the internals do not change. |
| 272 | |
| 273 | (defun emms-mpris-update-position-hash-value () |
| 274 | "Update the D-Bus hash-table. |
| 275 | |
| 276 | Refresh the value in the hash-table corresponding to the Position |
| 277 | property of the org.mpris.MediaPlayer2.Player interface." |
| 278 | (puthash (list :property :session "org.mpris.MediaPlayer2.Player" "Position") |
| 279 | (list (list nil |
| 280 | emms-mpris-service |
| 281 | emms-mpris-path |
| 282 | (list :read nil (list :variant :int64 (emms-mpris-sec-to-musec emms-playing-time))))) |
| 283 | dbus-registered-objects-table)) |
| 284 | |
| 285 | (defun emms-mpris-get-property-handler (&rest args) |
| 286 | "Handle Get and GetAll event for property in ARGS. |
| 287 | |
| 288 | The Position property gets refreshed before delegating |
| 289 | to `dbus-property-handler'." |
| 290 | (let* ((last-input-event last-input-event)) |
| 291 | (emms-mpris-update-position-hash-value) |
| 292 | (apply #'dbus-property-handler args))) |
| 293 | |
| 294 | (defvar emms-mpris-properties-iface-spec |
| 295 | '("org.freedesktop.DBus.Properties" |
| 296 | (("Get" emms-mpris-get-property-handler) |
| 297 | ("GetAll" emms-mpris-get-property-handler)) |
| 298 | nil) |
| 299 | "Partial Properties interface spec for dbus.") |
| 300 | |
| 301 | |
| 302 | ;;* Implementation |
| 303 | |
| 304 | ;;** Utilities |
| 305 | ;; Emms thinks in seconds but mpris in microseconds |
| 306 | (defun emms-mpris-musec-to-sec (ms) |
| 307 | "Convert MS microseconds to seconds." |
| 308 | (* ms .000001)) |
| 309 | |
| 310 | (defun emms-mpris-sec-to-musec (s) |
| 311 | "Convert S seconds to microseconds." |
| 312 | (truncate (* s 1000000))) |
| 313 | |
| 314 | ;; Track-id is a d-bus object id and these have rules... |
| 315 | (defun emms-mpris-track-id (track) |
| 316 | "Return track-id of TRACK as D-Bus object id." |
| 317 | ;; FIX ME: this won't work if we implement the tracklist interface |
| 318 | ;; and the tracklist has repeated tracks. |
| 319 | (concat "/" (mapconcat #'dbus-escape-as-identifier |
| 320 | (split-string (emms-track-get track 'name) "/" t) |
| 321 | "/"))) |
| 322 | |
| 323 | ;;** Update properties |
| 324 | (defun emms-mpris-update-property (iface property access value) |
| 325 | "Update PROPERTY on interface IFACE to VALUE." |
| 326 | (dbus-register-property :session |
| 327 | emms-mpris-service |
| 328 | emms-mpris-path |
| 329 | iface |
| 330 | property |
| 331 | access |
| 332 | value |
| 333 | t nil)) |
| 334 | |
| 335 | (defvar emms-mpris-ignore-signal-p nil |
| 336 | "Non-nil if we should ignore a PropertiesChanged signal. |
| 337 | |
| 338 | We do this when we have already taken action via the EMMS UI.") |
| 339 | |
| 340 | (defun emms-mpris-property-change-handler (_service changes _invalidated) |
| 341 | "Respond to PropertiesChanged signal by updating emms state to reflect CHANGES." |
| 342 | (if emms-mpris-ignore-signal-p |
| 343 | (setq emms-mpris-ignore-signal-p nil) |
| 344 | (when-let* ((payload (assoc "LoopStatus" changes))) |
| 345 | (pcase (caadr payload) |
| 346 | ("Track" (setq emms-repeat-playlist nil |
| 347 | emms-repeat-track t)) |
| 348 | ("Playlist" (setq emms-repeat-playlist t |
| 349 | emms-repeat-track nil)) |
| 350 | (_ (setq emms-repeat-playlist nil |
| 351 | emms-repeat-track nil)))) |
| 352 | (when-let* ((payload (assoc "Shuffle" changes))) |
| 353 | (setq emms-random-playlist (caadr payload)) |
| 354 | (if emms-random-playlist |
| 355 | (setq emms-player-next-function #'emms-random) |
| 356 | (setq emms-player-next-function #'emms-next-noerror))))) |
| 357 | |
| 358 | ;;*** Playback status |
| 359 | (defun emms-mpris-status () |
| 360 | "Return the playback status of EMMS as string: Playing, Paused or Stopped." |
| 361 | (if emms-player-playing-p |
| 362 | (if emms-player-paused-p |
| 363 | "Paused" "Playing") |
| 364 | "Stopped")) |
| 365 | |
| 366 | ;;*** Loop status |
| 367 | (defun emms-mpris-loop-status () |
| 368 | "Return the loop status of EMMS as a string: Track, Playlist or None." |
| 369 | (cond (emms-repeat-track "Track") |
| 370 | (emms-repeat-playlist "Playlist") |
| 371 | (t "None"))) |
| 372 | |
| 373 | (defun emms-mpris-advise-loop-status () |
| 374 | "Update dbus value of LoopStatus. |
| 375 | |
| 376 | Intended to advise emms-toggle-repeat-*." |
| 377 | (setq emms-mpris-ignore-signal-p t) |
| 378 | (dbus-set-property :session |
| 379 | emms-mpris-service |
| 380 | emms-mpris-path |
| 381 | "org.mpris.MediaPlayer2.Player" |
| 382 | "LoopStatus" |
| 383 | (emms-mpris-loop-status))) |
| 384 | |
| 385 | ;;*** Shuffle |
| 386 | (defun emms-mpris-advise-shuffle () |
| 387 | "Update dbus value of Shuffle. |
| 388 | |
| 389 | Intended to advise `emms-toggle-random-playlist'." |
| 390 | (setq emms-mpris-ignore-signal-p t) |
| 391 | (dbus-set-property :session |
| 392 | emms-mpris-service |
| 393 | emms-mpris-path |
| 394 | "org.mpris.MediaPlayer2.Player" |
| 395 | "Shuffle" |
| 396 | emms-random-playlist)) |
| 397 | |
| 398 | ;;*** Metadata |
| 399 | |
| 400 | (defvar emms-mpris-metadata-dict |
| 401 | '((info-album "xesam:album" :s) |
| 402 | (info-albumartist "xesam:albumArtist" :as) |
| 403 | (info-artist "xesam:artist" :as) |
| 404 | (info-composer "xesam:composer" :as) |
| 405 | (info-discnumber "xesam:discNumber" :int) |
| 406 | (info-tracknumber "xesam:trackNumber" :int) |
| 407 | (info-title "xesam:title" :s) |
| 408 | (play-count "xesam:useCount" :int)) |
| 409 | "Dictionary between emms metadata and mpris metadata. |
| 410 | |
| 411 | Each entry of the form (info-field mpris-field dbus-type).") |
| 412 | |
| 413 | (defun emms-mpris-dict (k v &optional type) |
| 414 | "Return a dbus dict-entry with key K and value V, optionally of type TYPE." |
| 415 | (if type |
| 416 | (list :dict-entry k (list :variant type v)) |
| 417 | (list :dict-entry k (list :variant v)))) |
| 418 | |
| 419 | (defun emms-mpris-convert-field (track info key type) |
| 420 | "Convert field INFO of TRACK into dbus dict-entry with key KEY and type TYPE." |
| 421 | (let ((data (emms-track-get track info)) |
| 422 | value) |
| 423 | (when data |
| 424 | (setq value (pcase type |
| 425 | (:as (list :array data)) |
| 426 | (:int (if (stringp data) (string-to-number data) data)) |
| 427 | (:s data))) |
| 428 | (emms-mpris-dict key value)))) |
| 429 | |
| 430 | (defun emms-mpris-metadata (track) |
| 431 | "Return mpris metadata for TRACK." |
| 432 | (let ((track-name (emms-track-get track 'name)) |
| 433 | metadata) |
| 434 | ;; standard fields |
| 435 | (dolist (field emms-mpris-metadata-dict) |
| 436 | (when-let* ((entry (apply #'emms-mpris-convert-field track field))) |
| 437 | (push entry metadata))) |
| 438 | ;; url |
| 439 | (push (emms-mpris-dict "xesam:url" (url-encode-url (concat "file:" track-name))) metadata) |
| 440 | ;; artUrl |
| 441 | ;; Shockingly, emms-browser-get-cover-from-path needs a graphical display to |
| 442 | ;; function (it eventually calls image-size) so we check there is one... |
| 443 | (when (seq-some #'display-graphic-p (frame-list)) |
| 444 | (when-let* ((art-file (emms-browser-get-cover-from-path track-name 'medium))) |
| 445 | (push (emms-mpris-dict "mpris:artUrl" (url-encode-url (concat "file://" art-file))) metadata))) |
| 446 | ;; length |
| 447 | (push |
| 448 | (emms-mpris-dict "mpris:length" |
| 449 | (emms-mpris-sec-to-musec (emms-track-get track 'info-playing-time 0)) |
| 450 | :int64) |
| 451 | metadata) |
| 452 | ;; trackid |
| 453 | (push |
| 454 | (emms-mpris-dict "mpris:trackid" |
| 455 | (emms-mpris-track-id track) |
| 456 | :object-path) |
| 457 | metadata) |
| 458 | (cons :array metadata))) |
| 459 | |
| 460 | (defun emms-mpris-current-metadata () |
| 461 | "Return metadata of current track if it exists, else return a placeholder." |
| 462 | (if-let* ((track (emms-playlist-current-selected-track))) |
| 463 | (emms-mpris-metadata track) |
| 464 | '(:array (:dict-entry "mpris:trackid" (:variant :object-path "/no/track/here"))))) |
| 465 | |
| 466 | ;;*** update them! |
| 467 | (defun emms-mpris-change-status () |
| 468 | "Notify emms status to dbus." |
| 469 | (let ((iface "org.mpris.MediaPlayer2.Player")) |
| 470 | (emms-mpris-update-property iface |
| 471 | "PlaybackStatus" |
| 472 | :read |
| 473 | (emms-mpris-status)) |
| 474 | (emms-mpris-update-property iface |
| 475 | "Metadata" |
| 476 | :read |
| 477 | (emms-mpris-current-metadata)))) |
| 478 | |
| 479 | |
| 480 | ;;** Seek and SetPosition |
| 481 | |
| 482 | ;;*** Signal position change (after Seek or SetPosition) |
| 483 | (defun emms-mpris-signal-position (pos) |
| 484 | "Send \"Seeked\" signal with new position POS (in seconds)." |
| 485 | (dbus-send-signal :session |
| 486 | nil |
| 487 | emms-mpris-path |
| 488 | "org.mpris.MediaPlayer2.Player" |
| 489 | "Seeked" |
| 490 | :int64 |
| 491 | (emms-mpris-sec-to-musec pos))) |
| 492 | |
| 493 | ;;*** Seek method |
| 494 | (defun emms-mpris-seek (ms) |
| 495 | "Method to seek by MS microseconds." |
| 496 | (emms-seek (number-to-string (emms-mpris-musec-to-sec ms))) |
| 497 | (emms-mpris-signal-position emms-playing-time) |
| 498 | :ignore) |
| 499 | |
| 500 | ;;*** SetPosition method |
| 501 | (defun emms-mpris-set-position (track-id pos) |
| 502 | "Method to seek to POS (in microseconds) if current track has id TRACK-ID." |
| 503 | (let* ((track (emms-playlist-current-selected-track)) |
| 504 | (duration (emms-track-get track 'info-playing-time 0)) |
| 505 | (current-track-id (emms-mpris-track-id track)) |
| 506 | (pos-in-secs (emms-mpris-musec-to-sec pos))) |
| 507 | (when (and (string-equal track-id current-track-id) |
| 508 | (<= 0.0 pos-in-secs duration)) |
| 509 | (emms-seek-to (number-to-string pos-in-secs)) |
| 510 | (emms-mpris-signal-position emms-playing-time)) |
| 511 | :ignore)) |
| 512 | |
| 513 | ;;** OpenURI |
| 514 | |
| 515 | (defun emms-mpris-open-uri (uri) |
| 516 | "Method for opening file URI and playing it." |
| 517 | (let* ((parsed-uri (url-generic-parse-url uri)) |
| 518 | (file (url-unhex-string (url-filename parsed-uri))) |
| 519 | (type (url-type parsed-uri))) |
| 520 | (when (and (string-equal type "file") (file-exists-p file)) |
| 521 | (cond ((file-regular-p file) (emms-play-file file)) |
| 522 | ((file-directory-p file) (emms-play-directory file))))) |
| 523 | :ignore) |
| 524 | |
| 525 | |
| 526 | ;;* Entry point |
| 527 | |
| 528 | (defvar emms-mpris-enabled-p nil |
| 529 | "Non-nil if the EMMS mpris service is enabled.") |
| 530 | |
| 531 | (defun emms-mpris-enable () |
| 532 | "Activate EMMS dbus service." |
| 533 | (interactive) |
| 534 | (unless emms-mpris-enabled-p |
| 535 | (emms-mpris-register-iface emms-mpris-mediaplayer-iface-spec) |
| 536 | (emms-mpris-register-iface emms-mpris-player-iface-spec) |
| 537 | (emms-mpris-register-iface emms-mpris-introspectable-iface-spec) |
| 538 | (emms-mpris-register-iface emms-mpris-properties-iface-spec) |
| 539 | (dbus-register-service :session emms-mpris-service :allow-replacement) |
| 540 | (dbus-register-signal :session |
| 541 | emms-mpris-service |
| 542 | emms-mpris-path |
| 543 | dbus-interface-properties |
| 544 | "PropertiesChanged" |
| 545 | #'emms-mpris-property-change-handler |
| 546 | :eavesdrop) |
| 547 | (advice-add 'emms-toggle-repeat-track :after #'emms-mpris-advise-loop-status) |
| 548 | (advice-add 'emms-toggle-repeat-playlist :after #'emms-mpris-advise-loop-status) |
| 549 | (advice-add 'emms-toggle-random-playlist :after #'emms-mpris-advise-shuffle) |
| 550 | (add-hook 'emms-player-started-hook #'emms-mpris-change-status) |
| 551 | (add-hook 'emms-player-paused-hook #'emms-mpris-change-status) |
| 552 | (add-hook 'emms-player-stopped-hook #'emms-mpris-change-status) |
| 553 | (add-hook 'emms-player-finished-hook #'emms-mpris-change-status) |
| 554 | (setq emms-mpris-enabled-p t))) |
| 555 | |
| 556 | (defun emms-mpris-disable () |
| 557 | "Turn off EMMS dbus service." |
| 558 | (interactive) |
| 559 | (when emms-mpris-enabled-p |
| 560 | (remove-hook 'emms-player-started-hook #'emms-mpris-change-status) |
| 561 | (remove-hook 'emms-player-paused-hook #'emms-mpris-change-status) |
| 562 | (remove-hook 'emms-player-stopped-hook #'emms-mpris-change-status) |
| 563 | (remove-hook 'emms-player-finished-hook #'emms-mpris-change-status) |
| 564 | (advice-remove 'emms-toggle-repeat-track #'emms-mpris-advise-loop-status) |
| 565 | (advice-remove 'emms-toggle-repeat-playlist #'emms-mpris-advise-loop-status) |
| 566 | (advice-remove 'emms-toggle-random-playlist #'emms-mpris-advise-shuffle) |
| 567 | ;; Call this twice: we have two methods for "Get" on the Properties |
| 568 | ;; interface (there /must/ be a better way to do this!): |
| 569 | (dbus-unregister-service :session emms-mpris-service) |
| 570 | (dbus-unregister-service :session emms-mpris-service) |
| 571 | (setq emms-mpris-enabled-p nil))) |
| 572 | |
| 573 | |
| 574 | (provide 'emms-mpris) |
| 575 | ;;; emms-mpris.el ends here |