fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
575 lines · 20.2 KB Raw Blame History
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